[Datenanalyse und Visualisierung] Zeichnen Sie einfache Animationen basierend auf Python

Animation ist ein effektives Visualisierungstool, das die Benutzerinteraktion und das visuelle Erlebnis verbessert und dabei hilft, Datenvisualisierungen auf sinnvolle Weise darzustellen. In diesem Artikel werden hauptsächlich zwei einfache Methoden zum Erstellen von Animationen in Python vorgestellt. Eine Methode besteht darin, das Animationsmodul von matplotlib zum Zeichnen von Animationen zu verwenden, und die andere Methode besteht darin, GIF-Animationen basierend auf Pillow zu generieren.

1 Animationsmodul

Das Animationsmodul von Matplotlib stellt die Klassen FuncAnimation und ArtistAnimation zum Erstellen von Matplotlib-Zeichnungsanimationen bereit. FuncAnimation und ArtistAnimation sind beide Unterklassen der Animationsklasse. Der Unterschied liegt in der Art der Animationsimplementierung und den Nutzungsszenarien. FuncAnimation eignet sich für Animationseffekte, die den Grafikstatus zeitabhängig aktualisieren, ist flexibler und wird häufiger verwendet. ArtistAnimation eignet sich dazu, vorhandene statische Bildsequenzen zu animierten Effekten zu kombinieren. Die spezifischen Unterschiede sind wie folgt:

  • FuncAnimation: FuncAnimation ist eine funktionsbasierte Methode zum Erstellen von Animationen. Es verwendet eine oder mehrere vom Benutzer bereitgestellte Funktionen, um den Status des Diagramms zu aktualisieren, und ruft diese Funktionen in bestimmten Abständen kontinuierlich auf, um Animationseffekte zu erzielen. Der Benutzer muss eine Aktualisierungsfunktion definieren, die die Eigenschaften des Grafikobjekts bei jedem Zeitschritt aktualisiert. Anschließend berechnet FuncAnimation automatisch die Bildsequenz der Animation basierend auf der Anzahl der Bilder, dem Zeitintervall und anderen vom Benutzer angegebenen Parametern. Diese Methode eignet sich für Animationseffekte, bei denen der Grafikstatus entsprechend zeitlichen Änderungen aktualisiert werden muss.

  • ArtistAnimation: ArtistAnimation ist eine Methode, die auf statischen Bildern basiert, um Animationen zu erstellen. Der Benutzer muss eine Reihe statischer Bilder bereitstellen, die als Künstlerobjekte bezeichnet werden. Bei diesen Bildern kann es sich um jede Art von Visualisierungsobjekt handeln, das mit Matplotlib erstellt wurde, z. B. Abbildung, Achsen, Line2D usw. Der Benutzer muss diese statischen Bilder in einer Liste speichern und dann die Reihenfolge dieser Bilder über ArtistAnimation anzeigen. ArtistAnimation zeigt diese Bilder Bild für Bild in benutzerdefinierten Zeitintervallen an, um Animationseffekte zu erzielen. Diese Methode eignet sich für Szenen, die bereits eine Reihe statischer Bilder enthalten, die zu einer Animation kombiniert werden müssen.

In diesem Abschnitt wird die Verwendung des Animationsmoduls anhand mehrerer Beispiele vorgestellt. Die vorgestellten Beispiele stammen aus: gallery-animation.

1.1 FuncAnimation-Klasse

Die Parameter des FuncAnimation-Konstruktors haben folgende Bedeutung:

  • fig: Zu animierendes Figurenobjekt.
  • func: Funktion zum Aktualisieren jedes Frames. Diese Funktion akzeptiert einen Parameterrahmen, der den aktuell zu zeichnenden Datenrahmen darstellt.
  • frames: Wird zum Generieren von Daten zum Plotten verwendet. Dabei kann es sich um eine Ganzzahl, eine Generatorfunktion oder einen Iterator handeln.
  • init_func: Initialisierungsfunktion, die vor dem Zeichnen der Animation aufgerufen wird.
  • fargs: Zusätzliche Parameter, die an die Funktion func übergeben werden (optional).
  • save_count: Geben Sie die Anzahl der zwischengespeicherten Frames in der Animation an (optional). Der Standardwert ist 100. Beachten Sie, dass dieser Parameter verwendet wird, um die Anzahl der Bilder zu bestimmen, die letztendlich zur Erstellung von Animationen und Videos verwendet werden.
  • interval: Das Zeitintervall zwischen den einzelnen Frames in Millisekunden, der Standardwert ist 200.
  • repeat: Steuern Sie, ob die Animation wiederholt abgespielt wird. Der Standardwert ist True.
  • repeat_delay: Verzögerungszeit (in Millisekunden) zwischen sich wiederholenden Animationen, Standardwert ist 0.
  • blit: Geben Sie an, ob die Blitting-Technologie zur Zeichnungsoptimierung verwendet werden soll. Der Standardwert ist False.
  • cache_frame_data: Geben Sie an, ob Frame-Daten zwischengespeichert werden sollen. Der Standardwert ist True.

