AI姿勢推定型モーションキャプチャを始めました – An experience of dynamically estimating human posture using an AI system

以前からとっても興味があったのですが,当時OpenPoseを自分のPCに導入することに大きな壁を感じてしまい,最近までその必要性を感じつつもなかなか近寄りがたいものでいました。ところが,民具技術を解明する上では,民具の形そのものだけでなく,それを使う人の動き,手足,体幹などと民具との関係性はとっても重要であることは当然なことなのです。そろそろ重い腰を上げなければと思っていました。

ありがたいことに,その重い腰を上げるきっかけをいただいたので,あらためて最近のモーションキャプチャの動向を探ってみました。すると,なんといろいろなパッケージがオープンソースで公開されているではありませんか。そして,モーションキャプチャ以上にその有用性を感じつつも二の足を踏んでいたPythonもそこに普通に存在していることに,いまさらながら感動を伴いつつ気づかされています。完全に乗り遅れてしまっていました。

とにかく遅ればせながらも,60後半の手習いとして,OpenPoseと比較すると高い精度を望んではいけないのかもしれませんが,素人でも扱いやすく,AI大活躍のモーションキャプチャとしてはとっても取っつき易いMediapipeと,それに必要最低限のPythonコードを自分も使えるようになりたいと思い立ち,この忘備録を書き始めます。まずはMediapipeで修行を積み,ゆくゆくはOpenPoseに再度挑戦してみたいと考えています。

普通のPCでもできそうです

モーションキャプチャを実施するには,それなりのCPUとメモリと,そしてグラフィックボードが装備されていないと鼻っからだめだと思い込んでいましたが,試みにPythonからのインストールを始めた日常使いのPCでも最後の目的に到達できそうです。

隙間予算で購入し,立ち上がりがいつものんびりしているPCですが,正直なところ“これでもできるんだ!”とびっくりしています。冷静に考えると,人の骨格の認識はそれほど異例も少ないし,グラフィックも骨・フレームを動画に乗っけるだけなので,一方向からの人の動画でも,大まかにその動きを追うことができますし,そこから推測した骨格のキーポイントの妥当性の最適化も,今からコーディングしろと言われれてもできないにもかかわらず,考え方としてはそれほど不可能でもないなと思われますので,意外とPCのパワーはなくても,それなりのことはできそうです。気づくと当たり前のことですが,自分にとっては目からうろこです。

とっても参考になる丁寧で有難い指南書がありましたので,それに従ってAIモーションキャプチャの環境整備を進めてみました。

Pythonのインストール

/https://www.python.org/downloads/windows/から Python3.9.13 を選びました。

インストールの際に,ChatGPTの指示である「☑ Add Python 3.x to PATH にチェックを入れる」を無視していたので,3回ほどインストールを繰り返すことになりましたが,どうにか憧れのPythonをいつものPCに装備することができました。

上のチェックを無視すると,Pythonにパスが通らず,何をしても反応してくれません。同然なのですが,パスが通ると,次に進めます。

Mediapipeのインストール

映像処理などを行うMediapipeは,ChatGPTによると

MediaPipe(メディアパイプ)とは、Googleが開発したクロスプラットフォームの機械学習ソリューションフレームワークです。主にリアルタイムでの映像・画像解析を行うためのライブラリで、特に「顔認識」「手・身体の姿勢推定」「物体検出」など、人間の動きや構造を捉えるタスクに強みを持っています。

とのことです。

このMediapipeをPythonから利用するためには,コマンドプロンプト(cmd)にて,以下のコマンド入れることで環境整備ができました。

python -m pip install –upgrade pip
python -m pip install –user mediapipe opencv-python

Visual Studio Code(VS Code)のインストール

このMediaPipeを使ったPythonプログラムを書くためのエディタであるVS Codeはhttps://code.visualstudio.com/からダウンロードできます。

インストールを終え,起動したら,VS Codeのウィンドウの

左端の赤線で囲んだExtensions(拡張機能)から「Python extension for Visual Studio Code」をインストールします。これで,Mediapipeをうまく使えそうです。ワクワクしてきます。

