Spotting What Changed from Space

Here's a question that never gets old: what's different about this place compared to last year? That's exactly what change detection answers. We compare satellite images from two dates, subtract one from the other, and let the pixels tell us where the landscape transformed.

Learning objectives

  • Compute before-after image differences to identify landscape change.
  • Calculate dNBR (differenced Normalized Burn Ratio) for burn severity mapping.
  • Interpret change maps and classify results into meaningful categories.
  • Understand the concept of LandTrendr for long-term change analysis.

Why it matters

The Earth's surface is restless. Forests burn, cities sprawl, glaciers shrink, and crops cycle through their seasons. Detecting these changes from satellite imagery gives us critical intelligence for disaster response, environmental monitoring, and urban planning.

Change detection is one of the most widely used techniques in applied remote sensing, and it's surprisingly intuitive once you see it in action.

Key vocabulary

Change detection
The process of identifying differences in the state of a landscape by observing it at different times.
Image differencing
Subtracting pixel values in one image from the corresponding pixels in another image.
dNBR
Differenced Normalized Burn Ratio: pre-fire NBR minus post-fire NBR. Higher values indicate more severe burning.
Thresholding
Classifying a continuous difference image into categories by setting value boundaries.
LandTrendr
An algorithm that detects changes across many years of imagery by fitting segmented trends to each pixel's time series.

Your first change map in 60 seconds

The 2020 Bobcat Fire burned over 115,000 acres in the Angeles National Forest, California. Let's see the damage instantly by comparing NDVI before and after the fire.

// Cloud masking function for Landsat 8 SR
function maskL8sr(image) {
  var qaMask = image.select('QA_PIXEL').bitwiseAnd(1 << 3).eq(0)
      .and(image.select('QA_PIXEL').bitwiseAnd(1 << 4).eq(0));
  return image.updateMask(qaMask)
      .multiply(0.0000275).add(-0.2);
}

// Define area of interest
var fireArea = ee.Geometry.Point([-117.86, 34.25]);

// Pre-fire: summer 2020
var preFire = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2020-05-01', '2020-06-30')
  .filterBounds(fireArea)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .map(maskL8sr)
  .median();

// Post-fire: late fall 2020
var postFire = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2020-11-01', '2020-12-31')
  .filterBounds(fireArea)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .map(maskL8sr)
  .median();

// Calculate NDVI for each period
var preNDVI = preFire.normalizedDifference(['SR_B5', 'SR_B4']).rename('preNDVI');
var postNDVI = postFire.normalizedDifference(['SR_B5', 'SR_B4']).rename('postNDVI');

// Compute the difference
var ndviChange = preNDVI.subtract(postNDVI).rename('NDVI_change');

// Display the result
Map.centerObject(fireArea, 11);
Map.addLayer(ndviChange, {min: -0.5, max: 0.5,
  palette: ['blue', 'white', 'red']}, 'NDVI Change');

What you should see