Beispiel – Generieren einer dynamischen Sinuswellenanimation

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()

Beispiel – Erstellen Sie dynamische Streudiagramme und Liniendiagramme

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()

Beispiel – Bayesianische 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()

Beispiel – Regentropfen simulieren

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()

Beispiel – Animieren über Nebenhandlungen hinweg

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()

Beispiel – Dynamisches Oszilloskop

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() 

Beispiel – Dynamische Anzeige der Bevölkerungszahlen in Großstädten der Welt

Dieser Beispielcode und diese Daten stammen aus: how-to-create-animations-in-python. Dieser Code unterstützt die Anzeige des sich ändernden Trends der Städte mit der höchsten Bevölkerungszahl von 1500 bis 2020. In diesem Beispiel wird nur das einfache Zeichnen dynamischer Balkendiagramme vorgestellt. Für ein exquisiteres Zeichnen von Balkendiagrammen können Sie Folgendes verwenden: bar_chart_race oder 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()

Das Ergebnis ist wie folgt:

1.2 ArtistAnimation-Klasse

Die Parameter des ArtistAnimation-Konstruktors haben folgende Bedeutung:

  • fig: Zu animierendes Figurenobjekt.
  • artists: Enthält eine Liste einer Reihe von Zeichenobjekten, die als Animationsrahmen verwendet werden.
  • interval: Das Zeitintervall zwischen den einzelnen Frames in Millisekunden, der Standardwert ist 200.
  • repeat: Steuern Sie, ob die Animation wiederholt abgespielt wird. Der Standardwert ist True.
  • repeat_delay: Verzögerungszeit (in Millisekunden) zwischen sich wiederholenden Animationen, Standardwert ist 0.
  • blit: Geben Sie an, ob die Blitting-Technologie zur Zeichnungsoptimierung verwendet werden soll. Der Standardwert ist False.

Beispiel-ArtistAnimation einfache Verwendung

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()

Beispiel – Erstellen Sie ein dynamisches Balkendiagramm

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 Speichern von Animationen

Matplotlib erstellt und zeigt Animationen über die Plot-Methode an. Um Animationen als Animationen oder Videos zu speichern, stellt die Klasse Animation die Speicherfunktion zur Verfügung. Allgemeine Parameter der Speicherfunktion sind wie folgt:

  • filename: Der Pfad und Name der gespeicherten Datei.
  • writer: Geben Sie den zu verwendenden Autor an. Wenn nicht angegeben, wird standardmäßig der ffmpeg-Writer verwendet.
  • fps: Legen Sie die Bildrate fest (wie viele Bilder pro Sekunde angezeigt werden). Der Standardwert ist „Keine“, was bedeutet, dass das Intervallattribut im Animationsobjekt als Bildrate verwendet wird.
  • dpi: Legen Sie die Auflösung des Ausgabebilds fest. Der Standardwert ist „Keine“, was bedeutet, dass der Systemstandardwert verwendet wird.
  • codec: Geben Sie den Videocodec an, nur gültig, wenn der Autor ffmpeg_writer ist.
  • bitrate: Stellen Sie die Bitrate ein, nur gültig, wenn der Autor ffmpeg_writer ist.
  • extra_args: Zusätzliche Parameter zur Übergabe an den Autor.
  • metadata: Wörterbuch mit Dateimetadaten.
  • extra_anim: Zusätzliche Animationen, die gleichzeitig mit der Hauptanimation abgespielt werden.
  • savefig_kwargs: Schlüsselwortargumente, die an savefig() übergeben werden.
  • progress_callback: Rückruffunktion zur Aktualisierung des Fortschritts während des Speicherns.

Der Autor kann so angegeben werden, dass er verschiedene Multimedia-Schreibprogramme (z. B. Pillow, ffpmeg, imagemagik) verwendet, um lokal zu speichern, wie folgt:

Schriftsteller Unterstützte Formate
~matplotlib.animation.PillowWriter .gif, .apng, .webp
~matplotlib.animation.HTMLWriter .htm, .html, .png
~matplotlib.animation.FFMpegWriter Alle von ffmpeg unterstützten Formate: ffmpeg -formats
~matplotlib.animation.ImageMagickWriter Alle von imagemagick unterstützten Formate: Magick-List-Format

Der Code zum Speichern von Animationen und Videos lautet wie folgt:

# 动图
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")

Es ist zu beachten, dass die beim Erstellen des Animationsobjekts festgelegten Parameter keinen Einfluss auf die Speicherfunktion haben. Wie unten gezeigt, legen Sie in FuncAnimation „repeat=False“ fest, dh die Animation wird nur einmal abgespielt. Aber die gespeicherte GIF-Datei wird in einer Schleife abgespielt. Dies liegt daran, dass die Speicherfunktion die Animations- oder Videoaufbewahrungsfunktion anderer Drittbibliotheken aufruft und die Parameter zurückgesetzt werden müssen.

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)