VS Codeに,先の指南書に従って姿勢追跡用のPythonコードを入力し実行します。

しばらくすると別ウインドウ(下図)が開き,Webカメラに映った自分の手足(実は目と口にも)にフレームが付与されています。そして,そのフレームは身体の動きに連動しています。当然なことなのですが,自分にとってはとっても新鮮です。

背景は生活感で溢れていますが,普通のPCで普通の空間で,AIによる姿勢推定とは言え,モーションキャプチャがここまで手軽にできてしまうことは自分にとっては驚きと嬉しさが交錯するものです。

自分では何もしていませんが,フリーな環境でここまでのモーションキャプチャができてしまうことは,高精度な測定結果が得られますがどうしても大掛かりで,そして大変なキャリブレーションを必要とする光学式モーションキャプチャが,幸運なことですが,比較的身近にあった自分としては,やはり感激モノです。これなら,測定環境に厳しい自然に近い資料館や民家での民具の作り手/使い手の身体の動きの測定も可能になるのです。

動画から検出された身体骨格フレームの録画

使用者の動きを身体フレームに置き換えて,その動きをmp4として出力する方法を体験してみます。ここでは,箕振りの動画がありましたので,

この動画からMediapipeによる基本となる身体フレームの抽出を試行してみました。

身体フレームの表示軸のサイズが上下方向と水平方向によって食い違いが起きているので,実際の骨格より身体フレームの方は左右方向につぶれているように見えます。これは表示の問題なので,出力されている座標値を直接扱えばおおむね必要なデータは得られると思いますが,直感的には好ましくないので,Pythonコードを学びながら修正していくように心がけます。

動画からキーポイントの座標値の出力

今のところは下のコードをVS Codeで実行して,この身体フレームの動画を出力しています。入力となる動画のファイル名は,\(input_video.mp4\)としています。これにより,まずは,動画のフレームごとに検出された身体各部位のキーポイント(landmark)の座標値がcsvファイル(\(pose2d.csv\))に出力されます。

import cv2

import mediapipe as mp

import csv

VIDEO_FILE = “input_video.mp4”

mp_pose = mp.solutions.pose

pose = mp_pose.Pose(

    static_image_mode=False,

    model_complexity=2,  # 高精度モード

)

cap = cv2.VideoCapture(VIDEO_FILE)

csv_file = open(“pose2d.csv”, “w”, newline=”)

csv_writer = csv.writer(csv_file)

csv_writer.writerow([“frame”, “landmark_id”, “x”, “y”, “z”, “visibility”])

frame_num = 0

while cap.isOpened():

    success, image = cap.read()

    if not success:

        break

    image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    results = pose.process(image_rgb)

    if results.pose_landmarks:

        h, w, _ = image.shape

        for i, lm in enumerate(results.pose_landmarks.landmark):

            x_px = lm.x * w

            y_px = lm.y * h

            csv_writer.writerow([frame_num, i, x_px, y_px, lm.z, lm.visibility])

    frame_num += 1

cap.release()

csv_file.close()

print(“✅ pose2d.csv を作成しました(ピクセル座標)”)

pose_extractor_complexity=2.py

\(pose2d.csv\)の内訳は,

Medispipeで準備されている,上表のB列でも確認できるlandmark_idの対応の一部を下表に示します。このidはフライング的に示しています上の身体フレーム(ここでのフレームは骨格,キーポイントをつないだもののことです。)の図中でも振られています。

Index部位名
0
1右目内側
2右目
3右目外側
4左目内側
5左目
6左目外側
7右耳
8左耳
9口角右
10口角左
11左肩
12右肩
13左肘
14右肘
15左手首
16右手首

ここまでで,動画撮影面での各関節の相対的な座標値の数値化できそうです。上表のE列の最上部にzが見えますが,これはカメラとの距離のようなので,関節の三次元の座標値を示すものではないそうです。あくまでも,この撮影面での二次元的な位置関係が大事であることになります。そのこともあり,ここでの出力ファイル名には\(pose2d.csv\)とされています

キーポイント座標値から身体フレームアニメーションへの出力

