Создаем анимацию графиков Классы FuncAnimation и ArtistAnimation

Иногда при отображении графиков требуется увидеть их изменение в некоторой динамике, то есть, нужно делать их анимированными. Благо, пакет matplotlib поддерживает такой функционал, и на этом занятии мы с ним познакомимся.

Режим программной анимации

Наверное, первый и самый простой вариант создания анимации, который приходит на ум, это выполнение обновления графика непосредственно в цикле и отображения в окне новых данных с некоторой временной задержкой. Давайте представим, что мы рисуем график косинусоиды, следующим образом:

import numpy as np
import matplotlib.pyplot as plt
 
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
y = np.cos(x)
 
plt.plot(x, y)
plt.show()

Тогда, почему бы нам не поместить функции plot() и show() в цикл и не обновлять кадр за кадром график косинусоиды, сдвигая ее на возрастающую фазу:

for delay in np.arange(0, np.pi, 0.1):
    y = np.cos(x+delay)
 
    plt.plot(x, y)
    plt.show()

Но это работать не будет, так как функция show() берет все управление на себя и цикл for будет ждать, пока мы не закроем текущее окно с графиком. Такая анимация нам не интересна. Чтобы увидеть настоящее обновление данных в окне на каждой итерации работы цикла for, необходимо включить, так называемый, интерактивный режим отображения данных. Это делается с помощью функции:

plt.ion()

Затем, в цикле на каждой итерации мы будем очищать предыдущие данные с помощью функции clf(), отображать текущий график функцией plot() и прорисовывать окно с новым содержимым, используя функции draw() и gcf().canvas.flush_events():

for delay in np.arange(0, np.pi, 0.1):
    y = np.cos(x+delay)
 
    plt.clf()
    plt.plot(x, y)
    plt.draw()
    plt.gcf().canvas.flush_events()
 
    time.sleep(0.02)

Функция canvas.flush_events() используется, чтобы дать возможность пакету matplotlib обработать свои внутренние события, в том числе и те, что отвечают за перерисовку окна. В последней строчке используется функция sleep() модуля time, которая делает задержку на 0,02 секунды, то есть, на 20 мс. Разумеется, чтобы воспользоваться модулем time, его нужно вначале импортировать:

import time

После цикла for (то есть, после анимации) выключим интерактивный режим и вызовем функцию show(), чтобы окно само не закрывалось:

plt.ioff()
plt.show()

После запуска программы, увидим анимацию «движения» косинусоиды.

Однако, это достаточно медленный вариант создания анимации. Здесь на каждой итерации выполняется полная перерисовка окна, а не только данных, связанных с косинусоидой. Для ускорения процесса следует использовать объектно-ориентированный подход к отображению данных. То есть, вначале нужно создать фигуру и координатную ось (или несколько, при необходимости):

fig, ax = plt.subplots()

Затем, в переменной line сохраним ссылку на объект, представляющий данные графика:

x = np.arange(-2*np.pi, 2*np.pi, 0.1)
y = np.cos(x)
 
line, = ax.plot(x, y)

А, потом, в цикле будем обновлять данные через объект line, следующим образом:

for delay in np.arange(0, 4*np.pi, 0.1):
    y = np.cos(x+delay)
 
    line.set_ydata(y)
 
    plt.draw()
    plt.gcf().canvas.flush_events()
 
    time.sleep(0.02)

Теперь на каждой итерации вызывается метод set_ydata() для обновления только данных по оси y, все остальное не меняем. Это заметно ускоряет процесс перерисовки графика.

После запуска видим ту же анимацию «движения» косинусоиды.

Создание анимации с помощью класса FuncAnimation

Для упрощения создания анимации в пакете matplotlib предусмотрено два специальных класса:

  • FuncAnimation – создание анимации на основе функции;
  • ArtistAnimation – создание покадровой анимации.

Подробную информацию по ним можно посмотреть по следующей ссылке:

https://matplotlib.org/stable/api/animation_api.html

Мы же рассмотрим общие принципы использования этих классов. Сначала воспользуемся FuncAnimation, так как с его помощью можно легко повторить анимацию с косинусоидой. Но, зачем вообще нужен этот класс, если мы сами можем создавать анимированные графики, используя некоторую функцию? Это сделано, главным образом, для упрощения программного кода. Вместо создания циклов и реализаций задержек между кадрами, достаточно импортировать класс FuncAnimation:

