javascriptgoogle-earth-enginesatellite-image

Google Earth Engine Landsat NDVI for polygons export table to Drive


I was experimenting with GEE for the first time (no javascript experience) using ChatGPT as my teacher to achieve the following: I have a shapefile with multiple polygon features. I want to use the entire landsat 5 and 7 satellite image archive to create a time series of mean NDVI values (multispectral index using the red and near-infrared bands) for each of my polygons in the shapefile. In the end I want one CSV file for each polygon in the shapefile that has three columns (Name of the landsat scene, date of the scene, mean NDVI value) and export those to my google drive. I keep getting an error "User-defined methods must return a value".

From my understanding, this error comes up when there is a function that doesn't return anything. This is my code:

// Load the shapefile
var shp = ee.FeatureCollection("shapefile");

// Create a map centered on the shapefile
Map.centerObject(shp, 12);

// Add the shapefile to the map as a layer
Map.addLayer(shp, {}, 'My Shapefile');

// Function to calculate NDVI
var addNDVI = function(image) {
  var ndvi = image.normalizedDifference(['B4', 'B3']).rename('ndvi');
  return image.addBands(ndvi);
};


// Find Landsat 5 and Landsat 7 images that cover the shapefile and have cloud cover less than 10%
var l5 = ee.ImageCollection('LANDSAT/LT05/C01/T1_SR')
  .filterBounds(shp)
  .filterDate('1984-01-01', '2012-05-05')
  .filter(ee.Filter.lt('CLOUD_COVER', 10))
  .map(addNDVI);

var l7 = ee.ImageCollection('LANDSAT/LE07/C01/T1_SR')
  .filterBounds(shp)
  .filterDate('1999-01-01', '2021-05-05')
  .filter(ee.Filter.lt('CLOUD_COVER', 10))
  .map(addNDVI);

// Merge the collections
var collection = ee.ImageCollection(l5.merge(l7));

// Function to calculate mean NDVI for each image and each polygon
var calculateMeanNDVI = function(image) {
  var results = ee.FeatureCollection(
    shp.map(function(feature) {
      var mean = ee.Image(image.select('ndvi')).reduceRegion({
        reducer: ee.Reducer.mean(),
        geometry: feature.geometry(),
        scale: 30,
        maxPixels: 1e9
      });
      return ee.Feature(feature.geometry(), {
        'mean_ndvi': mean.get('ndvi'),
        'image_id': image.id(),
        'date': image.date().format('YYYY-MM-dd')
      });
    })
  );
  return results;
};

// Map over the image collection to calculate mean NDVI for each image
var featureList = collection.map(calculateMeanNDVI).toList(collection.size());

// Convert the featureList to a list of dictionaries
var dictList = ee.List(featureList.map(function(feature) {
  feature = ee.Feature(feature);
  return ee.Dictionary({
    'image_id': feature.get('image_id'),
    'date': feature.get('date'),
    'mean_ndvi': feature.get('mean_ndvi')
  });
}));

// Export results to CSV files in Google Drive
var exportList = dictList.map(function(dict) {
  dict = ee.Dictionary(dict);
  var name = ee.String('NDVI_').cat(ee.String(dict.get('image_id')));
  return Export.table.toDrive({ // This is where I get the error: User-defined methods must return a value
    collection: ee.FeatureCollection([ee.Feature(null, dict)]),
    description: name,
    fileFormat: 'CSV',
    selectors: ['image_id', 'date', 'mean_ndvi']
  });
});

// Print the export list to the console
print('Export list:', exportList);```

Solution

  • ChatGPT is terrible when it comes to EE scripts. The error comes from the fact that a server-side map() function must always return something, and Export.table.toDrive() has no return values. As a matter of fact, you cannot export at all while you're doing server-side iteration. Read up on client vs server here.

    Also, you might want to mask out cloudy pixels. Then you could remove the filtering of images by CLOUD_COVER, or at least allow for imagery with a bit higher cloud cover. This, of course, depend on your actual use-case.

    Something like this might be a better starting point than what ChatGPT's script:

    shp.aggregate_array('system:index')
      .evaluate(function (indexes) { // Move from server- to client-side
        indexes.forEach(function (index) { // Client-side iteration
          var polygon = shp.filter(ee.Filter.eq('system:index', index)).geometry()
          exportPolygon(polygon, index)
        })
      })
      
      
    function exportPolygon(polygon, index) {
      var l5 = ee.ImageCollection('LANDSAT/LT05/C02/T1_L2')
        .filterBounds(polygon)
        .filter(ee.Filter.lt('CLOUD_COVER', 10))
        .map(toNDVI)
      
      var l7 = ee.ImageCollection('LANDSAT/LE07/C02/T1_L2')
        .filterBounds(polygon)
        .filter(ee.Filter.lt('CLOUD_COVER', 10))
        .map(toNDVI)
      
      var means = l5.merge(l7)
        .map(function (ndvi) {
          return meanNDVI(ndvi, polygon)
        })
        
      print('NDVI_' + index, means)
      
      Export.table.toDrive({
        collection: means,
        description: 'NDVI_' + index,
        selectors: ['image_id', 'date', 'mean_ndvi']
      })
    }
    
    function meanNDVI(ndvi, polygon) {
      var mean = ndvi.reduceRegion({
        reducer: ee.Reducer.mean(), 
        geometry: polygon, 
        scale: 30, 
        maxPixels: 1e13
      }).values().getNumber(0)
      return ee.Feature(null, {
        image_id: ndvi.get('system:index'), 
        date: ndvi.date().format('yyyy-MM-dd'), 
        mean_ndvi: mean
      })
    }
    
    function toNDVI(image) {
      // See QA_PIXEL bits here
      // https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LE07_C02_T1_L2
      var qa = image.select('QA_PIXEL')
      var clear = bitwiseExtract(qa, 6)
      var water = bitwiseExtract(qa, 7)
      return image.multiply(0.0000275).subtract(0.2) // Rescale
        .normalizedDifference(['SR_B4', 'SR_B3']) // NDVI
        .updateMask(clear.or(water))
        .rename('ndvi')
        .copyProperties(image, image.propertyNames()) // Keep the metadata
    }
    
    function bitwiseExtract(value, fromBit, toBit) {
      if (toBit === undefined)
        toBit = fromBit
      var maskSize = ee.Number(1).add(toBit).subtract(fromBit)
      var mask = ee.Number(1).leftShift(maskSize).subtract(1)
      return value.rightShift(fromBit).bitwiseAnd(mask)
    }
    

    https://code.earthengine.google.com/6af5e8ecf48f5251371dcb6c323e18cb