次に,時間変化するキーポイントの座標値が格納された\(pose2d.csv\)をinput_dataとして,身体フレームのアニメーションを作成し,mp4形式で出力します。そのためのPythonコードは以下のようになります。

import csv

import matplotlib.pyplot as plt

from matplotlib.animation import FuncAnimation, FFMpegWriter

POSE_CONNECTIONS = [

    (0, 1), (1, 2), (2, 3), (3, 7),

    (0, 4), (4, 5), (5, 6), (6, 8),

    (9, 10),

    (11, 12),

    (11, 13), (13, 15), (15, 17),

    (12, 14), (14, 16), (16, 18),

    (11, 23), (12, 24),

    (23, 24), (23, 25), (25, 27),

    (24, 26), (26, 28),

    (27, 29), (29, 31),

    (28, 30), (30, 32)

]

def load_pose_data(csv_path):

    poses = {}

    min_x = min_y = float(‘inf’)

    max_x = max_y = float(‘-inf’)

    with open(csv_path, newline=”) as f:

        reader = csv.DictReader(f)

        for row in reader:

            frame = int(row[‘frame’])

            lid = int(row[‘landmark_id’])

            x = float(row[‘x’])

            y = float(row[‘y’])

            if frame not in poses:

                poses[frame] = {}

            poses[frame][lid] = (x, y)

            if x < min_x: min_x = x

            if y < min_y: min_y = y

            if x > max_x: max_x = x

            if y > max_y: max_y = y

    # 余白を少し

    pad_x = 0.02 * (max_x – min_x if max_x > min_x else 1.0)

    pad_y = 0.02 * (max_y – min_y if max_y > min_y else 1.0)

    return poses, (min_x – pad_x, max_x + pad_x, min_y – pad_y, max_y + pad_y)

def animate_pose(poses, bounds, save_mp4=False, fps=20):

    x0, x1, y0, y1 = bounds

    width  = x1 – x0

    height = y1 – y0

    fig, ax = plt.subplots()

    # データ単位を等倍

    ax.set_aspect(‘equal’, adjustable=’box’)

    # 可能なら軸ボックス自体も動画の縦横比に

    try:

        ax.set_box_aspect(height / width)

    except Exception:

        pass

    # 画像座標に合わせてY反転&範囲設定

    ax.set_xlim(x0, x1)

    ax.set_ylim(y1, y0)

    scat = ax.scatter([], [], s=20)

    lines = [ax.plot([], [], ‘b’)[0] for _ in POSE_CONNECTIONS]

    texts = [ax.text(0, 0, ”, fontsize=6, color=’red’) for _ in range(33)]

    frames_sorted = sorted(poses.keys())

    def init():

        return scat, *lines, *texts

    def update(frame_num):

        pose = poses.get(frame_num, {})

        # ランドマーク順で並べる(表示が安定)

        xs = []

        ys = []

        for i in range(33):

            if i in pose:

                xs.append(pose[i][0])

                ys.append(pose[i][1])

        scat.set_offsets(list(zip(xs, ys)))

        for i, (a, b) in enumerate(POSE_CONNECTIONS):

            if a in pose and b in pose:

                lines[i].set_data([pose[a][0], pose[b][0]],

                                  [pose[a][1], pose[b][1]])

            else:

                lines[i].set_data([], [])

        for i, text in enumerate(texts):

            if i in pose:

                x, y = pose[i]

                text.set_position((x, y))

                text.set_text(str(i))

                text.set_visible(True)

            else:

                text.set_visible(False)

        return scat, *lines, *texts

    ani = FuncAnimation(

        fig, update, frames=frames_sorted, init_func=init,

        blit=True, interval=1000/fps

    )

    if save_mp4:

        print(“🎥 Saving animation with IDs as pose_animation.mp4 …”)

        writer = FFMpegWriter(fps=fps, metadata=dict(artist=’PoseExtractor’), bitrate=1800)

        ani.save(“pose_animation.mp4”, writer=writer)

        print(“✅ Saved as pose_animation.mp4”)

    else:

        plt.tight_layout()

        plt.show()