A map centered on the Angeles National Forest. Red areas show where NDVI dropped dramatically (that's vegetation loss from the fire). White areas had little change. Blue areas, if any, show increased vegetation. Pretty striking, right?

What are we actually detecting?

Change detection answers a beautifully simple question: what looks different between two points in time?

Satellites revisit the same location every few days, giving us a consistent record of the Earth's surface. By comparing images from two dates, we can pinpoint where and how the landscape has changed.

The simplest approach is image differencing. Think of it like this: we take a spectral index (like NDVI or NBR) from a "before" image, compute the same index for an "after" image, and subtract. Pixels with large differences have changed; pixels near zero have not.

Here are some common applications:

  • Wildfire damage: Mapping burn severity after a fire event.
  • Urban expansion: Detecting new impervious surfaces where vegetation once existed.
  • Deforestation: Identifying forest clearing in tropical regions.
  • Flood mapping: Comparing water extent before and after a storm.
  • Hurricane damage: Assessing vegetation loss along a storm's path.

The subtract-and-see approach

The workflow is refreshingly straightforward. First, we build a cloud-free composite for each date. Then we compute a spectral index for both composites. Finally, we subtract the post-event image from the pre-event image.

The sign of the result tells us the direction of change. For vegetation indices like NDVI, a positive difference means vegetation was lost (the "before" value was higher). A negative difference means vegetation actually increased.

// Step 1: Compute NDVI for both dates
var preNDVI = preFire.normalizedDifference(['SR_B5', 'SR_B4']);
var postNDVI = postFire.normalizedDifference(['SR_B5', 'SR_B4']);

// Step 2: Subtract (pre minus post)
// Positive values = vegetation loss
// Negative values = vegetation gain
var ndviDiff = preNDVI.subtract(postNDVI);

// Step 3: Visualize
Map.addLayer(ndviDiff, {
  min: -0.5, max: 0.5,
  palette: ['blue', 'white', 'red']
}, 'NDVI Difference');

Why pre minus post? When vegetation burns, post-fire NDVI drops. Subtracting a smaller post-fire value from a larger pre-fire value produces a positive number, making burned areas appear as high values on the map. This convention is standard in the field, and it's worth memorizing.

How badly did it burn? Meet dNBR

NDVI detects general vegetation change, but the Normalized Burn Ratio (NBR) is purpose-built for fire analysis. Here's the cool part: NBR uses the near-infrared (NIR) and shortwave infrared (SWIR) bands, which are particularly sensitive to burn conditions. Healthy vegetation reflects strongly in NIR and weakly in SWIR; burned areas show the opposite pattern.

The NBR formula for Landsat 8 is:

NBR = (SR_B5 - SR_B7) / (SR_B5 + SR_B7)

The differenced NBR (dNBR) is simply pre-fire NBR minus post-fire NBR. The USGS uses standard dNBR thresholds to classify burn severity, and we can do the same thing right here in Earth Engine.

// Calculate NBR for both periods
var preNBR = preFire.normalizedDifference(['SR_B5', 'SR_B7']).rename('preNBR');
var postNBR = postFire.normalizedDifference(['SR_B5', 'SR_B7']).rename('postNBR');

// Compute dNBR (pre minus post)
var dNBR = preNBR.subtract(postNBR).rename('dNBR');

// Display dNBR
Map.addLayer(dNBR, {
  min: -0.5, max: 1.3,
  palette: ['green', 'yellow', 'orange', 'red', 'darkred']
}, 'dNBR - Burn Severity');

print('dNBR range:', dNBR.reduceRegion({
  reducer: ee.Reducer.minMax(),
  geometry: fireArea.buffer(20000),
  scale: 30,
  maxPixels: 1e9
}));

USGS burn severity classes: The table below shows the standard thresholds used by the USGS Burned Area Emergency Response (BAER) teams. We'll use these same thresholds to classify our dNBR map.

dNBR Range Severity Class
< -0.25Enhanced Regrowth (High)
-0.25 to -0.1Enhanced Regrowth (Low)
-0.1 to 0.1Unburned
0.1 to 0.27Low Severity
0.27 to 0.44Moderate-Low Severity
0.44 to 0.66Moderate-High Severity
> 0.66High Severity

Turning numbers into categories

A continuous difference image is great for visualization, but for reporting and area calculations we need classified categories. Let's use the .gt() and .lt() methods to apply those USGS thresholds and create a classified burn severity map.

// Classify dNBR into burn severity categories
var burnSeverity = ee.Image(0)
  .where(dNBR.lt(-0.25), 1)   // Enhanced regrowth (high)
  .where(dNBR.gte(-0.25).and(dNBR.lt(-0.1)), 2)  // Enhanced regrowth (low)
  .where(dNBR.gte(-0.1).and(dNBR.lt(0.1)), 3)    // Unburned
  .where(dNBR.gte(0.1).and(dNBR.lt(0.27)), 4)    // Low severity
  .where(dNBR.gte(0.27).and(dNBR.lt(0.44)), 5)   // Moderate-low severity
  .where(dNBR.gte(0.44).and(dNBR.lt(0.66)), 6)   // Moderate-high severity
  .where(dNBR.gte(0.66), 7)   // High severity
  .clip(fireArea.buffer(20000))
  .selfMask();

// Display classified map
Map.addLayer(burnSeverity, {
  min: 1, max: 7,
  palette: ['#38a800', '#6fc400', '#ffffbe',
            '#ffff00', '#ffaa00', '#ff5500', '#e60000']
}, 'Burn Severity Classes');

// Calculate area of high severity burn (class 7)
var highSeverity = burnSeverity.eq(7);
var areaImage = highSeverity.multiply(ee.Image.pixelArea());
var area = areaImage.reduceRegion({
  reducer: ee.Reducer.sum(),
  geometry: fireArea.buffer(20000),
  scale: 30,
  maxPixels: 1e9
});
print('High severity area (hectares):', ee.Number(area.get('constant')).divide(10000));

What you should see

A classified map with distinct colors for each severity class. Red and dark red areas represent the most severely burned zones. The console prints the total area of high-severity burn in hectares. This is a powerful insight: from satellite data alone, we can quantify exactly how much land was devastated.

What about slow changes? The LandTrendr idea

Comparing two images works beautifully for sudden events like wildfires. But what about gradual changes? Think of slow deforestation, creeping urban sprawl, or drought stress that unfolds over years. Two dates just can't capture that story.

That's where LandTrendr (Landsat-based Detection of Trends in Disturbance and Recovery) comes in. Instead of comparing just two dates, LandTrendr analyzes the entire time series of Landsat imagery for each pixel. It fits a segmented trend line through decades of annual observations, identifying the year, magnitude, and duration of each disturbance event and recovery period.

We won't implement LandTrendr in this course, but understanding the concept is important. It represents the next step beyond our two-date approach: using the full temporal depth of the satellite archive to tell the complete story of landscape change.

Pro tips

  • Match seasons: Compare images from the same season to avoid detecting phenological changes as real disturbance. Summer-to-summer, winter-to-winter.
  • Cloud mask first: Always apply cloud masking before computing spectral indices. A single cloudy pixel can look like a massive change.
  • Buffer your area: Use .buffer() to expand your study area beyond a single point so you capture the full extent of the event.
  • Check image count: Print collection.size() to verify enough images exist for a clean composite.
  • Use median composites: Median is more robust to outliers than mean for change detection baselines.

Try it: detect a different kind of change

Let's apply the before-after differencing technique to a different event. Pick one of these scenarios and adapt the code:

  • Hurricane damage: Compare NDVI before and after Hurricane Michael (2018) near Panama City, FL. Pre: July-September 2018. Post: November-December 2018.
  • Urban expansion: Compare NDVI for a growing city between 2015 and 2023. Look for areas where vegetation was replaced by impervious surfaces.
// Template: change the point, dates, and index for your chosen event
var studyArea = ee.Geometry.Point([-85.66, 30.19]); // Panama City, FL

var before = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2018-07-01', '2018-09-30')
  .filterBounds(studyArea)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .map(maskL8sr).median();

var after = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
  .filterDate('2018-11-01', '2018-12-31')
  .filterBounds(studyArea)
  .filter(ee.Filter.lt('CLOUD_COVER', 20))
  .map(maskL8sr).median();

var change = before.normalizedDifference(['SR_B5', 'SR_B4'])
  .subtract(after.normalizedDifference(['SR_B5', 'SR_B4']));

Map.centerObject(studyArea, 11);
Map.addLayer(change, {min: -0.3, max: 0.3,
  palette: ['blue', 'white', 'red']}, 'Change');

Common mistakes

  • Using cloudy images: Clouds look like sudden bright changes. Always cloud-mask before computing indices.
  • Wrong date ranges: If your pre-event image is from winter and post-event from summer, you'll detect seasonal change, not the event you're looking for.
  • Forgetting scale/offset: Landsat 8 SR values need .multiply(0.0000275).add(-0.2) before computing indices.
  • Mixing up subtraction order: For dNBR, always compute pre minus post. Reversing the order flips the meaning of your results.
  • Too few images in composite: If only one or two images are available, the composite may still contain artifacts. Check collection.size().

Quick self-check

  1. Why do we subtract the post-event image from the pre-event image (and not the other way around) when computing dNBR?
  2. What two Landsat 8 bands are used to calculate NBR, and why are they effective for burn detection?
  3. A pixel has a dNBR value of 0.55. What burn severity class does it fall into according to USGS thresholds?
  4. Why is it important to compare images from the same season when doing change detection?

Going deeper

This module introduces change detection at a foundational level using two-date comparisons. For advanced techniques including time-series-based change detection, see:

What's next?