Generating animated time series

Background

Animations can be a powerful method for visualising change in the landscape across time using satellite imagery. Satellite data from Digital Earth Australia is an ideal subject for animations as it has been georeferenced, processed to analysis-ready surface reflectance, and stacked into a spatio-temporal ‘data cube’, allowing landscape conditions to be extracted and visualised consistently across time.

Using functions based on matplotlib.animation and xarray, we can take a time series of Digital Earth Australia satellite imagery and export a visually appealing time series animation that shows how any location in Australia has changed over the past 30+ years.

Description

This notebook demonstrates how to:

  1. Import a time series of cloud-free satellite imagery from multiple satellites (i.e. Sentinel-2A and 2B) as an xarray dataset

  2. Plot the data as a time series animation

  3. Export the resulting animations as either a GIF or MP4 file


Getting started

To run this analysis, run all the cells in the notebook, starting with the “Load packages” cell.

Load packages

[1]:
%matplotlib inline

import sys
import datacube
import matplotlib.pyplot as plt
from IPython.display import Image

sys.path.append('../Scripts')
from dea_plotting import animated_timeseries
from dea_plotting import rgb
from dea_datahandling import load_ard

Connect to the datacube

[2]:
dc = datacube.Datacube(app='Animated_timeseries')

Load satellite data from datacube

We can use the load_ard() function to load data from multiple satellites (i.e. Sentinel-2A and -2B), and return a single xarray.Dataset containing only observations with a minimum percentage of good quality pixels. This will allow us to create a visually appealing time series animation of observations that are not affected by cloud.

In the example below, we request that the function returns only observations which are 95% free of clouds and other poor quality pixels by specifyinge min_gooddata=0.95.

[3]:
# Set up a datacube query to load data for
query = {'x': (142.41, 142.57),
         'y': (-32.225, -32.325),
         'time': ('2018-09-01', '2019-09-01'),
         'measurements': ['nbart_red',
                          'nbart_green',
                          'nbart_blue',
                          'nbart_nir_1',
                          'nbart_swir_2'],
         'output_crs': 'EPSG:3577',
         'resolution': (-30, 30)}

# Load available data from both Sentinel 2 satellites
ds = load_ard(dc=dc,
              products=['s2a_ard_granule', 's2b_ard_granule'],
              min_gooddata=0.95,
              mask_pixel_quality=False,
              group_by='solar_day',
              **query)

Loading s2a_ard_granule data
    Filtering to 8 out of 33 observations
Loading s2b_ard_granule data
    Filtering to 11 out of 35 observations
Combining and sorting data
Masking out invalid values
    Returning 19 observations

To get a quick idea of what the data looks like, we can plot a selection of observations from the dataset in true colour using the rgb function:

[4]:
# Plot four images from the dataset
rgb(ds, index=[0, 6, 12, 18])

../../_images/notebooks_Frequently_used_code_Animated_timeseries_11_0.png

Plot time series as a RGB/three band animated GIF

The animated_timeseries() function is based on functionality within matplotlib.animation. It takes an xarray.Dataset and exports a one band or three band (e.g. true or false colour) GIF or MP4 animation showing changes in the landscape across time.

Here, we plot the dataset we loaded above as an animated GIF. The interval between the animation frames is set to to 200 milliseconds and the width of the animation to 500 pixels (the default values). For a three-band RGB animation like this, the function will automatically select an appropriate colour stretch by clipping the data to remove values smaller or greater than the 2 and 98th percentiles. This can be controlled further with the percentile_stretch parameter.

[5]:
# Produce time series animation of red, green and blue bands
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    interval=200,
                    width_pixels=300)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[5]:
<IPython.core.display.Image object>

We can also use different band combinations (e.g. false colour), add a title, and change the font size using annotation_kwargs, which passes a dictionary of values to the matplotlib plt.annotate function (see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.annotate.html for options):

[6]:
# Produce time series animation of red, green and blue bands
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    bands=['nbart_swir_2', 'nbart_nir_1', 'nbart_green'],
                    interval=200,
                    width_pixels=300,
                    title='Time-series animation',
                    percentile_stretch=[0.01, 0.99],
                    annotation_kwargs={'fontsize': 25})

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[6]:
<IPython.core.display.Image object>

Plotting single band animations

It is also possible to plot a single band image instead of a three band image. For example, we could plot an index like the Normalized Difference Water Index (NDWI), which has high values where a pixel is likely to be open water (e.g. NDWI > 0).

By default the colour bar limits are set based on percentile_stretch which will discard outliers/extreme values to optimise the colour stretch (set percentile_stretch=(0.0, 1.00) to show the full range of values from min to max).

[7]:
# Compute NDWI using the formula (green - nir) / (green + nir).
# This will calculate NDWI for every time-step in the dataset:
ds['NDWI'] = ((ds.nbart_green - ds.nbart_nir_1) /
              (ds.nbart_green + ds.nbart_nir_1))

# Produce time series animation of NDWI:
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    bands=['NDWI'],
                    title='NDWI',
                    width_pixels=300)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[7]:
<IPython.core.display.Image object>

We can customise animations based on a single band like NDWI by specifying parameters using onebandplot_kwargs, which is passed to the matplotlib plt.imshow function (see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.imshow.html for options). For example, we can use a more appropriate blue colour scheme with 'cmap': 'Blues', and set 'vmin': 0.0, 'vmax': 0.5 to overrule the default colour bar limits with manually specified values:

