Chapter 26: Rice Fields in Columbia

This chapter provides a workflow to identify rice fields based on change detection in the Tolima Department, Columbia. The full GEE code can be found here.

Functions

// HELPER FUNCTIONS
/**
 * Mask Landsat 8 image with cloud and shadow masks
 * @param  {ee.image} image - Landsat 8 image
 * @return {ee.Image}       - Masked Landsat 8 image
 *
 * Function adapted from:
 * https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LC08_C01_T1_SR
 */
function maskL8sr(image) {
  // Bits 3 and 5 are cloud shadow and cloud, respectively
  var cloudShadowBitMask = (1 << 3);
  var cloudsBitMask = (1 << 5);

  // Get the pixel QA band
  var qa = image.select('pixel_qa');

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

  // Mask image with clouds and shadows
  return image.updateMask(mask);
}

/**
 * Calculate and add NDVI band to Landsat 8 image
 * @param  {ee.image} image - Landsat 8 image
 * @return {ee.Image}       - Landsat 8 image with NDVI band added, named 'NDVI'
 */
function addNDVI(image) {
  // Calculate NDVI band
  var ndvi = image.normalizedDifference(['B5', 'B4']).rename('NDVI');

  // Add NDVI band to image
  return image.addBands(ndvi);
}

/**
 * Convert ImageCollection into list
 * @param  {ee.ImageCollection} collection - Collection of images
 * @return {ee.List}                       - List of images
 */
function imageCollectionToList(collection) {
  // Turn an ImageCollection into a list
  return collection.toList(collection.size());
}

/**
 * Compute NDVI difference between two images
 * @param  {ee.List} collectionAsList - List of images
 * @param  {number}  postImageIndex   - Index of the post-change image
 * @param  {number}  preImageIndex    - Index of the pre-change image
 * @return {image}                    - NDVI difference (dNDVI) image
 */
function ndviDiff(collectionAsList, postImageIndex, preImageIndex) {
  // Get post-change NDVI band
  var postImageNDVI = ee.Image(
    collectionAsList.get(postImageIndex)).select('NDVI');

  // Get pre-change NDVI band  
  var preImageNDVI = ee.Image(
    collectionAsList.get(preImageIndex)).select('NDVI');

  // Compute difference (post-pre)
  return postImageNDVI.subtract(preImageNDVI);
}

// MAIN FUNCTIONS
/**
 * Create NDVI difference between two Landsat 8 images in an ImageCollection
 * Depends on helper functions
 * @param  {ee.ImageCollection} imageCollection - Collection of Landsat 8 images
 * @param  {number}             indexPost       - Index of the post-change image
 * @param  {number}             indexPre        - Index of the pre-change image
 * @return {image}                              - NDVI difference (dNDVI) image
 */
function computeNDVIDiffL8(imageCollection, indexPost, indexPre) {

  // Mask clouds and shadows for each image in the collection
  var collectionMasked = imageCollection.map(maskL8sr);

  // Add NDVI band to each image in the collection
  var collectionNDVI = collectionMasked.map(addNDVI);

  // Convert ImageCollection into list
  var collectionList = imageCollectionToList(collectionNDVI);

  // Compute NDVI difference for specified image indices in the collection
  var difference = ndviDiff(collectionList, indexPost, indexPre);

  // Return NDVI difference
  return difference;
}

/**
 * Segment and classify one-band NDVI difference image with object-based
 * image analysis (OBIA) with Simple Non-Iterative Clustering (SNIC) based
 * on NDVI thresholds
 * @param  {ee.Image}             imageDiffNDVI     - NDVI difference (dNDVI) image
 * @param  {ee.FeatureCollection} studyAreaBoundary - Study area to clip imagery
 * @param  {array}                thresholdValues   - 4-value array with NDVI thresholds
 *                                                    [primary min, primary max, secondary min, secondary max]
 * @return {object}               riceFields        - Primary and secondary rice field candidates                      
 */
function segmentOBIA(imageDiffNDVI, studyAreaBoundary, thresholdValues) {

  // Create seed points within study area boundary
  var seeds = ee.Algorithms.Image.Segmentation.seedGrid(24).clip(studyAreaBoundary);

  // Segment based on SNIC (Simple Non-Iterative Clustering)
  var snic = ee.Algorithms.Image.Segmentation.SNIC({
    image: imageDiffNDVI,
    size: 36,
    compactness: 5,
    connectivity: 8,
    neighborhoodSize: 256,
    seeds: seeds
  }).select(
    ['NDVI_mean', 'clusters'],
    ['NDVI', 'clusters']);

  // Select the clusters
  var clusters = snic.select('clusters');

  // Add clusters to the NDVI image
  imageDiffNDVI = imageDiffNDVI.addBands(clusters);

  // Compute per-cluster means
  var clustersMeanReduce = imageDiffNDVI.reduceConnectedComponents(
    ee.Reducer.mean(), 'clusters', 256);

  // Define primary and secondary candidates based on thresholds
  var riceFieldsPrimary = clustersMeanReduce
    .select("NDVI").gte(thresholdValues[0])
    .and(clustersMeanReduce.select("NDVI").lte(thresholdValues[1]));

  var riceFieldsSecondary = clustersMeanReduce
    .select("NDVI").gte(thresholdValues[2])
    .and(clustersMeanReduce.select("NDVI").lte(thresholdValues[3]));

  // Mask non-rice field clusters
  riceFieldsPrimary = riceFieldsPrimary.updateMask(riceFieldsPrimary);
  riceFieldsSecondary = riceFieldsSecondary.updateMask(riceFieldsSecondary);  

  // Store rice fields in object
  var riceFields = {
    "primary" : riceFieldsPrimary,
    "secondary": riceFieldsSecondary
  };

  // Return object
  return riceFields;
}