Um das Problem des Speicherns von Animationen zu lösen, müssen Sie die Klasse zum Speichern von Animationen anpassen, wie unten gezeigt:

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 Generieren Sie animierte Grafiken basierend auf der Pillow-Bibliothek

Das Generieren von Animationen mit der Pillow-Bibliothek ist sehr einfach. Bereiten Sie zunächst eine Liste mit einer Reihe von Bildrahmen vor. Bei diesen Bildrahmen kann es sich um aufeinanderfolgende Bilder handeln, wobei jedes Bild einen Zeitpunkt der Animation darstellt. Als nächstes speichern Sie diese Bilder als GIF-Datei mit der Methode save() in der Pillow-Bibliothek. Beim Speichern der Animation können Sie auch einige Parameter festlegen, um den Animationseffekt zu steuern. Spezifische Gebrauchsanweisungen finden Sie in den folgenden Beispielen.

Beispiel – Gleitanimation

Dieses Beispiel zeigt einen Animationseffekt der Bildgleitanzeige, d. h. die allmähliche Umwandlung des anfänglichen Schwarzweißbilds in das Zielfarbbild durch einen gleitenden Farbverlauf. Die gezeigten Start- und Zielbilder sind wie folgt:

Die Animationsergebnisse sehen so aus:

Die wichtigsten einstellbaren Parameter des in diesem Beispiel bereitgestellten Codes werden wie folgt vorgestellt:

  • span(int): Split-Schrittgröße, Standard ist 100. Dieser Parameter wird verwendet, um die Segmentierungsschrittgröße während des Bildzusammenführungsprozesses zu steuern, d. h. die Entfernung, die jedes Mal zurückgelegt wird.

  • save(bool): Ob das Zwischenbild gespeichert werden soll, der Standardwert ist False. Wenn es auf „True“ gesetzt ist, wird jeder generierte Bildrahmen im angegebenen Ordner gespeichert.

  • orient(str): Zusammenführungsrichtung, Standard ist horizontal. Optionale Werte sind „horizontal“ (horizontale Richtung) oder „vertikal“ (vertikale Richtung). Wird verwendet, um die Zusammenführungsrichtung von Bildern zu steuern.

  • loop(int): Anzahl der Schleifen, Standard ist 0 (Endlosschleife). Bei einer positiven Ganzzahl wird die Animation für die angegebene Anzahl von Malen wiederholt; bei 0 wird die Animation endlos wiederholt.

  • duration(int): Frame-Dauer (Millisekunden), Standardwert ist 100. Wird verwendet, um die Anzeigezeit jedes Bildes in der Animation festzulegen.

  • repeat_delay(int): Verzögerungszeit zwischen Schleifen (Millisekunden), Standard ist 500. Wird verwendet, um die Verzögerungszeit zwischen den einzelnen Schleifen einzustellen.

  • save_name(str): Der Dateiname zum Speichern der Animation, der Standardwert ist „Ausgabe“. Wird verwendet, um den Namen der generierten Animationsdatei festzulegen.

Das Folgende ist ein Beispiel für die Code-Implementierung. Der Code liest zunächst das Startbild und das Zielbild und gibt dann die Teilungsposition an, um die Effekte auf beiden Seiten des Bildes festzulegen. Schließlich wird der Gleitgradienteneffekt durch Anpassen der Teilungsposition erreicht.

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
    )

Der obige Code demonstriert eine Methode zum direkten Generieren von Animationen. Darüber hinaus können Animationen generiert werden, indem eine Sammlung von Bildern von der Festplatte gelesen wird. Im Folgenden finden Sie Beispielcode zum Lesen eines zuvor gespeicherten Zwischenbilds und zum Erzeugen eines animierten Bilds:

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)

Es ist zu beachten, dass die Dateigröße von GIF-Bildern, die auf Basis der Pillow-Bibliothek generiert werden, oft zu groß ist. Dies liegt daran, dass die Pillow-Bibliothek zum Speichern von GIF-Bildern eine verlustfreie Komprimierung verwendet. Um dieses Problem zu lösen, können Sie die folgenden Methoden zum Komprimieren von GIF-Bildern ausprobieren:

  • Verwenden Sie eine Online-Website zur GIF-Bildkomprimierung, wie zum Beispiel:gif-compressor
  • Tools, die auf der Komprimierung oder Optimierung von GIF-Bildern basieren, wie zum Beispiel:gifsicle
  • Reduzieren Sie die Breite und Höhe des GIF-Bildes

3 Referenz

Supongo que te gusta

Origin blog.csdn.net/LuohenYJ/article/details/134010360
Recomendado
Clasificación