52
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

matplotlibでimagemagickを使わずにアニメGIFを保存する

Last updated at Posted at 2018-09-27

matplotlibで作ったアニメーションをGIFファイルに保存するには長らく外部ソフトウェアであるImagemagickが必要でしたが1、matplotlib 2.2からPythonの画像処理パッケージであるPillowを使ってGIFファイルに保存できるようになりました2。Imagemagickは画像関連の処理ならなんでもおまかせと言えるようなまさに魔法のような超有名ソフトウェアですが、Pythonパッケージとは異なりインストールが少し面倒です3。一方、Pillowはpipcondaで簡単に導入できます。すでにImagemagickが入っているならいいのですが、matplotlibのアニメGIFを保存するだけにインストール関連で苦労したくない、あるいは色々な事情で自由にソフトウェアをインストールできない場合にぴったりの選択肢ができたのはとてもよいことでしょう。ただ、公式サイトにはImagemagickなどの外部ソフトウェアを使った方がパフォーマンスは良いと書いてあるので、Imagemagickがあるに越したことはない模様です。

準備

おそらくmatplotlibの依存パッケージに明示されていると思うので、そもそもpillowがない場合はmatplotlibのimport時にエラーが出るのではないかと思います。また、依存パッケージなのでユーザーは明示的にimportする必要はありません。Anacondaを使っている場合はpillowは標準で入っているようです。なければconda install pillowpip 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

normaldist_kde_anim.gif

これまでの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()
  1. 「matplotlib アニメーション」でググると出てくる上位3位のQiitaエントリmatplotlib でアニメーションを作るボールが跳ねるアニメーションをpython(matplotlib)で書いてみたmatplotlibで簡単にアニメーションをつくる(mp4, gif) でもgifファイルの出力にはimagemagickを使っています。

  2. 公式サイトのNew in Matplotlib 2.2参照

  3. なんでもできるが故に脆弱性も多いようです。さようなら ImageMagick - Cybozu Inside Out | サイボウズエンジニアのブログ

  4. 執筆時点では公式サイトのExamplesにはwriter='pillow'を使った例はなく、MovieWriterクラスを明示的に指定する方法しか載っていません。例えばこれ

52
37
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?