/**
 * Convert raster to vector
 * @param  {ee.Image}             objectsRaster - Primary or secondary rice field candidates
 * @param  {ee.FeatureCollection} studyArea     - Study area boundary
 * @return {ee.FeatureCollection} objectsVector - Rice fields coverted to vector feature
 */
function rasterToVector(objectsRaster, studyArea){

  // Convert objects (raster) to vectors
  var objectsVector = objectsRaster.reduceToVectors({
    geometry: studyArea,
    maxPixels: 20000000,
    scale: 5
  });

  // Return vectors
  return objectsVector;
}

/**
 * Merge and unionize feature collections
 * @param  {ee.FeatureCollection} riceFieldsPrimary   - Primary rice field candidates
 * @param  {ee.FeatureCollection} riceFieldsSecindary - Primary rice field candidates
 * @return {ee.FeatureCollection} combinedFields      - Combined/merged rice fields
 */
function combineRiceFields(riceFieldsPrimary, riceFieldsSecondary){

  // Merge rice field candidates and create union
  var merge = riceFieldsPrimary.merge(riceFieldsSecondary);
  var combinedFields = merge.union(ee.ErrorMargin(1));

  // Return combined rice field candidates
  return combinedFields;
}

/**
 * Export vector as shapefile to Google Drive or to GEE Asset
 * @param  {ee.FeatureCollection} vector        - Vector that will be exported
 * @param  {string}               outputName    - Name of the output file (without extension)
 * @param  {string}               outputMethod  - Name of the export method/location ("Drive" for Google Drive, "Asset" for GEE Asset)
 * @return {string}               outputMessage - Message indicating the task to run to complete the export
 */
function exportVector(vector, outputName, outputMethod){

  // Create variable for output message
  var outputMessage;

  if (outputMethod.toLowerCase() == "drive") {

    // Export vectors as shapefile to Google Drive
    Export.table.toDrive({
      collection: vector,
      description: outputName,
      fileFormat: 'SHP'
    });

    // Assign output message
    outputMessage = "Task ready: " + outputName + "\nPlease run task to export to Google Drive.";

  } else if (outputMethod.toLowerCase() == "asset") {

    // Export vectors to GEE Asset
    Export.table.toAsset({
      collection: vector,
      description: outputName,
      assetId: outputName
    });

     // Assign output message
    outputMessage = "Task ready: " + outputName + "\nPlease run task to export to GEE Asset.";

  } else {

     // Assign output message
    outputMessage = "Invalid export method. Please specify 'Drive' or 'Asset'.";

  }

  // Return output message
  return outputMessage;
}

Data Acquisition & Preprocessing

// Define study area boundary and study area canals
var drtt = ee.FeatureCollection("users/calekochenour/vegetation-change/drtt_study_area_boundary");
var canals = ee.FeatureCollection("users/calekochenour/vegetation-change/drtt_study_area_canals");

// Load and clip imagery
var peakGreen = ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_008057_20181230').clip(drtt);
var harvest = ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_008057_20190216').clip(drtt);
var postHarvest = ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_008057_20190304').clip(drtt);

// Create ImageCollection from individual images
var collection = ee.ImageCollection([peakGreen, harvest, postHarvest]);

Data Processing

// COMPUTE NDVI DIFFERENCE
// Compute Peak Green to Harvest NDVI difference
var ndviDiffGreenHarvest = computeNDVIDiffL8(collection, 1, 0);

// Compute Peak Green to Post-Harvest NDVI difference
var ndviDiffGreenPostHarvest = computeNDVIDiffL8(collection, 2, 0);

// SEGMENT AND CLASSIFY NDVI DIFFERENCE
// Define NDVI thresholds (primMin, primMax, secMin, secMax)
var ndviThresholds = [-2.0, -0.35, -0.35, -0.1];

// Segment/classify
var ndviSegments = segmentOBIA(
  ndviDiffGreenPostHarvest,
  drtt,
  ndviThresholds
);

// CONVERT CLASSIFIED RASTER OBJECTS TO VECTORS
// Convert objects (raster) to vectors
var vectorPrimary = rasterToVector(
  ndviSegments.primary,
  drtt   
);

var vectorSecondary = rasterToVector(
  ndviSegments.secondary,
  drtt   
);

