[Data Analysis and Visualization] Draw simple animations based on Python

Animation is an effective visualization tool that enhances user engagement and visual experience, helping to present data visualizations in a meaningful way. This article mainly introduces two simple methods of making animations in Python. One method is to use matplotlib's Animations module to draw animations, and the other method is to generate GIF animations based on Pillow.

1 Animations module

Matplotlib's Animations module provides the FuncAnimation and ArtistAnimation classes to create matplotlib drawing animations. FuncAnimation and ArtistAnimation are both subclasses of the Animation class. The difference between them lies in the way to implement animation and the usage scenarios. FuncAnimation is suitable for animation effects that update graphics states based on time, and is more flexible and commonly used. ArtistAnimation is suitable for combining existing static image sequences into animated effects. The specific differences are as follows:

  • FuncAnimation: FuncAnimation is a function-based method for creating animations. It uses one or more functions provided by the user to update the state of the graph, and calls these functions continuously at certain intervals to achieve animation effects. The user needs to define an update function that updates the properties of the graphic object at each time step, and then FuncAnimation will automatically calculate the frame sequence of the animation based on the number of frames, time interval and other parameters specified by the user. This method is suitable for animation effects that need to update the graphics state according to time changes.

  • ArtistAnimation: ArtistAnimation is a method based on static images to create animations. It requires the user to provide a series of static images, called artist objects. These images can be any type of visualization object created through Matplotlib, such as Figure, Axes, Line2D, etc. The user needs to store these static images in a list and then display the sequence of these images through ArtistAnimation. ArtistAnimation displays these images frame by frame at user-specified time intervals to achieve animation effects. This method is suitable for scenes that already have a series of static images that need to be combined into animation.

This section will introduce the use of the Animations module through several examples. The examples introduced are from: gallery-animation.

1.1 FuncAnimation class

The parameters of the FuncAnimation constructor have the following meanings:

  • fig: Figure object to be animated.
  • func: Function used to update each frame. This function accepts a parameter frame, which represents the current data frame to be drawn.
  • frames: Used to generate data to be plotted, which can be an integer, generator function or iterator.
  • init_func: Initialization function called before drawing animation.
  • fargs: Additional parameters passed to the func function (optional).
  • save_count: Specify the number of cached frames in the animation (optional), the default is 100. Note that this parameter is used to determine the number of images used to finally generate animations and videos.
  • interval: The time interval between each frame, in milliseconds, the default is 200.
  • repeat: Control whether the animation is played repeatedly, the default is True.
  • repeat_delay: Delay time (in milliseconds) between repeating animations, default is 0.
  • blit: Specify whether to use blitting technology for drawing optimization, the default is False.
  • cache_frame_data: Specify whether to cache frame data, the default is True.

Example - Generating a dynamic sine wave animation

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:
            break
        print(cnt)
        # 计算时间t和对应的y值,使用np.sin()计算sin函数,np.exp()计算指数函数
        t = cnt / 10
        yield 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([], [], lw=2)
# 创建文本对象用于显示 x 和 y 值
text = ax.text(0., 0., '', transform=ax.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
# interval=100:每帧动画的时间间隔为100毫秒
# init_func=init:初始化函数,用于设置图形的初始状态
# repeat=True:动画重复播放
ani = animation.FuncAnimation(fig, run, data_gen(20), interval=100, init_func=init, repeat=True)

# 显示图形
plt.show()

Example - Create dynamic scatter plots and line charts

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], 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)

# 创建动画
# frames为数值表示动画的总帧数,即每次更新参数传入当前帧号
ani = animation.FuncAnimation(fig=fig, func=update, frames=40, interval=30)

# 显示图形
plt.show()

Example - Bayesian update animation

import math

import matplotlib.pyplot as plt
import numpy as np

from 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, prob=0.5):
        self.success = 0
        self.prob = prob
        self.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='--', color='black')

    def __call__(self, i):
        # 这样图形可以连续运行,我们只需不断观察过程的新实现
        if i == 0:
            self.success = 0
            self.line.set_data([], [])
            return self.line,

        # 根据超过阈值与均匀选择来选择成功
        if np.random.rand() < self.prob:
            self.success += 1
        y = 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, prob=0.7)

