1

I wrote this script in GEE to calculate NDVI as well as the NDVI mean and standard deviation.

/**
 * Function to mask clouds using the Sentinel-2 QA band
 * @param {ee.Image} image Sentinel-2 image
 * @return {ee.Image} cloud masked Sentinel-2 image
 */
function maskS2clouds(image) {
  var qa = image.select('QA60');

  // Bits 10 and 11 are clouds and cirrus, respectively.
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;

  // Both flags should be set to zero, indicating clear conditions.
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
      .and(qa.bitwiseAnd(cirrusBitMask).eq(0));

  return image.updateMask(mask).divide(10000);
}

// This is the Sentinel 2 collection
var S2_collection = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
  .filterBounds(geometry)
  .filterDate('2017-01-01', '2019-12-31') 
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',1))
  .map(maskS2clouds)
  .median();
                  
var composite = S2_collection.clip(geometry);
var composite = composite.toFloat()
// Add map layers
Map.addLayer(composite , {bands: ['B11', 'B8', 'B4']}, "composite");
                  

//Compute  NDVI 
var nir = S2_collection.select('B8');
var red = S2_collection.select('B4');
var ndvi = nir.subtract(red).divide(nir.add(red));
var ndvi = ndvi.clip(geometry);
// Add map layers
Map.addLayer(ndvi, {min: 0, max: 1, palette: ['black', 'yellow', 'green']}, 'continuous NDVI');

// Compute the mean and stdev of NDVI
var mean_ndvi = ndvi.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: geometry,
  scale: 10
});
var sd_ndvi = ndvi.reduceRegion({
  reducer: ee.Reducer.stdDev(),
  geometry: geometry,
  scale: 10
});
print(mean_ndvi);
print(sd_ndvi);

I need to calculate the Gaussian variable of this index such as: ([NDVI - NDVI(mean)] / [NDVI_sd])

By using the reducer for mean and std dev, I create a dictionary , but if I want to visualize the mean and sd map results, I get the error:

Cannot add an object of type to the map

How can I calculate mean and sd for each pixel of my image and calculate this index and map it?

Link to my script

Update to my question:

  1. I need the NDVI_gaussian value for a single image of 9th February 2019 over this geometry area:

    0: [16.53793865386438,40.509788101097925]

    1: [16.552594243921753,40.509788101097925]

    2: [16.552594243921753,40.51740638148722]

    3: [16.53793865386438,40.51740638148722]

    4: [16.53793865386438,40.509788101097925]

and it should look like this:

enter image description here

And 2) I need the NDVI guassian formula in the February–November 2019 period, which should look like this:

enter image description here

And the index is defined as:

enter image description here

4
  • 1
    mean_ndvi and sd_ndvi, that's the mean/standard deviation of all pixels within your geometry, based on your slightly confusingly named median composite S2_collection. That's what you intended to do? Those are two numbers, so you cannot put them on a map (or at least, they're not very interesting to see on a map). Or did you want to calculate mean and standard deviation separately for every pixel in your geometry, based on your two year date range? Commented Apr 18, 2023 at 16:18
  • oh you are right. No I do not want to calculate the mean/sd of all pixels within geometry. I am looking for the second option. I intended to calculate mean/sd for each pixel in that period, map them for each pixel, and then calculate the formula I wrote above as (NDVI - NDVI_mean) / (NDVI_sd) for each pixel and finally map this index over all region.
    – Paris
    Commented Apr 18, 2023 at 16:22
  • Do you want to apply this formula to every image in a collection of NDVI values, or to the median NDVI? Commented Apr 18, 2023 at 16:24
  • I need both actually. The paper that I read it from says the index between 2017 and 2019, So I do not know if it means for each image separately or teh median of all
    – Paris
    Commented Apr 18, 2023 at 16:26

2 Answers 2

1

I did a couple of changes:

  • Increased the CLOUDY_PIXEL_PERCENTAGE limit to 10, to make sure you get imagery for your date of interest.
  • Switched out the cloud masking to use the COPERNICUS/S2_CLOUD_PROBABILITY collection. It's slower but better.
  • Limited the date range used to calculate the statistics, not to include the landslide. When the landslide is part of the statistics, the pixel values afterwards wouldn't be an anomaly.

The below doesn't replicate the results exactly, but at least highlights the landslide.

var geometry = ee.Geometry.Polygon([
  [16.53793865386438,40.509788101097925],
  [16.552594243921753,40.509788101097925],
  [16.552594243921753,40.51740638148722],
  [16.53793865386438,40.51740638148722],
  [16.53793865386438,40.509788101097925]
])
var anomalyThreshold = -2
var cloudThreshold = 30

var filter = ee.Filter.and(
  ee.Filter.bounds(geometry),
  ee.Filter.date('2017-01-01', '2020-01-01')
)

// Uses slower but better cloud masking
var ndviCollection = ee.ImageCollection(
    ee.Join.saveFirst('cloudProbability').apply({
        primary: ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
          .filter(filter)
          .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10)), // Increased the limit, to include imagery for your date
        secondary: ee.ImageCollection('COPERNICUS/S2_CLOUD_PROBABILITY')
          .filter(filter),
        condition: ee.Filter.equals({leftField: 'system:index', rightField: 'system:index'})
    })
).map(toNdvi)

