Step-by-step flood mapping using Sentinel-1 and Google Earth Engine with population and land impact analysis.
This tutorial demonstrates how to map flood extent using Sentinel-1 SAR imagery and a change detection approach within Google Earth Engine (GEE). By comparing radar backscatter before and after a flood event, we can generate accurate flood extent maps—even under cloud cover or nighttime conditions.
1.🌍 Area of Interest (AOI)
For demonstration, the script uses a predefined polygon in Beira, which experienced a severe coastal flood event in March 2019.
This area is defined using the ee.Geometry.Polygon
object.
// Sample polygon over Beira (March 2019 cyclone flood)
var geometry = ee.Geometry.Polygon([
[[35.53377589953368, -19.6674648789114],
[34.50106105578368, -18.952058786515526],
[33.63314113390868, -19.87423907259203],
[34.74825343859618, -20.61123742951084]]
]);
This image displays a screenshot of the flood mapping project developed in the Google Earth Engine (GEE) Code Editor. It illustrates the user interface, script panel, and map view used to process and visualize SAR data for flood analysis.
2.📅 Time Periods for Analysis
The script defines two distinct time periods to compare radar signals before and after the flood:
// Define timeframes
var before_start = '2019-03-01';
var before_end = '2019-03-10';
var after_start = '2019-03-10';
var after_end = '2019-03-23';
This allows for a clean capture of flood impact by minimizing the influence of seasonal or unrelated land cover changes.
3. ⚙️ SAR Data Parameters
Key SAR parameters can be adjusted to optimize results depending on your region:
- Polarization:
VH
(recommended for flood) orVV
- Pass Direction:
DESCENDING
(preferred consistency) - Difference Threshold:
1.25
(adjustable for sensitivity)
var polarization = "VH";
var pass_direction = "DESCENDING";
var difference_threshold = 1.25;
Step 2: Define the Area of Interest (AOI)
In this step, a polygon geometry is defined using ee.Geometry.Polygon
. The area chosen in the example corresponds to Beira, Mozambique, a region affected by severe flooding in March 2019. You can use this demo or draw your own region of interest using Earth Engine’s polygon drawing tool.
// Demo geometry for Beira flood
var geometry = ee.Geometry.Polygon([
[[35.53377589953368, -19.6674648789114],
[34.50106105578368, -18.952058786515526],
[33.63314113390868, -19.87423907259203],
[34.74825343859618, -20.61123742951084]]
]);
📌 Tip: When testing other areas, make sure to draw your polygon in the Earth Engine Code Editor and assign it to geometry
. Don’t forget to uncheck the geometry box in the Imports pane so it doesn’t obscure the map.
Step 3: Define Flood Period Dates
Next, you define the date ranges for before and after the flood. Sentinel-1 data has a typical revisit time of 6 days, so the date ranges must accommodate that.
// Time frame before the flood
var before_start = '2019-03-01';
var before_end = '2019-03-10';
// Time frame after the flood
var after_start = '2019-03-10';
var after_end = '2019-03-23';
⏳ Why this matters: The entire flood mapping process depends on capturing changes in radar backscatter between these two periods.
Step 4: Configure SAR Parameters
These parameters filter the Sentinel-1 dataset to include only images relevant to your study. The default settings use the VH polarization and descending orbit direction, which are commonly suitable for flood detection due to higher contrast between water and land.
var polarization = "VH"; // Preferred for flood detection
var pass_direction = "DESCENDING"; // Consistent orbit direction is important
var difference_threshold = 1.25; // Sensitivity of flood detection
🎛️ Adjustments: You can change the polarization to VV or adjust the threshold if your detection yields too many false positives or negatives.
Step 5: Load and Filter Sentinel-1 SAR Imagery
The script now accesses the Sentinel-1 GRD dataset and filters it based on the SAR parameters you specified earlier (instrument mode, polarization, orbit direction, resolution, and AOI). The result is two collections: one before the flood and one after.
// Convert geometry to FeatureCollection
var aoi = ee.FeatureCollection(geometry);
// Load Sentinel-1 GRD data
var collection = ee.ImageCollection('COPERNICUS/S1_GRD')
.filter(ee.Filter.eq('instrumentMode','IW'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', polarization))
.filter(ee.Filter.eq('orbitProperties_pass', pass_direction))
.filter(ee.Filter.eq('resolution_meters', 10))
.filterBounds(aoi)
.select(polarization);
// Filter by dates
var before_collection = collection.filterDate(before_start, before_end);
var after_collection = collection.filterDate(after_start, after_end);
You can use the following function to print the range of acquisition dates from each collection:
// Extract min and max date of image collection
function dates(imgcol) {
var range = imgcol.reduceColumns(ee.Reducer.minMax(), ['system:time_start']);
return ee.String('from ')
.cat(ee.Date(range.get('min')).format('YYYY-MM-dd'))
.cat(' to ')
.cat(ee.Date(range.get('max')).format('YYYY-MM-dd'));
}
This helps ensure that valid Sentinel-1 acquisitions are being used for each flood phase.
Step 6: Create Mosaics and Apply Speckle Filtering
SAR data often contains noise known as "speckle." To reduce this and simplify the analysis, the script mosaics the available images and applies a smoothing filter using a 50-meter radius.
// Create mosaics and clip to AOI
var before = before_collection.mosaic().clip(aoi);
var after = after_collection.mosaic().clip(aoi);
// Apply speckle filtering
var smoothing_radius = 50;
var before_filtered = before.focal_mean(smoothing_radius, 'circle', 'meters');
var after_filtered = after.focal_mean(smoothing_radius, 'circle', 'meters');

Step 7: Detect Flood Extent Using Change Detection
The core of the flood detection algorithm compares backscatter differences between the two dates. A simple ratio is applied between the "after" and "before" images. The result is then thresholded to generate a binary flood mask.
// Compute change ratio and threshold
var difference = after_filtered.divide(before_filtered);
var threshold = difference_threshold;
var difference_binary = difference.gt(threshold);
⚠️ Important: Areas with values above the threshold are marked as flooded. Adjust this threshold to fine-tune the result for your region.
Step 8: Refine Flood Map Using Ancillary Data
The initial flood mask can include noise or false positives. To improve accuracy, we use three key filters:
- Permanent Water Removal: We mask out areas classified as perennial water bodies using the JRC Global Surface Water dataset (where water exists for more than 10 months per year).
- Pixel Connectivity: We remove isolated pixels by filtering out flood areas with fewer than 8 connected neighbors.
- Slope Filtering: We exclude areas with slopes steeper than 5% using the WWF HydroSHEDS DEM, since floods are less likely in steep terrain.
// Remove permanent water pixels
var swater = ee.Image('JRC/GSW1_0/GlobalSurfaceWater').select('seasonality');
var swater_mask = swater.gte(10).updateMask(swater.gte(10));
var flooded_mask = difference_binary.where(swater_mask, 0);
var flooded = flooded_mask.updateMask(flooded_mask);
// Remove isolated pixels
var connections = flooded.connectedPixelCount();
flooded = flooded.updateMask(connections.gte(8));
// Mask steep slope areas
var DEM = ee.Image('WWF/HydroSHEDS/03VFDEM');
var slope = ee.Algorithms.Terrain(DEM).select('slope');
flooded = flooded.updateMask(slope.lt(5));
Step 9: Calculate Flooded Area (in Hectares)
The script uses the pixel area and a mask to calculate total flooded area within the region of interest. Results are converted from square meters to hectares for reporting.
// Calculate flooded area in hectares
var flood_pixelarea = flooded.select(polarization).multiply(ee.Image.pixelArea());
var flood_stats = flood_pixelarea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 10,
bestEffort: true
});
var flood_area_ha = flood_stats.getNumber(polarization).divide(10000).round();
This gives an accurate estimate of the inundated surface in your study area.
🔗 Tip: For advanced post-processing or validation, export the flood raster or vector layers to QGIS or ArcGIS.
Step 10: Estimate Human and Agricultural Exposure
The script goes further by analyzing flood impact on population and land use. The JRC GHSL dataset is used for population exposure, while the MODIS Land Cover is used to quantify affected cropland and urban areas.
// Load population density map and calculate exposed people
var population_count = ee.Image('JRC/GHSL/P2016/POP_GPW_GLOBE_V1/2015').clip(aoi);
var flooded_res1 = flooded.reproject({ crs: population_count.projection() });
var population_exposed = population_count.updateMask(flooded_res1).updateMask(population_count);
var stats = population_exposed.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 250,
maxPixels: 1e9
});
var number_pp_exposed = stats.getNumber('population_count').round();
Ready for the next step? In the upcoming section, we will assess impacts on agriculture and urban infrastructure, and visualize everything with Earth Engine UI widgets.
Step 11: Analyze Affected Cropland Using MODIS
The MODIS Land Cover dataset (MCD12Q1) is used to identify croplands affected by flooding. The script filters out cropland pixels based on MODIS class codes and intersects them with the flood mask.
// Load MODIS land cover and extract cropland classes
var LC = ee.ImageCollection('MODIS/006/MCD12Q1')
.filterDate('2014-01-01', after_end)
.sort('system:index', false)
.select("LC_Type1")
.first()
.clip(aoi);
var cropmask = LC.eq(12).or(LC.eq(14));
var cropland = LC.updateMask(cropmask);
// Match scale of flood layer to MODIS land cover
var flooded_res = flooded.reproject({ crs: LC.projection() });
var cropland_affected = flooded_res.updateMask(cropland);
// Calculate affected cropland area in hectares
var crop_pixelarea = cropland_affected.multiply(ee.Image.pixelArea());
var crop_stats = crop_pixelarea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 500,
maxPixels: 1e9
});
var crop_area_ha = crop_stats.getNumber(polarization).divide(10000).round();
Step 12: Assess Impact on Urban Areas
Urban areas are also extracted from the MODIS land cover product using class code 13. The flood mask is intersected with this layer to calculate affected urban infrastructure.
// Extract and calculate affected urban areas
var urbanmask = LC.eq(13);
var urban = LC.updateMask(urbanmask);
var urban_affected = urban.mask(flooded_res).updateMask(urban);
var urban_pixelarea = urban_affected.multiply(ee.Image.pixelArea());
var urban_stats = urban_pixelarea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 500,
bestEffort: true
});
var urban_area_ha = urban_stats.getNumber('LC_Type1').divide(10000).round();
Step 13: Display Results Using Google Earth Engine UI
Earth Engine’s ui.Panel
elements are used to display flood extent, exposed population, affected cropland, and urban areas interactively on the map. These panels provide instant summaries for decision-making.
// Create results panel
var results = ui.Panel({ style: { position: 'bottom-left', padding: '8px 15px', width: '350px' } });
var title = ui.Label('Flood Impact Results', { fontSize: '18px', color: '#3333ff', margin: '0 0 15px 0' });
var text1 = ui.Label('Flood status between:', { fontWeight: 'bold' });
var number1 = ui.Label(after_start + ' and ' + after_end, { color: 'bf0f19', fontWeight: 'bold', margin: '0 0 15px 0' });
var text2 = ui.Label('Estimated flood extent:', { fontWeight: 'bold' });
var number2 = ui.Label('Loading...', { color: 'bf0f19', fontWeight: 'bold' });
flood_area_ha.evaluate(function(val) { number2.setValue(val + ' hectares'); });
var text3 = ui.Label('Exposed population:', { fontWeight: 'bold' });
var number3 = ui.Label('Loading...', { color: 'bf0f19', fontWeight: 'bold' });
number_pp_exposed.evaluate(function(val) { number3.setValue(val); });
var text4 = ui.Label('Affected cropland:', { fontWeight: 'bold' });
var number4 = ui.Label('Loading...', { color: 'bf0f19', fontWeight: 'bold' });
crop_area_ha.evaluate(function(val) { number4.setValue(val + ' hectares'); });
var text5 = ui.Label('Affected urban areas:', { fontWeight: 'bold' });
var number5 = ui.Label('Loading...', { color: 'bf0f19', fontWeight: 'bold' });
urban_area_ha.evaluate(function(val) { number5.setValue(val + ' hectares'); });
results.add(ui.Panel([title, text1, number1, text2, number2, text3, number3, text4, number4, text5, number5]));
Map.add(results);
📌 Insight: These result panels are essential for quick visual interpretation by disaster managers or environmental analysts.
➡️ In the final part, we will add a map legend, export the data for offline analysis, and include a multilanguage SEO footer for global discoverability.
4. Detecting Flood Extent
To identify flood-affected areas, we compute the ratio between the after and before SAR images using the following line of code:
var difference = after_filtered.divide(before_filtered);
This division highlights significant changes in backscatter values, which are indicative of water accumulation. A threshold is applied to classify pixels as flooded:
var threshold = difference_threshold;
var difference_binary = difference.gt(threshold);
Note: A threshold like 1.25
is often chosen empirically, and adjusting it may reduce false positives or negatives.
Refining the Flood Mask
The script uses auxiliary datasets to improve classification accuracy:
- Excludes permanent water bodies using JRC’s Global Surface Water dataset:
var swater = ee.Image('JRC/GSW1_0/GlobalSurfaceWater').select('seasonality');
var swater_mask = swater.gte(10).updateMask(swater.gte(10));
var flooded_mask = difference_binary.where(swater_mask, 0);
- Eliminates isolated noise using
connectedPixelCount()
. - Masks out steep slopes (> 5%) using a HydroSHEDS DEM:
var DEM = ee.Image('WWF/HydroSHEDS/03VFDEM');
var slope = ee.Algorithms.Terrain(DEM).select('slope');
var flooded = flooded.updateMask(slope.lt(5));
5. Calculating Flooded Area
The script calculates the total flood extent in hectares using pixel area multiplication:
var flood_pixelarea = flooded.select(polarization).multiply(ee.Image.pixelArea());
var flood_stats = flood_pixelarea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 10,
bestEffort: true
});
var flood_area_ha = flood_stats.getNumber(polarization).divide(10000).round();
📌 Tip: For highly accurate statistics, remove bestEffort
and set maxPixels
higher.
🔗 View Full Earth Engine Script: SAR-Based Flood Mapping (Sentinel-1)
Also see: How to Compute EVI in Google Earth Engine
🌐 Also searched as: Flood mapping with Sentinel-1 in GEE — Cartographie des inondations Sentinel-1 GEE — Hochwassererkennung Sentinel-1 GEE — Detección de inundaciones Sentinel-1 GEE — Sentinel-1 洪水マッピング GEE — رسم خريطة الفيضانات Sentinel-1 في GEE