# 创建动画对象
anim = FuncAnimation(fig, ud, frames=100, interval=100,
                     blit=True, repeat_delay=1000)

# 显示动画
plt.show()

Example - Simulating Raindrops

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.animation import FuncAnimation

# 设置随机种子以确保可复现性
np.random.seed(0)

# 创建画布和坐标轴对象
fig = plt.figure(figsize=(7, 7))
# 在画布上添加一个坐标轴对象。
# [0, 0, 1, 1]参数指定了坐标轴的位置和大小,分别表示左下角的 x 坐标、左下角的 y 坐标、宽度和高度。
# frameon=False参数表示不显示坐标轴的边框
ax = fig.add_axes([0, 0, 1, 1], frameon=False)
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],
                  s=rain_drops['size'], lw=0.5, edgecolors=rain_drops['color'],
                  facecolors='none')

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] = 5
    rain_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, interval=10, save_count=100)
plt.show()

Example - Animate across subplots

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.animation as animation
from matplotlib.patches import ConnectionPatch

# 创建一个包含左右两个子图的图形对象
fig, (axl, axr) = plt.subplots(
    ncols=2, # 指定一行中子图的列数为2,即创建两个子图
    sharey=True,  # 共享y轴刻度
    figsize=(6, 2),  
    # width_ratios=[1, 3]指定第二个子图的宽度为第一个子图的三倍
    # wspace=0 设置子图之间的水平间距为0
    gridspec_kw=dict(width_ratios=[1, 3], wspace=0), 
)

# 设置左侧子图纵横比为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", lw=0.3)

# 在左子图上绘制初始点
point, = axl.plot(0, 0, "o")

# 在右子图上绘制完整的正弦曲线,以设置视图限制
sine, = axr.plot(x, np.sin(x))

# 绘制连接两个图表的连线
con = ConnectionPatch(
    (1, 0), # 连接线的起始点坐标
    (0, 0), # 连接线的终点坐标
    "data",
    "data",
    axesA=axl, # 指定连接线的起始点所在的坐标轴
    axesB=axr, # 指定连接线的终点所在的坐标轴
    color="red", 
    ls="dotted", # 连接线类型
)
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, y
    con.xy2 = i, y
    return point, sine, con

# 创建动画对象
ani = animation.FuncAnimation(
    fig,
    animate,
    interval=50,  
    blit=False,   # 不使用blitting技术,这里Figure artists不支持blitting
    frames=x,     
    repeat_delay=100,  # 动画重复播放延迟100毫秒
)

# 展示动画
plt.show()

Example - Dynamic Oscilloscope

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.animation as animation
from matplotlib.lines import Line2D

# 创建一个 Scope 类用于绘制动态图形
class Scope:
    def __init__(self, ax, maxt=2, dt=0.02):
        """
        :param ax: Matplotlib 的坐标轴对象
        :param maxt: 时间的最大值,默认为2
        :param dt: 时间步长,默认为0.02
        """
        self.ax = ax
        self.dt = dt
        self.maxt = maxt
        self.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.dt

        self.tdata.append(t)
        self.ydata.append(y)
        self.line.set_data(self.tdata, self.ydata)
        return self.line,

def emitter(p=0.1):
    """以概率p(范围为[0, 1))返回一个随机值,否则返回0"""
    while 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, interval=50, blit=True, save_count=100)

plt.show() 

Example - Dynamic display of population numbers in major cities in the world

