連番画像の作成¶
連番の画像を作ることで,それらを結合した動画を作成することができます.本家GMTではシェルスクリプトによるループを自力で作成するほか,gmt movie というモジュール があります.PyGMTへのgmt movieモジュールの移植はv0.17.0時点でまだ実現していないようでが,Pythonのループを使えば,簡単に連番画像を作成することができます.
ここでは,1枚の画像を作る関数を for ループでパラメタを変えながら実行して画像保存する方法を一例として紹介します.
import pygmt
import numpy as np
import os
import tempfileまず,画像の作成です.ここでは簡単な三角関数のグラフを,波が伝播しているかのように横に平行移動させていくことにしましょう.
def plot_fig(phi):
""" 初期位相 `phi` の sin カーブを描画する."""
x = np.linspace(0, 5, 201)
y = np.sin(2*np.pi*x + phi)
fig = pygmt.Figure()
fig.plot(region = [0, 5, -1.1, 1.1], projection = 'X10c/4c',
x = x, y = y,
pen = 'thick,200/110/30', frame = ['WS', 'xaf', 'yaf'])
return figこれを単独で実行すると,以下のようになります.plot_fig()関数はPyGMTのfigオブジェクトを返すため,.show()メソッドにより画像を表示させています.
plot_fig(0.0).show() # 0.0の場合plot_fig(np.pi/2).show() # π/2の場合たしかに引数の初期位相を変えるとグラフが平行移動している様子がわかります. それでは,これをループにして少しずつ初期位相を変えつつ,連番のファイルにしていきます.
動画化には ffmpeg というツールを使います.このツールで連番ファイルから動画を作る際には,連番の桁数を揃えなければなりません.
# 初期位相のリスト
phi0 = np.arange(0, 2*np.pi, 2*np.pi/200)
# 画像保存先の一時ディレクトリ作成
tmpdir = tempfile.mkdtemp()
# enumerateを使って,初期位相の値+0からの連番を取り出す.
for i, phi in enumerate(phi0):
f = plot_fig(phi) #画像プロット
# f文字列を使って連番を3桁整数の文字列に
figname = f'plot_{i:03d}.png'
f.savefig(os.path.join(tmpdir, figname))説明はほぼコメントに書いてあるとおりですが,まず画像を動かすパラメタである初期位相のリストを作り,また同時に画像保存先の一時ディレクトリを作成しておきます.手動でディレクトリ名を指定してもよいのですが,ここではtmpfile.mkdtemp()関数を使いました.この関数は,乱数を使った名前の適当なディレクトリを作成し,そのディレクトリ名を返します.確認してみましょう.
print(tmpdir)! ls $tmpdirたしかにディレクトリが作られ,その中に画像が200枚作られています.
動画への変換¶
作成された画像から動画を作成するには,コマンドラインから ffmpegを使います.ここでは gif アニメーションとmp4 形式の動画を作成してみましょう.
gifアニメーション¶
%%bash -s "$tmpdir"
ffmpeg -i "$1/plot_%3d.png" \
-vf "scale=800:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" \
-y out.gif \
> /dev/null 2>&1 ここでは,いくつかのテクニックを使っています.
まず,Jupyter Notebookの cell magic で,%%bashを Pythonセルの最初に記載しておくと,そのセル内は bashのコマンドとして実行される,という機能を使っています.そこに -s オプションで,Pythonの tmpdir 変数を "$tmpdir" として与えました.すると,あたかもこの変数の中身(一時ディレクトリの名前)が,シェルスクリプトの実行時第一引数のように扱われます.つまり,$1 というシェル変数で利用できるのです.
ffmpeg は -i オプションで入力ファイルを,コマンドラインの最後に出力ファイル名を書きます.動画形式は出力ファイル名の拡張子からある程度判断されます.その直前にある-yは,出力ファイルがすでにあるときに確認をせずに上書きするためのオプションです.Jupyter Notebook上でbashコマンドを扱うときには,対話的な実行は難しいようなので,このオプションを使っています.
入力と出力の間にある -vf からなる行は,出力される画像サイズを指定(scale=800:-1 で横幅を800pxにしつつ縦横比を維持)するほか,256色しか使えないgifアニメーションを綺麗に出力するためのカラーパレットという機能を用いています.
最終行はffmpegからの標準エラー出力が大量に出てくるため,それらを捨てるリダイレクションです.
この結果,以下のような動画が作成できました.ちょうどループするように調整して作成したので,延々と波が動いているように見えます.

