基于Python實(shí)現(xiàn)繪制簡單動圖的示例詳解
動畫是一種高效的可視化工具,能夠提升用戶的吸引力和視覺體驗(yàn),有助于以富有意義的方式呈現(xiàn)數(shù)據(jù)可視化。本文的主要介紹在Python中兩種簡單制作動圖的方法。其中一種方法是使用matplotlib的Animations模塊繪制動圖,另一種方法是基于Pillow生成GIF動圖。
1 Animations模塊
Matplotlib的Animations模塊提供了FuncAnimation和ArtistAnimation類來創(chuàng)建matplotlib繪圖動畫,F(xiàn)uncAnimation和ArtistAnimation都是Animation類的子類。它們的區(qū)別在于實(shí)現(xiàn)動畫的方式和使用場景不同。FuncAnimation適用于根據(jù)時間更新圖形狀態(tài)的動畫效果,且更加靈活和常用。而ArtistAnimation適用于將已有的靜態(tài)圖像序列組合成動畫的效果。具體區(qū)別如下:
FuncAnimation
:FuncAnimation是基于函數(shù)的方法來創(chuàng)建動畫的。它使用用戶提供的一個或多個函數(shù)來更新圖形的狀態(tài),并按照一定的時間間隔連續(xù)地調(diào)用這些函數(shù),從而實(shí)現(xiàn)動畫效果。用戶需要定義一個更新函數(shù),該函數(shù)在每個時間步長上更新圖形對象的屬性,然后FuncAnimation會根據(jù)用戶指定的幀數(shù)、時間間隔等參數(shù)來自動計(jì)算動畫的幀序列。這種方法適用于需要根據(jù)時間變化來更新圖形狀態(tài)的動畫效果。
ArtistAnimation
:ArtistAnimation是基于靜態(tài)圖像的方法來創(chuàng)建動畫的。它要求用戶提供一系列的靜態(tài)圖像,稱為藝術(shù)家對象。這些圖像可以是通過Matplotlib創(chuàng)建的任何類型的可視化對象,例如Figure、Axes、Line2D等。用戶需要將這些靜態(tài)圖像存儲在一個列表中,然后通過ArtistAnimation來顯示這些圖像的序列。ArtistAnimation會按照用戶指定的時間間隔逐幀地顯示這些圖像,從而實(shí)現(xiàn)動畫效果。這種方法適用于已經(jīng)有一系列靜態(tài)圖像需要組合成動畫的場景。
本節(jié)將通過幾個示例來介紹Animations模塊的使用,所介紹的示例出自:gallery-animation。
1.1 FuncAnimation類
FuncAnimation構(gòu)造函數(shù)的參數(shù)含義如下:
fig
:要繪制動畫的Figure對象。func
:用于更新每一幀的函數(shù),該函數(shù)接受一個參數(shù)frame,表示當(dāng)前待繪制的數(shù)據(jù)幀。frames
:用于產(chǎn)生待繪制的數(shù)據(jù),可以是整數(shù)、生成器函數(shù)或迭代器。init_func
:在繪制動畫之前調(diào)用的初始化函數(shù)。fargs
:傳遞給func
函數(shù)的附加參數(shù)(可選)。save_count
:指定動畫中緩存的幀數(shù)量(可選),默認(rèn)為100。注意該參數(shù)用于確定最后生成動圖和視頻所用圖像的數(shù)量。interval
:每一幀之間的時間間隔,以毫秒為單位,默認(rèn)為200。repeat
:控制動畫是否重復(fù)播放,默認(rèn)為True。repeat_delay
:重復(fù)動畫之間的延遲時間(以毫秒為單位),默認(rèn)為0。blit
:指定是否使用blitting技術(shù)來進(jìn)行繪制優(yōu)化,默認(rèn)為False。cache_frame_data
:指定是否緩存幀數(shù)據(jù),默認(rèn)為True。
示例-生成動態(tài)的正弦波動畫
import itertools import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation # 定義生成數(shù)據(jù)的函數(shù) def data_gen(max_range): # 使用itertools.count()生成無限遞增的計(jì)數(shù)器 for cnt in itertools.count(): # 當(dāng)計(jì)數(shù)器超過最大范圍時停止生成數(shù)據(jù) if cnt > max_range: break print(cnt) # 計(jì)算時間t和對應(yīng)的y值,使用np.sin()計(jì)算sin函數(shù),np.exp()計(jì)算指數(shù)函數(shù) t = cnt / 10 yield t, np.sin(2*np.pi*t) * np.exp(-t/10.) # 初始化函數(shù),設(shè)置坐標(biāo)軸范圍和清空數(shù)據(jù) 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, # 創(chuàng)建圖形對象以及子圖對象 fig, ax = plt.subplots() # 創(chuàng)建線條對象 line, = ax.plot([], [], lw=2) # 創(chuàng)建文本對象用于顯示 x 和 y 值 text = ax.text(0., 0., '', transform=ax.transAxes) # 設(shè)置文本位置 text.set_position((0.7, 0.95)) # 將文本對象添加到圖形中 ax.add_artist(text) ax.grid() xdata, ydata = [], [] # 更新函數(shù),將新的數(shù)據(jù)添加到圖形中 def run(data): # 獲取傳入的數(shù)據(jù) t, y = data # 將時間和對應(yīng)的y值添加到xdata和ydata中 xdata.append(t) ydata.append(y) # 獲取當(dāng)前坐標(biāo)軸的范圍 xmin, xmax = ax.get_xlim() # 更新文本對象的值 text.set_text('x = {:.2f}, y = {:.2f}'.format(t, y)) # 如果時間t超過當(dāng)前范圍,更新坐標(biāo)軸范圍 if t >= xmax: ax.set_xlim(xmin, 2*xmax) # 重繪圖形 ax.figure.canvas.draw() # 更新線條的數(shù)據(jù) line.set_data(xdata, ydata) return line, text # 創(chuàng)建動畫對象 # fig:圖形對象 # run:更新函數(shù),用于更新圖形中的數(shù)據(jù) # data_gen(20):生成器函數(shù),產(chǎn)生數(shù)據(jù)的最大范圍為20 # interval=100:每幀動畫的時間間隔為100毫秒 # init_func=init:初始化函數(shù),用于設(shè)置圖形的初始狀態(tài) # repeat=True:動畫重復(fù)播放 ani = animation.FuncAnimation(fig, run, data_gen(20), interval=100, init_func=init, repeat=True) # 顯示圖形 plt.show()
示例-創(chuàng)建動態(tài)散點(diǎn)圖與折線圖
import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation # 創(chuàng)建一個圖形窗口和坐標(biāo)軸 fig, ax = plt.subplots() # 創(chuàng)建時間數(shù)組 t = np.linspace(0, 3, 50) # 自由落體加速度 g = -9.81 # 初始速度 v0 = 12 # 計(jì)算高度 z = g * t**2 / 2 + v0 * t # 第二個初始速度 v02 = 5 # 計(jì)算第二個高度 z2 = g * t**2 / 2 + v02 * t # 創(chuàng)建散點(diǎn)圖 scat = ax.scatter(t[0], z[0], c="b", s=5, label=f'v0 = {v0} m/s') # 創(chuàng)建線圖 line2 = ax.plot(t[0], z2[0], label=f'v0 = {v02} m/s')[0] # 設(shè)置坐標(biāo)軸范圍和標(biāo)簽 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] # 更新散點(diǎn)圖 data = np.stack([x, y]).T # 更新散點(diǎn)圖中每個點(diǎn)的位置 scat.set_offsets(data) # 更新線圖 line2.set_xdata(t[:frame]) line2.set_ydata(z2[:frame]) return (scat, line2) # 創(chuàng)建動畫 # frames為數(shù)值表示動畫的總幀數(shù),即每次更新參數(shù)傳入當(dāng)前幀號 ani = animation.FuncAnimation(fig=fig, func=update, frames=40, interval=30) # 顯示圖形 plt.show()
示例-貝葉斯更新動畫
import math import matplotlib.pyplot as plt import numpy as np from matplotlib.animation import FuncAnimation # 定義分布概率密度函數(shù) def beta_pdf(x, a, b): return (x**(a-1) * (1-x)**(b-1) * math.gamma(a + b) / (math.gamma(a) * math.gamma(b))) # 更新分布類,用于更新動態(tài)圖 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 # 設(shè)置圖形參數(shù) self.ax.set_xlim(0, 1) self.ax.set_ylim(0, 10) self.ax.grid(True) # 這條豎直線代表了理論值,圖中的分布應(yīng)該趨近于這個值 self.ax.axvline(prob, linestyle='--', color='black') def __call__(self, i): # 這樣圖形可以連續(xù)運(yùn)行,我們只需不斷觀察過程的新實(shí)現(xiàn) if i == 0: self.success = 0 self.line.set_data([], []) return self.line, # 根據(jù)超過閾值與均勻選擇來選擇成功 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, # 設(shè)置隨機(jī)狀態(tài)以便再現(xiàn)結(jié)果 np.random.seed(0) # 創(chuàng)建圖形和坐標(biāo)軸對象 fig, ax = plt.subplots() # 創(chuàng)建更新分布對象,并應(yīng)該收斂到的理論值為0.7 ud = UpdateDist(ax, prob=0.7) # 創(chuàng)建動畫對象 anim = FuncAnimation(fig, ud, frames=100, interval=100, blit=True, repeat_delay=1000) # 顯示動畫 plt.show()
示例-模擬雨滴
import matplotlib.pyplot as plt import numpy as np from matplotlib.animation import FuncAnimation # 設(shè)置隨機(jī)種子以確??蓮?fù)現(xiàn)性 np.random.seed(0) # 創(chuàng)建畫布和坐標(biāo)軸對象 fig = plt.figure(figsize=(7, 7)) # 在畫布上添加一個坐標(biāo)軸對象。 # [0, 0, 1, 1]參數(shù)指定了坐標(biāo)軸的位置和大小,分別表示左下角的 x 坐標(biāo)、左下角的 y 坐標(biāo)、寬度和高度。 # frameon=False參數(shù)表示不顯示坐標(biāo)軸的邊框 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([]) # 創(chuàng)建雨滴數(shù)據(jù) n_drops = 50 rain_drops = np.zeros(n_drops, dtype=[('position', float, (2,)), ('size', float), ('growth', float), ('color', float, (4,))]) # 隨機(jī)初始化雨滴的位置和生長速率 rain_drops['position'] = np.random.uniform(0, 1, (n_drops, 2)) rain_drops['growth'] = np.random.uniform(50, 200, n_drops) # 創(chuàng)建散點(diǎn)圖對象,用于在動畫中更新雨滴的狀態(tài) 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) # 使用新的顏色、大小和位置更新散點(diǎn)圖對象 scat.set_edgecolors(rain_drops['color']) scat.set_sizes(rain_drops['size']) scat.set_offsets(rain_drops['position']) # 創(chuàng)建動畫,并將update函數(shù)作為動畫的回調(diào)函數(shù) animation = FuncAnimation(fig, update, interval=10, save_count=100) plt.show()
示例-跨子圖動畫
import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation from matplotlib.patches import ConnectionPatch # 創(chuàng)建一個包含左右兩個子圖的圖形對象 fig, (axl, axr) = plt.subplots( ncols=2, # 指定一行中子圖的列數(shù)為2,即創(chuàng)建兩個子圖 sharey=True, # 共享y軸刻度 figsize=(6, 2), # width_ratios=[1, 3]指定第二個子圖的寬度為第一個子圖的三倍 # wspace=0 設(shè)置子圖之間的水平間距為0 gridspec_kw=dict(width_ratios=[1, 3], wspace=0), ) # 設(shè)置左側(cè)子圖縱橫比為1,即使得它的寬度和高度相等 axl.set_aspect(1) # 設(shè)置右側(cè)子圖縱橫比為1/3,即高度是寬度的三分之一 axr.set_box_aspect(1 / 3) # 右子圖不顯示y軸刻度 axr.yaxis.set_visible(False) # 設(shè)置右子圖x軸刻度以及對應(yīng)的標(biāo)簽 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) # 在左子圖上繪制初始點(diǎn) point, = axl.plot(0, 0, "o") # 在右子圖上繪制完整的正弦曲線,以設(shè)置視圖限制 sine, = axr.plot(x, np.sin(x)) # 繪制連接兩個圖表的連線 con = ConnectionPatch( (1, 0), # 連接線的起始點(diǎn)坐標(biāo) (0, 0), # 連接線的終點(diǎn)坐標(biāo) "data", "data", axesA=axl, # 指定連接線的起始點(diǎn)所在的坐標(biāo)軸 axesB=axr, # 指定連接線的終點(diǎn)所在的坐標(biāo)軸 color="red", ls="dotted", # 連接線類型 ) fig.add_artist(con) # 定義動畫函數(shù) 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 # 創(chuàng)建動畫對象 ani = animation.FuncAnimation( fig, animate, interval=50, blit=False, # 不使用blitting技術(shù),這里Figure artists不支持blitting frames=x, repeat_delay=100, # 動畫重復(fù)播放延遲100毫秒 ) # 展示動畫 plt.show()
示例-動態(tài)示波器
import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation from matplotlib.lines import Line2D # 創(chuàng)建一個 Scope 類用于繪制動態(tài)圖形 class Scope: def __init__(self, ax, maxt=2, dt=0.02): """ :param ax: Matplotlib 的坐標(biāo)軸對象 :param maxt: 時間的最大值,默認(rèn)為2 :param dt: 時間步長,默認(rèn)為0.02 """ self.ax = ax self.dt = dt self.maxt = maxt self.tdata = [0] # 時間數(shù)據(jù)的列表 self.ydata = [0] # y軸數(shù)據(jù)的列表 self.line = Line2D(self.tdata, self.ydata) # 創(chuàng)建一條線對象 self.ax.add_line(self.line) # 將線對象添加到坐標(biāo)軸上 self.ax.set_ylim(-.1, 1.1) # 設(shè)置y軸范圍 self.ax.set_xlim(0, self.maxt) # 設(shè)置x軸范圍 def update(self, y): """ 更新圖形數(shù)據(jù) :param y: 新的y軸數(shù)據(jù) :return: 更新后的線對象 """ lastt = self.tdata[-1] if lastt >= self.tdata[0] + self.maxt: # 如果當(dāng)前時間超過了最大時間,重新設(shè)置數(shù)組 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() # 進(jìn)行時間的計(jì)算 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))返回一個隨機(jī)值,否則返回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() # 創(chuàng)建一個圖形窗口和一對坐標(biāo)軸 scope = Scope(ax) # 創(chuàng)建一個Scope對象,用于繪制動態(tài)圖 # 使用scope的類函數(shù)update作為更新函數(shù) ani = animation.FuncAnimation(fig, scope.update, emitter, interval=50, blit=True, save_count=100) plt.show()
示例-世界主要城市的人口數(shù)量動態(tài)展示
本示例代碼和數(shù)據(jù)來自于: how-to-create-animations-in-python。這段代碼支持展示自1500年到2020年期間人口數(shù)排名靠前的城市的變化趨勢。該示例只是介紹簡單的動態(tài)條形圖繪制,更加精美的條形圖繪制可使用: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 # 定義一個函數(shù),用于生成顏色列表 def generate_colors(string_list): num_colors = len(string_list) # 使用tab10調(diào)色板,可以根據(jù)需要選擇不同的調(diào)色板 colormap = plt.cm.get_cmap('tab10', num_colors) colors = [] for i in range(num_colors): color = colormap(i) colors.append(color) return colors # 讀取CSV文件,并選擇所需的列 # 數(shù)據(jù)地址:https://media.geeksforgeeks.org/wp-content/cdn-uploads/20210901121516/city_populations.csv df = pd.read_csv('city_populations.csv', usecols=[ 'name', 'group', 'year', 'value']) # 將年份列轉(zhuǎn)換為整數(shù)型 df['year'] = df['year'].astype(int) # 將人口數(shù)量列轉(zhuǎn)換為浮點(diǎn)型 df['value'] = df['value'].astype(float) # 獲取城市分組列表 group = list(set(df.group)) # 生成城市分組對應(yīng)的顏色字典 group_clolor = dict(zip(group, generate_colors(group))) # 創(chuàng)建城市名稱與分組的字典 group_name = df.set_index('name')['group'].to_dict() # 定義繪制柱狀圖的函數(shù) def draw_barchart(year): # 根據(jù)年份篩選數(shù)據(jù),并按人口數(shù)量進(jìn)行降序排序,取出最大范圍的數(shù)據(jù) df_year = df[df['year'].eq(year)].sort_values( by='value', ascending=True).tail(max_range) ax.clear() # 繪制水平柱狀圖,并設(shè)置顏色 ax.barh(df_year['name'], df_year['value'], color=[ group_clolor[group_name[x]] for x in df_year['name']]) # 在柱狀圖上方添加文字標(biāo)簽 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') # 地區(qū)名 ax.text(value+dx, i, f'{value:,.0f}', size=12, ha='left', va='center') # 設(shè)置其他樣式 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軸的主要刻度格式化,不保留小數(shù) ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}')) # 將x軸的刻度位置設(shè)置在圖的頂部 ax.xaxis.set_ticks_position('top') # 設(shè)置x軸的刻度顏色為灰色(#777777),字體大小為16 ax.tick_params(axis='x', colors='#777777', labelsize=16) # 清除y軸的刻度標(biāo)簽 ax.set_yticks([]) # 在x軸和y軸上設(shè)置0.01的邊距 ax.margins(0, 0.01) # 在x軸上繪制主要網(wǎng)格線,線條樣式為實(shí)線 ax.grid(which='major', axis='x', linestyle='-') # 設(shè)置網(wǎng)格線繪制在圖像下方 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) # 創(chuàng)建繪圖所需的figure和axes fig, ax = plt.subplots(figsize=(12, 8)) start_year = 2000 end_year = 2020 # 設(shè)置最多顯示城市數(shù)量 max_range = 15 # 獲取數(shù)據(jù)中的最小年份和最大年份,并進(jìn)行校驗(yàn) 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}" # 創(chuàng)建動畫對象,調(diào)用draw_barchart函數(shù)進(jìn)行繪制 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()
結(jié)果如下:
1.2 ArtistAnimation類
ArtistAnimation構(gòu)造函數(shù)的參數(shù)含義如下:
fig
:要繪制動畫的Figure對象。artists
:包含了一系列繪圖對象的列表,這些繪圖對象將被作為動畫的幀。interval
:每一幀之間的時間間隔,以毫秒為單位,默認(rèn)為200。repeat
:控制動畫是否重復(fù)播放,默認(rèn)為True。repeat_delay
:重復(fù)動畫之間的延遲時間(以毫秒為單位),默認(rèn)為0。blit
:指定是否使用blitting技術(shù)來進(jìn)行繪制優(yōu)化,默認(rèn)為False。
示例-ArtistAnimation簡單使用
import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation fig, ax = plt.subplots() # 定義函數(shù) 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 是一個列表的列表,每一行是當(dāng)前幀要繪制的藝術(shù)品列表; # 在這里我們只在每一幀動畫中繪制一個藝術(shù)家,即圖像 ims = [] # 循環(huán)生成動畫的每一幀,并存入一個列表 for i in range(60): # 更新 x 和 y 的取值 x += np.pi / 15 y += np.pi / 30 # 調(diào)用函數(shù) f(x, y),并繪制其返回的圖像 im = ax.imshow(f(x, y), animated=True) if i == 0: # 首先顯示一個初始的圖像 ax.imshow(f(x, y)) # 將當(dāng)前幀添加到ims中 ims.append([im]) # 基于ims中的繪圖對象繪制動圖 ani = animation.ArtistAnimation(fig, ims, interval=50, blit=True, repeat_delay=1000) # 顯示動畫 plt.show()
示例-創(chuàng)建動態(tài)柱狀圖
import matplotlib.pyplot as plt import numpy as np import matplotlib.animation as animation fig, ax = plt.subplots() rng = np.random.default_rng(0) # # 創(chuàng)建一個包含5個元素的數(shù)組,表示數(shù)據(jù)集 data = np.array([20, 20, 20, 20,20]) # 創(chuàng)建一個包含5個字符串的列表,表示數(shù)據(jù)集的標(biāo)簽 x = ["A", "B", "C", "D","E"] # 創(chuàng)建一個空列表,用于存儲圖形對象 artists = [] # 創(chuàng)建一個包含5個顏色值的列表,用于繪制圖形 colors = ['tab:blue', 'tab:red', 'tab:green', 'tab:purple', 'tab:orange'] for i in range(20): # 隨機(jī)生成一個與data形狀相同的數(shù)組,并將其加到data中 data += rng.integers(low=0, high=10, size=data.shape) # 創(chuàng)建一個水平條形圖,并設(shè)置顏色 container = ax.barh(x, data, color=colors) # 設(shè)置x軸范圍 ax.set_xlim(0,150) # 將創(chuàng)建的圖形對象添加到列表中 artists.append(container) # 創(chuàng)建一個ArtistAnimation對象,指定圖形窗口和圖形對象列表以及動畫間隔時間 ani = animation.ArtistAnimation(fig=fig, artists=artists, interval=200) plt.show()
1.3 動畫保存
Matplotlib通過plot方法創(chuàng)建和顯示動畫。為了保存動畫為動圖或視頻,Animation類提供了save函數(shù)。save函數(shù)的常見參數(shù)如下:
filename
:保存文件的路徑和名稱。writer
:指定要使用的寫入器(Writer)。如果未指定,則默認(rèn)使用ffmpeg寫入器。fps
:設(shè)置幀速率(每秒顯示多少幀),默認(rèn)值為None,表示使用Animation對象中的interval屬性作為幀速率。dpi
:設(shè)置輸出圖像的分辨率,默認(rèn)值為None,表示使用系統(tǒng)默認(rèn)值。codec
:指定視頻編解碼器,僅當(dāng)writer為ffmpeg_writer時有效。bitrate
:設(shè)置比特率,僅當(dāng)writer為ffmpeg_writer時有效。extra_args
:用于傳遞給寫入器的額外參數(shù)。metadata
:包含文件元數(shù)據(jù)的字典。extra_anim
:與主要動畫同時播放的其他動畫。savefig_kwargs
:傳遞給savefig()的關(guān)鍵字參數(shù)。progress_callback
:用于在保存過程中更新進(jìn)度的回調(diào)函數(shù)。
writer寫入器可以指定使用各種多媒體寫入程序(例如:Pillow、ffpmeg、imagemagik)保存到本地,如下所示:
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 |
保存動圖和視頻的代碼如下:
# 動圖 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")
需要注意的是動圖構(gòu)建對象時所設(shè)置的參數(shù)不會影響save函數(shù),如下所示,在FuncAnimation中設(shè)置repeat=False,即動圖只播放一次。但是保存的gif文件卻循環(huán)播放。這是因?yàn)閟ave函數(shù)調(diào)用了其他第三庫的動圖或者視頻保持函數(shù),需要重新設(shè)置參數(shù)。
import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation # 創(chuàng)建畫布和坐標(biāo)軸 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, # 創(chuàng)建動畫對象 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)
要解決保存動畫問題,需要自定義動畫保存類,如下所示:
import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from matplotlib import animation # 創(chuàng)建畫布和坐標(biāo)軸 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, # 創(chuàng)建動畫對象 ani = FuncAnimation(fig, update, frames=100, interval=100, init_func=init, blit=True, repeat=False) # 創(chuàng)建自定義的動畫寫入類 class SubPillowWriter(animation.PillowWriter): def __init__(self, loop=1, **kwargs): super().__init__(**kwargs) # 將loop設(shè)置為0,表示無限循環(huán)播放;如果設(shè)置為一個大于0的數(shù)值,表示循環(huán)播放指定次數(shù) self.loop = loop # 定義播放結(jié)束時,保存圖片的代碼 def finish(self): # 調(diào)用了pillow包 self._frames[0].save(self.outfile, save_all=True, append_images=self._frames[1:], duration=int( 1000 / self.fps), loop=self.loop) # 創(chuàng)建動畫寫入對象 # fps=15:每秒幀數(shù),表示動畫的播放速度為每秒 15 幀。 # metadata=dict(artist='luohenyueji'):元數(shù)據(jù)信息,包括藝術(shù)家信息,將被添加到生成的GIF文件中。 writer = SubPillowWriter(fps=15, metadata=dict(artist='luohenyueji')) ani.save(filename="pillow_example.gif", writer=writer, dpi=150)
2 基于Pillow庫生成動圖
使用Pillow庫生成動圖非常簡單。首先,準(zhǔn)備一個包含一系列圖像幀的列表。這些圖像幀可以是連續(xù)的圖片,每張圖片表示動畫的一個時間點(diǎn)。接下來,使用Pillow庫中的save()方法將這些圖像幀保存為一個gif文件。在保存動圖時,還可以設(shè)置一些參數(shù)來控制動畫效果。參考以下示例,可獲取具體的使用說明。
示例-滑動動圖
該示例展示了一種圖像滑動展示的動畫效果,即通過滑動漸變的方式逐步將起始黑白圖片轉(zhuǎn)變?yōu)槟繕?biāo)彩色圖片。所示起始圖片和目標(biāo)圖片如下所示:
動畫結(jié)果如下所示:
本示例所提供代碼主要可調(diào)參數(shù)介紹如下:
span
(int): 分割步長,默認(rèn)為100。此參數(shù)用于控制圖片合并過程中的分割步長,即每次移動的距離。save
(bool): 是否保存中間幀圖像,默認(rèn)為False。如果設(shè)置為True,則會將生成的每一幀圖像保存到指定的文件夾中。orient
(str): 合并方向,默認(rèn)水平??蛇x值為'horizontal'(水平方向)或'vertical'(垂直方向)。用于控制圖像的合并方向。loop
(int): 循環(huán)次數(shù),默認(rèn)為0(無限循環(huán))。設(shè)置為正整數(shù)時,動畫會循環(huán)播放指定次數(shù);設(shè)置為0時,動畫會無限循環(huán)播放。duration
(int): 幀持續(xù)時間(毫秒),默認(rèn)為100。用于設(shè)置每一幀圖像在動畫中的顯示時間。repeat_delay
(int): 循環(huán)之間的延遲時間(毫秒),默認(rèn)為500。用于設(shè)置每次循環(huán)之間的延遲時間。save_name
(str): 保存動畫的文件名,默認(rèn)為"output"。用于設(shè)置生成的動畫文件的名稱。
以下是代碼實(shí)現(xiàn)的示例。該代碼首先讀取起始圖片和目標(biāo)圖片,然后指定分割位置以設(shè)置圖片兩側(cè)的效果。最后,通過調(diào)整分割位置來實(shí)現(xiàn)滑動漸變效果。
from PIL import Image, ImageDraw import os def merge_image(in_img, out_img, pos, orient="horizontal"): """ 合并圖像的函數(shù) 參數(shù): in_img (PIL.Image): 輸入圖像 out_img (PIL.Image): 輸出圖像 pos (int): 分割位置 orient (str): 圖像合并方向,默認(rèn)水平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"): """ 主函數(shù) 參數(shù): img_in_path (str): 起始圖片路徑 img_out_path (str): 目標(biāo)圖片路徑 span (int): 分割步長,默認(rèn)為100 save (bool): 是否保存中間幀圖像,默認(rèn)為False orient (str): 合并方向,默認(rèn)水平 loop (int): 循環(huán)次數(shù),默認(rèn)為0(無限循環(huán)) duration (int): 幀持續(xù)時間(毫秒),默認(rèn)為100 repeat_delay (int): 循環(huán)之間的延遲時間(毫秒),默認(rèn)為500 save_name (str): 保存動畫的文件名,默認(rèn)為"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動畫保存在當(dāng)前目錄下并命名為{save_name}.gif # format='GIF':表示輸出的文件格式為GIF格式 # append_images=frames[1:]:表示將frames列表中除了第一張圖片以外的剩余圖片作為輸出GIF動畫的后續(xù)幀 # save_all=True:表示將所有的幀都保存到輸出的GIF動畫中 # duration:表示每一幀的持續(xù)時間duration,可以是數(shù)值也可以是列表。如果是列表則單獨(dú)表示每一幀的時間 # loop=0:表示循環(huán)播放次數(shù)為0,即無限循環(huán)播放 # optimize=True:表示優(yōu)化圖片生成 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' # 目標(biāo)圖片路徑 img_out_path = 'out.jpg' # 調(diào)用 main 函數(shù),并傳入相應(yīng)的參數(shù) main( img_in_path, # 起始圖片路徑 img_out_path, # 目標(biāo)圖片路徑 save=True, # 是否保存中間結(jié)果 span=150, # 分割步長,默認(rèn)為 150 orient='horizontal', # 合并方向,默認(rèn)為水平(可選值為 'horizontal' 或 'vertical') duration=500, # 幀持續(xù)時間(毫秒),默認(rèn)為500 save_name="output", # 保存動畫的文件名,默認(rèn)為 "output" repeat_delay=2000 # 循環(huán)之間的延遲時間(毫秒)默認(rèn)為 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() # 創(chuàng)建圖片幀列表 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)
值得注意,基于Pillow庫生成的gif圖片,往往文件體積過大。這是因?yàn)镻illow庫采用無損壓縮的方式保存gif圖片。為了解決這個問題,可以嘗試以下方法對gif圖片進(jìn)行壓縮:
- 使用在線gif圖片壓縮網(wǎng)站,如:gif-compressor
- 基于壓縮或優(yōu)化gif圖片的工具,如:gifsicle
- 縮小gif圖像寬高
以上就是基于Python實(shí)現(xiàn)繪制簡單動圖的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Python繪制動圖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python通過psd-tools解析PSD文件的實(shí)現(xiàn)
本文主要介紹了Python通過psd-tools解析PSD文件的實(shí)現(xiàn),主要包括如何獲取PSD文件的基本信息、遍歷圖層、提取圖層詳細(xì)信息、保存和創(chuàng)建PSD文件,感興趣的可以了解一下2023-12-12Python解決多進(jìn)程間訪問效率低的方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了當(dāng)Python多進(jìn)程間訪問效率低時,應(yīng)該如何解決?文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-09-09Python在后臺自動解壓各種壓縮文件的實(shí)現(xiàn)方法
這篇文章主要介紹了Python在后臺自動解壓各種壓縮文件的實(shí)現(xiàn)方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11Python開發(fā)之基于模板匹配的信用卡數(shù)字識別功能
這篇文章主要介紹了基于模板匹配的信用卡數(shù)字識別功能,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01