[Python] moviepy + matplotlib で動画生成

## これはなに

matplotlibで描画した画像をつなぎ合わせてmp4ファイルを作ります。

## 準備

```
pip install matplotlib moviepy
```

## 使用例

まずは[公式ドキュメント](https://zulko.github.io/moviepy/getting_started/working_with_matplotlib.html)にならって、小さい例で基本的な使い方を示します。

```python
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from moviepy.editor import VideoClip
from moviepy.video.io.bindings import mplfig_to_npimage

def make_frame(t):
    # 座標(t,t)に長方形を描画
    fig, ax = plt.subplots(figsize=(6,6))
    ax.add_patch(Rectangle((t, t), 1,2))
    ax.set_xlim(0,10)
    ax.set_ylim(0,10)
    # bitmapに変換. bmp.shape==(432,432,3)
    bmp = mplfig_to_npimage(fig)
    plt.close()
    return bmp
    
fname = "output/out2.mp4"
# 10秒, 30fps の計300フレームからなる動画を生成
animation = VideoClip(make_frame, duration = 10)
animation.write_videofile(fname, fps = 30)
```

インポートすべきいものが多いのでゴツいですが、要するにVideoClipオブジェクトに画像を描画するmake_frame()関数を渡して、動画を生成しているだけです。
make_frame()関数は1フレームの描画のたびに呼び出され、引数には秒数tが与えられます。返り値は各ピクセルのRGBの値を表す3次元配列を必要がありますが、これはmplfig_to_npimage関数を使うことでmatplotlibのfigureオブジェクトから生成することができます。

出力された動画は以下のようなものになります: 

![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283539/402ec855-e39a-3e3c-caab-5f1d7f28d2bb.gif)


## 実用的な使い方

前節の使い方そのままだと、実際に大きなプログラムの中で描画～動画生成を行いたい場合にmake_frame関数の部分にすべての処理を記述することになってしまい、コードが複雑になってしまいます。あるいは、既にある大きなコードに動画出力の機能を足したい場合に、make_frame関数の中に既存のコードを移植することになってしまいます。

そこで実用的にはマルチスレッドにして

* メイン処理を行い、figを作るスレッド
* 作られたfigから動画を生成するスレッド

の2つに分けるのが見通しがよいです。

```python
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from moviepy.editor import VideoClip
from moviepy.video.io.bindings import mplfig_to_npimage
import threading
from queue import Queue

def func():
    """動画を生成
    """
    fname = "./out3.mp4"
    animation = VideoClip(make_frame, duration = 10)
    animation.write_videofile(fname, fps = 30)
def make_frame(t):
    """figqにbitmapが入ってきたらそれを取り出して返す
    """
    return figq.get()
def main():
    """メインの処理 + figqへのbitmapの追加
    """
    for i in range(310):
        fig, ax = plt.subplots(figsize=(6,6))
        ax.add_patch(Rectangle((i/30, i/30), 1,2))
        ax.set_xlim(0,10)
        ax.set_ylim(0,10)
        bmp = mplfig_to_npimage(fig)
        figq.put(bmp)
        plt.close()
    
figq = Queue()
thread = threading.Thread(target = func)
thread.start()
main()
thread.join()
```

先ほどとの変更点は、
* スレッドセーフなQueue(figq)を用意
* 動画の生成部分は別スレッド(thread)に切り出し
* make_frame()関数はfigqに入ってきたbitmapを取り出すだけの簡単なお仕事 (tは引数として与えられるが使わない)
* メイン処理～bitmapの生成・Queueへの追加をmain()関数に切り出し

といったあたりになります。結果としてはまったく同じ動画が出力されます。
既存のプログラムに動画生成の機能だけ付け加えたければ、main関数の

```python
figq.put(bmp)
```

のあたりだけ追加すれば、あとの部分はコピペでOKだと思います。


## 実用例

[AHC001](https://atcoder.jp/contests/ahc001/tasks/ahc001_a)のビジュアライザを作りました。

https://atcoder.jp/contests/ahc001/submissions/23262496

提出ではpypyで通るようにTEST=0としていますが、TEST=1とすると手元で動画を生成しながら動作します。
別途入力ファイルの準備も必要で、動かすのは面倒だと思うので眺める用にどうぞ。


できた動画は以下のような感じ: 

![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283539/bcde5403-d4ad-53da-0e34-f073a9698a7a.gif)



そもそもアルゴリズムの性能がイマイチだったり、あくまで本記事のために書いたのでこのコンテストで必要な情報(広告の必須枠の位置とか)が載ってませんが。。

## おまけ

jupyter notebookからであれば

```python
import IPython
IPython.display.Video("./out3.mp4")
```

のようにしてセルの下で動画を見ることができて便利です。


## 注意点

* 必要なフレーム枚数をfigqにputしないと動画生成のスレッドが完了しないためthread.join()でハングアップしてしまいます。
  * また、2番目の例では手元の試行だとなぜか(10秒*30fps=300枚ではなく)301枚読み込もうとするらしく、301枚以上にする必要がありました。make_frame()が呼ばれるtの値を確認したところ、t=0が2回呼ばれているようです。いずれにしても、bitmapは少し多めにputしておくのが無難かと思います。

* 大量のフレームを投げつける場合メモリ周りで注意が必要です。
  * figqに要素が溜まりすぎるとメモリ消費が激しくなるので、そのような場合はpushする側がsleepなりして調整しましょう。
  * デフォルトのmatplotlibの設定だと画像を出力しようとして(?)figureオブジェクトのメモリが解放されず(?) Fail to allocate bitmapで落ちることがあるようです。この場合は例えば下記のようにコードの先頭に書くことで回避できます(できた): 

```python
import matplotlib
matplotlib.use("Agg")
```

([こちら](https://github.com/matplotlib/mplfinance/issues/386)のmatplotlibのissueを参考)


## おわり

matplotlibだけでgif作る方が楽かなと思ってそっちも調べたのですが、汎用的な書き方があまりよくわからずでした。
mp4のほうがファイルサイズがずっと小さいのでmoviepy入れられるならこちらで良いのかなと。