I would like to derive the occurrence of surface water bodies from Sentinel-1 image collection in the Google Earth Engine.In my example I could already compute the water mask using Otsu’s method for image segmentation: https://code.earthengine.google.com/499580510635aa5f421886d4e409af03.

This procedure has now to be applied to the entire image collection, so that the variable threshold is recalculated for each image and then the water mask is determined.The occurrence of water bodies should be presented relatively (0%-no occurrence, 100%-always water present) as shown in Global Surface Water Explorer.

Does anyone have a solution or a hint for this?

Create the S1 image collection:

var s1 = ee.ImageCollection('COPERNICUS/S1_GRD')
  .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV'))
  .map(function(image){return image.clip(Map.getBounds(true))});

Otsu's method for image segmentation:

var histogram = image.select(s1_band).reduceRegion({
  reducer: ee.Reducer.histogram()
      .combine('mean', null, true)
      .combine('variance', null, true), 
  geometry: polygon, 
  scale: 10,
  bestEffort: true

// Chart the histogram
print(Chart.image.histogram(image.select(s1_band), polygon, 10));

// Return the DN that maximizes interclass variance in S1-band (in the region).
var otsu = function(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 indices = ee.List.sequence(1, size);

  // Compute between sum of squares, where each mean partitions the data.
  var bss = indices.map(function(i) {
    var aCounts = counts.slice(0, 0, i);
    var aCount = aCounts.reduce(ee.Reducer.sum(), [0]).get([0]);
    var aMeans = means.slice(0, 0, i);
    var aMean = aMeans.multiply(aCounts)
        .reduce(ee.Reducer.sum(), [0]).get([0])
    var bCount = total.subtract(aCount);
    var bMean = sum.subtract(aCount.multiply(aMean)).divide(bCount);
    return aCount.multiply(aMean.subtract(mean).pow(2)).add(

  print(ui.Chart.array.values(ee.Array(bss), 0, means));

  // Return the mean value corresponding to the maximum BSS.
  return means.sort(bss).get([-1]);

var threshold = otsu(histogram.get(s1_band+'_histogram'));
print('threshold', threshold);

Note on a tripping hazard for calculating the occurence:

The .filterBounds method based of an area of interest also selects truncated satellite images. Unfortunately I have no solution to prevent this.

Example of a truncated image

2 Answers 2


After some research and help, I finally have the solution to the described problem, which I also want to share with the community.


Concerning the truncated satellite pictures this hint helped me. https://groups.google.com/forum/#!msg/google-earth-engine-developers/g2ZByrxm8NE/exd7WWesBQAJ

With the following line of code I could filter the undesired images:

.filter(ee.Filter.contains({leftField: ".geo", rightValue: aoi})) // Filter partial S1-Images of AOI
  • Nice work Stephan
    – HDunn
    Feb 3, 2020 at 8:42

I think I found an error in the area chart:

*// Create and print a histogram chart
//Make time series of water pixels as area in km² within region
var ClassChart = ui.Chart.image.series({
  imageCollection: s1.select('waterMask'),
  region: aoi,
  reducer: ee.Reducer.sum(),
  scale: 100,

I think the units are wrong. The units make sense for me in km2 if you substitute the scale by "10" (which is the spatial resolution of Sentinel-1), and then divide the results by 100.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.