var stats = ndviCollection 
  .filterDate('2017-01-01', '2019-02-01') // Maybe limit this date range to some stable reference period?
  .reduce(
    ee.Reducer.mean()
        .combine(ee.Reducer.stdDev(), null, true)
  )
var gaussianCollection = ndviCollection.map(toGaussian)
var anomalyCount = gaussianCollection
  .filterDate('2019-02-01', '2019-12-01')
  .map(function (image) {
    return image.updateMask(image.lte(anomalyThreshold))
  })
  .reduce(ee.Reducer.count())
  .unmask(0)

var image = gaussianCollection
  .filterDate(ee.Date('2019-02-09').getRange('day'))
  .mosaic()

Map.addLayer(image, {min: -5, max: 5, palette: '#67001f, #b2182b, #d6604d, #f4a582, #fddbc7, #f7f7f7, #d1e5f0, #92c5de, #4393c3, #2166ac, #053061'}, 'image', true)
Map.addLayer(anomalyCount, {min: 0, max: 52, palette: '#042333, #2c3395, #744992, #b15f82, #eb7958, #fbb43d, #e8fa5b'}, 'anomaly count')
Map.centerObject(geometry)


function toGaussian(ndvi) {
  return ee.Image()
    .expression('(ndvi - ndvi_mean) / ndvi_stdDev', {
      ndvi: ndvi,
      ndvi_mean: stats.select('ndvi_mean'),
      ndvi_stdDev: stats.select('ndvi_stdDev')
    })
    .copyProperties(ndvi, ndvi.propertyNames())
}

function toNdvi(image) {
  var cloudFree = ee.Image(image.get('cloudProbability')).lt(cloudThreshold)
  return image.updateMask(cloudFree)
    .normalizedDifference(['B8', 'B4'])
    .float()
    .rename('ndvi')
    .copyProperties(image, image.propertyNames())
}

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)
}

https://code.earthengine.google.com/d7cc2331bbfa8e2ea81db30a47aaf198

12
  • Thanks very much @Daniel. I explain better my objective. I read in a paper that for studying the land cover changes after a landslide, it is helpful to map NDVI variations before and after the event since vegetation changes due to landslide. They also suggested that after mapping NDVI, if we calculate NDVI_mean & NDVI_std dev for each pixel and then calculate the gaussian index as the formula I wrote, we can observe the changes much better. So I managed to map NDVI over the map, but I wrote a dictionary instead of an image and calculated those values as a number for the whole image.
    – Paris
    Commented Apr 19, 2023 at 7:16
  • Thanks for your script. 'var Stats' reduces the imageCollection to mean and stdDev for each pixel, and then you used 'toGaussian' to apply the formula, is it correct? The vaules of 'gausian of median' and 'median of gausian' are the same, so did you apply the formula to the median NDVI? If I want to apply the formula to only one image of the collection, how should I change it?
    – Paris
    Commented Apr 19, 2023 at 7:32
  • 1
    stats and toGaussian() - you're correct. The two layers, gausian of median calculates the median ndvi and calculates the gausian of that, while median of gaussian calculates the gausian for every image in the collection and takes the median of that. Same result, yes, I just wanted to highlight these different approaches. Your last question, which image do you want to apply the formula to? Commented Apr 19, 2023 at 7:44
  • 1
    I updated my answer Commented Apr 19, 2023 at 9:23
  • 1
    First layer - correct. Second layer shows the number of anomalous observations, i.e. which have a value <= -2. That threshold comes from a screenshot in your question :-). The statistics are calculated for dates between 2017-01-01 and 2019-02-01 to exclude the landslide. Commented Apr 19, 2023 at 13:17
