Overview
In this lab, we will use Sentinel-1 SAR data to map the extent of flooding caused by the 2019 Cyclone Idai in Mozambique. This was one of the worst tropical cyclones ever recorded in the Southern Hemisphere, causing catastrophic flooding along the Buzi and Pungwe rivers near the city of Beira.
Here is the key advantage of SAR: optical satellites could not see through the storm clouds, but Sentinel-1 mapped the flood clearly. Radar does not care about clouds. Let's put that superpower to work.
Key terms
- Backscatter
- Radar signal reflected back to the sensor. Measured in decibels (dB). Water has very low backscatter.
- VV polarization
- Vertical transmit, vertical receive. Most sensitive to surface roughness and water detection.
- Speckle filtering
- Noise reduction technique for SAR images. Applied to the baseline image to create a cleaner reference.
- Change detection threshold
- The minimum backscatter drop (in dB) used to classify a pixel as flooded. Typically 3 to 5 dB.
Part 1: Setting the scene
Cyclone Idai made landfall near Beira, Mozambique on March 14, 2019. Let's define our study area over the low-lying floodplain where the Buzi and Pungwe rivers overflowed.
// Study area: Beira and surrounding floodplain, Mozambique
var beira = ee.Geometry.Point([34.87, -19.84]);
var studyArea = beira.buffer(60000); // 60 km radius
Map.centerObject(studyArea, 9);
Map.addLayer(studyArea, {color: 'yellow'}, 'Study area');
Understanding the code
We define a point near Beira and create a 60 km buffer to capture the entire flood zone.
The buffer() function creates a circle around the point in meters.
Part 2: Building our "before" baseline
Let's create a clean baseline image from before the cyclone hit. We will take the temporal median of Sentinel-1 images from February 2019. Using a median reduces the "salt and pepper" speckle noise that is common in radar images.
// Helper function to apply standard Sentinel-1 filters
function filterS1(collection) {
return collection
.filter(ee.Filter.eq('instrumentMode', 'IW'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VH'))
.filter(ee.Filter.eq('orbitProperties_pass', 'DESCENDING'));
}
// Before image: February 2019 (pre-cyclone baseline)
var beforeCollection = filterS1(
ee.ImageCollection('COPERNICUS/S1_GRD')
.filterBounds(studyArea)
.filterDate('2019-02-01', '2019-02-28')
);
var before = beforeCollection.select('VV').median();
print('Before images:', beforeCollection.size());
Map.addLayer(before, {min: -25, max: 0}, 'Before (Feb 2019) VV');
Understanding the code
The filterS1() function applies the standard Sentinel-1 filters: IW mode,
both VV and VH polarizations, and descending orbit. The temporal .median()
creates a clean baseline with reduced speckle. We use VV polarization because it is most
sensitive to water surfaces.
Part 3: Capturing the flood
Now let's load the Sentinel-1 image acquired during peak flooding (March 19 to 25, 2019). We use a single mosaic rather than a median here, because we want to capture the actual flood state. A median would smooth it away.
// During image: March 19-25, 2019 (peak flooding)
var duringCollection = filterS1(
ee.ImageCollection('COPERNICUS/S1_GRD')
.filterBounds(studyArea)
.filterDate('2019-03-19', '2019-03-25')
);
var during = duringCollection.select('VV').mosaic();
print('During-flood images:', duringCollection.size());
Map.addLayer(during, {min: -25, max: 0}, 'During flood (Mar 2019) VV');
Understanding the code
We use .mosaic() instead of .median() for the during-flood image.
A median would smooth out the flood signal if some images in the window were not flooded.
With a mosaic, we get the most recent pixel value, which should show the flood.
Part 4: Spotting the flood with subtraction
Here is the core idea: we subtract the during-flood image from the before image. Wherever the backscatter dropped significantly (land became smooth water), the difference will be large and positive. We set a threshold to create our flood mask. Think of it like finding everywhere the "volume" of the radar signal went quiet.
// Change detection: before minus during
var difference = before.subtract(during);
// Visualize the difference (positive = backscatter decreased = possible flood)
Map.addLayer(difference, {min: -5, max: 10, palette: ['blue', 'white', 'red']}, 'Backscatter difference');
// Apply threshold: backscatter dropped by more than 3 dB = flooded
var floodMask = difference.gt(3);
Map.addLayer(floodMask.selfMask(), {palette: ['cyan']}, 'Flood extent (threshold = 3 dB)');
Understanding the code
The .subtract() gives us the change in backscatter. Positive values mean
the surface became smoother (drier land turned to water). A 3 dB threshold is a standard
starting point for flood detection. Red areas in the difference map show the strongest
backscatter drops.
Try it: Experiment with thresholds
Try thresholds of 2, 3, 4, and 5 dB. How does the flood extent change? A lower threshold captures more area (including possible false positives); a higher threshold is more conservative. Which threshold best matches the major river floodplains?
Part 5: How much land was flooded?
Let's put a number on the destruction. We will use ee.Image.pixelArea() to
calculate the total flooded area in square kilometers.
// Calculate flooded area in square kilometers
var floodArea = floodMask.multiply(ee.Image.pixelArea());
var stats = floodArea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: studyArea,
scale: 10,
maxPixels: 1e9
});
var areaKm2 = ee.Number(stats.get('VV')).divide(1e6);
print('Flooded area (km²):', areaKm2);
What you should see
With a 3 dB threshold and 60 km study radius, you should see a flooded area between 1,500 and 4,000 km² depending on exact dates and orbit coverage. Published estimates of the Cyclone Idai flood extent range from 2,000 to 3,000 km². If your value is far outside this range, check your date filters and threshold.
Part 6: Cleaning up our flood map
Our raw flood mask may include areas that are always water (rivers, lakes). Let's remove those using the JRC dataset so we only see the new flooding caused by the cyclone. We will also add a Sentinel-2 true-color image for context.
// Remove permanent water using JRC Global Surface Water
var jrc = ee.Image('JRC/GSW1_4/GlobalSurfaceWater');
var permanentWater = jrc.select('occurrence').gt(50);
// Refined flood: only areas that are NOT permanent water
var floodRefined = floodMask.updateMask(permanentWater.not());
Map.addLayer(floodRefined.selfMask(), {palette: ['ff6600']}, 'Flood (excluding permanent water)');
// Add true-color context (Sentinel-2 before the event)
var s2Before = ee.ImageCollection('COPERNICUS/S2_SR')
.filterBounds(studyArea)
.filterDate('2019-02-01', '2019-02-28')
.sort('CLOUDY_PIXEL_PERCENTAGE')
.first()
.divide(10000);
Map.addLayer(s2Before, {bands: ['B4', 'B3', 'B2'], min: 0, max: 0.3}, 'Context (S2 true color)');
Understanding the code
The JRC Global Surface Water dataset tells us which areas are typically covered by water (occurrence > 50%). By masking these out, we isolate the new flooding that was caused by the cyclone. The Sentinel-2 true-color layer provides geographic context, showing the landscape before the disaster.
Troubleshooting
- No images found in my date range
- Sentinel-1 has a 6 to 12 day revisit time. Expand your date range by a few days.
Also check that you are filtering to
'DESCENDING'; try'ASCENDING'if you get zero results. - The flood mask covers the entire image
- Your threshold is too low. Try increasing it from 3 to 5 dB. Also confirm that your "before" and "during" images are from different dates.
- I see "stripes" in the SAR image
- This is caused by overlapping orbit swaths with slightly different acquisition times. Use
.mosaic()or.median()to combine them. - The flooded area value seems way too large
- Check that you are using
ee.Image.pixelArea()(which returns area in m²) and dividing by 1e6 for km². Also ensure you setscale: 10in the reducer to match Sentinel-1 resolution.
Key Takeaways
- SAR can map floods through clouds, which is critical for disaster response.
- The standard workflow is: baseline (temporal median), event (single image), difference, threshold.
- VV polarization is most effective for detecting open water.
- A 3 dB backscatter drop is a reasonable initial flood detection threshold.
- Removing permanent water bodies with JRC GSW isolates event-specific flooding.
- SAR and optical data are complementary: use SAR for flood detection, optical for context.
Submission
Submit the following:
- A shareable GEE script link with your completed flood mapping code.
- A screenshot of your final flood map (with the refined flood mask and true-color context).
- The calculated flooded area in km² (from the console output).
- Answer these questions:
- Why is SAR more suitable than optical imagery for mapping floods during a cyclone?
- How did changing the threshold from 3 dB to 5 dB affect the estimated flood extent? Which threshold do you think is more appropriate, and why?