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:
define land-cover classes
create or load training samples
build predictor variables from Sentinel-2 and terrain data
train a Random Forest classifier
classify a region of interest
assess classification accuracy
interpret a confusion matrix
export a classification map
Suggested classes¶
Four simple classes:
| Class code | Class name |
|---|---|
| 0 | Water |
| 1 | Forest |
| 2 | Wetland |
| 3 | Disturbed or open area |
Predictor variables¶
Useful predictor variables include:
Sentinel-2 spectral bands
NDVI, for vegetation greenness
NDMI, for canopy and surface moisture
NBR, for canopy structure and disturbance sensitivity
SRTM elevation
slope
aspect
For this activity, the script uses summer Sentinel-2 imagery and SRTM terrain variables.
Training samples¶
The script expects training samples with:
point or polygon geometry
a property named
classnumeric class codes, for example
0,1,2,3
In the Google Earth Engine Code Editor, you can draw separate geometry layers for each class. Name them:
water
forest
wetland
disturbedFor each drawn geometry layer, set the class property:
water: class = 0
forest: class = 1
wetland: class = 2
disturbed: class = 3Then 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/openYou 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¶
Which classes are easiest to separate in the classification map?
Which classes are most often confused with each other?
What does the confusion matrix tell us?
Which predictor variables seem most useful?
Where does the classification map appear unreliable?
Answers
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.
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.
The confusion matrix compares validation labels with predicted labels. It shows which classes were classified correctly and which classes were confused.
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.
The map is often less reliable near class boundaries, wetlands, shadows, roads, small clearings, mixed pixels, and areas with weak or sparse training samples.