動くグラフをつくりたい
発表資料とかでヌルヌル動くグラフを見せつけてドヤ顔したいというただそれだけの目的でアニメーションを試してみます。
matplotlbのパッケージ
matplotlibのanimationパッケージにアニメーション作成用のクラスがいくつか用意されてます。
今回はFuncAnimationクラスを使ってアニメーションを作成してみましょう。
モデル
プロットする対象として単振り子をアニメ化してみましょう。
紐に括りつけられたボールが左右に振れるアレです。
プログラム
FuncAnimationは、グラフの更新関数によって毎フレームごとにグラフの値を更新していくようなイメージです。
プログラム全体は以下です。
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from matplotlib.animation import PillowWriter import numpy as np # 変数定義 l = 1 # 振り子の長さ g = 9.8 # 重力加速度 theta_0 = np.pi / 5 # 初期の角度 omega = np.sqrt(g / l) # 角速度 T = round(2 * np.pi / omega, 1) # 振動周期 fig = plt.figure() ax = fig.add_subplot(1, 1, 1) im, = ax.plot([], [], color='k', marker='o', \ markersize=12, linestyle='None') line, = ax.plot([], [], color='k') ax.set_xlim(-1.2, 1.2) ax.set_ylim(-2.2, 0.2) ax.set_aspect('equal') # 初期化関数 def init(): # only required for blitting to give a clean slate. im.set_xdata([np.nan]) im.set_ydata([np.nan]) line.set_xdata([np.nan, np.nan]) line.set_ydata([np.nan, np.nan]) return im, line, # グラフ更新関数 def update_anim(dt): theta = theta_0 * np.cos(omega * dt) x = l * np.sin(theta) y = - l * np.cos(theta) im.set_data(x, y) line.set_data([0, x], [0, y]) print(dt) # 引数の確認用に出力 return im, line, ani = FuncAnimation( fig, # Figureオブジェクト update_anim, # グラフ更新関数 init_func=init, frames = np.arange(0, T, 0.1), # フレームを設定 interval = 100, # 更新間隔(ms) repeat = True, # 描画を繰り返す blit = True, ) # アニメーションのgif形式での保存 ani.save("anim_test.gif", writer='pillow') plt.show()
プログラム説明
FuncAnimationの引数
ani = FuncAnimation( fig, # Figureオブジェクト update_anim, # グラフ更新関数 init_func=init, frames = np.arange(0, T, 0.1), # フレームを設定 interval = 100, # 更新間隔(ms) repeat = True, # 描画を繰り返す blit = True, )
- 第一引数はアニメーションにする対象のfigureを指定します。
- 第二引数には自分で定義したグラフを更新する関数を指定します。
- init_funcはグラフ初期化用の関数を指定します。後で説明しますが、これを指定しておかないと繰り返しの際におかしな挙動になってハマりました。
- framesはここで定義した値がグラフ更新関数の引数になる。この場合は0, 0.1, 0.2,...,1.9が引数として順番に使われる。framesに整数のみ定義すると、range(整数)と同様の挙動になる。アニメーションをgif等で保存する場合にはこの指定は必須。
- intervalはアニメーションにした際のフレーム間の間隔で、msで指定する
- repeatは描画を繰り返すかどうかのフラグ。
- blitはよくわからないけど、描画処理が高速になる?らしい?
グラフ更新関数
# グラフ更新関数 def update_anim(dt): theta = theta_0 * np.cos(omega * dt) x = l * np.sin(theta) y = - l * np.cos(theta) im.set_data(x, y) line.set_data([0, x], [0, y]) print(dt) # 引数の確認用に出力 return im, line,
- 個の関数の引数のdt(名前は何でもいい)はFuncAnimationのframesで指定したものがひとつずつ代入される。
- それ以外の変数は関数外で定義していても問題なく動いてくれる。不思議…
- 値の更新は.set_dataで行う
- FuncAnimationでblit=Trueとしていた場合は返り値の指定は必須。更新したグラフを返り値として指定する。
初期化関数
# 初期化関数 def init(): # only required for blitting to give a clean slate. im.set_xdata([np.nan]) im.set_ydata([np.nan]) line.set_xdata([np.nan, np.nan]) line.set_ydata([np.nan, np.nan]) return im, line,
グラフを初期化する関数。これを指定しないと1フレーム目のグラフが初期グラフとして使われる。
何を書けばいいのかいまいちよくわかってないのですが、とりあえずグラフのデータを全部np.nanにする関数を書いたらうまく動きました。
指定した場合と指定しなかった場合で比べてみる。
(FuncAnimationにinit_func=initを付けた場合とつけない場合)
グラフ更新関数内にprint(dt)を書いて、引数の確認を行った。
期待する出力は0から0.1ずつ増えていき、1.9になったら次は0からまた繰り返しというものです。
指定した場合↓
指定しなかった場合↓
init_funcを指定しない場合は0が二回連続で出力されてしまっているため、期待していた出力が得られていません。
おまじないと思って指定しておくのがいいみたい。
アニメーションの出力
Pillowで出力するのがいいみたいです。
from matplotlib.animation import PillowWriter ani.save("anim_test.gif", writer='pillow')