matplotlibで作ったアニメーションをGIFファイルに保存するには長らく外部ソフトウェアであるImagemagickが必要でしたが1、matplotlib 2.2からPythonの画像処理パッケージであるPillowを使ってGIFファイルに保存できるようになりました2。Imagemagickは画像関連の処理ならなんでもおまかせと言えるようなまさに魔法のような超有名ソフトウェアですが、Pythonパッケージとは異なりインストールが少し面倒です3。一方、Pillowはpip
やconda
で簡単に導入できます。すでにImagemagickが入っているならいいのですが、matplotlibのアニメGIFを保存するだけにインストール関連で苦労したくない、あるいは色々な事情で自由にソフトウェアをインストールできない場合にぴったりの選択肢ができたのはとてもよいことでしょう。ただ、公式サイトにはImagemagickなどの外部ソフトウェアを使った方がパフォーマンスは良いと書いてあるので、Imagemagickがあるに越したことはない模様です。
準備
おそらくmatplotlibの依存パッケージに明示されていると思うので、そもそもpillowがない場合はmatplotlibのimport時にエラーが出るのではないかと思います。また、依存パッケージなのでユーザーは明示的にimportする必要はありません。Anacondaを使っている場合はpillowは標準で入っているようです。なければconda install pillow
やpip install pillow
でインストールしてください。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as anm
from matplotlib.animation import PillowWriter
from matplotlib._version import get_versions as mplv
from scipy.stats import gaussian_kde
# ユーザーがpillowをimportする必要はない
print(mplv()['version'])
# 2.2.2
アニメーション用データを用意する
Galton boardやbean machineと呼ばれるパチンコの仕組みで正規分布を可視化できるおもちゃを参考にした正規分布のアニメーションを作ってみます。
http://galtonboard.com/
# 平均0、標準偏差0.1の正規分布から取り出した1000x10の乱数を
# パチンコ玉を10個ずつ落とす操作を1000回繰り返した結果とみなします
n = 1000
balls = np.zeros((n, 10))
np.random.seed(100)
for i in range(n):
balls[i] = np.random.normal(0, 0.1, 10)
fig, ax = plt.subplots(1, 1, figsize=(3, 3))
ax.tick_params(direction='in')
def Gaussian(x, mu, sigma):
return 1/(sigma * np.sqrt(2 * np.pi)) * np.exp( - (x - mu)**2 / (2 * sigma**2))
# 真の分布
x = np.linspace(-0.4, 0.4, 100)
y_true = Gaussian(x, 0, 0.1)
ax.plot(x, y_true, '--', c='k', label='true', alpha=0.6)
# 初回のパチンコ玉10個分のヒストグラム
_, _, patches = ax.hist(balls[0], bins=30, range=(-0.4, 0.4), density=True, alpha=0.2)
ax.set_title(f'Normal distribution\n10 events')
fig.tight_layout()
# ついでにKDEの結果が真の分布に近づく様子も可視化します
kde_model = gaussian_kde(balls[0].ravel())
y_est = kde_model(x)
ax.plot(x, y_est, c='r', label='estimation', alpha=0.8)
ax.legend(loc='upper right', handlelength=1.4)
def update(i):
if i==0:
return None
else:
data = balls[0:i+1].ravel()
weights, edges = np.histogram(data, bins=30, range=(-0.4, 0.4), density=True)
kde_model = gaussian_kde(data)
y_est = kde_model(x)
ax.lines.pop()
ax.plot(x, y_est, c='r', alpha=0.8)
for (patch, weight) in zip(patches, weights):
patch.set_height(weight)
ax.set_title(f'Normal distribution\n{(i+1)*10} events')
# 1000回の試行のうちアニメに使う部分をリストとして抽出
frames = list(range(10))
frames.extend(list(range(19, 300, 10)))
frames.extend(list(range(329, 600, 30)))
frames.extend(list(range(639, 1000, 40)))
ani = anm.FuncAnimation(fig, update, frames=frames)
ちなみに、FuncAnimation
の段階でフレーム間の時間を決めるinterval
を指定することもできますが、GIFファイルに保存する際は保存時に指定するfps
が優先されるようです。
アニメーションをGIFファイルに保存する
writer='pillow'
ani.save(figdir/'normaldist_kde_anim.gif', writer='pillow') # fpsはデフォルトの5
これまでのImagemagickを使う例ではwriter='imagemagick'
となっていた部分がwriter='pillow'
に変わっただけです。
もっと動きの速いアニメーションにするにはfpsを大きくします。
ani.save(figdir/'normaldist_kde_anim.gif', writer='pillow', fps=10)
writer=PillowWriter()
writer
は文字列ではなくMovieWriter
クラスを直接指定することもできます4。
from matplotlib.animation import PillowWriter
ani.save(figdir/'normaldist_kde_anim.gif', writer=PillowWriter())
# この場合、フレームの切り替わりスピードを決めるfpsはデフォルトの5(1秒で5フレーム)
# writer=PillowWriter(fps=10)などで変更可能
saving
メソッド+grab_frame
メソッド
ani.save
を使わずに、MovieWriter
クラスのsaving
メソッドとgrab_frame
メソッドを使って以下のようにも書けます。ani.save
よりも少し面倒なこの書き方はAggバックエンドの時に便利だと[公式マニュアル](Frame grabbing — Matplotlib 3.0.0 documentation https://matplotlib.org/gallery/animation/frame_grabbing_sgskip.html#sphx-glr-gallery-animation-frame-grabbing-sgskip-py)には書いてありますが、私はAggバックエンドが必要な場面に遭遇した時がないので御利益はよくわかりません。
writer = PillowWriter(fps=5)
# ver 2.2.2ではデフォルトのdpiを使いたい場合でもdpi=Noneは必要でした。
# ver 3で改善されているかもしれません。
with writer.saving(fig, 'normaldist_anim_test_writer_saving.gif', dpi=None):
for i in frames:
update(i)
writer.grab_frame()
-
「matplotlib アニメーション」でググると出てくる上位3位のQiitaエントリmatplotlib でアニメーションを作る、ボールが跳ねるアニメーションをpython(matplotlib)で書いてみた、matplotlibで簡単にアニメーションをつくる(mp4, gif) でもgifファイルの出力にはimagemagickを使っています。 ↩
-
公式サイトのNew in Matplotlib 2.2参照 ↩
-
なんでもできるが故に脆弱性も多いようです。さようなら ImageMagick - Cybozu Inside Out | サイボウズエンジニアのブログ ↩
-
執筆時点では公式サイトのExamplesには
writer='pillow'
を使った例はなく、MovieWriter
クラスを明示的に指定する方法しか載っていません。例えばこれ ↩