Python animation with ffmpeg#

Software requirements:

  • Python 3

  • numpy

  • xarray

  • matplotlib

  • cartopy

  • ffmpeg

Example script#

animation_with_ffmpeg_normal_and_faster.py

#!/usr/bin/env python
# coding: utf-8
'''
DKRZ tutorial

Animation of geospatial data

It is quite easy to create a video with Matplotlib and FFmpeg from time-
dependent data. In this notebook the deployment of the temperature data over 
time is shown using two slightly different ways, which differ significantly 
in runtime.

Note:
  In case of problems with FFmpeg it is advisable to install the latest
  version and set the ffmpeg_path via plt.rcParams to animation.ffmpeg_path
  as already used in this notebook.

Content

- Create the animation as usual  (run time: 16.4s)
- Create the animation 8x faster (run time:  1.9s)
- save the animation in mpeg4 file

The example input file _rectilinear_grid_2D.nc_ can be downloaded from
      https://nextcloud.dkrz.de/s/WHGZgCWrZLPnpBn

FFmpeg home page: https://ffmpeg.org/

-------------------------------------------------------------------------------
2022 copyright DKRZ licensed under CC BY-NC-SA 4.0
               (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
-------------------------------------------------------------------------------
'''
import time, os
import numpy as np
import xarray as xr

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib import animation
import cartopy.crs as ccrs

def main():
    # Set the path of the ffmpeg binary, e.g. in a 'cartopy' conda environment
    ffmpeg_path = os.environ['HOME']+'/miniconda3/envs/cartopy/bin/ffmpeg'
    plt.rcParams['animation.ffmpeg_path'] = ffmpeg_path
    
    # Open data set
    infile = '../../data/rectilinear_grid_2D.nc'
    ds = xr.open_dataset(infile)
    
    # Read the data and coordinate variables.
    lon = ds.lon
    lat = ds.lat
    var = ds.tsurf
    
    # Set plotting parameters
    # -----------------------
    # Use the dictionary `plot_parameter` to define some plot settings.
    plot_parameter = {}
    plot_parameter['extent'] = [-180, 180, -90, 90]
    plot_parameter['projection'] = ccrs.PlateCarree()
    plot_parameter['vmin'] = 240.
    plot_parameter['vmax'] = 300.
    plot_parameter['cmap'] = 'RdBu_r'
    plot_parameter['title'] = var.long_name
    
    # Get number of frames
    # --------------------
    # The input dataset contains 40 time steps and we want to create the animation
    # over all.
    frames = ds.time.size
    
    # Get date strings
    # ----------------
    # The date string should be added on top of the plot and therefore we get the
    # date strings in a nicer notation.
    time_str = ds.time.dt.strftime('%Y-%m-%d').data
    
    #------------------------------------------------------------------------------
    # Define function plot_init()
    #
    # This function is needed as input for the `init_func` parameter of
    # Matplotlib's `FuncAnimation`. It presets in this case the extent of the
    # map, the title  and coastline drawing.
    #------------------------------------------------------------------------------
    def plot_init():
        ax.set_extent(plot_parameter['extent'])
        ax.coastlines()
        plt.title(plot_parameter['title'], fontsize=20)
        return plot
    
    #------------------------------------------------------------------------------
    # Create the animation as usual
    # -----------------------------
    # We use Matplotlib's `FuncAnimation` routine to make an animation from the
    # time-dependent variable _surface temperature_. `FuncAnimation` needs as
    # second input parameter a _function to call at each frame_. In this case we
    # define the function `update_frame()` for creating a plot for a given
    # timestep (frame).
    
    #------------------------------------------------------------------------------
    # Define function update_frame()
    #
    # This is the function that is called for each time step (frame) to generate
    # the corresponding plot.
    #------------------------------------------------------------------------------
    def update_frame(frame):
        plot = ax.pcolormesh(lon, lat, var[frame,:,:],
                         cmap=plot_parameter['cmap'],
                         vmin=plot_parameter['vmin'],
                         vmax=plot_parameter['vmax'])
        tx.set_text(time_str[frame])
        return plot, tx
    
    # Create the animation I
    # ----------------------
    # Now, we can generate the animation doing the following steps:
    #
    # 1. define figure and axis
    # 2. create the plot object for the first time step
    # 3. add a colorbar
    # 4. add the date string on top
    # 5. create the animation in memory
    # 6. save the animation to a mpeg4 file
    t1 = time.time()
    
    fig, ax = plt.subplots(figsize=(16,6),
                           subplot_kw={"projection": plot_parameter['projection']})
    
    plot = ax.pcolormesh(lon, lat, var[0,:,:],
                         cmap=plot_parameter['cmap'],
                         vmin=plot_parameter['vmin'],
                         vmax=plot_parameter['vmax'])
    cb = plt.colorbar(plot)
    tx = fig.text(0.69, 0.89, time_str[0])
    
    ani = FuncAnimation(fig, update_frame, frames=range(0, frames),
                         init_func=plot_init)
    
    ani.save('tsurf_animation.mp4',
             writer=animation.FFMpegWriter(fps=60, bitrate=5000, codec='h264'),
             dpi=100)
    
    t2 = time.time()
    print('run time: '+str(t2-t1))
    
    #------------------------------------------------------------------------------
    # Create the animation 8x faster
    # ------------------------------
    # Instead of creating a new plot for each time step, we only exchange the
    # data array for the plot in the `update_frame2()` function with Matplotlib's
    # `plot.set_array()` method. This makes the creation of the animation much
    # faster.
    
    #------------------------------------------------------------------------------
    # Define function update_frame2()
    #
    # Exchange the data array of the plot with Matplotlib's `plot.set_array()`
    # method. This makes the creation of the animation much faster.
    #------------------------------------------------------------------------------
    def update_frame2(frame):
        plot.set_array(np.array(var[frame,:,:]).ravel())
        tx.set_text(time_str[frame])
        return plot, tx
    
    # Create the animation II
    # -----------------------
    # Same as above but this time we use the `update_frame2()` function to create
    # the animation with Matplotlib's `FuncAnimation`.
    #
    # 1. define figure and axis
    # 2. create the plot object for the first time step
    # 3. add a colorbar
    # 4. add the date string on top
    # 5. create the animation in memory
    # 6. save the animation to a mpeg4 file
    t1 = time.time()
    
    fig, ax = plt.subplots(figsize=(16,6),
                           subplot_kw={"projection": plot_parameter['projection']})
    
    plot = ax.pcolormesh(lon, lat, var[0,:,:],
                         cmap=plot_parameter['cmap'],
                         vmin=plot_parameter['vmin'],
                         vmax=plot_parameter['vmax'])
    cb = plt.colorbar(plot)
    tx = fig.text(0.69, 0.89, time_str[0])
    
    ani = FuncAnimation(fig, update_frame2, frames=range(0, frames),
                         init_func=plot_init)
    
    ani.save('tsurf_animation.mp4',
             writer=animation.FFMpegWriter(fps=60, bitrate=5000, codec='h264'),
             dpi=100)
    
    t2 = time.time()
    print('run time: '+str(t2-t1))


if __name__ == '__main__':
    main()

Plot result#

image0