This sample code and data comes from: how-to-create-animations-in-python. This code supports displaying the changing trend of the cities with the highest population from 1500 to 2020. This example only introduces simple dynamic bar chart drawing. For more exquisite bar chart drawing, you can use: bar_chart_race or 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(
        by='value', ascending=True).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() / 200
    for i, (value, name) in enumerate(zip(df_year['value'], df_year['name'])):
        # 城市名
        ax.text(value-dx, i, name,
                size=12, weight=600,
                ha='right', va='bottom')
        ax.text(value-dx, i-0.25, group_name[name],
                size=10, color='#333333',
                ha='right', va='baseline')
        # 地区名
        ax.text(value+dx, i, f'{
      
      value:,.0f}',
                size=12, ha='left',  va='center')

    # 设置其他样式
    ax.text(1, 0.2, year, transform=ax.transAxes,
            color='#777777', size=46, ha='right',
            weight=800)
    ax.text(0, 1.06, 'Population (thousands)',
            transform=ax.transAxes, size=12,
            color='#777777')
    # 添加图例
    handles = []
    for name, color in group_clolor.items():
        patch = mpatches.Patch(color=color, label=name)
        handles.append(patch)
    ax.legend(handles=handles, fontsize=12, loc='center', bbox_to_anchor=(
        0.5, -0.03), ncol=len(group_clolor), frameon=False)
    
    # x轴的主要刻度格式化,不保留小数
    ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}'))
    # 将x轴的刻度位置设置在图的顶部
    ax.xaxis.set_ticks_position('top')
    # 设置x轴的刻度颜色为灰色(#777777),字体大小为16
    ax.tick_params(axis='x', colors='#777777', labelsize=16)
    # 清除y轴的刻度标签
    ax.set_yticks([])
    # 在x轴和y轴上设置0.01的边距
    ax.margins(0, 0.01)
    # 在x轴上绘制主要网格线,线条样式为实线
    ax.grid(which='major', axis='x', linestyle='-')
    # 设置网格线绘制在图像下方
    ax.set_axisbelow(True)

    # 添加绘图信息
    ax.text(0, 1.10, f'The {
      
      max_range} most populous cities in the world from {
      
      start_year} to {
      
      end_year}',
            transform=ax.transAxes, size=24, weight=600, ha='left')

    ax.text(1, 0, 'Produced by luohenyueji',
            transform=ax.transAxes, ha='right', color='#777777',
            bbox=dict(facecolor='white', alpha=0.8, edgecolor='white'))
    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, f"end_year cannot be lower than {
      
      min_year}"
assert end_year <= max_year, f"end_year cannot be higher  than {
      
      max_year}"

# 创建动画对象,调用draw_barchart函数进行绘制
ani = FuncAnimation(fig, draw_barchart, frames=range(
    start_year, end_year+1), repeat_delay=1000, interval=200)
fig.subplots_adjust(left=0.04, right=0.94, bottom=0.05)

# 显示图形
plt.show()

The result is as follows:

1.2 ArtistAnimation class

The parameters of the ArtistAnimation constructor have the following meanings:

  • fig: Figure object to be animated.
  • artists: Contains a list of a series of drawing objects that will be used as animation frames.
  • interval: The time interval between each frame, in milliseconds, the default is 200.
  • repeat: Control whether the animation is played repeatedly, the default is True.
  • repeat_delay: Delay time (in milliseconds) between repeating animations, default is 0.
  • blit: Specify whether to use blitting technology for drawing optimization, the default is False.

Example-ArtistAnimation simple use

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.animation as animation

fig, 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 / 15
    y += np.pi / 30
    # 调用函数 f(x, y),并绘制其返回的图像
    im = ax.imshow(f(x, y), animated=True)
    if i == 0:
        # 首先显示一个初始的图像
        ax.imshow(f(x, y))
    # 将当前帧添加到ims中
    ims.append([im])

# 基于ims中的绘图对象绘制动图
ani = animation.ArtistAnimation(fig, ims, interval=50, blit=True,
                                repeat_delay=1000)

# 显示动画
plt.show()

Example - Create a dynamic bar chart

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(low=0, high=10, size=data.shape)  
    # 创建一个水平条形图,并设置颜色
    container = ax.barh(x, data, color=colors)
    # 设置x轴范围
    ax.set_xlim(0,150)
    # 将创建的图形对象添加到列表中
    artists.append(container)  

# 创建一个ArtistAnimation对象,指定图形窗口和图形对象列表以及动画间隔时间
ani = animation.ArtistAnimation(fig=fig, artists=artists, interval=200) 
plt.show() 

1.3 Animation saving

