Detecting Change with CCDC on Google Earth Engine

Daniel Moraes
6 min readMar 25, 2022

--

Cloud computing platforms have contributed to make the analysis of satellite images time series easier. In this tutorial, the Continuous Change Dectection and Classification (CCDC) algorithm available on the Google Earth Engine (GEE) platform is used to detect the year of the most recent change in a time series of Sentinel-2 images.

Setting the Image Time Series (Image Collection)

In order to define our image collection, we need to indicate the collection name, start and end dates and the geometry of our study area. The study area can be defined by setting the west, south, east, northcoordinates or simply by entering a custom geometry defined by the user. The image collection used in this example is the Sentinel-2 Level2A, which can be imported on GEE as COPERNICUS/S2_SR.

Next, a function is created to clip an image to the limits of the study area, and then it is applied to all images in the collection using the map operator.

After that, we want to apply a filter to remove clouds. In this example, we are going to use the Sentinel-2 Scene Classification map (SCL) to do the filtering. We then use the map operator again to apply the filtering function to all images in the collection.

Lastly, we are interested in computing the Normalized Difference Vegetation Index (NDVI), since it’s going to be used in the process of change detection. It is important to multiply the normalized difference by 10,000, so that it matches the limits of the Sentinel-2 bands.

var study_area = my_geometry; //ee.Geometry.BBox(west, south, east, north);
var date_start = '2016-01-01';
var date_end = '2021-12-15';

// Center Map
var long = ee.Number(study_area.centroid()
.coordinates().get(0)).getInfo();
var lat = ee.Number(study_area.centroid()
.coordinates().get(1)).getInfo();
Map.setCenter(long,lat,11);

// Define image collection
var S2 = ee.ImageCollection("COPERNICUS/S2_SR")
.filterBounds(study_area)
.filterDate(date_start,date_end);
// Clip collection
function clipCollection(image) {
var clippedImage = image.clip(study_area);
return clippedImage;
}
var S2_clipped = S2.map(clipCollection);

// Apply cloud filter
function filterClouds(image){
var SCL = image.select('SCL');
var mask01 = ee.Image(0).where(
SCL.lt(8).and(SCL.gt(3))
,1); //Put a 1 on good pixels
return image.updateMask(mask01);
}
var S2filtered = S2_clipped.map(filterClouds);

// Add NDVI
function addNDVI(image) {
var ndvi = image.normalizedDifference(['B8',
'B4']).multiply(10000).int16();
return image.addBands(ndvi.rename('ndvi'));
}
var S2filtered = S2filtered.map(addNDVI);
Study area defined by custom geometry.

Setting CCDC Parameters

For detailed information regarding CCDC parameters, the reader is referred to the GEE documentation. Here, we’re going to configure the breakpointBands to use the NDVI, Green (B3) and SWIR2 (B12) for the change detection. We’re also setting tmaskBands to Green (B3) and SWIR2 (B12) to provide additional cloud filtering. We set as the collection our pre-processed collection S2filtered, use lambda as 100 and leave the remaining parameters as default.

var ccdc_params = {
collection: S2filtered.select(['ndvi', 'B3', 'B12']),
breakpointBands: ['ndvi','B3','B12'],
tmaskBands: ['B3','B12'],
minObservations: 6,
chiSquareProbability: .99,
minNumOfYearsScaler: 1.33,
dateFormat: 2,
lambda: 100,
maxIterations: 25000
};

Note: you can try to fine tune the parameters to obtain better detection results.

Run CCDC Algorithm

In this stage, we’re simply going to run the CCDC algorithm with the parameters defined previously.

var ccdc_result = ee.Algorithms.TemporalSegmentation.Ccdc(ccdc_params);

On the CCDC results

CCDC works by fitting a harmonic regression to the data to detect breaks in the time series, generating one or more segments of regression depending on the number of breaks found. The segments are characterized according to a variety of aspects, such as their start and end dates and number of observations present in the segment. The result obtained from CCDC consists of a multiband image, with a structure similar to the one represented in the image below.