if __name__ == “__main__”:

    poses, bounds = load_pose_data(“pose2d.csv”)

    animate_pose(poses, bounds, save_mp4=True, fps=20)

visualize_pose2d.py

ここまでで,身体フレームのアニメーションpose_animation.mp4を得ます。

上図の13番は左肘,14番は右肘に対応します。この13番と14番について座標値の時間変化を見てみます。

左肘と右肘の座標値変化の様子

元動画のフレーム率が\(29.97フレーム/sec\)なので,サンプリングタイムが\(0.0333667秒\)ということになると思いますので,pose2d.csvから,以下のように整理できます。

横軸は時間ですが,元動画の収録時間も\(40\)秒間程度ですので,上図の横軸が時間軸であり,右端に\(41.41\)と見えるのは妥当かなと思います。また,縦軸は,左肘,右肘の座標値\((x,y)\)の,\(time=0.8\)を基準位置とした変化量を示しています。動画収録開始\(0.8\)秒後ぐらいから箕振り動作が始まりましたので,ここを基準位置としました。検出された座標値で\(y\)の座標値は下向きを正としてあるようなので,上図では正負を反転し,実際に観察される状況に直感的に合うようにしてみました。

基本的には,この箕の振り方では,左右の肘ともに同じリズムで振られていることが分かります。\(time=18.15\)あたりで動きが弱くなりますが,実際にもこの瞬間は箕振りの体制を整えなおそうとしていました。箕の振り方も多様ですが,この箕は大ぶりの面岸箕でしたので,左右の振りに位相差をつけ,箕面を回転しながら上下させるというよりは,左右を同位相で上下させつつも,おおむねにおいて,左側の方を右側よりは強めに上下させている様子が確認できます。

箕振り作業の残り\(5\)秒間では,\(y\)の座標値は急激に下がり,そののちの\(time=40.04\)あたりから,\(x\)の座標値が上下に分かれていく様子が確認できますが,これは箕振りを終え,箕を床に置き,箕面に広がった籾を両手で掻き寄せていた作業と一致しています。

正直なところ,この結果はなかなか実際の動きに合っていると思われます。びっくりです。身体骨格フレームの左右・上下のサイズがずれた出力の様子からは信じられないほどの再現の良さと理解できます。このあとの勉強がますます楽しみになってきました。

pose2dからpose3dへ

まず,Mediapipeで出力したpose2d.csvから,VideoPose3Dで使用されるCOCO Keypoints(17点フォーマット)に変換します。COCOは Common Objects in Context の略で,Microsoftが中心となって整備した大規模画像データセットです。

Mediapipeの33ジョイント2D座標を,COCOの17ジョイント形式にマッピングして .npy ファイルとして保存します。そのファイルをVideoPose3Dに入力することで,2D座標列から3次元復元(pose3d)が可能になります。

Mediapipeの33ジョイント2D座標 を COCOの17ジョイント形式 に変換

以下にMediapipeの縦長CSVから COCO 17ジョイント形式の3D座標 を作り、さらに VideoPose3D 向けに Zを骨盤原点化して正規化 するスクリプトpose2d.csv_to_pose3d_coco_vp3d_aug.npy.pyを示します。

import numpy as np

import pandas as pd

# ===============================

# 1. CSV読み込み

# ===============================

csv_file = “pose2d.csv”  # Mediapipe CSV

df = pd.read_csv(csv_file)

# ===============================

# 2. Mediapipe → COCO 対応

# ===============================

mp_to_coco = {

    0:0,     # nose

    1:1,     # left_eye

    2:2,     # right_eye

    3:3,     # left_ear

    4:4,     # right_ear

    11:5,    # left_shoulder

    12:6,    # right_shoulder

    13:7,    # left_elbow

    14:8,    # right_elbow

    15:9,    # left_wrist

    16:10,   # right_wrist

    23:11,   # left_hip

    24:12,   # right_hip

    25:13,   # left_knee

    26:14,   # right_knee

    27:15,   # left_ankle

    28:16    # right_ankle

}

# 左右対応(flip用)

