Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Hands-on: Mapping land cover with Random Forest classification

Supervised classification uses training samples to assign map classes to pixels. In this exercise, you will use Random Forest to classify land-cover or forest classes in the Forêt Duparquet area.

Learning objectives

By the end of this activity, you should be able to:

Suggested classes

Four simple classes:

Class codeClass name
0Water
1Forest
2Wetland
3Disturbed or open area

Predictor variables

Useful predictor variables include:

For this activity, the script uses summer Sentinel-2 imagery and SRTM terrain variables.

Training samples

The script expects training samples with:

In the Google Earth Engine Code Editor, you can draw separate geometry layers for each class. Name them:

water
forest
wetland
disturbed

For each drawn geometry layer, set the class property:

water: class = 0
forest: class = 1
wetland: class = 2
disturbed: class = 3

Then merge them in the script:

var trainingSamples = water.merge(forest).merge(wetland).merge(disturbed);

Complete GEE script

Copy this script into the Google Earth Engine Code Editor.

// ============================================================
// Random Forest classification of land cover in Forêt Duparquet
// Forêt Duparquet ROI
// ============================================================

// ------------------------------------------------------------
// 1. User settings
// ------------------------------------------------------------

var year = 2023;

// Simple class scheme:
// 0 = water
// 1 = forest
// 2 = wetland
// 3 = disturbed/open

// ------------------------------------------------------------
// 2. Region of interest
// ------------------------------------------------------------

var roi = ee.Geometry.Polygon([
  [
    [-79.50241092370698, 48.3829536766289],
    [-79.1741943709726, 48.3829536766289],
    [-79.1741943709726, 48.53958791639697],
    [-79.50241092370698, 48.53958791639697],
    [-79.50241092370698, 48.3829536766289]
  ]
]);

Map.centerObject(roi, 11);

// Show ROI boundary only
var roiBoundary = ee.Image().byte().paint({
  featureCollection: ee.FeatureCollection([ee.Feature(roi)]),
  color: 1,
  width: 2
});

Map.addLayer(roiBoundary, {palette: ["red"]}, "ROI boundary");

// ------------------------------------------------------------
// 3. Sentinel-2 cloud mask
// ------------------------------------------------------------

function maskS2(image) {
  var scl = image.select("SCL");

  var mask = scl.neq(3)   // cloud shadow
    .and(scl.neq(8))      // medium probability cloud
    .and(scl.neq(9))      // high probability cloud
    .and(scl.neq(10))     // cirrus
    .and(scl.neq(11));    // snow or ice

  return image
    .updateMask(mask)
    .divide(10000)
    .copyProperties(image, ["system:time_start"]);
}

// ------------------------------------------------------------
// 4. Sentinel-2 summer composite
// ------------------------------------------------------------

var s2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
  .filterBounds(roi)
  .filterDate(ee.Date.fromYMD(year, 6, 1), ee.Date.fromYMD(year, 9, 30))
  .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 70))
  .map(maskS2)
  .median()
  .clip(roi);

var rgbVis = {
  bands: ["B4", "B3", "B2"],
  min: 0.02,
  max: 0.30
};

Map.addLayer(s2, rgbVis, "Sentinel-2 summer RGB");

// ------------------------------------------------------------
// 5. Spectral indices
// ------------------------------------------------------------

var ndvi = s2.normalizedDifference(["B8", "B4"]).rename("NDVI");
var ndmi = s2.normalizedDifference(["B8", "B11"]).rename("NDMI");
var nbr = s2.normalizedDifference(["B8", "B12"]).rename("NBR");

Map.addLayer(ndvi, {
  min: 0,
  max: 1,
  palette: ["white", "yellow", "green", "darkgreen"]
}, "NDVI", false);

Map.addLayer(ndmi, {
  min: -0.5,
  max: 0.8,
  palette: ["brown", "white", "blue"]
}, "NDMI", false);

Map.addLayer(nbr, {
  min: -0.5,
  max: 0.8,
  palette: ["brown", "yellow", "green"]
}, "NBR", false);

// ------------------------------------------------------------
// 6. Terrain variables
// ------------------------------------------------------------

var srtm = ee.Image("USGS/SRTMGL1_003").clip(roi);
var elevation = srtm.rename("elevation");
var slope = ee.Terrain.slope(srtm).rename("slope");
var aspect = ee.Terrain.aspect(srtm).rename("aspect");

// ------------------------------------------------------------
// 7. Predictor stack
// ------------------------------------------------------------

var predictors = s2
  .select(["B2", "B3", "B4", "B8", "B11", "B12"])
  .addBands(ndvi)
  .addBands(ndmi)
  .addBands(nbr)
  .addBands(elevation)
  .addBands(slope)
  .addBands(aspect);

var predictorBands = predictors.bandNames();

print("Predictor bands:", predictorBands);

// ------------------------------------------------------------
// 8. Training samples
// ------------------------------------------------------------

// IMPORTANT:
// Draw or import training samples before running the classifier.
// Each sample must have a property named "class".
//
// Example after drawing four geometry layers:
// var trainingSamples = water.merge(forest).merge(wetland).merge(disturbed);

// Placeholder. Replace this line with your merged training samples.
var trainingSamples = ee.FeatureCollection([]);

print("Training samples:", trainingSamples);

// Stop here if trainingSamples is empty.
// After you create training samples, replace the placeholder above and run the rest.