// Combine primary and secondary candidates
var vectorRiceFields = combineRiceFields(
  vectorPrimary,
  vectorSecondary
);

Data Postprocessing

// No data postprocessing in this lab.

Data Visualization

// Zoom to study area
Map.setCenter(-75.0978, 3.7722, 12);

// Create empty image into which to paint the features; create outlines of drtt and canals
var empty = ee.Image().byte();

var studyAreaOutline = empty.paint({
  featureCollection: drtt,
  color: 1,
  width: 3
});

var canalOutline = empty.paint({
  featureCollection: canals,
  color: 1,
  width: 3
});


// Define visualization parameters for RGB
var landsat8RGBVisParams = {
  bands: ['B4', 'B3', 'B2'],
  min: 0,
  max: 2500
};

// Define visualization parameters for NDVI
var ndviVisParams = {
  min: -1,
  max: 1,
  palette: ['blue', 'white', 'green']
};

// Define visualization parameters for NDVI difference
var ndviDiffVisParams = {
  min: -2,
  max: 2,
  palette: ['red', '#ece6d6', 'green']
};

/**
* Create Styled Layer Descriptor (SLD) for NDVI ranges. This allows
* for discrete intervals that are mapped to colors, which can highlight
* certain categories of NDVI change. The values and number of categories
* are context-depedent.
*/
var ndviSLD =
  '<RasterSymbolizer>' +
    '<ColorMap  type="interval" extended="false" >' +
      '<ColorMapEntry color="#d7191c" quantity="-2" label=""/>' +
      '<ColorMapEntry color="#fdae61" quantity="-0.5" label=""/>' +
      '<ColorMapEntry color="#ece6d6" quantity="-0.25" label=""/>' +
      '<ColorMapEntry color="#ece6d6" quantity="0.25" label="" />' +
      '<ColorMapEntry color="#a6d96a" quantity="0.5" label=""/>' +
      '<ColorMapEntry color="#1a9641" quantity="2" label="" />' +
    '</ColorMap>' +
  '</RasterSymbolizer>';

// Add study area boundary and canals to map
Map.addLayer(
  studyAreaOutline,
  {palette: 'FF0000'},
  'Study Area');

Map.addLayer(
  canalOutline,
  {palette: '0000FF'},
  'Canals');

// Add imagery to map
Map.addLayer(
  peakGreen,
  landsat8RGBVisParams,
  "Peak Green",
  false
);

Map.addLayer(
  harvest,
  landsat8RGBVisParams,
  "Harvest",
  false
);

Map.addLayer(
  postHarvest,
  landsat8RGBVisParams,
  "Post Harvest",
  false
);

// Add NDVI difference to map - continious color ramp
Map.addLayer(
  ndviDiffGreenHarvest,
  ndviDiffVisParams,
  "NDVI Diff - Green to Harvest",
  false
);

Map.addLayer(
  ndviDiffGreenPostHarvest,
  ndviDiffVisParams,
  "NDVI Diff - Green to Post Harvest",
  false
);

// Add NDVI difference to map - discrete color ramp
Map.addLayer(
  ndviDiffGreenHarvest.sldStyle(ndviSLD),
  {},
  'NDVI intervals - 5 categories - Peak Green to Harvest',
  false);

Map.addLayer(
  ndviDiffGreenPostHarvest.sldStyle(ndviSLD),
  {},
  'NDVI intervals - 5 categories - Peak Green to Post Harvest');

// Add primary and secondary rice field candidates to map
Map.addLayer(
ndviSegments.primary,
  {palette: ['green']},
  "OBIA - Rice Fields - Primary - Peak Green to Post-Harvest", false);

Map.addLayer(
  ndviSegments.secondary,
  {palette: ['lightgreen']},
  "OBIA - Rice Fields - Secondary - Peak Green to Post-Harvest", false);

// Add primary, secondary, and combined rice fields to map
Map.addLayer(
  vectorPrimary,
  {color: 'green'},
  "Vector - Rice Fields - Primary",
  false);

Map.addLayer(
  vectorSecondary,
  {color: 'lightgreen'},
  "Vector - Rice Fields - Secondary",
  false);

Map.addLayer(
  vectorRiceFields,
  {color: 'green'},
  "Vector - Rice Fields - Combined");

Data Export

// Create export tasks for primary, secondary, and combined rice fields
var exportPrimary = exportVector(
  vectorPrimary,
  "rice_fields_2018_semester_b_primary",
  "Drive"
);

var exportSecondary = exportVector(
  vectorSecondary,
  "rice_fields_2018_semester_b_secondary",
  "Drive"
);

var exportCombined = exportVector(
  vectorRiceFields,
  "rice_fields_2018_semester_b_combined",
  "Drive"
);

// Display task creation output to console - still need to run tasks
print(exportPrimary);
print(exportSecondary);
print(exportCombined);

// Export NDVI difference image
Export.image.toDrive({
  image: ndviDiffGreenPostHarvest,
  description: 'ndvi_difference_20181230_20190304',
  scale: 30,
  region: drtt
});