CCDC Output structure.

Since for each pixel in the image collection the CCDC algorithm generates information about the segments (which can be more than one), a special data structure is needed to store such information: array bands. Array bands allow a pixel to have an array of values, instead of having a single number (scalar) as usual. In order to understand what this means, let’s take a look at the chart in the image below, which represents the segments produced by CCDC for a given pixel.

Segments produced by CCDC for a given pixel.

If we use the GEE Inspector to check the information of this given pixel, it is possible to see that for each band of the CCDC output there are two values: one for each segment. These values are stored in the pixel in the form of an array of length 2. If we had “n” segments, then the pixel would have an array of length “n”.

Obtaining the date of the last break detected by CCDC

As we are interested in detecting the last time a break occurred, we need to get the value in the arrays at the position that corresponds to the last break detected by CCDC. In order to accomplish this task, the function ee.Image.arrayArgmax is used, returning the positional indices of the maximum value (most recent time) in the band “tBreak” of the CCDC output ccdc_result.

Next, we apply the function ee.Image.arrayFlatten to convert the result from array pixels to scalar pixels. Finally, we use the function ee.Image.arrayGet to get the values in tbreak at the positions indicated in argmax_scalar.

var tbreak = ccdc_result.select(['tBreak']);
var argmax_array = tbreak.arrayArgmax();
var argmax_scalar = argmax_array.arrayFlatten([['argmax_array']]);
var last_break = tbreak.arrayGet(argmax_scalar);

It’s done, we have an image last_breakwith the time of the last change in milliseconds. We can export it if we want. However, since the results are given in milliseconds, we can also convert them to a more meaningful value (e.g. year).

var year_last_break = ee.Image.constant(1970)
.add(last_break.divide(
ee.Image.constant(365*24*3600*1000)));

Finally, we can add the image to the map and create some custom symbology. We can also insert a legend.

// CREATE SYMBOLOGY TO APPLY TO THE IMAGE
var sld_intervals =
'<RasterSymbolizer>' +
'<ColorMap type="intervals" extended="false">' +
'<ColorMapEntry color="#ca0020" quantity="2016" label="2016"/>' +
'<ColorMapEntry color="#ec846e" quantity="2017" label="2017"/>' +
'<ColorMapEntry color="#fbd6a7" quantity="2018" label="2018"/>' +
'<ColorMapEntry color="#d4e2cc" quantity="2019" label="2019"/>' +
'<ColorMapEntry color="#76b4d5" quantity="2020" label="2020"/>' +
'<ColorMapEntry color="#0571b0" quantity="2021" label="2021"/>' +
'</ColorMap>' +
'</RasterSymbolizer>';
Map.addLayer(year_last_break.sldStyle(sld_intervals),{}, 'Year of Last Break');
//Add legend to map
// set position of panel
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 1 row of the legend.
var makeRow = function(color, name) {

// Create the label that is actually the colored box.
var colorBox = ui.Label({
style: {
backgroundColor: '#' + color,
// Use padding to give the box height and width.
padding: '8px',
margin: '0 0 4px 0'
}
});

// Create the label filled with the description text.
var description = ui.Label({
value: name,
style: {margin: '0 0 4px 6px'}
});

// return the panel
return ui.Panel({
widgets: [colorBox, description],
layout: ui.Panel.Layout.Flow('horizontal')
});
};

// Palette with the colors
var palette =['ca0020', 'ec846e', 'fbd6a7', 'd4e2cc', '76b4d5', '0571b0'];

// labels of legend items
var names = ['2016','2017', '2018', '2019', '2020', '2021'];

// Add colors and names
for (var i = 0; i < 6; i++) {
legend.add(makeRow(palette[i], names[i]));
}

// add legend to map (alternatively you can also print the legend to the console)
Map.add(legend);
Year of the last break as identified by the CCDC algorithm.

Link for the script on GEE:

https://code.earthengine.google.com/f51a114eacf15c75697ef56bb4e7d875

--

--