// ------------------------------------------------------------
// 9. Sample predictor values at training locations
// ------------------------------------------------------------

var samples = predictors.sampleRegions({
  collection: trainingSamples,
  properties: ["class"],
  scale: 10,
  geometries: true
});

print("Sampled training data:", samples);

// ------------------------------------------------------------
// 10. Split samples into training and validation sets
// ------------------------------------------------------------

var samplesWithRandom = samples.randomColumn("random", 42);

var training = samplesWithRandom.filter(ee.Filter.lt("random", 0.7));
var validation = samplesWithRandom.filter(ee.Filter.gte("random", 0.7));

print("Training samples used:", training.size());
print("Validation samples used:", validation.size());

// ------------------------------------------------------------
// 11. Train Random Forest classifier
// ------------------------------------------------------------

var classifier = ee.Classifier.smileRandomForest({
  numberOfTrees: 100,
  seed: 42
}).train({
  features: training,
  classProperty: "class",
  inputProperties: predictorBands
});

print("Classifier explanation:", classifier.explain());

// ------------------------------------------------------------
// 12. Classify the ROI
// ------------------------------------------------------------

var classified = predictors.classify(classifier);

var classPalette = [
  "#0000ff", // water
  "#006400", // forest
  "#00ffff", // wetland
  "#ffa500"  // disturbed/open
];

Map.addLayer(classified, {
  min: 0,
  max: 3,
  palette: classPalette
}, "Random Forest classification");

// ------------------------------------------------------------
// 13. Accuracy assessment
// ------------------------------------------------------------

var validated = validation.classify(classifier);
var matrix = validated.errorMatrix("class", "classification");

print("Confusion matrix:", matrix);
print("Overall accuracy:", matrix.accuracy());
print("Kappa:", matrix.kappa());
print("Producer accuracy:", matrix.producersAccuracy());
print("User accuracy:", matrix.consumersAccuracy());

// ------------------------------------------------------------
// 14. Export classification map
// ------------------------------------------------------------

// Export.image.toDrive({
//   image: classified.toInt8(),
//   description: "rf_classification_forest_duparquet_" + year,
//   folder: "GEE_exports",
//   fileNamePrefix: "rf_classification_forest_duparquet_" + year,
//   region: roi,
//   scale: 10,
//   crs: "EPSG:32617",
//   maxPixels: 1e13
// });

Script explanation

1. Define class codes

The simple class scheme is:

0 = water
1 = forest
2 = wetland
3 = disturbed/open

You can change these classes based on the workshop goal, but keep the class codes numeric.

2. Build predictor variables

The script uses Sentinel-2 bands and spectral indices:

var ndvi = s2.normalizedDifference(["B8", "B4"]).rename("NDVI");
var ndmi = s2.normalizedDifference(["B8", "B11"]).rename("NDMI");
var nbr = s2.normalizedDifference(["B8", "B12"]).rename("NBR");

It also adds terrain variables:

var elevation = srtm.rename("elevation");
var slope = ee.Terrain.slope(srtm).rename("slope");
var aspect = ee.Terrain.aspect(srtm).rename("aspect");

These predictors help the classifier separate classes with different spectral and topographic conditions.

3. Define training samples

The classifier needs a FeatureCollection called trainingSamples.

Each feature must have a property named class.

Example:

var trainingSamples = water.merge(forest).merge(wetland).merge(disturbed);

4. Sample predictor values

var samples = predictors.sampleRegions({
  collection: trainingSamples,
  properties: ["class"],
  scale: 10,
  geometries: true
});

This extracts predictor values from the image stack at the training locations.

5. Split training and validation data

var samplesWithRandom = samples.randomColumn("random", 42);
var training = samplesWithRandom.filter(ee.Filter.lt("random", 0.7));
var validation = samplesWithRandom.filter(ee.Filter.gte("random", 0.7));

This creates a 70/30 split. The classifier uses 70% of the samples for training and 30% for validation.

6. Train Random Forest

var classifier = ee.Classifier.smileRandomForest({
  numberOfTrees: 100,
  seed: 42
}).train({
  features: training,
  classProperty: "class",
  inputProperties: predictorBands
});

Random Forest builds many decision trees and combines their predictions.

7. Classify the image

var classified = predictors.classify(classifier);

This creates a classified raster for the ROI.

8. Assess accuracy

var matrix = validated.errorMatrix("class", "classification");

The confusion matrix compares validation labels with predicted labels.

Questions

  1. Which classes are easiest to separate in the classification map?

  2. Which classes are most often confused with each other?

  3. What does the confusion matrix tell us?

  4. Which predictor variables seem most useful?

  5. Where does the classification map appear unreliable?

Answers
  1. Water is usually the easiest class to separate because it has low near-infrared reflectance and low NDVI. Dense forest is also often clear because it has high NDVI and high NBR.

  2. Wetland, forest, and disturbed/open areas are often confused. Wetlands can include trees, shrubs, water, and herbaceous vegetation, so their spectral signal can overlap with several classes.

  3. The confusion matrix compares validation labels with predicted labels. It shows which classes were classified correctly and which classes were confused.

  4. NDVI, NDMI, NBR, near-infrared, and shortwave-infrared bands are often useful. Elevation, slope, and aspect can help if classes occur in different terrain positions.

  5. The map is often less reliable near class boundaries, wetlands, shadows, roads, small clearings, mixed pixels, and areas with weak or sparse training samples.