Calculating month of minimum NDVI per pixel using Google Earth Engine

google-earth-engine

I have been trying to generate an ee.Image object where each pixel value contains the number of the month where average NDVI is at a minimum. So the output image should have pixel values from 1-12, where 1 indicates January has the lowest mean NDVI values, 2 indicates February, etc.

I first calculated the maximum values for each month across the time series. Then I took the mean across all Januarys, all Februarys etc. to produce the mean monthly NDVI cycle as follows:

// Find MODIS NDVI images for relevant date range
var start_date = ee.Date("2010-01-01");
var end_date = ee.Date("2020-01-01");

var modis_filtered = ee.ImageCollection("MODIS/006/MOD13Q1")
                         .filterDate(start_date, end_date)
                         .select("NDVI");

// Calculate monthly max NDVI values
var monthly_images = function(IC) {
    
  // Get number of months in interval
  var n_months = ee.List.sequence(0, end_date.difference(start_date, "month").round().subtract(1));
    
  // Iterate monthly max over all months in the interval
  var images = n_months.map(function(n) {
    
    // Get images for given month
    var start = start_date.advance(n, "month");
    var end = start.advance(1, "month");
    var filtered = IC.filterDate(start, end);

    var max = filtered.mean();

    return max.set("month", start.get("month"), "year", start.get("year"));
    });
    
    return ee.ImageCollection.fromImages(images);
};

var ndvi_monthly_max = monthly_images(modis_filtered);

// Calculate average monthly cycle
var month_list = ee.List.sequence(1, 12);

var cycle = ee.ImageCollection.fromImages(month_list.map(function(x) {
  return ndvi_monthly_max.filter(ee.Filter.eq("month", x))
                      .mean()
                      .set("month", x);
}));

This gives an ImageCollection with one Image for each month. It's then simple to calculate the minimum value for each pixel, but I can't figure out a clean way to calculate which month that minimum occurs in. The best I have come up with is:

  1. Calculate the minimum across all months,
  2. Test whether the pixel values in each month are equal to that minimum,
  3. Replace all matching pixels in each month with the month index, and mask the rest,
  4. Mosaic the resulting 12 images together.
var min_ndvi_month = cycle.map(function(image) {
  // Image containing month number
  var month_image = ee.Image.constant(image.get("month"))
                                           .toInt16();
  // Replace pixel with month number if it is equal to min; otherwise mask
  var is_min = image.eq(min_ndvi);
  var is_min_mask = is_min.updateMask(is_min);
  var is_min_month = is_min_mask.where(1, month_image);
  
  return is_min_month;
});

// Mosaic output to one image
var output = min_ndvi_month.mosaic();

Map.addLayer(output, {min: 1, max: 12}, "month of min NDVI");
print(output);

This seems to work, but seems ugly, convoluted and error prone! Is there a better way?

Best Answer

Add a month band to each image (an integer from 1 to 12) and use the numInputs option to ee.Reducer.min() to get the month band that goes along with the min value. And since you're taking the mean of the means, so you don't need to map over months twice; you can use calendarRange instead of filterDate to just get all the images for month N from all years in the first function.

var month_list = ee.List.sequence(1, 12);
var monthly_images = ee.ImageCollection.fromImages(month_list.map(function(n) {
    var filtered = modis_filtered.filter(ee.Filter.calendarRange(n, n, 'month'))
    var mean = filtered.mean();
    var monthBand = ee.Image.constant(n).int().rename('month')
    return mean.addBands(monthBand)
}));

// Find min of the first band and bring along the matching second band.
var minMonth = monthly_images.reduce(ee.Reducer.min(2)).select(1)
Related Question