【python】matplotlib(animation)
文章目录
1、matplotlib.animation
1.1、FuncAnimation
matplotlib.animation.FuncAnimation
是 Matplotlib 库中用于创建动画的一个类。它允许你通过循环调用一个函数来更新图表,从而生成动画效果。这个函数通常被称为“更新函数”,它决定了每一帧图表的样子。FuncAnimation 类提供了一种灵活而强大的方式来创建和展示动画,使得数据可视化更加生动和直观。
(1)基本用法
使用 FuncAnimation 创建动画的基本步骤如下:
(2)示例代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 准备数据
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
# 创建图形和轴
fig, ax = plt.subplots()
line, = ax.plot([], [], 'r-') # 初始化一个空线条对象
ax.set_xlim(0, 2 * np.pi) # 设置X轴范围
ax.set_ylim(-1.5, 1.5) # 设置Y轴范围
# 定义更新函数
def update(frame):
line.set_data(x[:frame], y[:frame]) # 更新线条数据
return line,
# 创建 FuncAnimation 对象
ani = FuncAnimation(fig, update, frames=len(x), interval=50, blit=True)
# 显示动画
plt.show()
在这个例子中,update 函数根据当前的帧号(frame)更新线条的数据,使得线条逐渐变长,模拟了一个点沿正弦曲线移动的动画效果。
再看一个例子
#coding=utf-8
import sys
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig, ax = plt.subplots()
x = np.arange(0, 2*np.pi, 0.01)
line, = ax.plot(x, np.sin(x))
def animate(i):
line.set_ydata(np.sin(x + i/10.0))
return line,
def init():
line.set_ydata(np.ma.array(x, mask=True))
return line,
ani = animation.FuncAnimation(fig, animate, np.arange(1, 200), init_func=init,
interval=25, blit=True)
ani.save("animation.gif", writer="imagemagick", fps=30)
plt.show()
(3)matplotlib.animation.FuncAnimation
class matplotlib.animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs)
def __init__(self,
fig: Figure,
func: (...) -> Iterable[Artist],
frames: Iterable | int | () -> Generator | None = ...,
init_func: () -> Iterable[Artist] | None = ...,
fargs: tuple[Any, ...] | None = ...,
save_count: int | None = ...,
*,
cache_frame_data: bool = ...,
**kwargs: Any) -> None
`TimedAnimation` subclass that makes an animation by repeatedly calling a function *func*. .. note:: You must store the created Animation in a variable that lives as long as the animation should run. Otherwise, the Animation object will be garbage-collected and the animation stops. Parameters ---------- fig : `~matplotlib.figure.Figure` The figure object used to get needed events, such as draw or resize. func : callable The function to call at each frame. The first argument will be the next value in *frames*. Any additional positional arguments can be supplied using `functools.partial` or via the *fargs* parameter. The required signature is:: def func(frame, *fargs) -> iterable_of_artists It is often more convenient to provide the arguments using `functools.partial`. In this way it is also possible to pass keyword arguments. To pass a function with both positional and keyword arguments, set all arguments as keyword arguments, just leaving the *frame* argument unset:: def func(frame, art, *, y=None): ... ani = FuncAnimation(fig, partial(func, art=ln, y='foo')) If ``blit == True``, *func* must return an iterable of all artists that were modified or created. This information is used by the blitting algorithm to determine which parts of the figure have to be updated. The return value is unused if ``blit == False`` and may be omitted in that case. frames : iterable, int, generator function, or None, optional Source of data to pass *func* and each frame of the animation - If an iterable, then simply use the values provided. If the iterable has a length, it will override the *save_count* kwarg. - If an integer, then equivalent to passing ``range(frames)`` - If a generator function, then must have the signature:: def gen_function() -> obj - If *None*, then equivalent to passing ``itertools.count``. In all of these cases, the values in *frames* is simply passed through to the user-supplied *func* and thus can be of any type. init_func : callable, optional A function used to draw a clear frame. If not given, the results of drawing from the first item in the frames sequence will be used. This function will be called once before the first frame. The required signature is:: def init_func() -> iterable_of_artists If ``blit == True``, *init_func* must return an iterable of artists to be re-drawn. This information is used by the blitting algorithm to determine which parts of the figure have to be updated. The return value is unused if ``blit == False`` and may be omitted in that case. fargs : tuple or None, optional Additional arguments to pass to each call to *func*. Note: the use of `functools.partial` is preferred over *fargs*. See *func* for details. save_count : int, optional Fallback for the number of values from *frames* to cache. This is only used if the number of frames cannot be inferred from *frames*, i.e. when it's an iterator without length or a generator. interval : int, default: 200 Delay between frames in milliseconds. repeat_delay : int, default: 0 The delay in milliseconds between consecutive animation runs, if *repeat* is True. repeat : bool, default: True Whether the animation repeats when the sequence of frames is completed. blit : bool, default: False Whether blitting is used to optimize drawing. Note: when using blitting, any animated artists will be drawn according to their zorder; however, they will be drawn on top of any previous artists, regardless of their zorder. cache_frame_data : bool, default: True Whether frame data is cached. Disabling cache might be helpful when frames contain large objects.
Params:
fig – The figure object used to get needed events, such as draw or resize.
func – The function to call at each frame. The first argument will be the next value in *frames*. Any additional positional arguments can be supplied using `functools.partial` or via the *fargs* parameter. The required signature is:: def func(frame, *fargs) -> iterable_of_artists It is often more convenient to provide the arguments using `functools.partial`. In this way it is also possible to pass keyword arguments. To pass a function with both positional and keyword arguments, set all arguments as keyword arguments, just leaving the *frame* argument unset:: def func(frame, art, *, y=None): ... ani = FuncAnimation(fig, partial(func, art=ln, y='foo')) If ``blit == True``, *func* must return an iterable of all artists that were modified or created. This information is used by the blitting algorithm to determine which parts of the figure have to be updated. The return value is unused if ``blit == False`` and may be omitted in that case.
frames – Source of data to pass *func* and each frame of the animation - If an iterable, then simply use the values provided. If the iterable has a length, it will override the *save_count* kwarg. - If an integer, then equivalent to passing ``range(frames)`` - If a generator function, then must have the signature:: def gen_function() -> obj - If *None*, then equivalent to passing ``itertools.count``. In all of these cases, the values in *frames* is simply passed through to the user-supplied *func* and thus can be of any type.
init_func – A function used to draw a clear frame. If not given, the results of drawing from the first item in the frames sequence will be used. This function will be called once before the first frame. The required signature is:: def init_func() -> iterable_of_artists If ``blit == True``, *init_func* must return an iterable of artists to be re-drawn. This information is used by the blitting algorithm to determine which parts of the figure have to be updated. The return value is unused if ``blit == False`` and may be omitted in that case.
fargs – Additional arguments to pass to each call to *func*. Note: the use of `functools.partial` is preferred over *fargs*. See *func* for details.
save_count – Fallback for the number of values from *frames* to cache. This is only used if the number of frames cannot be inferred from *frames*, i.e. when it's an iterator without length or a generator.
cache_frame_data – Whether frame data is cached. Disabling cache might be helpful when frames contain large objects.
方法说明
(4)注意事项
1.2、修改 matplotlib 背景
在上述示例代码的情况下,我们引入一些修改颜色的配置,
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 准备数据
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
# 创建图形和轴
fig, ax = plt.subplots()
line, = ax.plot([], [], 'r-') # 初始化一个空线条对象
ax.set_xlim(0, 2 * np.pi) # 设置X轴范围
ax.set_ylim(-1.5, 1.5) # 设置Y轴范围
# 修改轴背景颜色
ax.set_facecolor("orange")
# OR
# ax.set(facecolor = "orange")
# 修改绘图背景颜色
fig.patch.set_facecolor('yellow')
fig.patch.set_alpha(1.0)
# 移除图表的上边框和右边框
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# 设置虚线网格线
ax.set_axisbelow(True)
ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
# 定义更新函数
def update(frame):
line.set_data(x[:frame], y[:frame]) # 更新线条数据
return line,
# 创建 FuncAnimation 对象
ani = FuncAnimation(fig, update, frames=len(x), interval=50, blit=True)
# ani.save("animation.gif", writer="imagemagick", fps=30)
# 显示动画
plt.show()
修改前
修改后
换个背景图试试
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 准备数据
x = np.linspace(0, 20 * np.pi, 100)
y = 9* np.sin(x)
# 创建图形和轴
fig, ax = plt.subplots()
line, = ax.plot([], [], 'r-') # 初始化一个空线条对象
img = plt.imread("123.jpg")
ax.imshow(img, extent=[0, 65, -10, 10]) # 横纵坐标范围
# 定义更新函数
def update(frame):
line.set_data(x[:frame], y[:frame]) # 更新线条数据
return line,
# 创建 FuncAnimation 对象
ani = FuncAnimation(fig, update, frames=len(x), interval=50, blit=True)
ani.save("animation.gif", writer="imagemagick", fps=30)
# 显示动画
plt.show()
原始图片
添加之后的效果
2、matplotlib + imageio
2.1、折线图
先画个简单的折线图
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
# 生成40个取值在30-40的数
y = np.random.randint(30, 40, size=(40))
print(y)
"""
[33 36 35 34 38 39 31 37 39 36 38 30 35 30 39 36 32 30 35 32 36 33 37 30
39 30 33 32 33 31 33 31 33 37 31 37 34 30 35 31]
"""
# 绘制折线
plt.plot(y)
# 设置y轴最小值和最大值
plt.ylim(20, 50)
# 显示
plt.show()
保存最后几个点的数据,然后绘制成 gif
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
# 生成40个取值在30-40的数
y = np.random.randint(30, 40, size=(40))
print(y)
"""
[33 36 35 34 38 39 31 37 39 36 38 30 35 30 39 36 32 30 35 32 36 33 37 30
39 30 33 32 33 31 33 31 33 37 31 37 34 30 35 31]
"""
# 绘制折线
plt.plot(y)
# 设置y轴最小值和最大值
plt.ylim(20, 50)
# 显示
plt.show()
# 第一张图
plt.plot(y[:-3])
plt.ylim(20, 50)
plt.savefig('1.png')
plt.show()
# 第二张图
plt.plot(y[:-2])
plt.ylim(20, 50)
plt.savefig('2.png')
plt.show()
# 第三张图
plt.plot(y[:-1])
plt.ylim(20, 50)
plt.savefig('3.png')
plt.show()
# 第四张图
plt.plot(y)
plt.ylim(20, 50)
plt.savefig('4.png')
plt.show()
# 生成Gif
with imageio.get_writer('mygif.gif', mode='I') as writer:
for filename in ['1.png', '2.png', '3.png', '4.png']:
image = imageio.imread(filename)
writer.append_data(image)
横坐标 0 至 36
横坐标 0 至 37
横坐标 0 至 38
横坐标 0 至 39
合并成为 gif(仅播放一次)
下面把所有点都保存下来,绘制动态图(仅播放一次)
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
# 生成40个取值在30-40的数
y = np.random.randint(30, 40, size=(40))
print(y)
"""
[33 36 35 34 38 39 31 37 39 36 38 30 35 30 39 36 32 30 35 32 36 33 37 30
39 30 33 32 33 31 33 31 33 37 31 37 34 30 35 31]
"""
# 绘制折线
plt.plot(y)
# 设置y轴最小值和最大值
plt.ylim(20, 50)
# 显示
plt.show()
filenames = []
num = 0
for i in y:
num += 1
# 绘制40张折线图
plt.plot(y[:num])
plt.ylim(20, 50)
# 保存图片文件
filename = f'{num}.png'
filenames.append(filename)
plt.savefig(filename)
plt.close()
# 生成gif
with imageio.get_writer('mygif.gif', mode='I') as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
# 删除40张折线图
for filename in set(filenames):
os.remove(filename)
2.2、条形图
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
[10, 30, 60, 30, 10],
[70, 40, 20, 40, 70],
[10, 20, 30, 40, 50],
[50, 40, 30, 20, 10],
[75, 0, 75, 0, 75],
[0, 0, 0, 0, 0]]
filenames = []
for index, y in enumerate(coordinates_lists):
# 条形图
plt.bar(x, y)
plt.ylim(0, 80)
# 保存图片文件
filename = f'{index}.png'
filenames.append(filename)
# 重复最后一张图形15帧(数值都为0),15张图片
if (index == len(coordinates_lists) - 1):
for i in range(15):
filenames.append(filename)
# 保存
plt.savefig(filename)
plt.close()
# 生成gif
with imageio.get_writer('mygif.gif', mode='I') as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
# 删除20张柱状图
for filename in set(filenames):
os.remove(filename)
生成的图片
生成的 gif(播放一次)
看起来太快了,优化代码使其平滑
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
n_frames = 10 # 怕内存不够的话可以设置小一些
x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
[10, 30, 60, 30, 10],
[70, 40, 20, 40, 70],
[10, 20, 30, 40, 50],
[50, 40, 30, 20, 10],
[75, 0, 75, 0, 75],
[0, 0, 0, 0, 0]]
print('生成图表\n')
filenames = []
for index in np.arange(0, len(coordinates_lists) - 1):
# 获取当前图像及下一图像的y轴坐标值
y = coordinates_lists[index]
y1 = coordinates_lists[index + 1]
# 计算当前图像与下一图像y轴坐标差值
y_path = np.array(y1) - np.array(y)
for i in np.arange(0, n_frames + 1):
# 分配每帧的y轴移动距离
# 逐帧增加y轴的坐标值
y_temp = (y + (y_path / n_frames) * i)
# 绘制条形图
plt.bar(x, y_temp)
plt.ylim(0, 80)
# 保存每一帧的图像
filename = f'frame_{index}_{i}.png'
filenames.append(filename)
# 最后一帧重复,画面停留一会
if (i == n_frames):
for i in range(5):
filenames.append(filename)
# 保存图片
plt.savefig(filename)
plt.close()
print('保存图表\n')
# 生成GIF
print('生成GIF\n')
with imageio.get_writer('mybars.gif', mode='I') as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
print('保存GIF\n')
print('删除图片\n')
# 删除图片
for filename in set(filenames):
os.remove(filename)
print('完成')
原理解释统计柱状图当前帧和下一帧的差值,然后插帧平滑过去,这里插帧数量配置为了 n_frames = 10
最终生成的 gif 如下(仅播放一次),可以观察到平滑了很多
接下来美化下界面
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
n_frames = 5
bg_color = '#95A4AD'
bar_color = '#283F4E'
gif_name = 'bars'
x = [1, 2, 3, 4, 5]
coordinates_lists = [[0, 0, 0, 0, 0],
[10, 30, 60, 30, 10],
[70, 40, 20, 40, 70],
[10, 20, 30, 40, 50],
[50, 40, 30, 20, 10],
[75, 0, 75, 0, 75],
[0, 0, 0, 0, 0]]
print('生成图表\n')
filenames = []
for index in np.arange(0, len(coordinates_lists) - 1):
y = coordinates_lists[index]
y1 = coordinates_lists[index + 1]
y_path = np.array(y1) - np.array(y)
for i in np.arange(0, n_frames + 1):
y_temp = (y + (y_path / n_frames) * i)
# 绘制条形图
fig, ax = plt.subplots(figsize=(8, 4))
ax.set_facecolor(bg_color)
plt.bar(x, y_temp, width=0.4, color=bar_color)
plt.ylim(0, 80)
# 移除图表的上边框和右边框
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# 设置虚线网格线
ax.set_axisbelow(True)
ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
# 保存每一帧的图像
filename = f'images/frame_{index}_{i}.png'
filenames.append(filename)
# 最后一帧重复,画面停留一会
if (i == n_frames):
for i in range(5):
filenames.append(filename)
# 保存图片
plt.savefig(filename, dpi=96, facecolor=bg_color)
plt.close()
print('保存图表\n')
# 生成GIF
print('生成GIF\n')
with imageio.get_writer(f'{gif_name}.gif', mode='I') as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
print('保存GIF\n')
print('删除图片\n')
# 删除图片
for filename in set(filenames):
os.remove(filename)
print('完成')
看看生成的 gif 效果(仅播放一次)
给图表添加了背景色、条形图上色、去除边框、增加网格线等。
2.3、散点图
import os
import numpy as np
import matplotlib.pyplot as plt
import imageio
np.random.seed(1234)
coordinates_lists = [[[0], [0]],
[[100, 200, 300], [100, 200, 300]],
[[400, 500, 600], [400, 500, 600]],
[[400, 500, 600, 400, 500, 600], [400, 500, 600, 600, 500, 400]],
[[500], [500]],
[[0], [0]]]
gif_name = 'movie'
n_frames = 5
bg_color = '#95A4AD'
marker_color = '#283F4E'
marker_size = 25
print('生成图表\n')
filenames = []
for index in np.arange(0, len(coordinates_lists) - 1):
# 获取当前图像及下一图像的x与y轴坐标值
x = coordinates_lists[index][0] # 当前帧
y = coordinates_lists[index][1]
x1 = coordinates_lists[index + 1][0] # 下一帧
y1 = coordinates_lists[index + 1][1]
# 查看两点差值
while len(x) < len(x1):
diff = len(x1) - len(x)
x = x + x[:diff]
y = y + y[:diff]
while len(x1) < len(x):
diff = len(x) - len(x1)
x1 = x1 + x1[:diff]
y1 = y1 + y1[:diff]
# 计算路径
x_path = np.array(x1) - np.array(x)
y_path = np.array(y1) - np.array(y)
for i in np.arange(0, n_frames + 1):
# 计算当前位置
x_temp = (x + (x_path / n_frames) * i)
y_temp = (y + (y_path / n_frames) * i)
# 绘制图表
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(aspect="equal"))
ax.set_facecolor(bg_color)
plt.scatter(x_temp, y_temp, c=marker_color, s=marker_size)
plt.xlim(0, 1000)
plt.ylim(0, 1000)
# 移除边框线
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# 网格线
ax.set_axisbelow(True)
ax.yaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
ax.xaxis.grid(color='gray', linestyle='dashed', alpha=0.7)
# 保存图片
filename = f'images/frame_{index}_{i}.png'
filenames.append(filename)
if (i == n_frames):
for i in range(5):
filenames.append(filename)
# 保存
plt.savefig(filename, dpi=96, facecolor=bg_color)
plt.close()
print('保存图表\n')
# 生成GIF
print('生成GIF\n')
with imageio.get_writer(f'{gif_name}.gif', mode='I') as writer:
for filename in filenames:
image = imageio.imread(filename)
writer.append_data(image)
print('保存GIF\n')
print('删除图片\n')
# 删除图片
for filename in set(filenames):
os.remove(filename)
print('完成')
思路,计算前后帧坐标点数量的差 diff
,然后 while 循环来复制以实现数量平衡 x = x + x[:diff]
,最后插帧平滑移动 x_temp = (x + (x_path / n_frames) * i)
3、参考
作者:bryant_meng