Matplotlib creates and displays animations through the plot method. In order to save animations as animations or videos, the Animation class provides the save function. Common parameters of the save function are as follows:

  • filename: The path and name of the saved file.
  • writer: Specify the writer to use. If not specified, the ffmpeg writer is used by default.
  • fps: Set the frame rate (how many frames are displayed per second). The default value is None, which means using the interval attribute in the Animation object as the frame rate.
  • dpi: Set the resolution of the output image. The default value is None, which means using the system default value.
  • codec: Specify the video codec, only valid when the writer is ffmpeg_writer.
  • bitrate: Set the bitrate, only valid when the writer is ffmpeg_writer.
  • extra_args: Extra parameters for passing to the writer.
  • metadata: Dictionary containing file metadata.
  • extra_anim: Additional animations that play simultaneously with the main animation.
  • savefig_kwargs: Keyword arguments passed to savefig().
  • progress_callback: Callback function used to update progress during saving.

The writer can be specified to use various multimedia writing programs (for example: Pillow, ffpmeg, imagemagik) to save to the local, as follows:

Writer Supported Formats
~matplotlib.animation.PillowWriter .gif, .apng, .webp
~matplotlib.animation.HTMLWriter .htm, .html, .png
~matplotlib.animation.FFMpegWriter All formats supported by ffmpeg: ffmpeg -formats
~matplotlib.animation.ImageMagickWriter All formats supported by imagemagick: magick -list format

The code to save animations and videos is as follows:

# 动图
ani.save(filename="pillow_example.gif", writer="pillow")
ani.save(filename="pillow_example.apng", writer="pillow")

# 视频,需要安装ffmpeg
ani.save(filename="ffmpeg_example.mkv", writer="ffmpeg")
ani.save(filename="ffmpeg_example.mp4", writer="ffmpeg")
ani.save(filename="ffmpeg_example.mjpeg", writer="ffmpeg")

It should be noted that the parameters set when constructing the object of the animation will not affect the save function. As shown below, set repeat=False in FuncAnimation, that is, the animation will only be played once. But the saved gif file plays in a loop. This is because the save function calls the animation or video retention function of other third libraries, and the parameters need to be reset.

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, frames=100, interval=100,
                    init_func=init, blit=True, repeat=False)

ani.save(filename="pillow_example.gif", writer=writer, dpi=150)

To solve the problem of saving animations, you need to customize the animation saving class, as shown below:

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, frames=100, interval=100,
                    init_func=init, blit=True, repeat=False)

# 创建自定义的动画写入类
class SubPillowWriter(animation.PillowWriter):
    def __init__(self, loop=1, **kwargs):
        super().__init__(**kwargs)
        # 将loop设置为0,表示无限循环播放;如果设置为一个大于0的数值,表示循环播放指定次数
        self.loop = loop

    # 定义播放结束时,保存图片的代码
    def finish(self):
        # 调用了pillow包
        self._frames[0].save(self.outfile, save_all=True, append_images=self._frames[1:], duration=int(
            1000 / self.fps), loop=self.loop)


# 创建动画写入对象
# fps=15:每秒帧数,表示动画的播放速度为每秒 15 帧。
# metadata=dict(artist='luohenyueji'):元数据信息,包括艺术家信息,将被添加到生成的GIF文件中。
writer = SubPillowWriter(fps=15, metadata=dict(artist='luohenyueji'))
ani.save(filename="pillow_example.gif", writer=writer, dpi=150)

2 Generate animated graphics based on Pillow library

Generating animations using the Pillow library is very simple. First, prepare a list containing a series of image frames. These image frames can be consecutive pictures, each picture representing a point in time of the animation. Next, save these image frames as a gif file using the save() method in the Pillow library. When saving the animation, you can also set some parameters to control the animation effect. Refer to the following examples for specific usage instructions.

Example - sliding animation

This example shows an animation effect of image sliding display, that is, gradually transforming the starting black and white picture into the target color picture through sliding gradient. The starting and target images shown are as follows:

The animation results look like this:

