今回はPython外部ライブラリのMatplotlibを使用してグラフ内にアニメーションを描画していきたいと思います。
グラフ描画ライブラリのMatplotlibでは幾つかの方法でリアルタイムな、つまりアニメーション描画を実装できる機能が備わっています。
今回はFuncAnimationクラスを中心に特徴的な処理を確認しながら複雑な設定へと順に進めて行きます。
FuncAnimationの他にArtistAnimationというクラスも備わっていて、共にアニメーション描画を実装することができるので、宜しければ以下をご参照してみて下さい。
実行環境 |
---|
Windows Subsystem for Linux |
Python 3.6.9 |
pip 9.0.1 |
jupyter notebook |
使用ライブラリ | ライセンス |
---|---|
matplotlib==3.1.1 | PSF |
FuncAnimationクラスの特徴としては、関数を駆使することにより複雑なアニメーションを実装できるということです。
FuncAnimation(
fig=figureオブジェクト, # 図
func=任意の関数 # グラフを処理するイテレーションのような関数
frames=None, # funcに渡した第一引数となるイテレーション出来るオブジェクト
init_func=None, # funcが呼び出される前に呼ばれる関数
fargs=None, #funcの第二引数以降のタプル要素
save_count=100, # フレーム数をキャッシュする値
interval=200, # 処理のミリ秒
repeat_delay=0, # 繰り返す間隔の遅延
repeat=True, # 処理を繰り返すか否か
blit=False, # 描画を最適化するか否か。デフォルトはFalse
cache_frame_data=True, # frameをキャッシュするか否か。デフォルトはTrue
)
引数「fig」「func」は必須で、それ以外の引数はデフォルト値となっています。
figにはFigureオブジェクトを設定し、funcには第1引数呼び出し可能な任意の関数を設定します。
# 基本設定
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# jupyter notebookでアニメーションする場合
%matplotlib
fig, ax = plt.subplots()
def plot_func(frame):
"""
この関数内でグラフが作成され更新されていく
第1引数のframeは、クラスオブジェクトの引数framesに設定されている要素が順次流れる
"""
pass
ani = FuncAnimation(fig, plot_func)
クラスオブジェクトの引数「frames」の設定により、任意で作成したplot_func(frame)にジェネレータオブジェクトとして呼び出されます。
上記コードではFuncAnimationクラスのデフォルト値「frames=None」なので、plot_func(frame)にはPython標準ライブラリの「itertools.count」が呼び出されます。
試しにprint関数で出力してみます。
...
def plot_func(frame):
print(frame)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=None,
)
# 0
# 0
# 1
# 2
# 3
# ...
クラスframes引数をNoneにした場合、バックエンドではitertools.countが実行されていますが、このメソッドのデフォルト値は「(start=0, step=1)」となっており0から1ずつ永遠とカウントが行われます。
しかしFuncAnimationを実行した際は、plot_func関数内で出力される要素は最初のステップだけ余分に同じ要素が出力されています。
私は少し理解に苦しみましたが、FuncAnimationクラス引数の「init_func」に関係があると気付きました。
なのでinit_funcについて見ていきます。
FuncAnimationクラスの引数「init_func」の役割は、グラフを作成するプロセスで最初に呼び出される関数です。
...
def init():
"""
初期化関数
"""
print("Hello World")
def plot_func(frame):
print(frame)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=None,
init_func=init, # 最初に呼び出される関数
)
# Hello World
# 0
# 1
# 2
# 3
# ...
plot_func関数では引数「frame」によってグラフが更新されていきますが、frameの長さが決まっている場合はクラス引数「repeat」がTrueである以上そのセットが繰り返し行われるので、再びinit関数が呼び出されます。
例えば以下のようにグラフの軸やタイトル、グリッドなどを最初に呼び出してからデータの更新を行います。
...
fig, ax = plt.subplots()
def init():
ax.set_xlim(0, 30)
ax.set_ylim(-1, 1)
ax.set_title("FuncAnimation")
ax.grid()
def plot_func(frame):
print(frame)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=None,
init_func=init,
)
# 0
# 1
# 2
# 3
# ...
FuncAnimationクラスの引数「init_func」では毎セットで初期化させたい要素を定義しておくのが良いかと思われます。
次にFuncAnimationクラスの引数「frames」について見ていきます。
FuncAnimationクラスの引数「frames」には、イテレーション可能なオブジェクトを与えることによって、与えられた長さの範囲をfuncごとに繰り返し実行します。
イテレーション可能なオブジェクトとは、リストやタプルやジェネレーター関数などの事です。
...
def plot_func(frame):
print(frame)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=range(3), # plot_funcの引数「frame」にnextされる
repeat=True, # 毎セット繰り返す
)
# 0
# 0
# 1
# 2
# 0
# 0
# 1
# ...
クラス引数のinit_funcを設定すると毎セット一度だけその関数が実行されます。
...
def init():
print("called?")
def plot_func(frame):
print(frame)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=range(3), # リスト([0, 1, 2])可
init_func=init, # セット前に一回実行される
repeat=True, # 毎セット繰り返す
)
# called?
# 0
# 1
# 2
# called?
# 0
# 1
# ...
一度のターンで複数の要素をplot_funcの引数「frame」に出力させたい場合は、ジェネレーター関数を定義しクラス引数「frames」に渡すのが便利です。
...
def gen_function():
"""
ジェネレーター関数
"""
x = range(3)
y = reversed(x) # xの要素を逆順にする
for _x, _y in zip(x, y):
yield [_x, _y] # 要素要素を戻り値として返す
def init():
print("called?")
def plot_func(frame):
"""
frame[0]: _xの要素
frame[1]: _yの要素
"""
print(frame[0], frame[1])
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=gen_function, # ジェネレーター関数を設定
init_func=init,
repeat=True,
)
# called?
# 0 2
# 1 1
# 2 0
# called?
# 0 2
# 1 1
# ...
上記の例でいうplot_funcの引数であるframeはグラフ内のデータを更新するために重要なので、複数の要素を持たせると便利です。
frameは第一引数として定義されていますが、FuncAnimationクラスの引数「fargs」を設定することにより第二引数、第三引数と拡張することができます。
ではFuncAnimationクラスの引数「fargs」を見ていきます。
FuncAnimationクラスの引数「fargs」には、タプルオブジェクトを与えることによってfunc関数の引数を拡張することができます。
...
def init():
pass
def plot_func(frame, farg):
"""
frame: 0, 1, 2
farg: "farg_1"
"""
print(frame)
print(farg)
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=range(3),
init_func=init,
fargs=("farg_1",), # タプルオブジェクトを設定
repeat=True,
)
# 0
# farg_1
# 1
# farg_1
# 2
# farg_1
# 0
# farg_1
# 1
# ...
クラス引数「fargs」に設定したタプル内の要素を増やすことによって、plot_funcの引数をさらに増やすことができます。
...
def init():
pass
def plot_func(frame, farg_1, farg_2, farg_3):
print(frame)
print(farg_1)
print(farg_2)
print(farg_3)
print('----')
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=range(3),
init_func=init,
fargs=("farg_1", "farg_2", 3), # タプルオブジェクトを設定
repeat=True,
)
# 0
# farg_1
# farg_2
# 3
# ----
# 1
# farg_1
# farg_2
# 3
# ----
# 2
# ...
タプルのリストとするとインデックス番号を指定して出力できます。
...
def init():
pass
def plot_func(frame, farg_1, farg_2):
"""
frame: 0, 1, 2
farg_1: ["farg_1", "farg_2"]
farg_2: 2
"""
print(frame)
print(farg_1[0], farg_1[1])
print(farg_2)
print('----')
ani = FuncAnimation(
fig=fig,
func=plot_func,
frames=range(3),
init_func=init,
fargs=(["farg_1", "farg_2"], 2), # タプルオブジェクトを設定
repeat=True,
)
# 0
# farg_1 farg_2
# 2
# ----
# 1
# farg_1 farg_2
# 2
# ----
# 2
# ...
その他のクラス引数はグラフの更新頻度の間隔(interval)や全ての更新が終了した後に最初から繰り返す(repeat)などを設定するだけです。
FuncAnimationクラスの引数における「frames、init_func、fargs」はfunc関数に直接影響を及ぼす設定なのでより複雑なアニメーションを実行することができます。
では実際にアニメーションを描画してみます。
ここでの実装は以下の記事でも実装しているアニメーションを描画していきます。
ArtistAnimationでの実装内容と比べることで違いが顕著になり理解が深まるかと思われます。
シンプルなアニメーションから順に進めて行きたいと思います。
FuncAnimationクラスの必須引数「fig、func」に加え、グラフの更新前に一度だけ呼び出される初期化関数を定義し、グラフの更新に必要なデータをジェネレーター関数として定義しています。
まずはシンプルにデータが追加更新されていく線グラフをアニメーションします。
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
%matplotlib
x_data = []
y_data = []
fig, ax = plt.subplots()
line,= ax.plot([], [])
def gen_function():
"""
ジェネレーター関数
"""
y = [0] * 30
x = range(len(y))
for _x, _y in zip(x, y):
yield [_x, _y] # イテレーションしながら順次要素を返す
def init():
"""
初期化関数
"""
ax.set_xlim(0, 30) # x軸固定
ax.set_ylim(-1, 1) # y軸固定
del (x_data[:], y_data[:]) # データを削除
return line, # 初期化されたプロットを返す
def plot_func(frame):
"""
frameにはジェネレーター関数の要素が代入される
frame: [_x, _y]
"""
x_data.append(frame[0])
y_data.append(frame[1])
line.set_data(x_data, y_data) # 繰り返しグラフを描画
return line, # 更新されたプロットを返す
anim = FuncAnimation(
fig=fig,
func=plot_func,
frames=gen_function,
init_func=init,
interval=100,
)
データに動きを付ける為に、ジェネレーター関数内のyリスト10番目~16番目の要素を別の値に置き換えます。
あとはグラフにタイトルを設定し更新頻度を取得します。
...
x_data = []
y_data = []
fig, ax = plt.subplots()
line,= ax.plot([], [])
title = ax.set_title(None, fontsize=15) # タイトルを追加
def gen_function():
"""
ジェネレーター関数
"""
y = [0] * 30
# yリストの10, 11, 12, 13, 14, 15, 16番目のデータを置き換える
y[10], y[11], y[12], y[13], y[14], y[15], y[16] = 0.05, -0.05, 0.05, -0.05, 0.05, -0.05, 0.4
x = range(len(y))
for _x, _y in zip(x, y):
yield [_x, _y]
def init():
"""
初期化関数
"""
ax.set_xlim(0, 30)
ax.set_ylim(-1, 1)
del (x_data[:], y_data[:])
title.set_text(None) # タイトルを初期化
return line,
def plot_func(frame):
"""
frameにはジェネレーター関数の要素が代入される
frame: [_x, _y]
"""
x_data.append(frame[0])
y_data.append(frame[1])
line.set_data(x_data, y_data)
title.set_text("FuncAnimation: {}".format(frame[0])) # 追加
return line,
anim = FuncAnimation(
fig=fig,
func=plot_func,
frames=gen_function,
init_func=init,
interval=100,
)
最後にMatplotlibのTextオブジェクトを操作して然るべき位置に顔文字を描画させていきます。
Textオブジェクトに与えるデータは、func関数の引数を拡張することのできるクラス引数「fargs」に設定します。
...
x_data = []
y_data = []
fig, ax = plt.subplots()
line,= ax.plot([], [])
title = ax.set_title(None, fontsize=15)
textvar = ax.text(0, 0, None) # Textオブジェクトの追加
def gen_function():
"""
ジェネレーター関数
"""
y = [0] * 30
y[10], y[11], y[12], y[13], y[14], y[15], y[16] = 0.05, -0.05, 0.05, -0.05, 0.05, -0.05, 0.4
x = range(len(y))
for _x, _y in zip(x, y):
yield [_x, _y]
def init():
"""
初期化関数
"""
ax.set_xlim(0, 30)
ax.set_ylim(-1, 1)
del (x_data[:], y_data[:])
title.set_text(None)
textvar.set_text(None) # テキストデータを初期化
return line,
def plot_func(frame, farg_faces, farg_nums): # 第2第3引数の追加
"""
frameにはジェネレーター関数の要素が代入される
frame: [_x, _y]
farg_faces: ['(^.^)y-~', '\(0o0)/ -']
farg_nums: [10, 12, 14, 16]
"""
x_data.append(frame[0])
y_data.append(frame[1])
line.set_data(x_data, y_data)
title.set_text("FuncAnimation: {}".format(frame[0]))
""" ------ ここから追加 ------ """
if frame[0] >= 10: # リストの10番目の要素からテキストを描画する
index = frame[0] - 9 # 顔文字のポジションをずらす
if index in farg_nums:
textvar.set_position((index, y_data[index]+0.1)) # 顔文字を配置する座標を更新
textvar.set_text(farg_faces[1]) # \(0o0)/ -
else:
textvar.set_position((index, y_data[index]+0.05)) # 顔文字を配置する座標を更新
textvar.set_text(farg_faces[0]) # (^.^)y-~
""" ------ ここまで ---------"""
return line,
anim = FuncAnimation(
fig=fig,
func=plot_func,
frames=gen_function,
init_func=init,
fargs=(['(^.^)y-~', '\(0o0)/ -'], [10, 12, 14, 16]), #funcの第二引数以降のタプル要素
interval=100,
)
MatplotlibのAnimationモジュールではgifファイルやmp4ファイルの保存をすることができます。
ただし各ファイルに保存するには特定のツールをインストール必要があります。
以下の記事では簡単な説明ですけど方法を記載している宜しければご参照してみてください。
以上でFuncAnimationクラスの実装を終わりますが、ArtistAnimationクラスと違って逐次グラフを更新していくので、何らかの監視システムと組み合わせてリアルタイムでモニターできるかもしれません。
複雑なクラスオブジェクトだけに高機能なソフトウェア開発も行えそうな一品でした。
最後までご覧いただきありがとうございました。