from matplotlib.animation import FuncAnimation

и создать его экземпляр со следующими параметрами:

animation = FuncAnimation(
    fig,                # фигура, где отображается анимация
    func=update_cos,    # функция обновления текущего кадра
    frames=phasa,       # параметр, меняющийся от кадра к кадру
    fargs=(line, x),    # дополнительные параметры для функции update_cos
    interval=30,       # задержка между кадрами в мс
    blit=True,          # использовать ли двойную буферизацию
    repeat=False)       # зацикливать ли анимацию

Здесь функция update_cos() – это, как раз, та самая функция, которая на каждой итерации должна обновлять текущий кадр. Определим ее выше, следующим образом:

def update_cos(frame, line, x):
    # frame - параметр, который меняется от кадра к кадру
    # в данном случае - это начальная фаза (угол)
    # line - ссылка на объект Line2D
    line.set_ydata( np.cos(x+frame) )
    return [line]

На входе она будет принимать первый обязательный параметр frame, который содержит данные для обновления текущего кадра. И еще укажем два дополнительных параметра:

  • line – ссылка на объект Line2D для обновления косинусоиды;
  • x – аргументы косинусоиды.

Функция update_cos() должна возвращать итерированный объект (в данном случае список) с объектами, наследуемыми от базового класса:

matplotlib.artist.Artist

В частности, объект Line2D именно такой. Поэтому, достаточно вернуть его в виде списка.

Параметр phasa – это коллекция, которая будет перебираться на каждой итерации:

phasa = np.arange(0, 4*np.pi, 0.1)

то есть, первый параметр функции update_cos(), как раз и будет являться текущим значением этого списка.

Затем, через параметр fargs мы указываем дополнительные аргументы для функции update_cos(). И далее идут три следующих очевидных параметра.

В конце программы оставим строчку:

plt.show()

чтобы окно после анимации не закрывалось. Все, анимация готова и при запуске мы увидим то же «движение» косинусоиды.

Создание анимации с помощью класса ArtistAnimation

С помощью класса ArtistAnimation анимация создается на основе уже готового списка объектов для каждого кадра. Поэтому, такой подход требует гораздо больше памяти устройства, чем предыдущий с использованием класса FuncAnimation. Но плюсом является возможность создания более сложной анимационной картины.

Список для ArtistAnimation должен состоять из объектов, производных от класса:

matplotlib.artist.Artist

Данный класс хорошо подходит для анимирования 3D фигур, так как на их формирование требуется некоторое время. Давайте выполним такую анимацию в нашей программе.

Вначале импортируем класс ArtistAnimation:

from matplotlib.animation import ArtistAnimation

Сформируем 3D ось:

fig = plt.figure(figsize=(10, 6))
ax_3d = fig.add_subplot(projection='3d')

И координаты сетки в плоскости xy:

x = np.arange(-2*np.pi, 2*np.pi, 0.2)
y = np.arange(-2*np.pi, 2*np.pi, 0.2)
xgrid, ygrid = np.meshgrid(x, y)

Затем, определим список для изменяемого параметра и коллекцию для хранения объектов Artist:

phasa = np.arange(0, 2*np.pi, 0.1)
frames = []

Далее, в цикле сформируем объекты Artist для последующей их анимации:

for p in phasa:
    zgrid = np.sin(xgrid+p) * np.sin(ygrid) / (xgrid * ygrid)
 
    line = ax_3d.plot_surface(xgrid, ygrid, zgrid, color='b')
    frames.append([line])

Обратите внимание, коллекция frames должна в качестве элементов содержать список объектов, поэтому, line здесь записана в квадратных скобках.

Сама анимация будет выглядеть уже знакомым нам образом:

animation = ArtistAnimation(
    fig,                # фигура, где отображается анимация
    frames,              # кадры
    interval=30,        # задержка между кадрами в мс
    blit=True,          # использовать ли двойную буферизацию
    repeat=True)       # зацикливать ли анимацию

И в конце, как всегда, используем функцию:

plt.show()

Все, наша анимация 3D графика готова. Как видите, все делается достаточно просто и вы теперь сможете все это, при необходимости, использовать в своих проектах.