这里记录一下 Python 的 matplotlib
的动画效果,注意我们需要分别考虑如下三种运行环境,它们对于动画的支持是不一样的:
浏览器启动 Jupyter Notebook 或 Jupyter Lab
直接运行 Python 脚本
VSCode 启动 Jupyter(目前仍然不支持动画)
例如浏览器启动 Jupyter 可能需要相关的插件(例如 ipympl
插件),并且需要魔法命令
在启动内核后,至少需要执行这个魔法命令一次,否则无法播放动画,两个同时播放的动画甚至还会相互影响。
在直接运行 Python 脚本时,Jupyter
的魔法指令是语法错误,对于动画而言,直接运行 Python 脚本反而比 Jupyter
更方便。
matplotlib 绘制动画主要包括两种方法:FuncAnimation 和
ArtistAnimation,下面的代码都需要导入如下的库
1 2 3 import numpy as npimport matplotlib.pyplot as pltimport 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 ): x = t[:frame] y = z[:frame] data = np.stack([x, y]).T scat.set_offsets(data) 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 ) def update (*args ): 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