LoginSignup
56
52

More than 3 years have passed since last update.

[Python/matplotlib] FuncAnimationを理解して使う

Last updated at Posted at 2020-08-17

matplotlib でアニメーションを作成するには, matplotlib.animation.ArtistAnimationmatplotlib.animation.FuncAnimation というふたつの方法があります. ArtistAnimation の方が簡単で, これなら割と思い通りに作成できていたのですが, 事前にすべての要素を作成しメモリに保持するため重いという問題がありました. FuncAnimation は逐次的に生成するためより軽そうなのですが, どうも挙動がよくわからず, いつも面倒になって連番画像を書き出して後で mp4 に変換していました. なのでいい加減理解して使いこなそうと気合を入れて勉強してきました. これはそのまとめノートです.

基本

matplotlib.animation.FuncAnimation にアニメーションを作成するのに必要な情報を渡せばアニメーションを作成してくれます. なので理解すべきはその引数とこの関数の挙動です.

最初のふたつの引数は必須で, たいていはそれに加えて引数 frames, interval を設定すると思います.

  • 第1引数 figFigure です. これは普段からよく使っているので説明不要ですね.
  • 第2引数 func が本題です. これは次小節で詳しく見ます.
  • 引数 frames は, 各フレームの「名前」に相当するものを生成するイテレータです. 例えば range(128)np.linspace(0, 2*np.pi, 128) などを渡します. この場合, どちらもフレーム数128のアニメーションを作成します.
  • 引数 interval はフレームを切り替える時間をミリ秒単位で指定します. デフォルトは200です.

FuncAnimation の考え方

FuncAnimation の考え方は次のコード例に集約されます. (API doc より引用)

for d in frames:
   artists = func(d, *fargs)
   fig.canvas.draw_idle()
   fig.canvas.start_event_loop(interval)

つまり, イテレータ frames を回して, 各「フレーム名」 d を関数 func に渡し, 帰ってきた artist を描画します. これを時間 interval おいて frames が終了するまで繰り返します. それだけです.

例1: 動く点P

という訳で, さっそくやってみましょう. x軸上を 0, 1, 2, ... と動いていく点Pのアニメーションを作成してみます. 今学んだことを素直にコードに落とすとこうなるでしょうか.

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = fig.add_subplot(111)

def update(frame):
    ax.plot(frame, 0, "o")

anim = FuncAnimation(fig, update, frames=range(8), interval=1000)

anim.save("c01.gif", writer="imagemagick") # 画面に表示させたい場合は plt.show() で良い
plt.close()

結果はこちら.

c01.gif

なんだか意図と違います. なにが違うかというと, 以前のフレームで描画した点Pの過去の位置がその後も描画されっぱなしになってしまっています. これを修正するには, update 関数の冒頭で axes に書き込まれている要素をすべてクリアすればよさそうです.

例2: 動く点P (修正版)

という訳で, 修正版のコードおよび実行結果がこちらです.

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = fig.add_subplot(111)

def update(frame):
    ax.cla() # ax をクリア
    ax.plot(frame, 0, "o")

anim = FuncAnimation(fig, update, frames=range(8), interval=1000)

anim.save("c02.gif", writer="imagemagick")
plt.close()

c02.gif

意図通りのアニメーションができました.

注意として, ax.cla() を呼ぶと軸ラベル ax.set_xlabel() やグリッド線 ax.grid(), 描画範囲 ax.set_xlim() などもクリアされてしまうため, ax.cla() を読んだ後に再設定する必要があります.

応用

特定の Artist のみをクリア

上の例ではすべての描画内容を各フレームで消去していましたが, 一部の Artist のみをクリアしたいということが往々にしてあると思います. 例えば次の例は単位円上を動く点 P のアニメーションですが, 毎回グリッドや単位円を描画していてなんだか無駄に見えます.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = fig.add_subplot(111, aspect=1)

theta = np.linspace(0, 2*np.pi, 128)

def update(f):
    ax.cla() # ax をクリア
    ax.grid()
    ax.plot(np.cos(theta), np.sin(theta), c="gray")
    ax.plot(np.cos(f), np.sin(f), "o", c="red")

anim = FuncAnimation(fig, update, frames=np.pi*np.arange(0,2,0.25), interval=200)

anim.save("c03.gif", writer="imagemagick")
plt.close()

c03.gif

この場合, すべてをクリアするのではなく, 消去したい Artist を明示的に remove すれば目的が達成できます.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
ax = fig.add_subplot(111, aspect=1)

theta = np.linspace(0, 2*np.pi, 128)
p = []
ax.grid()
ax.plot(np.cos(theta), np.sin(theta), c="gray")

def update(f):
    if len(p) > 0:
        item = p.pop(0)
        ax.lines.remove(item)
    p.append( ax.plot([np.cos(f)], [np.sin(f)], "o", c="red")[0] )

anim = FuncAnimation(fig, update, frames=np.pi*np.arange(0,2,0.25), interval=200)

anim.save("c03.gif", writer="imagemagick")
plt.close()

もっとも, このふたつの例では描画速度の面ではどちらが有利なのか私にはよくわかりません. FuncAnimation の引数で blit=True と併用するのが良いように見えますが.

複数のサブプロットを持つアニメーション

描画内容をクリアするために plt.cla() を使っていると, これは ax.cla() と同じ意味 (ax は直前に触っていた Axes) なので, 複数のサブプロットを持つアニメーションで表示内容をクリアし損ねるサブプロットが出てきてしまうことがあります. すべての Axes について ax.cla() しましょう.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig = plt.figure()
axs = [
    fig.add_subplot(121),
    fig.add_subplot(122, aspect=1),
]
theta = np.linspace(0, 4*np.pi, 128)

def update(f):
    for ax in axs:
        ax.cla() # ax をクリア
        ax.grid()
    axs[0].set_xlim([0, 4*np.pi])
    axs[0].plot(theta, np.cos(theta - f), c="blue")
    axs[0].plot(0, np.cos(f), "o", c="blue")

    axs[1].plot(np.sin(theta), np.cos(theta), c="gray")
    axs[1].plot(np.sin(f), np.cos(f), "o", c="blue")
    axs[1].plot(0, np.cos(f), "o", c="blue")
    axs[1].plot([0, np.sin(f)], [np.cos(f), np.cos(f)], c="blue")

anim = FuncAnimation(fig, update, frames=np.pi*np.arange(0,2,0.1), interval=200)

anim.save("c04.gif", writer="imagemagick")
plt.close()

c04.gif

参考文献

56
52
0

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
56
52