Open code.earthengine.google.com. Create: Scripts > NEW > File > Course01_LULC.
Dynamic World: AI-powered global LULC at 10m, 9 classes, updated every 2-5 days. Each image has 9 probability bands + 1 label band.
| Val | Class | Prob Band | Hex |
|---|---|---|---|
| 0 | Water | water | #419BDF |
| 1 | Trees | trees | #397D49 |
| 2 | Grass | grass | #88B053 |
| 3 | Flooded Veg | flooded_vegetation | #7A87C6 |
| 4 | Crops | crops | #E49635 |
| 5 | Shrub/Scrub | shrub_and_scrub | #DFC35A |
| 6 | Built Area | built | #ED022A |
| 7 | Bare Ground | bare | #EDE9E4 |
| 8 | Snow/Ice | snow_and_ice | #B9D0F2 |
ee.Reducer.sum() adds up pixel values; ee.Reducer.frequencyHistogram() counts how many pixels of each class exist. We pre-define them at the top to avoid GEE scope errors (a common beginner trap).// ---- PRE-DEFINE REDUCERS (top-level) ----
// Reducer: an operation that combines many values into one.
// ee.Reducer.sum() → adds all pixel values in a region.
var reducerSum = ee.Reducer.sum();
// ee.Reducer.first() → picks the first value (useful for point sampling)
var reducerFirst = ee.Reducer.first();
// ee.Reducer.frequencyHistogram() → counts occurrences of each integer value
// Perfect for LULC: how many pixels are Water (0), Trees (1), etc.
var reducerFreqHist = ee.Reducer.frequencyHistogram();
// ---- 1. STUDY AREA ----
// Rectangle: [minLon, minLat, maxLon, maxLat] around Martil
var studyArea = ee.Geometry.Rectangle([-5.32, 35.58, -5.22, 35.66]);
Map.setCenter(-5.27, 35.62, 13);
Map.addLayer(studyArea, {color: 'red'}, 'Study Area', false);ee.Reducer.sum() inside a function can fail because of how the server compiles code. Assigning to a variable at the top level (outside any function) makes the code more robust. Good practice for beginners.ImageCollection is a stack of many satellite images over time. We filter by date and location, then we can combine them (e.g., median, mean). Sentinel-2 true color uses bands B4(red), B3(green), B2(blue).// ---- 2. LOAD DYNAMIC WORLD ----
// Dynamic World is an ImageCollection: each image has 9 probability bands
var dw = ee.ImageCollection('GOOGLE/DYNAMICWORLD/V1')
.filterBounds(studyArea) // keep only images overlapping our area
.filterDate('2024-01-01', '2024-12-31'); // whole year 2024
print('Images found:', dw.size()); // check how many images available// ---- 3. SENTINEL-2 TRUE COLOR ----
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(studyArea)
.filterDate('2024-06-01', '2024-08-31') // summer months
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10)) // less than 10% clouds
.median(); // median composite: reduces clouds and noise
Map.addLayer(s2.clip(studyArea),
{bands: ['B4', 'B3', 'B2'], min: 0, max: 3000},
'Sentinel-2 - True Color');dw.select(probBands).mean() → For each pixel, average the probability of each class over all images in 2024. Result: 9 bands, each containing the average probability (0 to 1) for that class..toArray() → Converts the 9 bands into a single array per pixel (length 9)..arrayArgmax() → Finds the index (0 to 8) of the highest probability in that array. This is the most likely class..arrayGet(0) → Extracts that index as a number..rename('label').toByte() → Renames the band to 'label' and converts to integer (Byte type)..mode(): Mode (most frequent class) ignores probabilities. Mean probability + argmax considers the confidence of each observation.// ---- 4. CLASS NAMES, COLORS, PROBABILITY BANDS ----
var classNames = [
'Water', 'Trees', 'Grass', 'Flooded Veg',
'Crops', 'Shrub and Scrub', 'Built Area',
'Bare Ground', 'Snow and Ice'
];
var probBands = [
'water', 'trees', 'grass', 'flooded_vegetation',
'crops', 'shrub_and_scrub', 'built', 'bare', 'snow_and_ice'
];
var colors = [
'419BDF', '397D49', '88B053', '7A87C6',
'E49635', 'DFC35A', 'ED022A', 'EDE9E4', 'B9D0F2'
];
// ---- 5. LULC COMPOSITE (mean probability + argmax) ----
// Step A: average each probability band over time
var meanProb = dw.select(probBands).mean(); // still 9 bands
// Step B: find which class has the highest mean probability per pixel
var composite = meanProb.toArray() // 9 values → array of 9
.arrayArgmax() // index of max value
.arrayGet(0) // extract index
.rename('label').toByte(); // integer class label (0-8)
// ---- 6. DW SUMMER PREVIEW (only June-August) ----
var dwSummerProb = dw
.filterDate('2024-06-01', '2024-08-31')
.select(probBands).mean();
var dwSummer = dwSummerProb.toArray().arrayArgmax().arrayGet(0)
.rename('label').toByte();
Map.addLayer(dwSummer.clip(studyArea),
{min: 0, max: 8, palette: colors}, 'DW Summer 2024');
// ---- 7. FULL YEAR MAP (January-December) ----
Map.addLayer(composite.clip(studyArea),
{min: 0, max: 8, palette: colors}, 'LULC 2024 - Full Year');reduceRegion() applies a reducer over all pixels inside the study area. ee.Reducer.frequencyHistogram() counts how many times each integer value appears. For our composite (values 0 to 8), it returns a dictionary like: {0: 45000, 1: 12000, 2: 8000, ...}. Then multiply each count by pixel area (100 m²) to get square meters, then divide by 10,000 for hectares.// ---- 8. AREA STATISTICS (frequencyHistogram) ----
// reduceRegion: applies reducer over the geometry
var histogram = composite.reduceRegion({
reducer: reducerFreqHist, // counts pixel occurrences per class
geometry: studyArea,
scale: 10, // 10m resolution
maxPixels: 1e9
});
print('Pixel count per class:', histogram.get('label'));
var pixelScale = 100; // 10m x 10m = 100 square meters
var histDict = ee.Dictionary(histogram.get('label'));
print('Class pixel counts:', histDict);// ---- 9. PIE CHART ----
var areaList = [];
for (var i = 0; i < classNames.length; i++) {
areaList.push(ee.Feature(null, {
class_name: classNames[i],
class_id: i,
area_m2: ee.Number(histDict.get(ee.String(ee.Number(i).format()), 0))
.multiply(pixelScale)
}));
}
var areaFC = ee.FeatureCollection(areaList);
print('Area by class (m2):', areaFC);
var chart = ui.Chart.feature.byFeature(areaFC, 'class_name', 'area_m2')
.setChartType('PieChart')
.setOptions({
title: 'LULC Distribution - Martil 2024',
slices: {
0: {color: '#419BDF'}, 1: {color: '#397D49'},
2: {color: '#88B053'}, 3: {color: '#7A87C6'},
4: {color: '#E49635'}, 5: {color: '#DFC35A'},
6: {color: '#ED022A'}, 7: {color: '#EDE9E4'},
8: {color: '#B9D0F2'}
}
});
print(chart);// ---- 10. LEGEND ----
var legend = ui.Panel({
style: {position: 'bottom-left', padding: '10px 14px', backgroundColor: 'white'}
});
legend.add(ui.Label({
value: 'Dynamic World LULC',
style: {fontWeight: 'bold', fontSize: '14px', margin: '0 0 8px 0', color: '#1A1A2E'}
}));
for (var i = 0; i < classNames.length; i++) {
var colorBox = ui.Label({
value: i.toString(),
style: {
backgroundColor: '#' + colors[i],
color: (i === 7 || i === 8) ? '#333' : '#fff',
padding: '2px 6px', margin: '0 8px 4px 0',
fontSize: '10px', fontWeight: 'bold', textAlign: 'center',
border: '1px solid #ccc'
}
});
var nameLabel = ui.Label({
value: classNames[i],
style: {fontSize: '12px', margin: '0 0 4px 0', color: '#374151'}
});
legend.add(ui.Panel({
widgets: [colorBox, nameLabel],
layout: ui.Panel.Layout.Flow('horizontal')
}));
}
legend.add(ui.Label({
value: '10m | Google/WRI | 2024',
style: {fontSize: '9px', color: '#9CA3AF', margin: '6px 0 0 0'}
}));
Map.add(legend);// ---- 11. CLICK INSPECTOR ----
var inspectorPanel = ui.Panel({
style: {position: 'top-right', padding: '10px 14px',
backgroundColor: 'white', width: '260px'}
});
inspectorPanel.add(ui.Label({
value: 'Click Inspector',
style: {fontWeight: 'bold', fontSize: '14px',
margin: '0 0 6px 0', color: '#1A1A2E'}
}));
var instrLabel = ui.Label({
value: 'Click on the map to identify LULC class.',
style: {fontSize: '11px', color: '#6B7280'}
});
inspectorPanel.add(instrLabel);
var classResult = ui.Label({value: '',
style: {fontSize: '13px', fontWeight: 'bold', margin: '8px 0 2px 0'}});
var coordResult = ui.Label({value: '',
style: {fontSize: '10px', color: '#6B7280'}});
inspectorPanel.add(classResult);
inspectorPanel.add(coordResult);
Map.add(inspectorPanel);
Map.onClick(function(coords) {
var point = ee.Geometry.Point([coords.lon, coords.lat]);
var value = composite.reduceRegion({
reducer: reducerFirst,
geometry: point, scale: 10
});
value.get('label').evaluate(function(classVal) {
if (classVal === null || classVal === undefined) {
classResult.setValue('No data at this location');
classResult.style().set('color', '#EF4444');
coordResult.setValue('');
} else {
var name = classNames[classVal] || 'Unknown';
var hex = colors[classVal] || 'CCCCCC';
classResult.setValue('Class ' + classVal + ': ' + name);
classResult.style().set('color', '#' + hex);
coordResult.setValue(
'Lon: ' + coords.lon.toFixed(5) +
' | Lat: ' + coords.lat.toFixed(5));
}
});
});// ---- 12. EXPORT ----
Export.image.toDrive({
image: composite.clip(studyArea),
description: 'Martil_LULC_2024_DynamicWorld',
folder: 'GEE_Exports',
fileNamePrefix: 'Martil_LULC_2024',
region: studyArea, scale: 10,
crs: 'EPSG:4326', maxPixels: 1e9
});dw.select('built').mean()['white','yellow','red']// =============================================
// COURSE 01: LULC Classification with Dynamic World
// Study Area: Martil, Tetouan, Morocco
// Author: Dr. Ayoub Lahlouh - UM6P / CUS
// =============================================
// Beginner notes:
// - Reducers are functions that aggregate data (sum, count, histogram)
// - .mean() averages all images in the collection per band
// - .toArray().arrayArgmax() finds the class with highest probability
// - frequencyHistogram counts pixel occurrences per integer class
// ---- PRE-DEFINE REDUCERS (top-level avoids errors) ----
var reducerSum = ee.Reducer.sum();
var reducerFirst = ee.Reducer.first();
var reducerFreqHist = ee.Reducer.frequencyHistogram();
// ---- 1. STUDY AREA (rectangle around Martil) ----
var studyArea = ee.Geometry.Rectangle([-5.32, 35.58, -5.22, 35.66]);
Map.setCenter(-5.27, 35.62, 13);
Map.addLayer(studyArea, {color: 'red'}, 'Study Area', false);
// ---- 2. LOAD DYNAMIC WORLD (ImageCollection) ----
var dw = ee.ImageCollection('GOOGLE/DYNAMICWORLD/V1')
.filterBounds(studyArea)
.filterDate('2024-01-01', '2024-12-31');
print('Images found:', dw.size());
// ---- 3. SENTINEL-2 TRUE COLOR (summer composite) ----
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(studyArea)
.filterDate('2024-06-01', '2024-08-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10))
.median();
Map.addLayer(s2.clip(studyArea),
{bands: ['B4', 'B3', 'B2'], min: 0, max: 3000},
'Sentinel-2 - True Color');
// ---- 4. CLASS NAMES, COLORS, PROBABILITY BANDS ----
var classNames = [
'Water', 'Trees', 'Grass', 'Flooded Veg',
'Crops', 'Shrub and Scrub', 'Built Area',
'Bare Ground', 'Snow and Ice'
];
var probBands = [
'water', 'trees', 'grass', 'flooded_vegetation',
'crops', 'shrub_and_scrub', 'built', 'bare', 'snow_and_ice'
];
var colors = [
'419BDF', '397D49', '88B053', '7A87C6',
'E49635', 'DFC35A', 'ED022A', 'EDE9E4', 'B9D0F2'
];
// ---- 5. LULC COMPOSITE (mean probability + argmax) ----
var meanProb = dw.select(probBands).mean();
var composite = meanProb.toArray().arrayArgmax().arrayGet(0)
.rename('label').toByte();
// ---- 6. DW SUMMER PREVIEW ----
var dwSummerProb = dw
.filterDate('2024-06-01', '2024-08-31')
.select(probBands).mean();
var dwSummer = dwSummerProb.toArray().arrayArgmax().arrayGet(0)
.rename('label').toByte();
Map.addLayer(dwSummer.clip(studyArea),
{min: 0, max: 8, palette: colors}, 'DW Summer 2024');
// ---- 7. FULL YEAR MAP ----
Map.addLayer(composite.clip(studyArea),
{min: 0, max: 8, palette: colors}, 'LULC 2024 - Full Year');
// ---- 8. AREA STATISTICS (frequencyHistogram) ----
var histogram = composite.reduceRegion({
reducer: reducerFreqHist,
geometry: studyArea,
scale: 10,
maxPixels: 1e9
});
print('Pixel count per class:', histogram.get('label'));
var pixelScale = 100; // 10m x 10m = 100 m²
var histDict = ee.Dictionary(histogram.get('label'));
print('Class pixel counts:', histDict);
// ---- 9. PIE CHART ----
var areaList = [];
for (var i = 0; i < classNames.length; i++) {
areaList.push(ee.Feature(null, {
class_name: classNames[i],
class_id: i,
area_m2: ee.Number(histDict.get(ee.String(ee.Number(i).format()), 0))
.multiply(pixelScale)
}));
}
var areaFC = ee.FeatureCollection(areaList);
print('Area by class (m2):', areaFC);
var chart = ui.Chart.feature.byFeature(areaFC, 'class_name', 'area_m2')
.setChartType('PieChart')
.setOptions({
title: 'LULC Distribution - Martil 2024',
slices: {
0: {color: '#419BDF'}, 1: {color: '#397D49'},
2: {color: '#88B053'}, 3: {color: '#7A87C6'},
4: {color: '#E49635'}, 5: {color: '#DFC35A'},
6: {color: '#ED022A'}, 7: {color: '#EDE9E4'},
8: {color: '#B9D0F2'}
}
});
print(chart);
// ---- 10. LEGEND ----
var legend = ui.Panel({
style: {position: 'bottom-left', padding: '10px 14px', backgroundColor: 'white'}
});
legend.add(ui.Label({
value: 'Dynamic World LULC',
style: {fontWeight: 'bold', fontSize: '14px', margin: '0 0 8px 0', color: '#1A1A2E'}
}));
for (var i = 0; i < classNames.length; i++) {
var colorBox = ui.Label({
value: i.toString(),
style: {
backgroundColor: '#' + colors[i],
color: (i === 7 || i === 8) ? '#333' : '#fff',
padding: '2px 6px', margin: '0 8px 4px 0',
fontSize: '10px', fontWeight: 'bold', textAlign: 'center',
border: '1px solid #ccc'
}
});
var nameLabel = ui.Label({
value: classNames[i],
style: {fontSize: '12px', margin: '0 0 4px 0', color: '#374151'}
});
legend.add(ui.Panel({
widgets: [colorBox, nameLabel],
layout: ui.Panel.Layout.Flow('horizontal')
}));
}
legend.add(ui.Label({
value: '10m | Google/WRI | 2024',
style: {fontSize: '9px', color: '#9CA3AF', margin: '6px 0 0 0'}
}));
Map.add(legend);
// ---- 11. CLICK INSPECTOR ----
var inspectorPanel = ui.Panel({
style: {position: 'top-right', padding: '10px 14px',
backgroundColor: 'white', width: '260px'}
});
inspectorPanel.add(ui.Label({
value: 'Click Inspector',
style: {fontWeight: 'bold', fontSize: '14px',
margin: '0 0 6px 0', color: '#1A1A2E'}
}));
var instrLabel = ui.Label({
value: 'Click on the map to identify LULC class.',
style: {fontSize: '11px', color: '#6B7280'}
});
inspectorPanel.add(instrLabel);
var classResult = ui.Label({value: '',
style: {fontSize: '13px', fontWeight: 'bold', margin: '8px 0 2px 0'}});
var coordResult = ui.Label({value: '',
style: {fontSize: '10px', color: '#6B7280'}});
inspectorPanel.add(classResult);
inspectorPanel.add(coordResult);
Map.add(inspectorPanel);
Map.onClick(function(coords) {
var point = ee.Geometry.Point([coords.lon, coords.lat]);
var value = composite.reduceRegion({
reducer: reducerFirst,
geometry: point, scale: 10
});
value.get('label').evaluate(function(classVal) {
if (classVal === null || classVal === undefined) {
classResult.setValue('No data at this location');
classResult.style().set('color', '#EF4444');
coordResult.setValue('');
} else {
var name = classNames[classVal] || 'Unknown';
var hex = colors[classVal] || 'CCCCCC';
classResult.setValue('Class ' + classVal + ': ' + name);
classResult.style().set('color', '#' + hex);
coordResult.setValue(
'Lon: ' + coords.lon.toFixed(5) +
' | Lat: ' + coords.lat.toFixed(5));
}
});
});
// ---- 12. EXPORT ----
Export.image.toDrive({
image: composite.clip(studyArea),
description: 'Martil_LULC_2024_DynamicWorld',
folder: 'GEE_Exports',
fileNamePrefix: 'Martil_LULC_2024',
region: studyArea, scale: 10,
crs: 'EPSG:4326', maxPixels: 1e9
});