1

This can be done with the following expression:

(ndvi.subtract(mean_ndvi)).divide(sd_ndvi)

But, since both outputs are dictionaries, convert it to images:

(ndvi.subtract(mean_ndvi.toImage())).divide(sd_ndvi.toImage())

Appliying to each date:

var geometry = 
    /* color: #d63000 */
    /* shown: false */
    /* displayProperties: [
      {
        "type": "rectangle"
      }
    ] */
    ee.Geometry.Polygon(
        [[[16.529551494306034, 40.525518691809275],
          [16.529551494306034, 40.505160304711104],
          [16.566973674481815, 40.505160304711104],
          [16.566973674481815, 40.525518691809275]]], null, false);

/**
 * Function to mask clouds using the Sentinel-2 QA band
 * @param {ee.Image} image Sentinel-2 image
 * @return {ee.Image} cloud masked Sentinel-2 image
 */
function maskS2clouds(image) {
  var qa = image.select('QA60');

  // Bits 10 and 11 are clouds and cirrus, respectively.
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;

  // Both flags should be set to zero, indicating clear conditions.
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
      .and(qa.bitwiseAnd(cirrusBitMask).eq(0));

  return image.updateMask(mask).divide(10000);
}

var S2_collection = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
  .filterBounds(geometry)
  .filterDate('2017-01-01', '2019-12-31') 
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',1))
  .map(maskS2clouds);

var ndvi = S2_collection.map(function(img){return img.normalizedDifference(['B8', 'B4'])});
var ndvi = ndvi.map(function(img){return img.clip(geometry)});

var mean_ndvi = ndvi.reduce(ee.Reducer.mean());
var sd_ndvi = ndvi.reduce(ee.Reducer.stdDev());
var zscore = ndvi.map(function(img){return (img.subtract(mean_ndvi)).divide(sd_ndvi)});

Map.addLayer(mean_ndvi, {}, 'mean NDVI');
Map.addLayer(sd_ndvi, {}, 'sd NDVI');
Map.addLayer(zscore, {}, 'z-score');
Map.centerObject(geometry);
6
  • Thanks for your reply. I modified my script : code.earthengine.google.co.in/eab3ffe805e27105fa87243d2d0eb888, is there a way to calculate the mean and sd without dictionaries and adding each as images and then used the expression you mentioned? Thanks
    – Paris
    Commented Apr 18, 2023 at 16:11
  • 1
    You can display both outputs with Map.addLayer(mean_ndvi.toImage().clip(geometry), {}, 'mean'); and Map.addLayer(sd_ndvi.toImage().clip(geometry), {}, 'sd');. Both will be added as black images since there is no color palette associated. There are other approaches but for computing temporal mean and sd for each pixel, which is not the case of your code, although I don't know your objective here
    – aldo_tapia
    Commented Apr 18, 2023 at 16:15
  • My final objective is to map the guassian index I mentioned and classify it based on its values.
    – Paris
    Commented Apr 18, 2023 at 16:17
  • 1
    If you are going to use several layers and a ML model I get it, but I see no difference using this index or using the NDVI values for single layer classification. For ML models, z-score is the most used standardization method and it's the same index you are using here, but it's normally used for NNs or when you use many input layers. for RF or other models, isn't needed
    – aldo_tapia
    Commented Apr 18, 2023 at 16:27
  • 1
    Daniel is right, that's my point as well. For a single image it won't work. I edited my answer. Your issue is using .median(), that convert the imageCollection to a image removing any date property, hence, the ability to compute mean or sd for every pixel. Now is with the function you posted
    – aldo_tapia
    Commented Apr 19, 2023 at 12:51

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.