The main adjustable parameters of the code provided in this example are introduced as follows:

  • span(int): Split step size, default is 100. This parameter is used to control the segmentation step size during the image merging process, that is, the distance moved each time.

  • save(bool): Whether to save the intermediate frame image, the default is False. If set to True, each generated image frame will be saved to the specified folder.

  • orient(str): merge direction, default is horizontal. Optional values ​​are 'horizontal' (horizontal direction) or 'vertical' (vertical direction). Used to control the merging direction of images.

  • loop(int): Number of loops, default is 0 (infinite loop). When set to a positive integer, the animation will loop for the specified number of times; when set to 0, the animation will loop infinitely.

  • duration(int): Frame duration (milliseconds), default is 100. Used to set the display time of each frame of image in the animation.

  • repeat_delay(int): Delay time between loops (milliseconds), default is 500. Used to set the delay time between each loop.

  • save_name(str): The file name to save the animation, the default is "output". Used to set the name of the generated animation file.

The following is an example of code implementation. The code first reads the starting image and the target image, and then specifies the split position to set the effects on both sides of the image. Finally, the sliding gradient effect is achieved by adjusting the split position.

from PIL import Image, ImageDraw
import os


def merge_image(in_img, out_img, pos, orient="horizontal"):
    """
    合并图像的函数

    参数:
        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), width=3)

    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), width=3)

    return result_image


def main(img_in_path, img_out_path, span=100, save=False, orient='horizontal', loop=0, duration=100, repeat_delay=500, save_name="output"):
    """
    主函数

    参数:
        img_in_path (str): 起始图片路径
        img_out_path (str): 目标图片路径
        span (int): 分割步长,默认为100
        save (bool): 是否保存中间帧图像,默认为False
        orient (str): 合并方向,默认水平
        loop (int): 循环次数,默认为0(无限循环)
        duration (int): 帧持续时间(毫秒),默认为100
        repeat_delay (int): 循环之间的延迟时间(毫秒),默认为500
        save_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 images"

    if save:
        output_dir = 'output'
        os.makedirs(output_dir, exist_ok=True)

    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(f"output/{
      
      pos:04}.jpg")
        frames.append(result_image)

    if save:
        img_in.save("output/0000.jpg")
        img_out.save(f"output/{
      
      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
    # format='GIF':表示输出的文件格式为GIF格式
    # append_images=frames[1:]:表示将frames列表中除了第一张图片以外的剩余图片作为输出GIF动画的后续帧
    # save_all=True:表示将所有的帧都保存到输出的GIF动画中
    # duration:表示每一帧的持续时间duration,可以是数值也可以是列表。如果是列表则单独表示每一帧的时间
    # loop=0:表示循环播放次数为0,即无限循环播放
    # optimize=True:表示优化图片生成
    frames[0].save(f'{
      
      save_name}.gif', format='GIF', append_images=frames[1:],
                   save_all=True, duration=durations, loop=loop, optimize=True)


if __name__ == "__main__":
    # 起始图片路径
    img_in_path = 'in.jpg'
    # 目标图片路径
    img_out_path = 'out.jpg'
    # 调用 main 函数,并传入相应的参数
    main(
        img_in_path,                   # 起始图片路径
        img_out_path,                  # 目标图片路径
        save=True,                     # 是否保存中间结果
        span=150,                      # 分割步长,默认为 150
        orient='horizontal',           # 合并方向,默认为水平(可选值为 'horizontal' 或 'vertical')
        duration=500,                  # 帧持续时间(毫秒),默认为500
        save_name="output",            # 保存动画的文件名,默认为 "output"
        repeat_delay=2000              # 循环之间的延迟时间(毫秒)默认为 500
    )

The above code demonstrates a method of directly generating animations. In addition, animations can be generated by reading a collection of images from disk. The following is sample code for reading a previously saved intermediate image and generating an animated image:

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, format='GIF', append_images=frames[1:], save_all=True, duration=200, loop=0)

It is worth noting that the file size of gif images generated based on the Pillow library is often too large. This is because the Pillow library uses lossless compression to save gif images. In order to solve this problem, you can try the following methods to compress GIF images:

  • Use an online gif image compression website, such as:gif-compressor
  • Tools based on compressing or optimizing gif images, such as:gifsicle
  • Reduce gif image width and height

3 Reference

Guess you like

Origin blog.csdn.net/LuohenYJ/article/details/134010360