What You'll Learn
- Build a water mask using MNDWI thresholding
- Create reach-perpendicular transects from a centerline
- Compute approximate river width statistics
- Export transect measurements and masks for further analysis
Why This Matters
River morphology monitoring supports water management:
- Flood modeling: Channel width is a key hydraulic parameter
- Erosion/deposition: Track changes over time
- Ecosystem health: River width affects habitat and temperature
- Navigation: Shipping requires minimum channel widths
Before You Start
- Prerequisites: Understand spectral indices and vector geometry operations.
- Estimated time: 75 minutes
- Materials: Earth Engine account and a river of interest.
Key Terms
- MNDWI (Modified NDWI)
- A water index using Green and SWIR bands: (Green - SWIR) / (Green + SWIR). Better at distinguishing water from built-up areas than NDWI.
- Centerline
- A line representing the center of a river channel, used as a reference for transects.
- Transect
- A line perpendicular to the centerline used to measure river width.
- River Width
- The distance across a river channel, typically measured perpendicular to flow direction.
Approach Overview
- Water Detection: Calculate MNDWI and threshold to create binary water mask
- Centerline: Draw or import a line representing the river channel
- Transects: Generate perpendicular lines at regular intervals
- Width Measurement: Measure water extent along each transect
Step 1: Create Water Mask with MNDWI
// AOI - Lower Mississippi River example
var aoi = ee.Geometry.Rectangle([-91.45, 30.35, -91.05, 30.60]);
Map.centerObject(aoi, 11);
// Load Landsat 8 and mask clouds
var L = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
.filterBounds(aoi)
.filterDate('2021-07-01','2021-09-30')
.filter(ee.Filter.lt('CLOUD_COVER', 20));
function maskL2(img){
var qa = img.select('QA_PIXEL');
var cloud = qa.bitwiseAnd(1 << 3).neq(0);
var shadow = qa.bitwiseAnd(1 << 4).neq(0);
return img.updateMask(cloud.or(shadow).not());
}
function toSR(img){
var scale = 0.0000275; var offset = -0.2;
var sr = maskL2(img).select(['SR_B3','SR_B6']).multiply(scale).add(offset)
.rename(['green','swir1']);
return sr.copyProperties(img, img.propertyNames());
}
var med = L.map(toSR).median().clip(aoi);
// Calculate MNDWI
var mndwi = med.expression('(g - s) / (g + s)', {
g: med.select('green'),
s: med.select('swir1')
}).rename('MNDWI');
Map.addLayer(mndwi, {min:-0.5, max:0.5, palette:['#8c510a','#f6e8c3','#01665e']}, 'MNDWI');
// Create water mask
var water = mndwi.gt(0.2).selfMask();
Map.addLayer(water, {palette:['#2b83ba']}, 'Water mask');
Step 2: Create Centerline and Transects
Draw a centerline in the Code Editor or use a predefined one:
// Fallback centerline if none is drawn
var fallbackCenterline = ee.FeatureCollection([
ee.Feature(ee.Geometry.LineString([[-91.40, 30.38], [-91.10, 30.58]]))
]);
var centerlineFc = (typeof centerline !== 'undefined') ? centerline : fallbackCenterline;
// Create points along the centerline every 200 meters
var spacing = 200; // meters between transects
var line = ee.Feature(centerlineFc.first()).geometry();
var length = line.length();
var pts = ee.FeatureCollection(ee.List.sequence(0, length, spacing).map(function(d){
d = ee.Number(d);
var p = line.coordinateAlong(d);
return ee.Feature(ee.Geometry.Point(p), {d: d});
}));
// Calculate perpendicular angles at each point
var bearings = pts.map(function(f){
var d = ee.Number(f.get('d'));
var a = line.coordinateAlong(d);
var b = line.coordinateAlong(d.add(10));
var dx = ee.Number(ee.List(b).get(0)).subtract(ee.Number(ee.List(a).get(0)));
var dy = ee.Number(ee.List(b).get(1)).subtract(ee.Number(ee.List(a).get(1)));
var angle = ee.Number.atan2(dy, dx);
var normal = angle.add(Math.PI/2.0);
return f.set({normal: normal});
});
// Build transect lines (800m total width)
var half = 400; // half-length in meters
var transects = bearings.map(function(f){
f = ee.Feature(f);
var p = ee.Geometry.Point(f.geometry().coordinates());
var ang = ee.Number(f.get('normal'));
var dx = ee.Number(half).multiply(ee.Number(ang.cos()));
var dy = ee.Number(half).multiply(ee.Number(ang.sin()));
var p1 = p.translate(dx, dy, 'meters');
var p2 = p.translate(dx.multiply(-1), dy.multiply(-1), 'meters');
return ee.Feature(ee.Geometry.LineString([p1.coordinates(), p2.coordinates()]), f.toDictionary());
});
Map.addLayer(transects, {color: 'yellow'}, 'Transects');
Step 3: Estimate River Width
// Estimate width by measuring water fraction along each transect
var widths = transects.map(function(t){
var geom = ee.Feature(t).geometry();
var length = geom.length();
var sampled = water.reduceRegion({
reducer: ee.Reducer.mean(),
geometry: geom.buffer(15),
scale: 30,
bestEffort: true
});
var frac = ee.Number(sampled.get('MNDWI'));
var w = ee.Algorithms.If(frac, ee.Number(frac).multiply(length), 0);
return ee.Feature(geom, {approx_width_m: w});
});
print('Approximate widths (m)', widths.limit(10));
Limitation: This is an approximate method. For more accurate measurements, consider using the RivWidthCloud package or similar algorithms.
Step 4: Export Results
// Export transect widths as CSV
Export.table.toDrive({
collection: widths,
description: 'river_widths_transects',
fileFormat: 'CSV'
});
// Export water mask as GeoTIFF
Export.image.toDrive({
image: water.rename('water_mask'),
description: 'water_mask_mndwi',
region: aoi,
scale: 30,
maxPixels: 1e13
});
// Export transects as GeoJSON
Export.table.toDrive({
collection: transects,
description: 'transects_geojson',
fileFormat: 'GeoJSON'
});
Check Your Understanding
- Why do we use MNDWI instead of NDWI for water detection?
- What challenges arise when measuring width on braided rivers?
- How would seasonal water level changes affect your width measurements?
- What would you need to add to track channel migration over time?
Troubleshooting
Solution: Adjust the MNDWI threshold or add additional filtering (e.g., slope mask for hillshadows).
Solution: This approach works best for straight reaches. For meandering rivers, compute local tangent more carefully.
Solution: Check transect length and ensure your water mask doesn't include floodplain lakes.
Pro Tips
- Use RivWidthCloud: A GEE package specifically designed for river width measurement
- JRC Global Surface Water: Pre-computed water occurrence can supplement Landsat
- Multi-temporal: Compare low and high flow seasons for width variability
- Validate with field data: Compare to gauging station cross-sections if available
?? Lab Submission
Subject: Lab 31 - River Morphology - [Your Name]
Submit:
- Map: MNDWI, water mask, centerline, and transects
- Table: approximate width (m) along transects (CSV export)
- Short note: Limitations of this approach and how you'd improve it
- EE script URL