Gif animationとして作成された連番アニメーション動画
Mpeg-4 Movie¶
一方,mp4形式の動画を作成するには,たとえば以下のようにします.
%%bash -s "$tmpdir"
ffmpeg -i "$1/plot_%3d.png" \
-vf scale=800:328 \
-qscale:v 1 \
-pix_fmt yuv420p \
-y out.mp4 \
> /dev/null 2>&1すると,以下のような動画が作成されるはずです.mp4の場合には-qscale:v 1 オプションを付けておくと,クオリティを劣化させずに動画にできます(昔の -qscale 0 オプションに相当します).
MP4 movieとして作成された連番アニメーション動画
大量の画像の効率的な生成¶
アニメーションを作成するためには,すこしずつ違ったパラメタを用いた大量の画像を作る必要があります.これまでの例ではJupyter Notebookで対話的に実行できる例を紹介してきましたが,図を1枚ずつ作成するのにはどうしても時間がかかります.たとえば,前述の200枚の連番画像を生成するのに,筆者の環境では約140秒かかりました.凝った図のアニメーションを作成しようとすると,図の生成にかかる時間は飛躍的に伸びてしまいます.
そこで,異なる番号の画像を作成するPyGMTプロセスを複数個並列に実行することで,より高速に大量の画像を生成する方法を考えます.
Pytnonには,マルチスレッドで複数の関数を並列に実行するconcurrentやthreadingといったモジュールもあります.しかし,筆者の環境で試したところ,単一のPythonカーネル内でこれらのライブラリを用いてPyGMTの互いに異なる画像を並列に生成することはできない(文法的に問題なさそうでも実行時エラーが生じる)ようでした.そこで,1枚の画像を生成するPythonスクリプトを,シェル(bashなど)コマンドを組み合わせることにより複数並列に実行する方針を採ることにします.
まずは,そのためのPythonスクリプトを作成します.作成済みの plot_fig() 関数をそのまま使って,以下のようなスクリプトファイル exe_plot_fig.py にします.
import sys
import os
import pygmt
import numpy as np
def plot_fig(phi):
""" 初期位相 `phi` の sin カーブを描画する."""
x = np.linspace(0, 5, 201)
y = np.sin(2*np.pi*x + phi)
fig = pygmt.Figure()
fig.plot(region = [0, 5, -1.1, 1.1], projection = 'X10c/4c',
x = x, y = y,
pen = 'thick,200/110/30', frame = ['WS', 'xaf', 'yaf'])
return fig
if __name__ == "__main__": # スクリプトとして実行したときにはこの行以下が実行される
# コマンドライン引数
dir_out = sys.argv[1]
i = int(sys.argv[2])
# 引数 i -> 角度 phi
nmax = 200
phi = 2 * np.pi * i / nmax
# 画像生成
f = plot_fig(phi)
# 画像保存
figname = f'plot_{i:03d}.png'
f.savefig(os.path.join(dir_out, figname))これは,Jupyter上で画像を作成して保存したこととほとんど同じことを実行するスクリプトです.違いとしては,sys.argv[] リストを用いて,スクリプト実行時の引数(スクリプト名のあとに空白区切りで記載)で保存先のディレクトリと画像の番号を取得し,その番号1枚の画像だけを作成します.たとえば,
python ./exe_plot_fig.py . 3というコマンドをシェルで実行するか,あるいはJupyter上でなら
%%bash
python ./exe_plot_fig.py . 3とすることで,カレントディレクトリ(.)にplot_003.pngが作成されます.これを踏まえて,以下のように xargs コマンドを用いてスクリプトを並列実行します.
%%bash
tmpdir=$(mktemp -d)
echo ${tmpdir}
seq 0 199 | xargs -P10 -n1 python ./exe_plot_fig.py ${tmpdir}最初の2行は画像作成用の一時ディレクトリの作成とその表示です.最終行は2つのコマンドに分かれていて,まず seq コマンドにより 0 1 2 ... 199 の連番を生成し,それをパイプ | を通じて xargs に渡します.xargsコマンドは,標準入力から受け取ったものを,それ以降にかかれているコマンドに「適当な数ずつ」渡す,というものです.ここでは,オプション -n1 を指定することで,デフォルトでは自動で判断される受け渡しの数を1つに限定しています.すなわち,seqで生成した連番が1つずつ exe_plot_fig.py に渡されるという仕組みです.さらに,xargsのオプション-P10で,xargsで指定されたコマンドを複数(この場合は10個)ずつ並列に実行させています.自動生成した${tmpdir}を見ていると,10枚ずつほぼ同時に画像が生成されていくことが確認できるでしょう.ここでは省略していますが,同じセルのなかでffmpegまで実行して動画を作ってしまうとよりよいでしょう.
類似の方法としては,以下のようなbashのシェルスクリプトを用いてpythonスクリプトを並列に実行する方法もあります.
#!/bin/bash
tmpdir=$(mktemp -d)
(( j = 0 ))
for i in $(seq 0 199)
do
./exe_plot_fig.py ${tmpdir} ${i} &
(( j ++ ))
if (( j == 10 )); then
(( j = 0 ))
wait
fi
done一つ一つの可視化スクリプトをバックグラウンド(&)で実行させつつ,別の変数 $j をカウンタとして利用して,一定個数のスクリプトがバックグラウンドで実行されたら wait によりすべてのスクリプトの実行が終了するのを待つ,という方針です.
比較すると,xargsのほうが圧倒的に簡単で,並列数の制御もしやすいと思います.ただし,xargsはそれに続くコマンドの最後の引数に標準入力を与えること,seqコマンドと組み合わせるなら,可視化スクリプトが整数値を受け取って,それをパラメタとして画像を生成するように書く必要があることには注意が必要です.
ほかには,ここでは扱いませんが,データファイル1つに対して画像ファイル1枚が対応している場合には,makefileを書くことによって並列にスクリプトを実行することもできます.PyGMTの例ではありませんが,詳しい解説がたとえばここにあります.