flip_pairs = [

    (1,2),   # left_eye ↔ right_eye

    (3,4),   # left_ear ↔ right_ear

    (5,6),   # left_shoulder ↔ right_shoulder

    (7,8),   # left_elbow ↔ right_elbow

    (9,10),  # left_wrist ↔ right_wrist

    (11,12), # left_hip ↔ right_hip

    (13,14), # left_knee ↔ right_knee

    (15,16)  # left_ankle ↔ right_ankle

]

num_joints_coco = 17

num_frames = df[‘frame’].nunique()

# ===============================

# 3. 配列作成 (X, Y, Z)

# ===============================

pose3d_coco = np.zeros((num_frames, num_joints_coco, 3))

for f, frame_df in df.groupby(‘frame’):

    for mp_idx, coco_idx in mp_to_coco.items():

        joint = frame_df[frame_df[‘landmark_id’]==mp_idx]

        if not joint.empty:

            pose3d_coco[f-1, coco_idx, 0] = joint[‘x’].values[0]

            pose3d_coco[f-1, coco_idx, 1] = joint[‘y’].values[0]

            pose3d_coco[f-1, coco_idx, 2] = joint[‘z’].values[0]

# ===============================

# 4. 骨盤を原点にする

# ===============================

left_hip_idx, right_hip_idx = 11, 12

pelvis = (pose3d_coco[:, left_hip_idx, :] + pose3d_coco[:, right_hip_idx, :]) / 2

pose3d_coco -= pelvis[:, np.newaxis, :]

# ===============================

# 5. 正規化(スケール統一)

# ===============================

max_distance = np.max(np.linalg.norm(pose3d_coco, axis=2))

if max_distance > 0:

    pose3d_coco /= max_distance

# ===============================

# 6. データ拡張

# ===============================

augmented = []

for seq in [pose3d_coco]:

    # (a) 左右反転

    flipped = seq.copy()

    flipped[:,:,0] *= -1  # X軸反転

    for a, b in flip_pairs:

        flipped[:,[a,b],:] = flipped[:,[b,a],:]

    augmented.append(flipped)

    # (b) ランダムスケーリング

    scale = np.random.uniform(0.9, 1.1)

    scaled = seq * scale

    augmented.append(scaled)

    # (c) ランダム回転(Z軸)

    theta = np.deg2rad(np.random.uniform(-15, 15))

    rot_matrix = np.array([

        [np.cos(theta), -np.sin(theta), 0],

        [np.sin(theta),  np.cos(theta), 0],

        [0,              0,             1]

    ])

    rotated = np.einsum(‘ij,fkj->fki’, rot_matrix, seq)

    augmented.append(rotated)

# 拡張データを結合

pose3d_augmented = np.concatenate([pose3d_coco[np.newaxis]] + [a[np.newaxis] for a in augmented], axis=0)

# ===============================

# 7. 保存

# ===============================

np.save(“pose3d_coco_vp3d_aug.npy”, pose3d_augmented)

print(“pose3d_coco_vp3d_aug.npy に変換・正規化・データ拡張完了!”)

このスクリプトを実行することで,VideoPose3D のフレームアニメーション(スティックフィギュアの動画)において,視点を変えることができる動画を得るための,pose3d_coco_vp3d_aug.npyを出力できます。VideoPose3D は、2Dの人体キーポイント(関節座標列)から 3Dポーズ(奥行きを含む骨格構造)を復元する 深層学習ベースのモデル/フレームワーク です。

VideoPose3Dによるフレームアニメーションの出力

以下のスクリプトanimate_pose3d.pyを実行することで,pose3d_coco_vp3d_aug.npyを入力データとして,一方向から測定した作業者の動画から,その人体の動きを3Dのスティックフィギュアの動画に変換することが可能になります。

import numpy as np

import matplotlib.pyplot as plt

from matplotlib.animation import FuncAnimation

import csv

# ============================

# 1. npy読み込み

# ============================

pose3d = np.load(“pose3d_coco_vp3d_aug.npy”)[0]  # (num_frames, 17, 3)

# COCOの骨格接続(VideoPose3Dと同じ)

