这里记录一下 Python 的 matplotlib 的动画效果,注意我们需要分别考虑如下三种运行环境,它们对于动画的支持是不一样的:

  • 浏览器启动 Jupyter Notebook 或 Jupyter Lab
  • 直接运行 Python 脚本
  • VSCode 启动 Jupyter(目前仍然不支持动画)

例如浏览器启动 Jupyter 可能需要相关的插件(例如 ipympl 插件),并且需要魔法命令

1
%matplotlib notebook

在启动内核后,至少需要执行这个魔法命令一次,否则无法播放动画,两个同时播放的动画甚至还会相互影响。

在直接运行 Python 脚本时,Jupyter 的魔法指令是语法错误,对于动画而言,直接运行 Python 脚本反而比 Jupyter 更方便。

matplotlib 绘制动画主要包括两种方法:FuncAnimation 和 ArtistAnimation,下面的代码都需要导入如下的库

1
2
3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

FuncAnimation

FuncAnimation 的原理是首先绘制第一帧图像,然后逐帧调用更新函数进行修改,达到动画效果。

官方文档的示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
%matplotlib notebook
fig, ax = plt.subplots()
t = np.linspace(0, 3, 40)
g = -9.81
v0 = 12
z = g * t**2 / 2 + v0 * t
v02 = 5
z2 = g * t**2 / 2 + v02 * t

scat = ax.scatter(t[0], z[0], c="b", s=5, label=f'v0 = {v0} m/s')
line2 = ax.plot(t[0], z2[0], label=f'v0 = {v02} m/s')[0]
ax.set(xlim=[0, 3], ylim=[-4, 10], xlabel='Time [s]', ylabel='Z [m]')
ax.legend()

def update(frame):
# for each frame, update the data stored on each artist.
x = t[:frame]
y = z[:frame]
# update the scatter plot:
data = np.stack([x, y]).T
scat.set_offsets(data)
# update the line plot:
line2.set_xdata(t[:frame])
line2.set_ydata(z2[:frame])
return (scat, line2)


ani = animation.FuncAnimation(fig=fig, func=update, frames=40, interval=30)
ani.save('demo.gif')
plt.show()

这里的参数含义依次为:

  • fig=fig:指定第一帧图像
  • func=update:指定更新函数,调用更新函数时会传递当前帧数 frame,额外的参数传递可以通过偏函数实现,例如
1
2
3
4
def update(frame, art, *, y=None):
...

ani = animation.FuncAnimation(fig=fig, update=partial(update, art=ln, y='foo'))
  • frames=40:一共绘制 40 帧,这里还可以传递迭代器,事实上直接使用数字类似于frames=range(40)
  • interval=30:两帧之间的间隔时间,单位毫秒,默认值为 200

还有一些可能需要的参数:参考官方文档

  • repeat: 是否重复,默认为 True
  • repeat_delay: 重复时的延时
  • init_func: 初始化函数,可以用于在第一帧之前清空图像
  • fargs: 传递给更新函数的额外参数,但是更建议用偏函数封装
  • blit:优化绘图效率,进行块状更新,默认为 False

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%matplotlib notebook
w = 0.1
Lambda = 4
k = 2 * np.pi / Lambda

def update(t, w, k):
x = np.linspace(0, 10, 100)
y = np.cos(w * t - k * x)
line.set_data((x, y))
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
return line

fig, ax = plt.subplots()
(line,) = ax.plot([], [], lw=2)
ani = animation.FuncAnimation(fig=fig, func=update, frames=100, fargs=(w, k))
ani.save('demo2.gif')
plt.show()

ArtistAnimation

和前面的基于第一帧不断进行更新不同,ArtistAnimation 的原理是预先产生每一帧图像组成的列表,然后逐帧播放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
%matplotlib notebook
fig, ax = plt.subplots()

def f(x, y):
return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)

img_list = []
for i in range(100):
x += np.pi / 15.
y += np.pi / 20.
im = plt.imshow(f(x, y), animated=True)
img_list.append([im])

ani = animation.ArtistAnimation(fig, img_list, interval=200, blit=True)
plt.show()

注意这里不接受frames参数,也可以换成 FuncAnimation 等价实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%matplotlib notebook
fig, ax = plt.subplots()

def f(x, y):
return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 400)
y = np.linspace(0, 2 * np.pi, 300).reshape(-1, 1)

im = plt.imshow(f(x, y), animated=True) # 调用imshow实现绘图.这里参数animated=True很重要

def update(*args): # FuncAnimation会将updatefig中的数据传递给绘图句柄,从而更新绘图
global x, y
x += np.pi / 15.
y += np.pi / 20.
im.set_array(f(x, y))
return im, # 注意这里的,很重要,否则返回值不合适

ani = animation.FuncAnimation(fig=fig, func=update, interval=200, frames=100, blit=True)
ani.save("demo3.gif",dpi=300)
plt.show()

注意:

  • 如果要提高输出动画的质量,首先需要完善绘图的细节,例如增加点数,然后在保存时可以指定 dpi,默认的 dpi 为 100,通常范围为 100-300。dpi 值越大,则图像越清晰,文件越大。
  • 在保存动画文件时,可能会报警告MovieWriter ffmpeg unavailable; using Pillow instead.,这是因为没有找到ffmpeg,手动下载即可:conda install ffmpeg