逃げるしっぽを追いかけろ

生物学専攻大学院生の趣味ブログ・マイペース更新

pythonで3Dアニメーション(スティックピクチャー)を描く

わりと汎用性の高いコードを書けたので公開しておく。
普段は世間的に全く需要のなさそうなコードばかり書いているので、「これは珍しく公開する価値があるのでは?」と嬉しくなってしまった。

前提

最新のanacondaをインストール済み。
任意個のマーカーの座標を、時系列に沿って(縦向きに)記録したデータがあると仮定。
(参考までにデータの読み込みから書いてある)

コード

モジュールと事前入力部
import csv #データによっては必要かも
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mpl_toolkits.mplot3d import Axes3D

##事前入力部分
filename = ' '#読みたいファイル名.拡張子 を入力
fname = ' '#出力する動画名(fname+'stickpic.mp4'として出力される)
t_start = #データ切り出しの頭
t_end = +1#データ切り出しのおしり+1
t_range =t_end-t_start
num = #マーカーの数 マーカーの番号は控えておく!
fps = #フレームレート
sticks = np.array([[始点のマーカー番号1,終点のマーカー番号1],[始点のマーカー番号2,終点のマーカ番号2]…])
xmax = ;xmin = 
ymax = ;ymin = 
zmax = ;zmin = #マーカーのx~z座標が取りうる最大値、最小値
ファイルの読み込みから切り出し
###ファイルの読みこみ~切り出しまで
##読み込み
file = np.genfromtxt(filename, delimiter="\t", skip_header=6, usecols=(range(0,152)), dtype='float', encoding="shift-jis")
#header: はじめの任意の行数をとばせる
#usecols: 読み込む列数を指定(データに抜けがある時とかに便利?)
#delimiter: .csvなら','の方が良いかも

##データ格納先準備
f = np.zeros(file.shape[0]*num*3).reshape(file.shape[0], num, 3)
#f[frame][marker][xyz]
#file.shape[0] = frame数抽出

##データ格納
for i in range(file.shape[0]):
    for j in range(num):
        f[i][j][0] = file[i][j*3+2+0]
        f[i][j][1] = file[i][j*3+2+1]
        f[i][j][2] = file[i][j*3+2+2]

##時間でデータを切り出す
f = f[t_start:t_end, :, :]
アニメーション描画
###アニーメーション描画
figure = plt.figure(figsize=(9,9))#縦横比は揃える
ax = figure.add_subplot(111,projection='3d')

##目盛り幅を揃える
max_range = np.array([xmax-xmin, ymax-ymin, zmax-zmin]).max() * 0.5
mid_x = (xmax+xmin) * 0.5
mid_y = (ymax+ymin) * 0.5
mid_z = (zmax+zmin) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)

##視点、向きの指定
ax.view_init(elev=90, azim=180)
#elev:視点の高さ、仰角。90で真上から見た図になる(degrees)
#azim:グラフをz軸方向に対し反時計回りさせる(degrees)

##軸ラベル 無い方が見やすいけど、試行錯誤中はあった方が便利
ax.set_xlabel("x", size = 14, color = "r")
ax.set_ylabel("y", size = 14, color = "r")
ax.set_zlabel("z", size = 14, color = "r")

##時刻毎プロット
imss = []
for i in range(t_range):
    ims = []
    for j in sticks: #スティックのループ
        ims += ax.plot ([f[i][j[0]-1][0], f[i][j[1]-1][0]],[f[i][j[0]-1][1], f[i][j[1]-1][1]],[f[i][j[0]-1][2], f[i][j[1]-1][2]],lw=0.6,color='K')
        #lw: 線の太さ。デフォルトは1 color: 色。Kは黒
    imss.append(ims)
   
ani = animation.ArtistAnimation(figure,imss,interval = 50)
#interval: 画像が切り替わる間隔
plt.show()
ani.save(fname+'stickpic.mp4')
まとめたコード(注記削除済み)
import csv
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mpl_toolkits.mplot3d import Axes3D

##事前入力部分
filename = ' '
fname = ' '
t_start = 
t_end = +1
t_range =t_end-t_start
num = 
fps = 
sticks = np.array([[ , ],[ , ]…])
xmax = ;xmin = 
ymax = ;ymin = 
zmax = ;zmin = 

###ファイルの読みこみ~切り出しまで
##読み込み
file = np.genfromtxt(filename, delimiter="\t", skip_header=6, usecols=(range(0,152)), dtype='float', encoding="shift-jis")

##データ格納先準備
f = np.zeros(file.shape[0]*num*3).reshape(file.shape[0], num, 3)
#f[frame][marker][xyz]

##データ格納
for i in range(file.shape[0]):
    for j in range(num):
        f[i][j][0] = file[i][j*3+2+0]
        f[i][j][1] = file[i][j*3+2+1]
        f[i][j][2] = file[i][j*3+2+2]

##データを切り出す
f = f[t_start:t_end, :, :]

###アニーメーション描画
figure = plt.figure(figsize=(9,9))
ax = figure.add_subplot(111,projection='3d')

##目盛り幅を揃える
max_range = np.array([xmax-xmin, ymax-ymin, zmax-zmin]).max() * 0.5
mid_x = (xmax+xmin) * 0.5
mid_y = (ymax+ymin) * 0.5
mid_z = (zmax+zmin) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)

##視点、向きの指定
ax.view_init(elev=90, azim=180)

##軸ラベル
ax.set_xlabel("x", size = 14, color = "r")
ax.set_ylabel("y", size = 14, color = "r")
ax.set_zlabel("z", size = 14, color = "r")

##時刻毎プロット
imss = []
for i in range(t_range):
    ims = []
    for j in sticks: 
        ims += ax.plot ([f[i][j[0]-1][0], f[i][j[1]-1][0]],[f[i][j[0]-1][1], f[i][j[1]-1][1]],[f[i][j[0]-1][2], f[i][j[1]-1][2]],lw=0.6,color='K')
    imss.append(ims)
   
ani = animation.ArtistAnimation(figure,imss,interval = 50)
plt.show()
ani.save(fname+'stickpic.mp4')

参考文献や引用について

目盛り幅を揃えるところは、以下に大変お世話になりました。
matplotlibで3次元プロットする際に3軸のスケールを揃える - Qiita
こちらもとても勉強になりました。
Python 3次元グラフのテーマカラー,グラフの表示角度を変更 - Qiita
plotの理解にはこちらがわかりやすかったです。
matplotlibにおける2つのグラフ作成スタイル【Python】 | BioTech ラボ・ノート


ここで公開しているコードも、必要な方々に活用していただければ幸いです。

よもやま話

プロットのところ、一応オブジェクト指向で統一してみた。pyplotスタイルと違うことが今回わかったので、今後きちんと理解していきたい(figure内の構造とかがまだまだ)。
あと、割と時間がかかるプログラムなので、時短テクも取り入れていきたい。