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

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


# 準備
おそらくmatplotlibの依存パッケージに明示されていると思うので、そもそもpillowがない場合はmatplotlibのimport時にエラーが出るのではないかと思います。また、依存パッケージなのでユーザーは明示的にimportする必要はありません。Anacondaを使っている場合はpillowは標準で入っているようです。なければ`conda install pillow`や`pip install pillow`でインストールしてください。

```python
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/

```python
# 平均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'`
```python
ani.save(figdir/'normaldist_kde_anim.gif', writer='pillow') # fpsはデフォルトの5
```
![normaldist_kde_anim.gif](https://qiita-image-store.s3.amazonaws.com/0/222849/c2613b99-c47c-9db0-19f7-c3518222efed.gif)

これまでのImagemagickを使う例では`writer='imagemagick'`となっていた部分が`writer='pillow'`に変わっただけです。

もっと動きの速いアニメーションにするにはfpsを大きくします。

```python
ani.save(figdir/'normaldist_kde_anim.gif', writer='pillow', fps=10)
```


## `writer=PillowWriter()`
`writer`は文字列ではなく`MovieWriter`クラスを直接指定することもできます[^4]。

```python
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バックエンドが必要な場面に遭遇した時がないので御利益はよくわかりません。

```python
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 でアニメーションを作る](https://qiita.com/yubais/items/c95ba9ff1b23dd33fde2)、[ボールが跳ねるアニメーションをpython(matplotlib)で書いてみた](https://qiita.com/MENDY/items/e1b432df1e0bfe8b680c)、[matplotlibで簡単にアニメーションをつくる（mp4, gif)](https://qiita.com/msrks/items/e264872efa062c7d6955) でもgifファイルの出力にはimagemagickを使っています。
[^2]: 公式サイトの[New in Matplotlib 2.2](https://matplotlib.org/users/prev_whats_new/whats_new_2.2.html#writing-animations-with-pillow)参照
[^3]: なんでもできるが故に脆弱性も多いようです。[さようなら ImageMagick - Cybozu Inside Out | サイボウズエンジニアのブログ](https://blog.cybozu.io/entry/2018/08/21/080000)
[^4]: 執筆時点では公式サイトのExamplesには`writer='pillow'`を使った例はなく、`MovieWriter`クラスを明示的に指定する方法しか載っていません。例えば[これ](https://matplotlib.org/gallery/animation/simple_anim.html#sphx-glr-gallery-animation-simple-anim-py)