Иногда при
отображении графиков требуется увидеть их изменение в некоторой динамике, то
есть, нужно делать их анимированными. Благо, пакет 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, необходимо
включить, так называемый, интерактивный режим отображения данных. Это делается
с помощью функции:
Затем, в цикле
на каждой итерации мы будем очищать предыдущие данные с помощью функции 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, его нужно вначале
импортировать:
После цикла for (то есть, после
анимации) выключим интерактивный режим и вызовем функцию show(), чтобы окно
само не закрывалось:
После запуска
программы, увидим анимацию «движения» косинусоиды.
Однако, это
достаточно медленный вариант создания анимации. Здесь на каждой итерации
выполняется полная перерисовка окна, а не только данных, связанных с
косинусоидой. Для ускорения процесса следует использовать
объектно-ориентированный подход к отображению данных. То есть, вначале нужно
создать фигуру и координатную ось (или несколько, при необходимости):
Затем, в
переменной 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(). И далее идут
три следующих очевидных параметра.
В конце
программы оставим строчку:
чтобы окно после
анимации не закрывалось. Все, анимация готова и при запуске мы увидим то же
«движение» косинусоиды.
Создание анимации с помощью класса 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) # зацикливать ли анимацию
И в конце, как
всегда, используем функцию:
Все, наша
анимация 3D графика готова.
Как видите, все делается достаточно просто и вы теперь сможете все это, при
необходимости, использовать в своих проектах.