Universal Code Hub Upgrade
Script Library
ResearchDisasterAdvanced

Sentinel-1 Rapid Flood Mapping (Otsu + JRC permanent-water mask)

Pre/post SAR change with automatic threshold; excludes permanent water; reports exposed population.

Cite: DeVries et al. 2020; Otsu 1979; Pekel et al. 2016
Pipeline (7 steps)
  1. Pre-event S1 IW VV composite (mean of last 30 days before event)
  2. Post-event S1 IW VV composite (≤7 days after event)
  3. Refined-Lee speckle filter
  4. Change = post - pre (dB)
  5. Otsu auto-threshold on change histogram
  6. Mask JRC permanent water (occurrence > 50%)
  7. Cross with WorldPop → exposed-population statistic
Datasets
  • sentinel1
  • jrc-gsw
  • worldpop
AOI modes
adminassetdraw
Simulated previewPaste the script in Earth Engine for live results
Classification legend
Permanent water
Flood (new water)
Land
Outputs the script produces
map Flood extent
map Change image (dB)
table Exposed population
chart Histogram + Otsu line
export COG export
Open Code Editor ↗
adv-sar-flood-otsu.js · 75 lines
// ============================================================
// Sentinel-1 Rapid Flood Mapping (Otsu + JRC permanent water)
// Output: flood-only mask + exposed-population summary
// ============================================================
var eventDate = ee.Date('2024-08-25');                     // adjust
var preStart  = eventDate.advance(-30, 'day');
var postEnd   = eventDate.advance(7, 'day');

function refinedLee(img) {
  var bandNames = img.bandNames();
  img = ee.Image(10).pow(img.divide(10.0));
  var k = ee.Kernel.square({radius: 3, units: 'pixels'});
  var mean = img.reduceNeighborhood(ee.Reducer.mean(), k);
  var v    = img.reduceNeighborhood(ee.Reducer.variance(), k);
  var b = v.divide(mean.multiply(mean));
  var w = b.divide(b.add(1));
  var f = mean.add(img.subtract(mean).multiply(w));
  return ee.Image(10).multiply(f.log10()).rename(bandNames);
}

var s1 = ee.ImageCollection('COPERNICUS/S1_GRD')
  .filterBounds(aoi)
  .filter(ee.Filter.eq('instrumentMode', 'IW'))
  .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV'))
  .select('VV');

var pre  = s1.filterDate(preStart, eventDate).map(refinedLee).median().rename('pre');
var post = s1.filterDate(eventDate, postEnd).map(refinedLee).median().rename('post');
var diff = post.subtract(pre).rename('diff');

function otsu(histogram) {
  var counts = ee.Array(ee.Dictionary(histogram).get('histogram'));
  var means  = ee.Array(ee.Dictionary(histogram).get('bucketMeans'));
  var size = means.length().get([0]);
  var total = counts.reduce(ee.Reducer.sum(), [0]).get([0]);
  var sum   = means.multiply(counts).reduce(ee.Reducer.sum(), [0]).get([0]);
  var mean  = sum.divide(total);
  var idx = ee.List.sequence(1, size);
  var bss = idx.map(function(i){
    var aC = counts.slice(0,0,i); var aN = aC.reduce(ee.Reducer.sum(),[0]).get([0]);
    var aM = means.slice(0,0,i).multiply(aC).reduce(ee.Reducer.sum(),[0]).get([0]).divide(aN);
    var bN = total.subtract(aN);
    var bM = sum.subtract(aN.multiply(aM)).divide(bN);
    return aN.multiply(aM.subtract(mean).pow(2))
      .add(bN.multiply(bM.subtract(mean).pow(2)));
  });
  return means.sort(bss).get([-1]);
}
var hist = diff.reduceRegion({reducer: ee.Reducer.histogram(255, 0.2),
  geometry: aoi, scale: 30, bestEffort: true, tileScale: 4});
var t = ee.Number(otsu(hist.get('diff')));
print('Otsu threshold (dB drop):', t);

// flood = strong decrease in VV (water = low backscatter)
var floodRaw = diff.lt(t);

// remove JRC permanent water (occurrence > 50%)
var perm = ee.Image('JRC/GSW1_4/GlobalSurfaceWater').select('occurrence').gt(50).unmask(0);
var flood = floodRaw.updateMask(floodRaw).updateMask(perm.not()).rename('flood');

// exposed population
var pop = ee.ImageCollection('WorldPop/GP/100m/pop').filter(ee.Filter.eq('year', 2020))
  .filterBounds(aoi).mosaic().rename('pop');
var exposed = pop.updateMask(flood).reduceRegion({
  reducer: ee.Reducer.sum(), geometry: aoi, scale: 100, bestEffort: true, tileScale: 4, maxPixels: 1e13
});
print('Exposed population:', exposed);

Map.centerObject(aoi, 9);
Map.addLayer(perm, {palette:['#0c4a6e']}, 'Permanent water', false);
Map.addLayer(flood, {palette:['#22d3ee']}, 'Flood mask');

Export.image.toDrive({image: flood.clip(aoi), description: 'flood_mask',
  folder: 'gee_exports', region: aoi, scale: 30, maxPixels: 1e13,
  fileFormat: 'GeoTIFF', formatOptions: {cloudOptimized: true}});
#SAR#Sentinel-1#Otsu#flood#rapid mapping#population exposure