skeleton = [

    (0,1),(0,2),(1,3),(2,4),

    (5,6),(5,7),(7,9),(6,8),(8,10),

    (11,12),(11,13),(13,15),(12,14),(14,16),

    (5,11),(6,12)

]

# COCO 17点の名称

joint_names = [

    “Nose”, “LEye”, “REye”, “LEar”, “REar”,

    “LShoulder”, “RShoulder”, “LElbow”, “RElbow”, “LWrist”, “RWrist”,

    “LHip”, “RHip”, “LKnee”, “RKnee”, “LAnkle”, “RAnkle”

]

# ============================

# 2. CSV出力準備

# ============================

csv_file = “pose3d_output.csv”

with open(csv_file, “w”, newline=””) as f:

    writer = csv.writer(f)

    # ヘッダー行

    header = [“frame”, “joint_name”, “x”, “y”, “z”]

    writer.writerow(header)

    for frame_idx, joints in enumerate(pose3d):

        for jname, (x, y, z) in zip(joint_names, joints):

            writer.writerow([frame_idx, jname, x, y, z])

print(f”CSV出力完了: {csv_file}”)

# ============================

# 3. アニメーション描画

# ============================

fig = plt.figure(figsize=(10, 10))

ax = fig.add_subplot(111, projection=’3d’)

# 初期プロット(骨格ライン)

lines = []

for (i,j) in skeleton:

    line, = ax.plot([], [], [], ‘o-‘, lw=2)

    lines.append(line)

# 関節ラベルを初期化

texts = [ax.text(0, 0, 0, name, fontsize=8, color=”red”) for name in joint_names]

# 軸範囲をデータに合わせて設定

ax.set_xlim3d(np.min(pose3d[:,:,0]), np.max(pose3d[:,:,0]))

ax.set_ylim3d(np.min(pose3d[:,:,1]), np.max(pose3d[:,:,1]))

ax.set_zlim3d(np.min(pose3d[:,:,2]), np.max(pose3d[:,:,2]))

ax.set_xlabel(‘X’)

ax.set_ylabel(‘Y’)

ax.set_zlabel(‘Z’)

ax.view_init(elev=20, azim=-60)  # 視点調整

def update(frame):

    joints = pose3d[frame]

    # 骨格ラインの更新

    for line, (i,j) in zip(lines, skeleton):

        x = [joints[i,0], joints[j,0]]

        y = [joints[i,1], joints[j,1]]

        z = [joints[i,2], joints[j,2]]

        line.set_data(x, y)

        line.set_3d_properties(z)

    # テキストラベルの更新(少し上にオフセット)

    for text, (x, y, z) in zip(texts, joints):

        text.set_position((x, y))

        text.set_3d_properties(z + 0.02)

    return lines + texts

ani = FuncAnimation(fig, update, frames=pose3d.shape[0], interval=50, blit=False)

plt.show()

これにより,

animate_pose3d.pyによる箕を振る人の身体フレームアニメーション

オリジナル動画に合わせるための座標回転

上述のフレームアニメーションでは座標軸と身体フレームの位置関係に角度が付いているために,直感的に分かりづらいところがあります。そこで1st-timeの身体フレームをつなぐ各キーポイントの座標値から,両足首の中心から頭に向けての軸と,左肩から右肩への軸を決定し,このお互いに直交する2つの軸によって構成される平面の法線方向も三番目の座標軸として定義してみました。

座標回転後の身体フレーム

この座標系のビューアーにはデフォルトでパースが効いているので,難しいところがありますが,想定してものとは少し異なっています。とはいえ,初期のものよりはずいぶんすっきりと観察でき,この図での\(z\)軸方向は元々の現地で録画した作業者の動きの撮影視点とほぼ同じなので,しばらくはこれで良いかなと思っています。このあとPython修行を積みながら精度を上げていきます。

合わせて,このアニメーションはmp4にも書き出しています。

箕を簸る人の身体フレーム.mp4

