网站的优化用什么软件,南昌好的做网站的公司,wordpress的wiki主题,镇江网站设计建设价格动画是一种高效的可视化工具#xff0c;能够提升用户的吸引力和视觉体验#xff0c;有助于以富有意义的方式呈现数据可视化。本文的主要介绍在Python中两种简单制作动图的方法。其中一种方法是使用matplotlib的Animations模块绘制动图#xff0c;另一种方法是基于Pillow生成…动画是一种高效的可视化工具能够提升用户的吸引力和视觉体验有助于以富有意义的方式呈现数据可视化。本文的主要介绍在Python中两种简单制作动图的方法。其中一种方法是使用matplotlib的Animations模块绘制动图另一种方法是基于Pillow生成GIF动图。 文章目录 1 Animations模块1.1 FuncAnimation类1.2 ArtistAnimation类1.3 动画保存 2 基于Pillow库生成动图3 参考 1 Animations模块
Matplotlib的Animations模块提供了FuncAnimation和ArtistAnimation类来创建matplotlib绘图动画FuncAnimation和ArtistAnimation都是Animation类的子类。它们的区别在于实现动画的方式和使用场景不同。FuncAnimation适用于根据时间更新图形状态的动画效果且更加灵活和常用。而ArtistAnimation适用于将已有的静态图像序列组合成动画的效果。具体区别如下 FuncAnimationFuncAnimation是基于函数的方法来创建动画的。它使用用户提供的一个或多个函数来更新图形的状态并按照一定的时间间隔连续地调用这些函数从而实现动画效果。用户需要定义一个更新函数该函数在每个时间步长上更新图形对象的属性然后FuncAnimation会根据用户指定的帧数、时间间隔等参数来自动计算动画的帧序列。这种方法适用于需要根据时间变化来更新图形状态的动画效果。 ArtistAnimationArtistAnimation是基于静态图像的方法来创建动画的。它要求用户提供一系列的静态图像称为艺术家对象。这些图像可以是通过Matplotlib创建的任何类型的可视化对象例如Figure、Axes、Line2D等。用户需要将这些静态图像存储在一个列表中然后通过ArtistAnimation来显示这些图像的序列。ArtistAnimation会按照用户指定的时间间隔逐帧地显示这些图像从而实现动画效果。这种方法适用于已经有一系列静态图像需要组合成动画的场景。
本节将通过几个示例来介绍Animations模块的使用所介绍的示例出自gallery-animation。
1.1 FuncAnimation类
FuncAnimation构造函数的参数含义如下
fig要绘制动画的Figure对象。func用于更新每一帧的函数该函数接受一个参数frame表示当前待绘制的数据帧。frames用于产生待绘制的数据可以是整数、生成器函数或迭代器。init_func在绘制动画之前调用的初始化函数。fargs传递给func函数的附加参数可选。save_count指定动画中缓存的帧数量可选默认为100。注意该参数用于确定最后生成动图和视频所用图像的数量。interval每一帧之间的时间间隔以毫秒为单位默认为200。repeat控制动画是否重复播放默认为True。repeat_delay重复动画之间的延迟时间以毫秒为单位默认为0。blit指定是否使用blitting技术来进行绘制优化默认为False。cache_frame_data指定是否缓存帧数据默认为True。
示例-生成动态的正弦波动画
import itertools
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation# 定义生成数据的函数
def data_gen(max_range):# 使用itertools.count()生成无限递增的计数器for cnt in itertools.count():# 当计数器超过最大范围时停止生成数据if cnt max_range:breakprint(cnt)# 计算时间t和对应的y值使用np.sin()计算sin函数np.exp()计算指数函数t cnt / 10yield t, np.sin(2*np.pi*t) * np.exp(-t/10.)# 初始化函数设置坐标轴范围和清空数据
def init():ax.set_ylim(-1.1, 1.1)ax.set_xlim(0, 1)del xdata[:]del ydata[:]line.set_data(xdata, ydata)return line,# 创建图形对象以及子图对象
fig, ax plt.subplots()
# 创建线条对象
line, ax.plot([], [], lw2)
# 创建文本对象用于显示 x 和 y 值
text ax.text(0., 0., , transformax.transAxes)
# 设置文本位置
text.set_position((0.7, 0.95))
# 将文本对象添加到图形中
ax.add_artist(text)
ax.grid()
xdata, ydata [], []# 更新函数将新的数据添加到图形中
def run(data):# 获取传入的数据t, y data# 将时间和对应的y值添加到xdata和ydata中xdata.append(t)ydata.append(y)# 获取当前坐标轴的范围xmin, xmax ax.get_xlim()# 更新文本对象的值text.set_text(x {:.2f}, y {:.2f}.format(t, y))# 如果时间t超过当前范围更新坐标轴范围if t xmax:ax.set_xlim(xmin, 2*xmax)# 重绘图形ax.figure.canvas.draw()# 更新线条的数据line.set_data(xdata, ydata)return line, text# 创建动画对象
# fig图形对象
# run更新函数用于更新图形中的数据
# data_gen(20)生成器函数产生数据的最大范围为20
# interval100每帧动画的时间间隔为100毫秒
# init_funcinit初始化函数用于设置图形的初始状态
# repeatTrue动画重复播放
ani animation.FuncAnimation(fig, run, data_gen(20), interval100, init_funcinit, repeatTrue)# 显示图形
plt.show()示例-创建动态散点图与折线图
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation# 创建一个图形窗口和坐标轴
fig, ax plt.subplots()# 创建时间数组
t np.linspace(0, 3, 50)# 自由落体加速度
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], cb, s5, labelfv0 {v0} m/s)# 创建线图
line2 ax.plot(t[0], z2[0], labelfv0 {v02} m/s)[0]# 设置坐标轴范围和标签
ax.set(xlim[0, 3], ylim[-4, 10], xlabelTime [s], ylabelZ [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)# 创建动画
# frames为数值表示动画的总帧数即每次更新参数传入当前帧号
ani animation.FuncAnimation(figfig, funcupdate, frames40, interval30)# 显示图形
plt.show()示例-贝叶斯更新动画
import mathimport matplotlib.pyplot as plt
import numpy as npfrom matplotlib.animation import FuncAnimation# 定义分布概率密度函数
def beta_pdf(x, a, b):return (x**(a-1) * (1-x)**(b-1) * math.gamma(a b)/ (math.gamma(a) * math.gamma(b)))# 更新分布类用于更新动态图
class UpdateDist:def __init__(self, ax, prob0.5):self.success 0self.prob probself.line, ax.plot([], [], k-)self.x np.linspace(0, 1, 200)self.ax ax# 设置图形参数self.ax.set_xlim(0, 1)self.ax.set_ylim(0, 10)self.ax.grid(True)# 这条竖直线代表了理论值图中的分布应该趋近于这个值self.ax.axvline(prob, linestyle--, colorblack)def __call__(self, i):# 这样图形可以连续运行我们只需不断观察过程的新实现if i 0:self.success 0self.line.set_data([], [])return self.line,# 根据超过阈值与均匀选择来选择成功if np.random.rand() self.prob:self.success 1y beta_pdf(self.x, self.success 1, (i - self.success) 1)self.line.set_data(self.x, y)return self.line,# 设置随机状态以便再现结果
np.random.seed(0)# 创建图形和坐标轴对象
fig, ax plt.subplots()# 创建更新分布对象并应该收敛到的理论值为0.7
ud UpdateDist(ax, prob0.7)# 创建动画对象
anim FuncAnimation(fig, ud, frames100, interval100,blitTrue, repeat_delay1000)# 显示动画
plt.show()示例-模拟雨滴
import matplotlib.pyplot as plt
import numpy as npfrom matplotlib.animation import FuncAnimation# 设置随机种子以确保可复现性
np.random.seed(0)# 创建画布和坐标轴对象
fig plt.figure(figsize(7, 7))
# 在画布上添加一个坐标轴对象。
# [0, 0, 1, 1]参数指定了坐标轴的位置和大小分别表示左下角的 x 坐标、左下角的 y 坐标、宽度和高度。
# frameonFalse参数表示不显示坐标轴的边框
ax fig.add_axes([0, 0, 1, 1], frameonFalse)
ax.set_xlim(0, 1), ax.set_xticks([])
ax.set_ylim(0, 1), ax.set_yticks([])# 创建雨滴数据
n_drops 50
rain_drops np.zeros(n_drops, dtype[(position, float, (2,)),(size, float),(growth, float),(color, float, (4,))])# 随机初始化雨滴的位置和生长速率
rain_drops[position] np.random.uniform(0, 1, (n_drops, 2))
rain_drops[growth] np.random.uniform(50, 200, n_drops)# 创建散点图对象用于在动画中更新雨滴的状态
scat ax.scatter(rain_drops[position][:, 0], rain_drops[position][:, 1],srain_drops[size], lw0.5, edgecolorsrain_drops[color],facecolorsnone)def update(frame_number):# 获取一个索引用于重新生成最旧的雨滴current_index frame_number % n_drops# 随着时间的推移使所有雨滴的颜色更加透明rain_drops[color][:, 3] - 1.0 / len(rain_drops)rain_drops[color][:, 3] np.clip(rain_drops[color][:, 3], 0, 1)# 所有雨滴变大rain_drops[size] rain_drops[growth]# 为最旧的雨滴选择一个新的位置重置其大小、颜色和生长速率rain_drops[position][current_index] np.random.uniform(0, 1, 2)rain_drops[size][current_index] 5rain_drops[color][current_index] (0, 0, 0, 1)rain_drops[growth][current_index] np.random.uniform(50, 200)# 使用新的颜色、大小和位置更新散点图对象scat.set_edgecolors(rain_drops[color])scat.set_sizes(rain_drops[size])scat.set_offsets(rain_drops[position])# 创建动画并将update函数作为动画的回调函数
animation FuncAnimation(fig, update, interval10, save_count100)
plt.show()示例-跨子图动画
import matplotlib.pyplot as plt
import numpy as npimport matplotlib.animation as animation
from matplotlib.patches import ConnectionPatch# 创建一个包含左右两个子图的图形对象
fig, (axl, axr) plt.subplots(ncols2, # 指定一行中子图的列数为2即创建两个子图shareyTrue, # 共享y轴刻度figsize(6, 2), # width_ratios[1, 3]指定第二个子图的宽度为第一个子图的三倍# wspace0 设置子图之间的水平间距为0gridspec_kwdict(width_ratios[1, 3], wspace0),
)# 设置左侧子图纵横比为1即使得它的宽度和高度相等
axl.set_aspect(1)
# 设置右侧子图纵横比为1/3即高度是宽度的三分之一
axr.set_box_aspect(1 / 3)# 右子图不显示y轴刻度
axr.yaxis.set_visible(False)# 设置右子图x轴刻度以及对应的标签
axr.xaxis.set_ticks([0, np.pi, 2 * np.pi], [0, r$\pi$, r$2\pi$])# 在左子图上绘制圆
x np.linspace(0, 2 * np.pi, 50)
axl.plot(np.cos(x), np.sin(x), k, lw0.3)# 在左子图上绘制初始点
point, axl.plot(0, 0, o)# 在右子图上绘制完整的正弦曲线以设置视图限制
sine, axr.plot(x, np.sin(x))# 绘制连接两个图表的连线
con ConnectionPatch((1, 0), # 连接线的起始点坐标(0, 0), # 连接线的终点坐标data,data,axesAaxl, # 指定连接线的起始点所在的坐标轴axesBaxr, # 指定连接线的终点所在的坐标轴colorred, lsdotted, # 连接线类型
)
fig.add_artist(con)# 定义动画函数
def animate(i):x np.linspace(0, i, int(i * 25 / np.pi))sine.set_data(x, np.sin(x))x, y np.cos(i), np.sin(i)point.set_data([x], [y])con.xy1 x, ycon.xy2 i, yreturn point, sine, con# 创建动画对象
ani animation.FuncAnimation(fig,animate,interval50, blitFalse, # 不使用blitting技术这里Figure artists不支持blittingframesx, repeat_delay100, # 动画重复播放延迟100毫秒
)# 展示动画
plt.show()示例-动态示波器
import matplotlib.pyplot as plt
import numpy as npimport matplotlib.animation as animation
from matplotlib.lines import Line2D# 创建一个 Scope 类用于绘制动态图形
class Scope:def __init__(self, ax, maxt2, dt0.02)::param ax: Matplotlib 的坐标轴对象:param maxt: 时间的最大值默认为2:param dt: 时间步长默认为0.02self.ax axself.dt dtself.maxt maxtself.tdata [0] # 时间数据的列表self.ydata [0] # y轴数据的列表self.line Line2D(self.tdata, self.ydata) # 创建一条线对象self.ax.add_line(self.line) # 将线对象添加到坐标轴上self.ax.set_ylim(-.1, 1.1) # 设置y轴范围self.ax.set_xlim(0, self.maxt) # 设置x轴范围def update(self, y):更新图形数据:param y: 新的y轴数据:return: 更新后的线对象lastt self.tdata[-1]if lastt self.tdata[0] self.maxt: # 如果当前时间超过了最大时间重新设置数组self.tdata [self.tdata[-1]]self.ydata [self.ydata[-1]]self.ax.set_xlim(self.tdata[0], self.tdata[0] self.maxt)self.ax.figure.canvas.draw()# 进行时间的计算t self.tdata[0] len(self.tdata) * self.dtself.tdata.append(t)self.ydata.append(y)self.line.set_data(self.tdata, self.ydata)return self.line,def emitter(p0.1):以概率p范围为[0, 1)返回一个随机值否则返回0while True:v np.random.rand()if v p:yield 0.else:yield np.random.rand()np.random.seed(0)fig, ax plt.subplots() # 创建一个图形窗口和一对坐标轴
scope Scope(ax) # 创建一个Scope对象用于绘制动态图# 使用scope的类函数update作为更新函数
ani animation.FuncAnimation(fig, scope.update, emitter, interval50, blitTrue, save_count100)plt.show() 示例-世界主要城市的人口数量动态展示
本示例代码和数据来自于 how-to-create-animations-in-python。这段代码支持展示自1500年到2020年期间人口数排名靠前的城市的变化趋势。该示例只是介绍简单的动态条形图绘制更加精美的条形图绘制可使用bar_chart_race或pandas_alive。
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from matplotlib.animation import FuncAnimation
import matplotlib.patches as mpatches # 定义一个函数用于生成颜色列表
def generate_colors(string_list):num_colors len(string_list)# 使用tab10调色板可以根据需要选择不同的调色板colormap plt.cm.get_cmap(tab10, num_colors)colors []for i in range(num_colors):color colormap(i)colors.append(color)return colors# 读取CSV文件并选择所需的列
# 数据地址https://media.geeksforgeeks.org/wp-content/cdn-uploads/20210901121516/city_populations.csv
df pd.read_csv(city_populations.csv, usecols[name, group, year, value])# 将年份列转换为整数型
df[year] df[year].astype(int)
# 将人口数量列转换为浮点型
df[value] df[value].astype(float)# 获取城市分组列表
group list(set(df.group))# 生成城市分组对应的颜色字典
group_clolor dict(zip(group, generate_colors(group)))# 创建城市名称与分组的字典
group_name df.set_index(name)[group].to_dict()# 定义绘制柱状图的函数
def draw_barchart(year):# 根据年份筛选数据并按人口数量进行降序排序取出最大范围的数据df_year df[df[year].eq(year)].sort_values(byvalue, ascendingTrue).tail(max_range)ax.clear()# 绘制水平柱状图并设置颜色ax.barh(df_year[name], df_year[value], color[group_clolor[group_name[x]] for x in df_year[name]])# 在柱状图上方添加文字标签dx df_year[value].max() / 200for i, (value, name) in enumerate(zip(df_year[value], df_year[name])):# 城市名ax.text(value-dx, i, name,size12, weight600,haright, vabottom)ax.text(value-dx, i-0.25, group_name[name],size10, color#333333,haright, vabaseline)# 地区名ax.text(valuedx, i, f{value:,.0f},size12, haleft, vacenter)# 设置其他样式ax.text(1, 0.2, year, transformax.transAxes,color#777777, size46, haright,weight800)ax.text(0, 1.06, Population (thousands),transformax.transAxes, size12,color#777777)# 添加图例handles []for name, color in group_clolor.items():patch mpatches.Patch(colorcolor, labelname)handles.append(patch)ax.legend(handleshandles, fontsize12, loccenter, bbox_to_anchor(0.5, -0.03), ncollen(group_clolor), frameonFalse)# x轴的主要刻度格式化不保留小数ax.xaxis.set_major_formatter(ticker.StrMethodFormatter({x:,.0f}))# 将x轴的刻度位置设置在图的顶部ax.xaxis.set_ticks_position(top)# 设置x轴的刻度颜色为灰色(#777777)字体大小为16ax.tick_params(axisx, colors#777777, labelsize16)# 清除y轴的刻度标签ax.set_yticks([])# 在x轴和y轴上设置0.01的边距ax.margins(0, 0.01)# 在x轴上绘制主要网格线线条样式为实线ax.grid(whichmajor, axisx, linestyle-)# 设置网格线绘制在图像下方ax.set_axisbelow(True)# 添加绘图信息ax.text(0, 1.10, fThe {max_range} most populous cities in the world from {start_year} to {end_year},transformax.transAxes, size24, weight600, haleft)ax.text(1, 0, Produced by luohenyueji,transformax.transAxes, haright, color#777777,bboxdict(facecolorwhite, alpha0.8, edgecolorwhite))plt.box(False)# 创建绘图所需的figure和axes
fig, ax plt.subplots(figsize(12, 8))
start_year 2000
end_year 2020
# 设置最多显示城市数量
max_range 15# 获取数据中的最小年份和最大年份并进行校验
min_year, max_year min(set(df.year)), max(set(df.year))
assert min_year start_year, fend_year cannot be lower than {min_year}
assert end_year max_year, fend_year cannot be higher than {max_year}# 创建动画对象调用draw_barchart函数进行绘制
ani FuncAnimation(fig, draw_barchart, framesrange(start_year, end_year1), repeat_delay1000, interval200)
fig.subplots_adjust(left0.04, right0.94, bottom0.05)# 显示图形
plt.show()结果如下: 1.2 ArtistAnimation类
ArtistAnimation构造函数的参数含义如下
fig要绘制动画的Figure对象。artists包含了一系列绘图对象的列表这些绘图对象将被作为动画的帧。interval每一帧之间的时间间隔以毫秒为单位默认为200。repeat控制动画是否重复播放默认为True。repeat_delay重复动画之间的延迟时间以毫秒为单位默认为0。blit指定是否使用blitting技术来进行绘制优化默认为False。
示例-ArtistAnimation简单使用
import matplotlib.pyplot as plt
import numpy as npimport matplotlib.animation as animationfig, ax plt.subplots()# 定义函数 f(x, y)返回 np.sin(x) np.cos(y)
def f(x, y):return np.sin(x) np.cos(y)# 生成 x 和 y 的取值范围
x np.linspace(0, 2 * np.pi, 120)
y np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)# ims 是一个列表的列表每一行是当前帧要绘制的艺术品列表
# 在这里我们只在每一帧动画中绘制一个艺术家即图像
ims []# 循环生成动画的每一帧并存入一个列表
for i in range(60):# 更新 x 和 y 的取值x np.pi / 15y np.pi / 30# 调用函数 f(x, y)并绘制其返回的图像im ax.imshow(f(x, y), animatedTrue)if i 0:# 首先显示一个初始的图像ax.imshow(f(x, y))# 将当前帧添加到ims中ims.append([im])# 基于ims中的绘图对象绘制动图
ani animation.ArtistAnimation(fig, ims, interval50, blitTrue,repeat_delay1000)# 显示动画
plt.show()示例-创建动态柱状图
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.animation as animation fig, ax plt.subplots()
rng np.random.default_rng(0)
# # 创建一个包含5个元素的数组表示数据集
data np.array([20, 20, 20, 20,20])
# 创建一个包含5个字符串的列表表示数据集的标签
x [A, B, C, D,E] # 创建一个空列表用于存储图形对象
artists []
# 创建一个包含5个颜色值的列表用于绘制图形
colors [tab:blue, tab:red, tab:green, tab:purple, tab:orange] for i in range(20):# 随机生成一个与data形状相同的数组并将其加到data中data rng.integers(low0, high10, sizedata.shape) # 创建一个水平条形图并设置颜色container ax.barh(x, data, colorcolors)# 设置x轴范围ax.set_xlim(0,150)# 将创建的图形对象添加到列表中artists.append(container) # 创建一个ArtistAnimation对象指定图形窗口和图形对象列表以及动画间隔时间
ani animation.ArtistAnimation(figfig, artistsartists, interval200)
plt.show() 1.3 动画保存
Matplotlib通过plot方法创建和显示动画。为了保存动画为动图或视频Animation类提供了save函数。save函数的常见参数如下
filename保存文件的路径和名称。writer指定要使用的写入器Writer。如果未指定则默认使用ffmpeg写入器。fps设置帧速率每秒显示多少帧默认值为None表示使用Animation对象中的interval属性作为帧速率。dpi设置输出图像的分辨率默认值为None表示使用系统默认值。codec指定视频编解码器仅当writer为ffmpeg_writer时有效。bitrate设置比特率仅当writer为ffmpeg_writer时有效。extra_args用于传递给写入器的额外参数。metadata包含文件元数据的字典。extra_anim与主要动画同时播放的其他动画。savefig_kwargs传递给savefig()的关键字参数。progress_callback用于在保存过程中更新进度的回调函数。
writer写入器可以指定使用各种多媒体写入程序例如Pillow、ffpmeg、imagemagik保存到本地如下所示
WriterSupported Formats~matplotlib.animation.PillowWriter.gif, .apng, .webp~matplotlib.animation.HTMLWriter.htm, .html, .png~matplotlib.animation.FFMpegWriterAll formats supported by ffmpeg: ffmpeg -formats~matplotlib.animation.ImageMagickWriterAll formats supported by imagemagick: magick -list format
保存动图和视频的代码如下:
# 动图
ani.save(filenamepillow_example.gif, writerpillow)
ani.save(filenamepillow_example.apng, writerpillow)# 视频需要安装ffmpeg
ani.save(filenameffmpeg_example.mkv, writerffmpeg)
ani.save(filenameffmpeg_example.mp4, writerffmpeg)
ani.save(filenameffmpeg_example.mjpeg, writerffmpeg)需要注意的是动图构建对象时所设置的参数不会影响save函数如下所示在FuncAnimation中设置repeatFalse即动图只播放一次。但是保存的gif文件却循环播放。这是因为save函数调用了其他第三库的动图或者视频保持函数需要重新设置参数。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation# 创建画布和坐标轴
fig, ax plt.subplots()
xdata, ydata [], []
ln, plt.plot([], [], r-)def init():ax.set_xlim(0, 2*np.pi)ax.set_ylim(-1, 1)return ln,def update(frame):x np.linspace(0, 2*np.pi, 100)y np.sin(x frame/10)ln.set_data(x, y)return ln,# 创建动画对象
ani FuncAnimation(fig, update, frames100, interval100,init_funcinit, blitTrue, repeatFalse)ani.save(filenamepillow_example.gif, writerwriter, dpi150)要解决保存动画问题需要自定义动画保存类如下所示
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib import animation# 创建画布和坐标轴
fig, ax plt.subplots()
xdata, ydata [], []
ln, plt.plot([], [], r-)def init():ax.set_xlim(0, 2*np.pi)ax.set_ylim(-1, 1)return ln,def update(frame):x np.linspace(0, 2*np.pi, 100)y np.sin(x frame/10)ln.set_data(x, y)return ln,# 创建动画对象
ani FuncAnimation(fig, update, frames100, interval100,init_funcinit, blitTrue, repeatFalse)# 创建自定义的动画写入类
class SubPillowWriter(animation.PillowWriter):def __init__(self, loop1, **kwargs):super().__init__(**kwargs)# 将loop设置为0表示无限循环播放如果设置为一个大于0的数值表示循环播放指定次数self.loop loop# 定义播放结束时保存图片的代码def finish(self):# 调用了pillow包self._frames[0].save(self.outfile, save_allTrue, append_imagesself._frames[1:], durationint(1000 / self.fps), loopself.loop)# 创建动画写入对象
# fps15每秒帧数表示动画的播放速度为每秒 15 帧。
# metadatadict(artistluohenyueji)元数据信息包括艺术家信息将被添加到生成的GIF文件中。
writer SubPillowWriter(fps15, metadatadict(artistluohenyueji))
ani.save(filenamepillow_example.gif, writerwriter, dpi150)2 基于Pillow库生成动图
使用Pillow库生成动图非常简单。首先准备一个包含一系列图像帧的列表。这些图像帧可以是连续的图片每张图片表示动画的一个时间点。接下来使用Pillow库中的save()方法将这些图像帧保存为一个gif文件。在保存动图时还可以设置一些参数来控制动画效果。参考以下示例可获取具体的使用说明。
示例-滑动动图
该示例展示了一种图像滑动展示的动画效果即通过滑动渐变的方式逐步将起始黑白图片转变为目标彩色图片。所示起始图片和目标图片如下所示 动画结果如下所示 本示例所提供代码主要可调参数介绍如下 span (int): 分割步长默认为100。此参数用于控制图片合并过程中的分割步长即每次移动的距离。 save (bool): 是否保存中间帧图像默认为False。如果设置为True则会将生成的每一帧图像保存到指定的文件夹中。 orient (str): 合并方向默认水平。可选值为’horizontal’水平方向或’vertical’垂直方向。用于控制图像的合并方向。 loop (int): 循环次数默认为0无限循环。设置为正整数时动画会循环播放指定次数设置为0时动画会无限循环播放。 duration (int): 帧持续时间毫秒默认为100。用于设置每一帧图像在动画中的显示时间。 repeat_delay (int): 循环之间的延迟时间毫秒默认为500。用于设置每次循环之间的延迟时间。 save_name (str): 保存动画的文件名默认为output。用于设置生成的动画文件的名称。
以下是代码实现的示例。该代码首先读取起始图片和目标图片然后指定分割位置以设置图片两侧的效果。最后通过调整分割位置来实现滑动渐变效果。
from PIL import Image, ImageDraw
import osdef merge_image(in_img, out_img, pos, orienthorizontal):合并图像的函数参数:in_img (PIL.Image): 输入图像out_img (PIL.Image): 输出图像pos (int): 分割位置orient (str): 图像合并方向默认水平horizontal,可选垂直vertical返回:result_image (PIL.Image): 合并后的图像if orient horizontal:# 将图像分为左右两部分left_image out_img.crop((0, 0, pos, out_img.size[1]))right_image in_img.crop((pos, 0, in_img.size[0], in_img.size[1]))# 合并左右两部分图像result_image Image.new(RGB, (left_image.size[0] right_image.size[0], left_image.size[1]))result_image.paste(left_image, (0, 0))result_image.paste(right_image, (left_image.size[0], 0))# 添加滑动线条draw ImageDraw.Draw(result_image)draw.line([(left_image.size[0], 0), (left_image.size[0],left_image.size[1])], fill(0, 255, 255), width3)elif orient vertical:# 将图像分为上下两部分top_image out_img.crop((0, 0, out_img.size[0], pos))bottom_image in_img.crop((0, pos, in_img.size[0], in_img.size[1]))# 合并上下两部分图像result_image Image.new(RGB, (top_image.size[0], top_image.size[1] bottom_image.size[1]))result_image.paste(top_image, (0, 0))result_image.paste(bottom_image, (0, top_image.size[1]))# 添加滑动线条draw ImageDraw.Draw(result_image)draw.line([(0, top_image.size[1]), (top_image.size[0],top_image.size[1])], fill(0, 255, 255), width3)return result_imagedef main(img_in_path, img_out_path, span100, saveFalse, orienthorizontal, loop0, duration100, repeat_delay500, save_nameoutput):主函数参数:img_in_path (str): 起始图片路径img_out_path (str): 目标图片路径span (int): 分割步长默认为100save (bool): 是否保存中间帧图像默认为Falseorient (str): 合并方向默认水平loop (int): 循环次数默认为0无限循环duration (int): 帧持续时间毫秒默认为100repeat_delay (int): 循环之间的延迟时间毫秒默认为500save_name (str): 保存动画的文件名默认为output# 读取原始图像img_in Image.open(img_in_path).convert(RGB)img_out Image.open(img_out_path).convert(RGB)assert img_in.size img_out.size, Unequal size of two input imagesif save:output_dir outputos.makedirs(output_dir, exist_okTrue)frames []frames.append(img_in)span_end img_in.size[0] if orient horizontal else img_in.size[1]# 逐张生成gif图片每一帧for pos in range(span, span_end, span):print(pos)result_image merge_image(img_in, img_out, pos, orient)if save:result_image.save(foutput/{pos:04}.jpg)frames.append(result_image)if save:img_in.save(output/0000.jpg)img_out.save(foutput/{img_in.size[0]:04}.jpg)# 添加过渡效果durations [duration]*len(frames)durations.append(repeat_delay)frames.append(img_out)# 生成动图# frames[0].save表示将frames列表中的第一张图片作为输出GIF动画的第一帧# {save_name}.gif表示将输出的GIF动画保存在当前目录下并命名为{save_name}.gif# formatGIF表示输出的文件格式为GIF格式# append_imagesframes[1:]表示将frames列表中除了第一张图片以外的剩余图片作为输出GIF动画的后续帧# save_allTrue表示将所有的帧都保存到输出的GIF动画中# duration表示每一帧的持续时间duration可以是数值也可以是列表。如果是列表则单独表示每一帧的时间# loop0表示循环播放次数为0即无限循环播放# optimizeTrue表示优化图片生成frames[0].save(f{save_name}.gif, formatGIF, append_imagesframes[1:],save_allTrue, durationdurations, looploop, optimizeTrue)if __name__ __main__:# 起始图片路径img_in_path in.jpg# 目标图片路径img_out_path out.jpg# 调用 main 函数并传入相应的参数main(img_in_path, # 起始图片路径img_out_path, # 目标图片路径saveTrue, # 是否保存中间结果span150, # 分割步长默认为 150orienthorizontal, # 合并方向默认为水平可选值为 horizontal 或 verticalduration500, # 帧持续时间毫秒默认为500save_nameoutput, # 保存动画的文件名默认为 outputrepeat_delay2000 # 循环之间的延迟时间毫秒默认为 500)上述代码演示了一种直接生成动图的方法。此外还可以通过读取磁盘中的图片集合来生成动图。以下是示例代码用于读取之前保存的中间图片并生成动图
from PIL import Image
import os# 图片文件夹路径
image_folder output# 保存的动图路径及文件名
animated_gif_path output.gif# 获取图片文件列表
image_files [f for f in os.listdir(image_folder) if f.endswith(.jpg) or f.endswith(.png)]
image_files.sort()
# 创建图片帧列表
frames []
for file_name in image_files:image_path os.path.join(image_folder, file_name)img Image.open(image_path)frames.append(img)# 保存为动图
frames[0].save(animated_gif_path, formatGIF, append_imagesframes[1:], save_allTrue, duration200, loop0)值得注意基于Pillow库生成的gif图片往往文件体积过大。这是因为Pillow库采用无损压缩的方式保存gif图片。为了解决这个问题可以尝试以下方法对gif图片进行压缩
使用在线gif图片压缩网站如gif-compressor基于压缩或优化gif图片的工具如gifsicle缩小gif图像宽高
3 参考
gallery-animationhow-to-create-animations-in-pythonbar_chart_racepandas_alivegif-compressorgifsicle