2

I want to reduce an ImageCollection in Google Earth Engine and calculate the percentage of unmasked values for each pixel across the ImageCollection. In the example code the question would be, how many times each pixel was masked across the entire dataset.

function maskS2clouds(image) {
  var qa = image.select('QA60');
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
      .and(qa.bitwiseAnd(cirrusBitMask).eq(0));
  return image.updateMask(mask).divide(10000);
}

var dataset = ee.ImageCollection('COPERNICUS/S2')
                  .filterBounds(roi)
                  .filterDate('2018-01-01', '2018-12-31')
                  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
                  .map(maskS2clouds);

Map.setCenter(13.40, 52.43, 8);
Map.addLayer(dataset.median().clip(roi), {
  min: 0.0,
  max: 0.3,
  bands: ['B4', 'B3', 'B2'],
}, 'RGB');

/// Count percentage of non-mask values for each pixel across dataset
/// count is not the doing it
Map.addLayer(dataset.select("B1").count().clip(roi), {palette: ['00FFFF', '0000FF'], opacity: 0.7}, "perc");

code: https://code.earthengine.google.com/?scriptPath=users%2Fslisovski%2Fdefault%3APercMaskUnmaks

2 Answers 2

5
+50

A simple solution would be to extract the mask for each image using ee.Image.mask, then sum over all masks and divide by the number of images:

var percentMasked = dataset
  .map(function(image){return image.mask().clip(roi).eq(0)}) 
  .sum()
  .divide(dataset.size())

The .eq(0) in line 2 sets masked pixels to '1', otherwise '0'.


Somewhat of a caveat to the above solution would be the case that roi is not entirely located within the footprint of an image in dataset. For example, if percentMasked is intended to serve as some indicator for cloud frequency, this could be problematic since pixels that lie outside of the image's footprint are masked by default. Also, if you are using images from multiple tiles (MGRS tiles in Sentinel's case), the number of times a pixel is 'seen' may not necessary be equal to the size of dataset. E.g. if you are combining images from two tiles, pixels that always overlap would be seen two times more frequently than pixels that don't.

The second caveat could be addressed by extracting some mask from each image in dataset that reveals the image footprint. This could be done using ee.Image.mask prior to applying the cloud mask. Note that this assumes images in ee.ImageCollection('COPERNICUS/S2') are by default unmasked. This 'footprint mask' could also be used to address the first caveat by setting pixels outside of the image footprint to be 'unmasked', i.e. if masked pixels have a value of '1', set these pixels to '0'.

The code example bellow adds to the map two images that display the percentage (actually a fraction) of masked pixels using (1) the initial simple solution, and (2) the extended solution.

// I tested the code for my own roi over Berlin, Germany
var roi = ee.Geometry.Polygon(
          [[[13.043317115232748, 52.69051508710512],
          [13.043317115232748, 52.32608773988643],
          [13.779401099607748, 52.32608773988643],
          [13.779401099607748, 52.69051508710512]]])

function getmasks(image) {
  var qa = image.select('QA60');
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
      .and(qa.bitwiseAnd(cirrusBitMask).eq(0))
      .rename('cloudmask');
  var FPmask = image.mask().select([0]).rename('FPmask');
  return image.updateMask(mask).addBands(FPmask);
  // the idea here is that the cloud masked is applied to all bands 
  // but the footprint mask 'FPmask'
}

var dataset = ee.ImageCollection('COPERNICUS/S2')
                  .filterBounds(roi)
                  .filterDate('2018-04-01', '2018-7-31')
                  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
                  .map(getmasks);

// simple solution
var percentMasked1 = dataset
  .map(function(image){
    var mask = image.mask().eq(0); 
    return mask.clip(roi)})
  .sum()
  .divide(dataset.size());

// extended solution
var percentMasked2 = dataset
  .map(function(image){
    var mask = image.mask().add(image.select('FPmask').eq(0)).eq(0);
    return mask.clip(roi)})
  .sum()
  .divide(dataset.select('FPmask').sum());

// RGB composite 
Map.addLayer(dataset.median().clip(roi), {
  min: 0,
  max: 3000,
  bands: ['B4', 'B3', 'B2'],
}, 'RGB');

// note the streaks in the image due to the different image footprints (using my roi); see screenshot 'A' below
Map.addLayer(percentMasked1, {min:0, max:1}, 'percent masked simple')

// note that the streaks almost completely disappear; see screenshot 'B'below
Map.addLayer(percentMasked2, {min:0, max:1}, 'percent masked extend')

1

0
0

Assuming by masked you mean pixel values equalling 0 (technically, these aren't masked; they have a value of 0), you could do this:

var dataset = dataset.map(function(img){
  var totalPixels = ee.Image(1).clip(roi)
                                .reduceRegion({reducer:ee.Reducer.sum(), 
                                               geometry: roi, 
                                                scale: dataset.first().projection().nominalScale()})
  var maskedPixels = img.clip(roi)
                        .reduceRegion({reducer:ee.Reducer.sum(), 
                                       geometry: roi, 
                                       scale: dataset.first().projection().nominalScale()})
  var pctmasked = ee.Number(maskedPixels.get('binary')).divide(totalPixels.get('constant'))
  
  return img.set('pctmasked', pctmasked)
})

This first calculates the total number of pixels in your region of interest, by which the number of pixels greater than your threshold value get divided to get the percentage. This value is added as a property to each image in the collection.

2
  • Thanks for this. I may have to be more specific. I want to have a pixel based calculation: How many values are 1 divided by the number of values that are != 1 across the ImageCollection. The outcome would be an Image with the relative amount of values of 1 across all images in the ImageCollection.
    – SimeonL
    Jan 14, 2022 at 12:51
  • I changed the code and hope that my aims are more clear.
    – SimeonL
    Jan 14, 2022 at 14:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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