右足の動きが時折り不自然なのは,この撮影が冬季であり,身体全体を包み込む服の厚さで,画像からは関節などの身体的特徴が読み取りにくいことと,倉庫の中で実施しため,背景に置かれた物体と作業者との境目が複雑であったことによるものかと考えています。教訓としては,できるだけ身体のキーポイントが確認しやすい服装と,余計な情報が入り込みにくい環境で撮影を行うべきと,当たり前なのですが基本的な大切さに気づかされています。

pose3D.csvの出力と評価

そして最終目標の身体フレームのキーポイントの三次元座標値の取り出しと評価への試行です。

下のスクリプトで確認できますが,骨盤中央の原点として標準化した各キーポイントの座標値はpose3d_output.csvに書き出されます。

pose3d_output.csv

ここで,左手首(LWrist),右手首(RWrist),左肩(LShoulder),右肩(RShoulder)に注目してみたいと思います。それぞれのキーポイントの座標値の時間変化の軌跡を見ていきたいと思います。

実際の録画平面である\(x-y\)座標面,つまり\(z\)軸から見る限りはそれほど破綻なく,うまく作業者の動きをトレースしているように見えます。これと同様な視点での身体フレームの様子を再掲しますと

の感じです。これだけなら,pose3d.csvに格納されたキーポイントの座標値で安心して議論できそうですが,視点を変えて\(y\)軸方向から見ると

両腕の動きが両肩をつなぐ方向に対して直交した軸を中心にばらついているのではなく,両手の動きの軌跡は,実際の動きより\(z\)軸方向よりにずれているような結果となっています。確かに,実測の面は\(x-y\)平面での動きを見ているので,その面に対する奥行き方向つまり\(z\)軸方向では,現時点でのPCの環境設定によるものですが(自分自身の学習不十分によるものと思います),今扱っているVideoPose3D,その入力データを準備するCOCO,そしてMediapipeでのAIによる姿勢推定の能力をまだまだ活かせきれていないようです。それ以上に右肩がうまく認識されていないのかもしれません。とは言えもともとの撮影方向に関してはそのターゲットの身体の動きの特徴を思いのほかうまく再現しているようなので,オリジナルの録画データを取得する際の工夫によってはもっと望ましい結果が得られるのではとも感じています。

とにかく,ほとんどフリーソフトだけで,しかもロースペックな(このPCさんに怒られてしまいますが)PCでもここまでできてしまうことに,やはり感動です:-)感謝しかありません。

身体フレームアニメーションの表示とpose3d_scaled_auto.csvへの出力のスクリプト

import numpy as np

import matplotlib.pyplot as plt

from matplotlib.animation import FuncAnimation, FFMpegWriter

import csv

# ============================

# 1. npy読み込み

# ============================

pose3d = np.load(“pose3d_coco_vp3d_aug.npy”)[0]  # (num_frames, 17, 3)

# COCO 17点の名称

joint_names = [

    “Nose”,”LEye”,”REye”,”LEar”,”REar”,

    “LShoulder”,”RShoulder”,”LElbow”,”RElbow”,”LWrist”,”RWrist”,

    “LHip”,”RHip”,”LKnee”,”RKnee”,”LAnkle”,”RAnkle”

]

# ============================

# 2. 肩幅・身長基準でスケーリング

# ============================

actual_shoulder_width = 0.4  # 例: 40cm

actual_height = 1.7           # 例: 1.7m

# モデル上の平均肩幅

shoulder_distances = np.linalg.norm(pose3d[:,5,:] – pose3d[:,6,:], axis=1)

mean_model_shoulder_width = np.mean(shoulder_distances)

# モデル上の平均身長(頭→両足首平均)

nose_to_lankle = np.linalg.norm(pose3d[:,0,:] – pose3d[:,15,:], axis=1)

nose_to_rankle = np.linalg.norm(pose3d[:,0,:] – pose3d[:,16,:], axis=1)

mean_model_height = np.mean((nose_to_lankle + nose_to_rankle)/2)

# 初期スケーリング(肩幅・身長基準)

scale_x = actual_shoulder_width / mean_model_shoulder_width

scale_y = scale_x

scale_z = actual_height / mean_model_height

pose3d_scaled = pose3d.copy()

