Practical Course 01 • Beginner Friendly

Land Use / Land Cover Classification
with Dynamic World in GEE

Mapping 9 LULC classes over Martil, Tetouan using Google's AI-powered global model
Martil, Tetouan, Morocco~2 hoursMaster - Climate Change

Contents

  1. Prerequisites
  2. Dynamic World Overview
  3. Define Study Area
  4. Load Data (DW + Sentinel-2)
  5. LULC Composite (argmax)
  6. Summer Preview + Full Year Map
  7. Area Statistics
  8. Pie Chart
  9. Legend
  10. Click Inspector
  11. Export
  12. Exercises
1
Prerequisites
10 min

Open code.earthengine.google.com. Create: Scripts > NEW > File > Course01_LULC.

i
GEE Layout: Left = Scripts/Docs/Assets. Center = Code. Right = Console/Inspector/Tasks. Bottom = Map.

2
Dynamic World Overview
10 min - Theory

Dynamic World: AI-powered global LULC at 10m, 9 classes, updated every 2-5 days. Each image has 9 probability bands + 1 label band.

ValClassProb BandHex
0Waterwater#419BDF
1Treestrees#397D49
2Grassgrass#88B053
3Flooded Vegflooded_vegetation#7A87C6
4Cropscrops#E49635
5Shrub/Scrubshrub_and_scrub#DFC35A
6Built Areabuilt#ED022A
7Bare Groundbare#EDE9E4
8Snow/Icesnow_and_ice#B9D0F2

3
Define Study Area + Pre-define Reducers
15 min (with explanations)
📘What is a Reducer in GEE?
A reducer is a function that aggregates data over space, time, or bands. Think of it like "SUM", "AVERAGE", "COUNT" in Excel, but working on massive satellite images. 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).
Step 3 - Code with explanations
// ---- 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);
❓
Why pre-define? In GEE, sometimes writing 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.

4
Load Dynamic World + Sentinel-2
15 min
🛰️ImageCollection vs Image
An 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).
Step 4a - Dynamic World
// ---- 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
Step 4b - Sentinel-2 True Color
// ---- 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');

5
LULC Composite (Mean Probability + Argmax)
20 min - Core concept
🧠How the composite works (step by step):
1️⃣ 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.
2️⃣ .toArray() → Converts the 9 bands into a single array per pixel (length 9).
3️⃣ .arrayArgmax() → Finds the index (0 to 8) of the highest probability in that array. This is the most likely class.
4️⃣ .arrayGet(0) → Extracts that index as a number.
5️⃣ .rename('label').toByte() → Renames the band to 'label' and converts to integer (Byte type).
Why this works better than .mode(): Mode (most frequent class) ignores probabilities. Mean probability + argmax considers the confidence of each observation.
Step 5 - Composite code
// ---- 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)
📊
Visualizing the process: For a single pixel, imagine water_prob=0.7, trees_prob=0.2, built_prob=0.05, etc. Argmax picks water (index 0). The final map shows class 0 as blue.

6
Summer Preview + Full Year Map
5 min
Step 6 - Visualize
// ---- 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');

7
Area Statistics (using frequencyHistogram)
15 min
📐How frequencyHistogram works:
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.
Step 7 - Area calculation
// ---- 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);

8
Pie Chart
10 min
Step 8 - Chart
// ---- 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);

9
Legend
10 min
Step 9 - Legend
// ---- 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);

10
Click Inspector
10 min
Step 10 - Inspector
// ---- 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));
    }
  });
});

11
Export
5 min
Step 11 - Export
// ---- 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
});

12
Exercises
Homework

A - Seasonal Comparison

  1. Create composites for winter (Jan-Mar) and summer (Jun-Aug)
  2. Compare class distributions

B - Built-Up Probability Map

  1. Use dw.select('built').mean()
  2. Visualize with ['white','yellow','red']

C - Change 2018 vs 2024

  1. Create composites for both years
  2. Find where Crops changed to Built Area

*
Complete Script (Tested + Explained for Beginners)
Copy & Run
FULL SCRIPT - with beginner comments
// =============================================
// 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
});
Course 01 (Beginner Edition) - LULC with Dynamic World | Dr. Ayoub Lahlouh | UM6P/CUS | 2025/2026