Introduction
Google Earth Engine (GEE) is a powerful platform for analyzing remote sensing data. It provides access to an extensive library of datasets, including satellite imagery from Landsat and Sentinel missions, as well as a variety of climate and weather records. With decades of archived data and a wealth of user-contributed datasets, GEE enables rapid data processing and visualization, allowing users to easily locate and analyze imagery of interest.
In this tutorial, I’ll guide you through the process of performing a supervised classification using Google Earth Engine. Since GEE uses a JavaScript-based API, we’ll start by exploring some basic functions and gradually build up to custom functions that enhance our classification workflow. This tutorial assumes you have a basic understanding of both GEE and programming concepts.
Getting Started
Before we begin the classification process, let’s prepare our imagery and workspace in GEE.
Visit the Google Earth Engine Code Editor to access the API. Here are a few tips to help you set up efficiently:
- Link your Google Drive: Make sure you’re signed in so your outputs and scripts can be saved.
- Organize your files: Create a folder in the left-side panel to keep things tidy.
- Save frequently: Changes are saved when you run the code, but it’s good practice to save manually as well.
- Use multiple scripts: Break your project into smaller files for better organization. This helps reduce clutter and makes debugging easier.
- Screen space matters: If you’re working on a smaller screen, keeping things organized becomes even more important.
Accessing and Working with Image Collections in Google Earth Engine
Google Earth Engine offers a vast array of datasets, most of which are organized into image collections. Understanding how to access and work with these collections is essential for effectively using the platform.
An image collection consists of multiple satellite images from the same data source, captured at different times and locations. These collections allow users to analyze changes over time or compare different regions with consistency in data quality and format.
For this tutorial, we’ll be using Landsat 8 imagery, which provides high-quality multispectral data ideal for various remote sensing tasks. To explore other available datasets, you can browse the Google Earth Engine Data Catalog.
To get started with Landsat 8:
- Go to the Landsat 8 Level 2 dataset page in the catalog.
- Copy the provided code snippet to load the collection into your script.
This will serve as the foundation for our supervised classification work. In the next section, we’ll begin filtering and visualizing this dataset for our specific area of interest.
var dataset = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
.filterDate('2021-05-01', '2021-06-01');
// Applies scaling factors.
function applyScaleFactors(image) {
var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0);
return image.addBands(opticalBands, null, true)
.addBands(thermalBands, null, true);
}
dataset = dataset.map(applyScaleFactors);
var visualization = {
bands: ['SR_B4', 'SR_B3', 'SR_B2'],
min: 0.0,
max: 0.3,
};
Map.addLayer(dataset, visualization, 'True Color (432)');
};
You can include as many available bands as needed from the dataset, but for this example, we’ll use Bands 4, 3, and 2 to display a true color composite of the area. Additionally, the Map.setCenter function centers the map on our area of interest.
Creating Geometry in Google Earth Engine
The first step in any spatial analysis is defining the area of interest (AOI). In Google Earth Engine (GEE), this is typically done by creating a geometry, such as a polygon or rectangle, which you will use to filter and clip satellite imagery.
How to Create a Geometry in GEE
Follow these steps to create your AOI directly within the Code Editor interface:
- Open the map view by pulling up the bottom panel.
- Hover over the “Geometry Imports” tab in the top-left corner of the map.
- Click “+ new layer” at the bottom of that tab.
- Use the dropdown next to “Geometry Imports” to select either a rectangle or polygon (it defaults to a point).
- Click and draw your desired area on the map.
Once your geometry is drawn, rename it to something clear and meaningful, such as extent or AOI, as it will be referenced frequently throughout your code.
Note: In this tutorial, my geometry is named extent, and you’ll see this used in the code examples.
Importing Predefined Geometry
In many cases, you may already have a predefined area boundary in the form of a vector (like a shapefile or CSV) or raster file. GEE allows you to import such files from your local computer:
- In the Code Editor, go to the “Assets” tab on the left panel.
- Click “New”, then select “Table upload”.
- Choose “Shapefiles” (if you’re uploading a
.shpfile) or CSV, depending on your data. - Upload all necessary files that accompany your shapefile (e.g.,
.shp,.shx,.dbf,.prj). - Give your asset a name and click “Import”.
Once uploaded, your geometry will appear in your script and can be used just like a drawn polygon to subset and analyze satellite imagery.
Displaying Images in Google Earth Engine
Visualizing images in Google Earth Engine (GEE) is straightforward, but there are a few key details to understand in order to make your imagery look correct—especially when working with satellite data like Landsat 8.
Although we haven’t covered how to load an image just yet, it’s important to grasp how image display works, since this step will be repeated throughout the tutorial.
Basic Display Command
To display an image in GEE, use the following line of code:
Map.addLayer(image)
That’s it! However, when you first add an image, you might run into two common issues:
- The image appears too dark By default, GEE sets the display range of pixel values between 0 and 1, which is usually too narrow for satellite imagery.
- The colors look incorrect GEE uses bands 1, 2, and 3 for Red, Green, and Blue by default. For Landsat 8, however, the correct visible bands are:
- B4 (Red)
- B3 (Green)
- B2 (Blue)
Fixing Image Display
To adjust the appearance of the image:
- Click on the image layer in the Layers panel.
- Open the dropdown next to “Visualization Parameters”.
- Change the stretch to either 3σ (three standard deviations) or 100%. This doesn’t alter the actual data—just how it looks on the map.
- Update the band combination to:
- Red: B4
- Green: B3
- Blue: B2
This will provide a natural color visualization for Landsat 8 imagery.
Setting Bands in Code
You can also specify the band combination directly in your code like this:
Map.addLayer(image, bands = ['B4', 'B3', 'B2'])
This ensures the correct Red-Green-Blue visualization is applied automatically when the image loads. While GEE doesn’t allow you to set a custom stretch in code, you can manually adjust the value range like so:
Map.addLayer(image, {
bands: ['B4', 'B3', 'B2'],
min: 0,
max: 3000 // Adjust based on your data
});
This wy, your image is not only visible but also visually accurate.
Preparing Imagery in Google Earth Engine
Google Earth Engine (GEE) provides access to a vast array of remote sensing datasets. You can easily search for collections using the search bar within the Code Editor. For this tutorial, we’ll be working with the USGS Landsat 8 Collection 1 Tier 1 Raw Scenes—a high-quality dataset ideal for analysis.
Exploring the Dataset
To begin, it’s helpful to inspect the properties of the image collection or a specific image. This helps you better understand metadata like acquisition dates, band names, and bit values. You can do this by using the print() function:
print(image);
This will display the details in the Console tab on the right side of your screen.
To make the Landsat 8 dataset easily accessible in your script, define a variable like so:
var L8 = ee.ImageCollection('LANDSAT/LC08/C01/T1');
Creating a Cloud-Free Mosaic
Now that we’ve linked the L8 variable to the Landsat 8 image collection, we can filter and process the imagery. In this example, we’ll create a cloud-free mosaic of Fort McMurray during the month following the major wildfire in May 2016.
We’ll use two custom functions:
- A cloud mask function using the
BQAband to remove cloud-contaminated pixels. - A composite function to generate a mosaic from the filtered images.
The BQA (Bit Quality Assurance) band in Landsat 8 imagery allows us to identify and mask clouds with moderate or high confidence. Below is a conceptual overview of what we’ll do:
- Filter the image collection by date and location.
- Apply the cloud mask function to each image.
- Create a median composite or mosaic to generate a single, cloud-free image.
Here’s a simplified snippet to illustrate the setup:
// Cloud mask for L8 imagery
var L8CloudMask = function(collection) {
var qa = collection.select('BQA'); //selects the BQA (quality) band
var CloudOrShadow = qa.bitwiseAnd(1<<4) //chooses pixels where cloud is present
.and(qa.bitwiseAnd(1<<6)) //and pixels where cloud confidence ic greater than 34%
.or(qa.bitwiseAnd(1<<8)) //or pixels where cloud shadow confidence is greater than 34%
.or(qa.bitwiseAnd(1<<12)); //or pixels where cirrus confidence is greater than 34%
var masked = collection.mask(); //creates a mask
return collection.updateMask(CloudOrShadow.not()).updateMask(masked); //updates mask to remove any pixels as described above in 'CloudOrShadow'
};
Pro Tip: Visualize Each Step
As you process your imagery, try adding intermediate results to the map using Map.addLayer(). It’s a great way to verify that each step is working as intended and deepen your understanding of the data.
Also, don’t forget to explore all available bands in your imagery. Landsat 8 offers a wide range of useful bands beyond just the visible spectrum—many of which can provide powerful insights depending on your application.
Creating a Composite Image in Google Earth Engine
Now that we’ve applied the cloud mask to our imagery, it’s time to generate a composite. This step is straightforward thanks to GEE’s built-in simpleComposite function, which selects the clearest pixels from a collection to create a single best-possible image.
Composite Function
Here’s the function we’ll use to generate our composite:
// Function to create composite
var createComp = function(collection) {
var comp = ee.Algorithms.Landsat.simpleComposite({
collection: collection,
asFloat: true
});
return comp;
};
With this function in place, we can now filter the image collection, apply the cloud mask, generate the composite, and clip it to our area of interest.
Generating and Clipping the Final Image
// Select all images within June 2016 that touch the extent and have the least removed from the cloud mask
var after = L8
.filterDate('2016-06-01', '2016-06-30')
.filterBounds(extent)
.map(L8CloudMask); // Applies the cloud mask
// Create composite of images chosen above
var compAfter = createComp(after);
// Clip to extent
var clipAfter = compAfter.clip(extent);
Adding Custom Bands (Optional)
Once you’ve created your composite image, you may want to enhance it by adding custom bands depending on your analysis needs. For instance, in areas affected by wildfire, the Normalized Burn Ratio (NBR) is a useful index for identifying burned regions.
Google Earth Engine makes it easy to compute NBR using the normalizedDifference() function:
// UDF to create and add an NBR band
var calcNBR = function(image){
var NBR = image.normalizedDifference(['B5', 'B7']).rename("NBR"); // NBR is the normalized difference between NIR and SWIR
var newImage = image.addBands(NBR);
return newImage;
};
// Add a normalized burn ratio band into the image
var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'NBR'];
var clipAfter = calcNBR(clipAfter).select(bands);
Tip: Before adding the NBR band, we define a list of bands we want to retain for classification. This helps filter out unnecessary bands like coastal aerosol or the panchromatic band, keeping the dataset clean and focused.
Final Visualization
After preparing a cloud-free mosaic, adding the NBR band, and adjusting the visual parameters to show SWIR, NIR, and Green as Red, Green, and Blue respectively (a standard false-color combination for burn analysis), your image should now effectively highlight burned areas.
Getting Training Data for Classification
Now that our satellite image is prepared, the next step is to gather training data by creating polygons that represent different land cover classes. For this tutorial, I’ll use four basic classes: Water, Vegetation, Urban, and Burn.
Creating Land Cover Polygons
Similar to how we created our extent polygon, we’ll create new geometry layers—but with an important difference. This time, after drawing each polygon, you’ll go into its settings and change the type from ‘Geometry’ to ‘FeatureCollection’. This allows us to:
- Add multiple features (polygons) to each land cover class
- Assign a classification value to each class (e.g., Water = 0, Vegetation = 1, Urban = 2, Burn = 3)
- Use a consistent property name like
"LC"or"landcover"for all classes
Once created, your layers might look something like this:
- Water → property:
LC = 0 - Vegetation → property:
LC = 1 - Urban → property:
LC = 2 - Burn → property:
LC = 3
Tip: When drawing your training polygons, try to capture the full range of spectral variation within each land cover class. Avoid placing polygons that might overlap or closely resemble other classes in terms of pixel values.
We’ll evaluate the quality of these classes in the next step.
Assessing the Quality of Training Data
Before running a classification, it’s important to validate the usability of your training data. A good way to do this is by examining histograms that show the distribution of digital number (DN) values across the bands for each land cover class.
Here’s a handy function to generate these histograms in the GEE Code Editor:
// Prints a histogram of the polygons
var print_hist = function(image, poly, title){
var options = {
title: title,
fontSize: 20,
hAxis: {title: 'DN'},
vAxis: {title: 'count of DN'},
};
var histogram = ui.Chart.image.histogram(image, poly, 30)
.setOptions(options);
print(histogram);
};
Once executed, this function will display an interactive histogram showing the distribution of DN values across all bands for the selected class. You can explore the charts, download them as CSV or PNG files, and compare results between classes.
✅ Note: For this tutorial, I’ll move forward with the classes I created. They show consistent distribution without significant overlap, which is adequate for classification.
With clean training data in place, we’re now ready to train a supervised classifier and generate land cover maps. Let’s dive into that next!
Gathering Data from Training Polygons
With our land cover polygons now selected and verified, it’s time to convert them into usable training data. The first step is to merge all individual class layers into a single FeatureCollection. Each polygon will retain its assigned property (LC), so you’ll still know which pixels belong to Water (0), Vegetation (1), Burn (2), or Urban (3).
var ABtrainingPolys = polyWater.merge(polyVegetation) //Merges all points
.merge(polyBurn)
.merge(polyUrban);
Right now, these polygons are just floating over the image—they don’t yet represent the pixels beneath them. To extract that information, we’ll use sampleRegions() to capture the band values from the image beneath each polygon.
var ABtrainingPixels = clipAfter.sampleRegions({
collection: ABtrainingPolys,
properties: ['LC'],
scale: 30
});
This generates a new collection where each sample contains the band data for a pixel located within a training polygon, along with the associated land cover label (LC). The scale: 30 matches the resolution of Landsat imagery.
⚠️ Note: This collection can contain thousands of points. Printing it to the console might slow down your script or even time it out.
Creating Training and Validation Sets
To ensure our classifier performs well, we need to split the data into training and validation sets. This lets us train the model on one portion of the data and test its performance on a separate, unseen subset. Here’s a helper function that does just that:
// Splits data into training and validation groups. Set at 70% training/30% validation
var splitData = function(data){
var dict = {};
var randomTpixels = data.randomColumn();
var trainingData = randomTpixels.filter(ee.Filter.lt('random', 0.7));
var valiData = randomTpixels.filter(ee.Filter.gte('random', 0.7));
dict.training = trainingData;
dict.validation = valiData;
return dict;
};
var ABdata = splitData(ABtrainingPixels);
This function returns a dictionary with two keys: training and validation, each containing the appropriate pixel samples for use in the classification workflow.
Training the Classifier
With the data now split, we can move on to training the classifier. In this example, we’ll use the Random Forest algorithm with 10 trees—a strong general-purpose model for land cover classification.
var ABclassifiedTD = ee.Classifier.smileRandomForest(10).train(ABdata.training, "LC", bands);
This command trains the classifier using the training data. The "LC" label indicates which class each sample belongs to, and the model uses the spectral bands we selected earlier as input features.
Tip: You can explore other classifiers in Earth Engine’s documentation.
Validating the Classifier
Before classifying the full image, it’s critical to test the classifier against the validation dataset. Since this data wasn’t used during training, it’s a good indicator of how well the model generalizes to new, unseen data.
First, classify the validation set:
var ABvalidation = ABdata.validation.classify(ABclassifiedTD); // Classifies the validation data
Next, use this helper function to calculate and print an error matrix:
// Creates error matrix
var createMatrix = function(data){
var trainAccuracy = data.errorMatrix("LC", "classification");
print('Resubstitution error matrix: ', trainAccuracy);
print('Training overall accuracy: ', trainAccuracy.accuracy());
};
var ABvalidation = ABdata.validation.classify(ABclassifiedTD); // Classifies the validation data
createMatrix(ABvalidation); // Print the error matrix
The output will show how well your classifier performed. For instance, if your matrix contains:
0: [6225,0,5,0]
1: [2,18217,0,1]
2: [4,6,6853,0]
3: [0,0,3,8799]
It means that most pixels were correctly classified, with minor confusion between classes—like 5 water pixels being classified as urban. This level of error is acceptable in many use cases, but if you see substantial misclassification, you may need to revisit your training polygons and consider refining them.
Running the Classifier on the Full Image
Now that we have a validated classifier with good accuracy, it’s time to apply it to the entire image. The process is identical to classifying the validation dataset—only now, we use the full image instead.
var postBurn = clipAfter.classify(ABClassifiedTD);
var ABcolours = ['1923d6', '6fba00', '000000', 'ef0b0b']; // Create a colour pallete
Map.addLayer(postBurn,
{palette: ABcolours, min: 0, max: 3}, // min and max indicate that there will be 4 classes coloured
"After Burn"); // Add the classification to the map with the four classes being coloured according to ABcolours
Here’s what the output looks like:
PostBurn.png
The classification seems to perform well. Keep in mind, I only used 10 trees in the Random Forest classifier. Increasing the number of trees could slightly improve accuracy—but be cautious: overfitting can be just as harmful as underfitting.
Improving Classification Accuracy
To enhance classification results, incorporating additional datasets can be very beneficial. Indices like NDVI, NDBI, MNDWI, and BSI help distinguish between land cover types more effectively.
Here’s how to add these indices to your image:
var addIndices = function(image) {
var ndvi = image.normalizedDifference(['B5', 'B4']).rename(['ndvi']);
var ndbi = image.normalizedDifference(['B6', 'B5']).rename(['ndbi']);
var mndwi = image.normalizedDifference(['B3', 'B6']).rename(['mndwi']);
var bsi = image.expression(
'(( X + Y ) - (A + B)) /(( X + Y ) + (A + B)) ', {
'X': image.select('B6'), //swir1
'Y': image.select('B4'), //red
'A': image.select('B5'), // nir
'B': image.select('B2'), // blue
}).rename('bsi');
return image.addBands(ndvi).addBands(ndbi).addBands(mndwi).addBands(bsi)
};
These extra bands provide stronger signals for your classifier to distinguish between land cover types.
Normalizing Input Data
Machine learning models perform better when input features are scaled similarly. Normalizing the input data ensures values are in a 0–1 range, boosting model performance and convergence.
function normalize(image){
var bandNames = image.bandNames();
// Compute min and max of the image
var minDict = image.reduceRegion({
reducer: ee.Reducer.min(),
geometry: boundary,
scale: 10,
maxPixels: 1e9,
bestEffort: true,
tileScale: 16
});
var maxDict = image.reduceRegion({
reducer: ee.Reducer.max(),
geometry: boundary,
scale: 10,
maxPixels: 1e9,
bestEffort: true,
tileScale: 16
});
var mins = ee.Image.constant(minDict.values(bandNames));
var maxs = ee.Image.constant(maxDict.values(bandNames));
var normalized = image.subtract(mins).divide(maxs.subtract(mins))
return normalized
} ;
Calculating Area by Classes
Understanding the spatial extent of each land cover type is crucial—especially in post-burn scenarios. The following function calculates the area covered by each class in square kilometers:
var classArea = function(image){
var areaImage = ee.Image.pixelArea().addBands(
image);
var areas = areaImage.reduceRegion({
reducer: ee.Reducer.sum().group({
groupField: 1,
groupName: 'classification',
}),
geometry: image.geometry(),
scale: 30,
maxPixels: 1e8
});
var classAreas = ee.List(areas.get('groups'));
var classAreaLists = classAreas.map(function(item) { // Function within a function to create a dictionary with the values for every group
var areaDict = ee.Dictionary(item);
var classNumber = ee.Number(areaDict.get('classification')).format();
var area = ee.Number(
areaDict.get('sum')).divide(1e6).round(); // The result will be in square meters, this converts them into square kilometers
return ee.List([classNumber, area]);
});
var result = ee.Dictionary(classAreaLists.flatten()); // Flattens said dictionary so it is readable for us
return(result);
};
print('after:', classArea(postBurn))
Output (in km²):
after:
Object (4 properties)
0: 43
1: 592
2: 97
3: 645
Visualizing with a Land Cover Chart
A more intuitive way to understand the area distribution is through a bar chart. The function below helps visualize the class areas using Earth Engine’s chart tools:
var create_chart = function(classification, AOI, classList){ // for classList, create a list of your classes as strings
var options = {
hAxis: {title: 'Class'},
vAxis: {title: 'Area'},
title: 'Area by class',
series: { // You can change these to be whatever colours you'd like. Simply add numbers to match how many classes you have
0: {color: 'blue'},
1: {color: 'green'},
2: {color: 'orange'},
3: {color: 'red'}}
};
var areaChart = ui.Chart.image.byClass({
image: ee.Image.pixelArea().addBands(classification),
classBand: 'classification',
scale: 30,
region: AOI,
reducer: ee.Reducer.sum()
}).setSeriesNames(classList)
.setOptions(options)
;
print(areaChart);
};\][
var create_chart = function(classification, AOI, classList){ // classList should be a list of class names
var options = {
hAxis: {title: 'Class'},
vAxis: {title: 'Area'},
title: 'Area by class',
series: {
0: {color: 'blue'},
1: {color: 'green'},
2: {color: 'orange'},
3: {color: 'red'}}
};
var areaChart = ui.Chart.image.byClass({
image: ee.Image.pixelArea().addBands(classification),
classBand: 'classification',
scale: 30,
region: AOI,
reducer: ee.Reducer.sum()
}).setSeriesNames(classList)
.setOptions(options);
print(areaChart);
};
Like the histograms earlier, this chart is interactive—hovering over each bar reveals the exact area. A helpful, visual way to communicate your classification results.
Adding a Legend to the Map
A legend is essential for interpreting the features represented on your map. Without one, users unfamiliar with the color codes would struggle to understand what each color means. By adding a legend, we clearly label each land cover type with its corresponding color, improving map readability and usability.
var legend = ui.Panel({
style: {
position: 'bottom-left',
padding: '8px 15px'
}
});
// Create legend title
var legendTitle = ui.Label({
value: 'Legend',
style: {
fontWeight: 'bold',
fontSize: '18px',
margin: '0 0 4px 0',
padding: '0'
}
});
// Add the title to the panel
legend.add(legendTitle);
// Creates and styles one row of the legend.
var makeRow = function(color, name) {
// Create the colored box
var colorBox = ui.Label({
style: {
backgroundColor: '#' + color,
padding: '8px',
margin: '0 0 4px 0'
}
});
// Create the label with the description text
var description = ui.Label({
value: name,
style: {margin: '0 0 4px 6px'}
});
// Return the complete row
return ui.Panel({
widgets: [colorBox, description],
layout: ui.Panel.Layout.Flow('horizontal')
});
};
// Define color palette and corresponding class names
var palette = ['1923d6', '6fba00', '000000', 'ef0b0b'];
var names = ['Water', 'Vegetation', 'Burn', 'Urban'];
// Add legend rows to the panel
for (var i = 0; i < 4; i++) {
legend.add(makeRow(palette[i], names[i]));
}
// Add legend to the map (you can also print it to the console if preferred)
Map.add(legend);
Output: 
Exporting the Classified Image
Once your classification is complete, you may want to save the result for further analysis or sharing. Exporting the image to your Google Drive is straightforward. First, make sure you have a folder created in your Drive—here, we’re using one called earthengine.
var exportImage = postBurn;
Export.image.toDrive({
image: exportImage,
description: 'LandCover',
folder: 'earthengine',
fileNamePrefix: 'LandCover',
region: extent,
scale: 10,
maxPixels: 1e9
});
Final Thoughts
Google Earth Engine is an incredibly powerful tool with vast capabilities. What we’ve explored here is just the beginning—a simple supervised classification using the Random Forest algorithm. If you’ve followed along, you now have a complete classification workflow under your belt.
But don’t stop here! Experiment with:
- Different classification algorithms (e.g., CART, SVM, Naïve Bayes)
- Alternate band combinations
- Additional indices to refine your results
And if you run into any roadblocks, the Google Earth Engine Documentation is an excellent resource—written by the developers themselves, packed with tutorials, guides, and helpful tips.
Conclusion
Google Earth Engine offers a vast array of tools and capabilities for remote sensing and geospatial analysis. In this tutorial, we’ve only touched the surface by exploring supervised classification using the Random Forest algorithm. If you’ve followed along, you should now have a working classification of your own.
I encourage you to go further—experiment with other classification algorithms, test different combinations of spectral bands, and integrate additional indices to enhance your results.
If you run into any questions or want to dive deeper, be sure to explore the official Google Earth Engine documentation. It’s a comprehensive resource created by the developers themselves, packed with valuable insights, tips, and practical examples.