pose3d_scaled[:,:,0] *= scale_x

pose3d_scaled[:,:,1] *= scale_y

pose3d_scaled[:,:,2] *= scale_z

# ============================

# 3. Z軸自動補正(X,Yに合わせる)

# ============================

x_range = np.max(pose3d_scaled[:,:,0]) – np.min(pose3d_scaled[:,:,0])

y_range = np.max(pose3d_scaled[:,:,1]) – np.min(pose3d_scaled[:,:,1])

z_range = np.max(pose3d_scaled[:,:,2]) – np.min(pose3d_scaled[:,:,2])

# X,Yの平均スケールに合わせる

xy_avg_range = (x_range + y_range) / 2

z_scale_extra = xy_avg_range / z_range

pose3d_scaled[:,:,2] *= z_scale_extra

# ============================

# 4. CSV出力

# ============================

csv_file = “pose3d_scaled_auto.csv”

with open(csv_file, “w”, newline=””) as f:

    writer = csv.writer(f)

    writer.writerow([“frame”,”joint_name”,”x”,”y”,”z”])

    for frame_idx, joints in enumerate(pose3d_scaled):

        for jname, (x,y,z) in zip(joint_names, joints):

            writer.writerow([frame_idx, jname, x, y, z])

print(f”CSV出力完了: {csv_file}”)

# ============================

# 5. アニメーション描画

# ============================

fig = plt.figure(figsize=(10,10))

ax = fig.add_subplot(111, projection=’3d’)

skeleton = [

    (0,1),(0,2),(1,3),(2,4),

    (5,6),(5,7),(7,9),(6,8),(8,10),

    (11,12),(11,13),(13,15),(12,14),(14,16),

    (5,11),(6,12)

]

lines = [ax.plot([],[],[],’o-‘,lw=2)[0] for _ in skeleton]

texts = [ax.text(0,0,0,name,fontsize=8,color=”red”) for name in joint_names]

ax.set_xlim3d(np.min(pose3d_scaled[:,:,0]), np.max(pose3d_scaled[:,:,0]))

ax.set_ylim3d(np.min(pose3d_scaled[:,:,1]), np.max(pose3d_scaled[:,:,1]))

ax.set_zlim3d(np.min(pose3d_scaled[:,:,2]), np.max(pose3d_scaled[:,:,2]))

ax.set_xlabel(‘X’); ax.set_ylabel(‘Y’); ax.set_zlabel(‘Z’)

ax.set_box_aspect([1,1,1])

ax.view_init(elev=20, azim=-60)

def update(frame):

    joints = pose3d_scaled[frame]

    for line,(i,j) in zip(lines,skeleton):

        x=[joints[i,0],joints[j,0]]

        y=[joints[i,1],joints[j,1]]

        z=[joints[i,2],joints[j,2]]

        line.set_data(x,y)

        line.set_3d_properties(z)

    for text,(x,y,z) in zip(texts,joints):

        text.set_position((x,y))

        text.set_3d_properties(z+0.02)

    return lines+texts

ani = FuncAnimation(fig, update, frames=pose3d_scaled.shape[0], interval=50, blit=False)

plt.show()

# ============================

# 6. 動画保存

# ============================

ffmpeg_path = r”C:\ffmpeg\ffmpeg-7.1.1-essentials_build\bin\ffmpeg.exe”

plt.rcParams[‘animation.ffmpeg_path’] = ffmpeg_path

writer = FFMpegWriter(fps=20, codec=”libx264″, extra_args=[“-pix_fmt”,”yuv420p”])

output_file = “pose3d_animation_scaled_auto.mp4”

ani.save(output_file, writer=writer, dpi=100)

plt.close(fig)

print(f”動画保存完了: {output_file}”)

次に向けて

初めてAI姿勢推定型モーションキャプチャに直接触れ,楽しさのあまりに書きなぐってしまいましたが,一週間もしないうちに細かい設定は忘れてしまいそうなので,引き続き全体を見直しながら,モーションキャプチャへの挑戦を続けたいと思います。


comment

コメントを残す

OnkoLab Co., Ltd.をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む