[8]:
# Produce time series animation using a custom colour scheme and limits:
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    bands=['NDWI'],
                    width_pixels=300,
                    title='NDWI',
                    onebandplot_kwargs={'cmap': 'Blues',
                                        'vmin': 0.0,
                                        'vmax': 0.5})

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[8]:
<IPython.core.display.Image object>

Two special kwargs (tick_fontsize, tick_colour) can be used to control the tick labels on the colourbar. This can be useful for example when the tick labels are difficult to see against a dark background:

[9]:
# Produce time series animation using a custom colour scheme and limits:
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    bands=['NDWI'],
                    width_pixels=300,
                    title='NDWI',
                    onebandplot_kwargs={'cmap':'Blues',
                                        'vmin':0.0,
                                        'vmax':0.5,
                                        'tick_fontsize': 10,
                                        'tick_colour': 'grey'})

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')
Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[9]:
<IPython.core.display.Image object>

One band animations show a colour bar by default, but this can be disabled:

[10]:
# Produce time series animation without a colour bar:
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    bands=['NDWI'],
                    width_pixels=300,
                    title='NDWI',
                    onebandplot_kwargs={'cmap': 'Blues',
                                        'vmin': 0.0,
                                        'vmax': 0.5},
                    onebandplot_cbar=False)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[10]:
<IPython.core.display.Image object>

Adding shapefile overlays

The animation code supports plotting shapefiles over satellite imagery. To do this, pass in a shapefile path with shapefile_path. The shapefile must be in the same projection system as the satellite data (e.g. Australian Albers EPSG:3577 below):

[11]:
# Get shapefile path
shp_path = '../Supplementary_data/Animated_timeseries/poly.shp'

# Produce time series animation
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    width_pixels=300,
                    shapefile_path=shp_path)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[11]:
<IPython.core.display.Image object>

You can customise styling for the shapefile overlays using the shapefile_kwargs argument:

[12]:
# Get shapefile path
shp_path = '../Supplementary_data/Animated_timeseries/poly.shp'

# Produce time series animation
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    width_pixels=300,
                    shapefile_path=shp_path,
                    shapefile_kwargs = {'linewidth': 2,
                                        'edgecolor': 'black',
                                        'facecolor': 'white'})

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[12]:
<IPython.core.display.Image object>

Multiple shapefiles can be overlaid by providing a list of shapefile paths:

[13]:
# Create list of shapefile paths
shp_paths = ['../Supplementary_data/Animated_timeseries/poly.shp',
             '../Supplementary_data/Animated_timeseries/point.shp',
             '../Supplementary_data/Animated_timeseries/line.shp']

# Produce time series animation
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    width_pixels=300,
                    shapefile_path=shp_paths)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[13]:
<IPython.core.display.Image object>

To customise styling for each shapefile individually, you can pass in a list of dictionaries to shapefile_kwargs. This list should be equal in length to the number of shapefile paths you pass to shapefile_path (e.g. one dictionary corresponding to each path).

[14]:
# Create list of shapefile paths
shp_paths = ['../Supplementary_data/Animated_timeseries/poly.shp',
             '../Supplementary_data/Animated_timeseries/point.shp',
             '../Supplementary_data/Animated_timeseries/line.shp']

# Create list of shapefile styles
shp_styles = [
              # Style for first shapefile (poly.shp)
              {'linewidth': 2,
               'edgecolor': 'black',
               'facecolor': 'white'},

              # Style for second shapefile (point.shp)
               {'markersize': 100,
                'edgecolor': 'black',
                'facecolor': 'black'},

              # Style for third shapefile (line.shp)
               {'linewidth': 4,
                'edgecolor': 'blue'}
             ]

# Produce time series animation
animated_timeseries(ds=ds,
                    output_path='animated_timeseries.gif',
                    width_pixels=300,
                    shapefile_path=shp_paths,
                    shapefile_kwargs=shp_styles)

# Plot animated gif
plt.close()
Image(filename='animated_timeseries.gif')

Generating 19 frame animation
    Exporting animation to animated_timeseries.gif
[14]:
<IPython.core.display.Image object>

Available output formats

The above examples have focused on exporting animated GIFs, but MP4 files can also be generated. The two formats have their own advantages and disadvantages:

  • .mp4: fast to generate, smallest file sizes and often highest quality; suitable for Twitter/social media and recent versions of Powerpoint

  • .gif: slow to generate, large file sizes; suitable for all versions of Powerpoint and Twitter/social media

[15]:
# Animate datasets as a MP4 file
animated_timeseries(ds=ds, output_path='animated_timeseries.mp4')

Generating 19 frame animation
    Exporting animation to animated_timeseries.mp4
../../_images/notebooks_Frequently_used_code_Animated_timeseries_33_1.png

Additional information

License: The code in this notebook is licensed under the Apache License, Version 2.0. Digital Earth Australia data is licensed under the Creative Commons by Attribution 4.0 license.

Contact: If you need assistance, please post a question on the Open Data Cube Slack channel or on the GIS Stack Exchange using the open-data-cube tag (you can view previously asked questions here). If you would like to report an issue with this notebook, you can file one on Github.

Last modified: December 2019

Compatible datacube version:

[16]:
print(datacube.__version__)
1.7

Tags

Browse all available tags on the DEA User Guide’s Tags Index

Tags: NCI compatible, sandbox compatible, sentinel 2, dea_datahandling, dea_plotting, load_ard, rgb, animated_timeseries, matplotlib.animation, NDWI, animation