Visualizing changing landscapes with Google Earth Engine

Google Earth
Google Earth and Earth Engine
5 min readMay 27, 2020



, Technical Writer, Earth Engine

Earth’s landscapes are constantly changing. Ephemeral seasonal cycles blanket landscapes in snow, paint hillsides with colorful wildflowers, and dress trees in green. Longer-lasting change is happening too. Forest harvesting and wildfires alter vegetation composition, urban development paves over nature, and erosion sculpts the land. Earth-observing satellites have been a witness to these changes. Now Google Earth Engine is making it easy to convert the image data into time lapse animations that capture the dynamism of nature and humans.

In this follow-up to a previous post introducing basic animation techniques in Earth Engine, I’ll use a section of Peru’s wildly dynamic Ucayali River to demonstrate more advanced animation techniques that emphasize change by imprinting historical context into each frame of the animation using fading and cumulative history effects. A river animation app is provided, so if coding is not your thing, feel free to skip to the end.

Note that while this tutorial uses river dynamics to demonstrate fading and cumulative history animation effects, these techniques are applicable to any thematic map class that changes over space and time. For instance, the same methods can be used to relate forest clearcut history, air pollutant transport, wildfire history, or glacier retreat, to name a few.

Peru’s meandering Ucayali River seen from Google Earth.

Basic animation

First things first — we’ll need some data. Taking a look at the Earth Engine Data Catalog, you’ll find the JRC Yearly Water Classification History dataset. These data are global maps of surface water for each year from 1984 through 2018 with 30-meter resolution — just what we need!

Fire up the Code Editor and import the dataset. Map a function over the image collection to set the values representing water pixels in each annual layer to its respective year. These values will be used later to stretch a color palette over the data. Reduce speckle from small ephemeral water bodies in the data by masking out connected pixel groups with less than 15 members. Also, set all non-water pixels as transparent so we can control river channel opacity.

var col = ee.ImageCollection('JRC/GSW1_1/YearlyHistory').map(function(img) {
var year ='year');
var yearImg = img.gte(2).multiply(year);
var despeckle = yearImg.connectedPixelCount(15, true).eq(15);
return yearImg.updateMask(despeckle).selfMask().set('year', year);

Each image in the collection represents a frame in the animation. Since we’re animating flow, let’s define a function that will reverse the frame sequence once the end is reached to produce a smooth, flowy transition back to the beginning. This is accomplished by making a copy of the image collection in reverse chronological order and appending it to the original collection.

function appendReverse(col) {
return col.merge(col.sort('year', false));

Next, color water pixels blue and land pixels white to make the subject of the animation more self-evident. Map over the image collection to define visualization properties for each image. Append the reversed frame set to the forward set.

var bgColor = 'FFFFFF'; // Assign white to background pixels.
var riverColor = '0D0887'; // Assign blue to river pixels.
var annualCol = {
return img.unmask(0)
.visualize({min: 0, max: 1, palette: [bgColor, riverColor]})
.set('year', img.get('year'));
var basicAnimation = appendReverse(annualCol);

Finally, define a section of stream to animate, set animation arguments, and render it.

var aoi = ee.Geometry.Rectangle(-74.327, -10.087, -73.931, -9.327);var videoArgs = {
600, // Max dimension (pixels), min dimension is proportionally scaled.
region: aoi,
framesPerSecond: 10
print(ui.Thumbnail(basicAnimation, videoArgs));
An animation of JRC’s Global Surface Water dataset depicting 32 years of channel dynamics for Peru’s Ucayali River.

As you can see, this particular section of the river is quite unbounded! The animation looks okay, but it has some unpolished qualities and does not convey channel history very well. Let’s add a fading memory of river channels past to smooth out the animation and convey a better sense of movement by providing historical reference.

Fading history animation

Currently, only a single year’s channel is shown per frame. In this next animation, we’ll use opacity and compositing to retain and progressively fade all prior years per frame. To do this, add a nearly transparent fade filter overlay to each annual river image, and then construct a collection where each image represents the temporal composite of all previous fade-filtered images overlaid by an unfiltered image from the given year. As more images are included in the composite, the fade filters accumulate making the earliest pixels more opaque, eventually resolving to the color of the fade filter.

var bgImg = ee.Image(1).visualize({palette: bgColor});
var fadeFilter = ee.Image(1).visualize({palette: bgColor, opacity: 0.1});
var fadeFilterCol = {
var imgVis = img.visualize({palette: riverColor});
return imgVis.blend(fadeFilter).set('year', img.get('year'));
var yearSeq = ee.List.sequence(1984, 2018);var fadeCol = ee.ImageCollection.fromImages( {
var fadeComp =
fadeFilterCol.filter(ee.Filter.lte('year', year)).sort('year').mosaic();
var thisYearImg = col.filter(ee.Filter.eq('year', year)).first().visualize({
palette: riverColor
return bgImg.blend(fadeComp).blend(thisYearImg).set('year', year);
print(ui.Thumbnail(appendReverse(fadeCol), videoArgs));
Animation of Peru’s Ucayali River demonstrating a fading memory technique to smooth the animation and provide historical context.

Much better! The fading channel effect smooths out inter-annual transitions and provides historical context to more easily comprehend dynamics.

Cumulative history animation

We can alter the previous animation slightly to show the full river channel history by removing the fade filter image overlay and assigning a distinct color to each year’s river channel. The process is the same as the previous script, except that we don’t blend the fade filter image, and instead of assigning the color blue to water pixels, they are colored according to year by stretching a palette across the range of years.

var cumulativeVis = {
min: 1984,
max: 2018,
palette: ['0D0887', '5B02A3', '9A179B', 'CB4678', 'ED1E79']
var cumulativeFilterCol = {
return img.visualize(cumulativeVis).set('year', img.get('year'));
var cumulativeCol = ee.ImageCollection.fromImages( {
var cumulativeComp = cumulativeFilterCol.filter(ee.Filter.lte('year', year))
return bgImg.blend(cumulativeComp).set('year', year);
print(ui.Thumbnail(appendReverse(cumulativeCol), videoArgs));
Animation of Peru’s Ucayali River demonstrating a cumulative history technique to smooth the animation and provide historical context.

There you have it: 32 years of river channel history in a few seconds! I hope you’ve enjoyed this code walkthrough demonstrating image overlay and compositing techniques to customize time series animations with Earth Engine. If you’re interested in learning more, please see the Earth Engine ImageCollection Visualization Guide.

Surface water history app

I was intrigued by how active the Ucayali River is and was curious to know how many other rivers meander so freely, so I built an Earth Engine App to make exploring easier. It features a thematic map that displays the most recent channel path year and allows you to draw a rectangle over a region to generate an animation anywhere on Earth. Customize the frame and history fade rate before you download the animated GIF image.

Interactive JRC Surface Water animation app built using Google Earth Engine Apps.

Post your animations to Twitter and let us know what you discover by tagging: @googleearth #RiverGIF