Open3D + MediaPipe による3Dモーションキャプチャ/アバター駆動
【目次】
- はじめに:環境・前提・読み方
- Python 3.12 のインストール
- Build Tools・CUDA Toolkit・PyTorch のインストール
- Python の開発環境 Visual Studio Code のインストールと Python 用の設定
- Python プログラム実行手順
- ライブラリのインストール、ファイルの配置
- 演習1.BVHモーションデータの入手と内容理解
- 演習2.Open3DでBVHの1フレームを骨格として静止描画
- 演習3.Open3DでBVHを骨格アニメーション再生
- 演習4.カメラ+MediaPipeでリアルタイム3D骨格描画と2D重畳
- 演習5.カメラ+MediaPipeで円柱メッシュアバターを駆動
- 演習6.カメラ+MediaPipeでglTF標準アバターをスキニング駆動
はじめに:環境・前提・読み方
この資料で学ぶこと
この資料は、モーションキャプチャデータ(BVH)の表示から、カメラ映像による姿勢推定、メッシュアバターの駆動までを6つの演習で扱う。各演習は単独で実行でき、後の演習ほど前の演習で学んだ知識を利用する。
主要用語
本資料で用いる用語を、最初に使用する箇所より前に定義する。
- 順運動学(FK:Forward Kinematics):各関節の回転から、各部位の位置を親から子へ順に計算する手法。
- バインド姿勢(bind pose):スキン(メッシュ)が骨に関連付けられたときの基準姿勢。多くのモデルでは両腕を下ろした姿勢、またはTポーズである。
- ジョイント(joint)/ボーン(bone):スキンを変形させる骨格の節点。本資料では同じ意味で「ボーン」に統一する。
- ウェイト(weight):各頂点が各ボーンの動きをどの程度受けるかを示す重み。
- inverseBindMatrices:頂点をバインド姿勢の各ボーン座標系へ変換する行列。glTFに含まれる。
- リニアブレンドスキニング(LBS:Linear Blend Skinning):複数ボーンの変換をウェイトで線形に合成し、頂点を移動させるスキニング手法。
単位とスケール
BVHの位置は、BVHファイル固有の長さ単位で与えられる。BVH形式自体は物理単位を定めておらず、このCMUデータでは全身がおよそ数十単位の大きさになる。一方、MediaPipeの pose_world_landmarks はメートルで与えられる。座標系もスケールも異なるため、描画サイズは演習ごとに調整する。描画パラメータは次の通り。
- 演習2(BVH単位基準):関節の球の半径 1.5、座標軸の長さ 20。
- 演習4以降(MediaPipe、m基準):関節の球の半径 0.05、座標軸の長さ 0.5。
動作環境
Windows PC で動作する。GPU・CPU のいずれでも動作する(GPUは必須ではない)。Python は 3.9〜3.12 を用いる。
Python 3.12 のインストール
Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。
[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]
Windows での Python 3.12 のインストール
以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。
方法 1:winget によるインストール
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。
REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""
REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"
方法 2:インストーラーによるインストール
- Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
- ダウンロードしたインストーラーを実行する。
- 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから
pythonコマンドを実行できない。 - 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。
インストールの確認
コマンドプロンプトで以下を実行する。
python --version
バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。
Build Tools・CUDA Toolkit・PyTorch のインストール
本章では、C++ ビルドツール、NVIDIA CUDA Toolkit、PyTorch のインストールを行い、GPU を活用した機械学習プログラムを実行する環境を整える。扱う環境は、Windows 搭載パソコンである。
[Build Tools・CUDA Toolkit・PyTorch のインストール手順を見るには、ここをクリック]
Windows での Build Tools for Visual Studio 2026 のインストール
Build Tools for Visual Studio 2026 は、C++ ソースコードを Windows 用バイナリにコンパイルするための開発ツール群である。unsloth 等の一部 Python パッケージは、インストール時に C++ コードのビルドを必要とするため、これらのツールが必須となる。
以下のコマンドは、Build Tools が未インストールの場合は winget で新規インストールし、インストール済みの場合は setup.exe modify でコンポーネントを追加する(バージョンは変更しない)。
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
REM VC++ ランタイム
winget install --scope machine --id Microsoft.VCRedist.2015+.x64 -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "/quiet /norestart"
REM ============================================================
REM Visual Studio Build Tools + Desktop development with C++
REM (VCTools、MSBuildTools、CMake連携、Clang、Windows 11 SDK)
REM ============================================================
REM 進行中のインストーラーを停止(ロック競合回避)
taskkill /F /IM vs_setup.exe /T >nul 2>&1
taskkill /F /IM vs_installer.exe /T >nul 2>&1
taskkill /F /IM vs_installerservice.exe /T >nul 2>&1
REM 未インストール時: winget で新規インストール
REM インストール済み時: setup.exe modify でコンポーネント追加(バージョンは変更しない)
winget list --id Microsoft.VisualStudio.BuildTools 2>nul | findstr /i "BuildTools" >nul 2>&1
if %ERRORLEVEL% EQU 0 (
for /f "usebackq delims=" %P in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products Microsoft.VisualStudio.Product.BuildTools -property installationPath`) do start /wait "" "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "%P" --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --includeRecommended --quiet --norestart --nocache
) else (
winget install --scope machine --id Microsoft.VisualStudio.BuildTools -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100"
)
REM 破損時の修復(任意、動作がおかしくなった場合)
REM "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" repair --installPath "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" --quiet --norestart
REM 導入確認(インストールパスが表示されれば正常)
"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products * -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath
上記のコマンドでは、Build Tools 本体と Visual C++ 再頒布可能パッケージをインストールし、続いて以下のコンポーネントを追加している。
- VCTools:C++ デスクトップ開発ワークロード(
--includeRecommendedにより、MSVC コンパイラ、C++ AddressSanitizer、vcpkg、CMake ツール、Windows 11 SDK 等の推奨コンポーネントが含まれる) - MSBuildTools:MSBuild によるビルドツールのワークロード
- VC.CMake.Project:Windows 向け C++ CMake ツール
- VC.Llvm.Clang:Windows 向け C++ Clang コンパイラ
- VC.Llvm.ClangToolset:MSBuild から Clang を使用するための clang-cl ツールセット
- Windows11SDK.26100:Windows 11 SDK(ビルド 10.0.26100)
追加のコンポーネントが必要になった場合は Visual Studio Installer で個別にインストールできる。
Windows での NVIDIA CUDA Toolkit のインストール
NVIDIA CUDA Toolkit は、NVIDIA GPU 上で計算を行うためのコンパイラ・ライブラリ群である。PyTorch や vLLM 等が GPU を利用するために必要となる。GPU を使用しない場合、この手順は不要である。
前提条件:NVIDIA GPU、NVIDIA ドライバ、Build Tools for Visual Studio もしくは Visual Studio が必要である。
インストール中の注意:他のウインドウは閉じておくこと。
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
REM NVIDIA CUDA Toolkit 12.8 をシステム領域にインストール
winget install --scope machine --id Nvidia.CUDA --version 12.8 -e --silent --disable-interactivity --force --uninstall-previous --accept-source-agreements --accept-package-agreements --override "-s -n"
REM 環境変数TEMP, TMPの設定(一時ファイルの保存先を短いパスに変更)
mkdir C:\TEMP
setx TEMP "C:\TEMP" /M
setx TMP "C:\TEMP" /M
環境変数 TEMP および TMP を C:\TEMP に変更しているのは、後続のインストール処理で長いパス名や空白を含むパス名がエラーの原因となる場合があるためである。
Windows での PyTorch のインストール
https://pytorch.org のインストールガイドに従い、自環境の CUDA バージョンに対応したコマンドを取得して実行する。CUDA バージョンは以下で確認できる。
nvcc --version
Python 3.12、CUDA 12.6 以上の場合は、管理者権限でコマンドプロンプトを起動し、以下を実行する。cu128 は CUDA 12.8 用のタグである。CUDA バージョンが異なる場合は、上記公式サイトで該当するタグを確認し、URL 末尾の cu128 を置き換えること。
pip install --no-user -U numpy torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
以降の章では、必要に応じて題材に応じた必要なソフトウェアを追加する。
Python の開発環境 Visual Studio Code のインストールと Python 用の設定
Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。
[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]
Windows での Visual Studio Code のインストールと Python 用の設定手順
1. VS Code と拡張機能のインストール
以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。
インストールコマンド
REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul
REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements
REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T
REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"
REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f
REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"
REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===
2. Python インタプリタの選択
同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.
- コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(
Ctrl+Shift+P) Python: Select Interpreterと入力する
- 表示される一覧から,使用する Python(例:
C:\Program Files\Python312\python.exe)を選択する.
Python プログラム実行手順
[Windows での Python プログラム実行手順を見るには、ここをクリック]
Windows での Python 実行手順(Visual Studio Codeを使用)
プログラムファイルの作成と保存
- 左サイドバーの「エクスプローラー」アイコン(
Ctrl+Shift+E)をクリックする
- 「NO FOLDER OPENED」(作業対象フォルダが未選択の状態)と表示される場合は,「Open Folder」をクリックし,プログラムを保存するフォルダを選択する
続いて「フォルダを信用するか」を確認する画面(フォルダ内のコードを実行してよいか確認する VS Code の仕組み)が表示されるので,チェックして Yes を選択する
- フォルダ名の右側に表示される「新しいファイル」アイコンをクリックする
- ファイル名(例:
aitask.py.ファイル名は何でも良い)を入力しEnterを押す.拡張子は.py(Python ファイルを示す拡張子)とする
- 実行したいコードを選択し,
Ctrl+Cでコピーする.VS Code のエディタ領域にCtrl+Vで貼り付ける Ctrl+Sで保存する
プログラムの実行
- エディタ右上の三角形「▷」アイコン(Run Python File:現在開いている Python ファイルを実行するボタン)をクリックする.または,エディタ上で右クリックし「ターミナルで Python ファイルを実行」を選択する
- VS Code 下部のターミナル(コマンドの入出力を表示する画面)に,実行結果(
print関数の出力等)が表示される
- tkinter(Python 標準の GUI ライブラリ)のファイル選択ダイアログを使うプログラムを実行した場合は,ダイアログが開くので対象画像を選択する
- VS Code 下部のターミナルで実行結果を確認する.OpenCV ウィンドウ(OpenCV が画像を表示するために開く専用ウィンドウ)が開いた場合はそちらも確認する.OpenCV ウィンドウは,マウスクリックでウィンドウをアクティブ(操作対象の状態)にしてからキーを押すと終了する
ライブラリのインストール、ファイルの配置
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。
次のコマンドを順に実行し、OpenCV を一度削除してから入れ直す(複数のOpenCVパッケージが混在すると競合するため)。
pip uninstall -y opencv-python-headless opencv-contrib-python-headless opencv-python
pip install -U --no-user --force-reinstall opencv-contrib-python
pip install -U --no-user open3d pygltflib mediapipe opencv-contrib-python numpy
ファイルの配置
作業ディレクトリはスクリプトと同じ場所とする。次のファイルを同一ディレクトリ以下に置く。
- BVHデータ:
./mocap_data/... - 姿勢推定モデル:
./pose_landmarker_lite.task - アバター:
./avatar.glb
各演習のコードは、ファイルが無い場合のみ初回実行時に自動でダウンロードする。
カメラと終了操作の共通仕様
演習4以降はWebカメラを用いる。カメラが無い場合は、コード中の cv2.VideoCapture(0) を cv2.VideoCapture("movie.mp4") に変更して動画ファイルで代替できる。
終了は、カメラ映像ウィンドウがアクティブな状態で ESC キーを押す(Open3Dのウィンドウではなく、OpenCVの映像ウィンドウを選択した状態で押す)。
座標変換と特異点の共通仕様
MediaPipe の world ランドマーク (x, y, z) は、Open3D 描画用に [x, -y, -z] へ変換する(鏡像反転は行わない)。
2つの位置が一直線に近づくと、方向ベクトルから回転を求める計算で除算が不安定になる。本資料では、ベクトル長がほぼ0のときは回転を適用せず直前の状態を保つことで、この特異点を回避する。
主要ランドマークインデックス
MediaPipe Pose の主要な番号は、鼻 0、左肩 11、右肩 12、左肘 13、右肘 14、左手首 15、右手首 16、左腰 23、右腰 24、左膝 25、右膝 26、左足首 27、右足首 28、左つま先 31、右つま先 32。
姿勢推定モードと描画の前提
MediaPipe は VIDEO(同期)モードを使用する。VIDEO モードはフレームを順に同期処理し、各フレームに単調増加するタイムスタンプを渡す(LIVE_STREAM モードは非同期コールバックで結果が返るため、描画ループとの同期が複雑になる。本資料では実装を単純化するため VIDEO モードを用いる)。タイムスタンプには、MediaPipeを用いる各演習を通じて、time.perf_counter で得た秒値をミリ秒へ換算した整数を用いる。
Open3D の視点(カメラ方向)は、マウス操作で変わる。そのため画面上での左右の見え方は視点に依存する。左右の対応を確認するときは、被写体の右手を動かし、アバターの右側のボーンが動くかで判断する。
演習1.BVHモーションデータの入手と内容理解
手順
次のURLからBVHファイルをダウンロードし、./mocap_data/001/01_01.bvh に保存する。ファイルを開き、骨格の階層構造、フレーム数、フレーム間隔(FPS)を読み取って表示する。コードは標準ライブラリのみで記述する(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)。
import os
import urllib.request
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BVH_URL = "https://raw.githubusercontent.com/una-dinosauria/cmu-mocap/master/data/001/01_01.bvh"
BVH_PATH = os.path.join(BASE_DIR, "mocap_data", "001", "01_01.bvh")
# ファイルが無い場合のみダウンロード
if not os.path.exists(BVH_PATH):
os.makedirs(os.path.dirname(BVH_PATH), exist_ok=True)
urllib.request.urlretrieve(BVH_URL, BVH_PATH)
with open(BVH_PATH, "r") as f:
lines = f.readlines()
# HIERARCHY部の関節名と階層の深さを表示
depth = 0
joint_count = 0
for line in lines:
s = line.strip()
if s.startswith("{"):
depth += 1
elif s.startswith("}"):
depth -= 1
elif s.startswith("ROOT") or s.startswith("JOINT"):
name = s.split()[1]
joint_count += 1
print(" " * depth + name)
elif s.startswith("Frames:"):
frames = int(s.split()[1])
elif s.startswith("Frame Time:"):
frame_time = float(s.split()[2])
print("関節数:", joint_count)
print("フレーム数:", frames)
print("フレーム間隔(秒):", frame_time, " FPS:", round(1.0 / frame_time, 2))
print("再生時間(秒):", round(frames * frame_time, 2))
ヒント
BVHは HIERARCHY 部(骨格定義)と MOTION 部(フレームごとの数値)に分かれる。Frames: 行にフレーム数、Frame Time: 行に1フレームの秒数が記載される。
考察ポイント
階層の深さと関節数、フレーム数とフレーム間隔から再生時間を求められること、ルート関節のみ位置と回転を持ち、他の関節は回転のみを持つことを確認する。
演習2.Open3DでBVHの1フレームを骨格として静止描画
手順
演習1と同じBVHを用いる。指定フレームについてFKで各関節位置を計算し、Open3Dで関節を球(半径1.5)、骨を LineSet として描画する。FRAME_INDEX = 1 を描画対象とする。コードは open3d と numpy を用いる(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)。
import os
import urllib.request
import numpy as np
import open3d as o3d
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BVH_URL = "https://raw.githubusercontent.com/una-dinosauria/cmu-mocap/master/data/001/01_01.bvh"
BVH_PATH = os.path.join(BASE_DIR, "mocap_data", "001", "01_01.bvh")
FRAME_INDEX = 1
if not os.path.exists(BVH_PATH):
os.makedirs(os.path.dirname(BVH_PATH), exist_ok=True)
urllib.request.urlretrieve(BVH_URL, BVH_PATH)
# BVHを解析し、関節の親子関係・オフセット・チャンネル・モーションを取得
def parse_bvh(path):
with open(path, "r") as f:
tokens = f.read().split()
names, parents, offsets, channels = [], [], [], []
stack = [-1]
i = 0
while tokens[i] != "MOTION":
t = tokens[i]
if t in ("ROOT", "JOINT"):
names.append(tokens[i + 1])
parents.append(stack[-1])
offsets.append([0.0, 0.0, 0.0])
channels.append([])
current = len(names) - 1
i += 2
elif t == "OFFSET":
offsets[current] = [float(tokens[i + 1]), float(tokens[i + 2]), float(tokens[i + 3])]
i += 4
elif t == "CHANNELS":
n = int(tokens[i + 1])
channels[current] = tokens[i + 2:i + 2 + n]
i += 2 + n
elif t == "{":
stack.append(current)
i += 1
elif t == "}":
stack.pop()
i += 1
elif t == "End":
# End Site は末端のオフセットのみを持ち、関節ではない。
# ブロック全体(End Site { OFFSET x y z })の8トークンを読み飛ばす
i += 8
else:
i += 1
# MOTION部
fi = tokens.index("Frames:")
frames = int(tokens[fi + 1])
ft = float(tokens[fi + 4])
start = fi + 5
values = [float(v) for v in tokens[start:]]
motion = np.array(values).reshape(frames, -1)
return names, parents, offsets, channels, motion, ft
# 回転(オイラー角)を回転行列に変換
def euler_to_matrix(axis, angle_deg):
a = np.radians(angle_deg)
c, s = np.cos(a), np.sin(a)
if axis == "X":
return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
if axis == "Y":
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
# FKで各関節のグローバル位置を計算
def forward_kinematics(names, parents, offsets, channels, frame):
positions = [None] * len(names)
globals_rot = [None] * len(names)
idx = 0
for j in range(len(names)):
local_pos = np.array(offsets[j], dtype=float)
rot = np.eye(3)
for ch in channels[j]:
v = frame[idx]
idx += 1
if ch.endswith("position"):
axis = "XYZ".index(ch[0])
local_pos[axis] += v
else:
rot = rot @ euler_to_matrix(ch[0], v)
if parents[j] == -1:
globals_rot[j] = rot
positions[j] = local_pos
else:
p = parents[j]
globals_rot[j] = globals_rot[p] @ rot
positions[j] = positions[p] + globals_rot[p] @ local_pos
return positions
names, parents, offsets, channels, motion, ft = parse_bvh(BVH_PATH)
positions = forward_kinematics(names, parents, offsets, channels, motion[FRAME_INDEX])
# 球(関節)と LineSet(骨)を作成
geoms = []
for p in positions:
sphere = o3d.geometry.TriangleMesh.create_sphere(radius=1.5)
sphere.translate(p)
sphere.paint_uniform_color([0.8, 0.3, 0.3])
sphere.compute_vertex_normals()
geoms.append(sphere)
points = [p.tolist() for p in positions]
lines = [[parents[j], j] for j in range(len(names)) if parents[j] != -1]
line_set = o3d.geometry.LineSet()
line_set.points = o3d.utility.Vector3dVector(points)
line_set.lines = o3d.utility.Vector2iVector(lines)
geoms.append(line_set)
o3d.visualization.draw_geometries(geoms)
ヒント
FKでは、ルートから順に各関節の回転を親の変換へ掛け合わせ、グローバル位置を求める。骨は親子の関節位置を結ぶ線分として表す。
考察ポイント
球の位置と骨の接続が、演習1で確認した階層構造と一致するかを確認する。
演習3.Open3DでBVHを骨格アニメーション再生
手順
全フレームの関節位置を事前に計算する。Open3D の Visualizer をループで回し、各フレームで幾何データを更新する(処理順は update_geometry → poll_events → update_renderer)。time.sleep でBVHのフレーム間隔に合わせて再生速度を調整する。演習2の parse_bvh・euler_to_matrix・forward_kinematics を同じ定義で再利用する(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)。
import os
import time
import urllib.request
import numpy as np
import open3d as o3d
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BVH_URL = "https://raw.githubusercontent.com/una-dinosauria/cmu-mocap/master/data/001/01_01.bvh"
BVH_PATH = os.path.join(BASE_DIR, "mocap_data", "001", "01_01.bvh")
LOOP = True
if not os.path.exists(BVH_PATH):
os.makedirs(os.path.dirname(BVH_PATH), exist_ok=True)
urllib.request.urlretrieve(BVH_URL, BVH_PATH)
# parse_bvh を解析し、関節の親子関係・オフセット・チャンネル・モーションを取得
def parse_bvh(path):
with open(path, "r") as f:
tokens = f.read().split()
names, parents, offsets, channels = [], [], [], []
stack = [-1]
i = 0
while tokens[i] != "MOTION":
t = tokens[i]
if t in ("ROOT", "JOINT"):
names.append(tokens[i + 1])
parents.append(stack[-1])
offsets.append([0.0, 0.0, 0.0])
channels.append([])
current = len(names) - 1
i += 2
elif t == "OFFSET":
offsets[current] = [float(tokens[i + 1]), float(tokens[i + 2]), float(tokens[i + 3])]
i += 4
elif t == "CHANNELS":
n = int(tokens[i + 1])
channels[current] = tokens[i + 2:i + 2 + n]
i += 2 + n
elif t == "{":
stack.append(current)
i += 1
elif t == "}":
stack.pop()
i += 1
elif t == "End":
# End Site は末端のオフセットのみを持ち、関節ではない。
# ブロック全体(End Site { OFFSET x y z })の8トークンを読み飛ばす
i += 8
else:
i += 1
# MOTION部
fi = tokens.index("Frames:")
frames = int(tokens[fi + 1])
ft = float(tokens[fi + 4])
start = fi + 5
values = [float(v) for v in tokens[start:]]
motion = np.array(values).reshape(frames, -1)
return names, parents, offsets, channels, motion, ft
# 回転(オイラー角)を回転行列に変換
def euler_to_matrix(axis, angle_deg):
a = np.radians(angle_deg)
c, s = np.cos(a), np.sin(a)
if axis == "X":
return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
if axis == "Y":
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
# FKで各関節のグローバル位置を計算
def forward_kinematics(names, parents, offsets, channels, frame):
positions = [None] * len(names)
globals_rot = [None] * len(names)
idx = 0
for j in range(len(names)):
local_pos = np.array(offsets[j], dtype=float)
rot = np.eye(3)
for ch in channels[j]:
v = frame[idx]
idx += 1
if ch.endswith("position"):
axis = "XYZ".index(ch[0])
local_pos[axis] += v
else:
rot = rot @ euler_to_matrix(ch[0], v)
if parents[j] == -1:
globals_rot[j] = rot
positions[j] = local_pos
else:
p = parents[j]
globals_rot[j] = globals_rot[p] @ rot
positions[j] = positions[p] + globals_rot[p] @ local_pos
return positions
names, parents, offsets, channels, motion, ft = parse_bvh(BVH_PATH)
# 全フレームの関節位置を事前計算
all_positions = []
for frame in motion:
all_positions.append(forward_kinematics(names, parents, offsets, channels, frame))
lines = [[parents[j], j] for j in range(len(names)) if parents[j] != -1]
line_set = o3d.geometry.LineSet()
line_set.points = o3d.utility.Vector3dVector([p.tolist() for p in all_positions[0]])
line_set.lines = o3d.utility.Vector2iVector(lines)
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector([p.tolist() for p in all_positions[0]])
pcd.paint_uniform_color([0.8, 0.3, 0.3])
vis = o3d.visualization.Visualizer()
vis.create_window()
vis.add_geometry(line_set)
vis.add_geometry(pcd)
frame_index = 0
running = True
while running:
pts = [p.tolist() for p in all_positions[frame_index]]
line_set.points = o3d.utility.Vector3dVector(pts)
pcd.points = o3d.utility.Vector3dVector(pts)
vis.update_geometry(line_set)
vis.update_geometry(pcd)
running = vis.poll_events() # ウィンドウを閉じると False
vis.update_renderer()
time.sleep(ft)
frame_index += 1
if frame_index >= len(all_positions):
if LOOP:
frame_index = 0
else:
break
vis.destroy_window()
ヒント
幾何データを更新したら必ず update_geometry を呼ぶ(呼ばないと描画が更新されない)。ウィンドウを閉じると poll_events が False を返す。最初にTポーズ(基準姿勢)が表示され、続いて動き出す。LOOP = False にすると最終フレームで停止する。
考察ポイント
再生速度がフレーム間隔と一致するか、最終フレームで停止するかループするかを確認する。
演習4.カメラ+MediaPipeでリアルタイム3D骨格描画と2D重畳
手順
使用パッケージは mediapipe、opencv-contrib-python、open3d、numpy。姿勢推定モデルを ./pose_landmarker_lite.task へダウンロードする(ファイルが無い場合のみ)。OpenCVでカメラフレームを取得し、BGRからRGBへ変換してMediaPipe(VIDEOモード)へ渡す。world ランドマークを [x, -y, -z] に変換し、Open3D の LineSet と球で3D骨格を描画する。同時に、2DランドマークをOpenCVの映像へ重畳表示する(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)。
import os
import time
import urllib.request
import numpy as np
import cv2
import open3d as o3d
import mediapipe as mp
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task"
MODEL_PATH = os.path.join(BASE_DIR, "pose_landmarker_lite.task")
if not os.path.exists(MODEL_PATH):
urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
# MediaPipe Pose の骨の接続(主要部位)
POSE_CONNECTIONS = [
(11, 12), (11, 13), (13, 15), (12, 14), (14, 16),
(11, 23), (12, 24), (23, 24),
(23, 25), (25, 27), (24, 26), (26, 28),
]
# world ランドマークを Open3D 用座標に変換(鏡像なし)
def landmarks_to_points(landmarks):
return np.array([[lm.x, -lm.y, -lm.z] for lm in landmarks])
# PoseLandmarker(VIDEOモード)を作成
options = vision.PoseLandmarkerOptions(
base_options=mp_python.BaseOptions(model_asset_path=MODEL_PATH),
running_mode=vision.RunningMode.VIDEO,
)
landmarker = vision.PoseLandmarker.create_from_options(options)
cap = cv2.VideoCapture(0) # カメラが無い場合は "movie.mp4" に変更
# Open3D 初期化
line_set = o3d.geometry.LineSet()
line_set.lines = o3d.utility.Vector2iVector(POSE_CONNECTIONS)
pcd = o3d.geometry.PointCloud()
vis = o3d.visualization.Visualizer()
vis.create_window()
added = False
start = time.perf_counter()
running = True
while running:
ok, frame = cap.read()
if not ok:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
timestamp_ms = int((time.perf_counter() - start) * 1000)
result = landmarker.detect_for_video(mp_image, timestamp_ms)
if result.pose_world_landmarks:
pts = landmarks_to_points(result.pose_world_landmarks[0])
line_set.points = o3d.utility.Vector3dVector(pts)
pcd.points = o3d.utility.Vector3dVector(pts)
pcd.paint_uniform_color([0.8, 0.3, 0.3])
if not added:
vis.add_geometry(line_set)
vis.add_geometry(pcd)
added = True
vis.update_geometry(line_set)
vis.update_geometry(pcd)
# 2Dランドマークを映像に重畳
h, w = frame.shape[:2]
for lm in result.pose_landmarks[0]:
cv2.circle(frame, (int(lm.x * w), int(lm.y * h)), 4, (0, 255, 0), -1)
running = vis.poll_events()
vis.update_renderer()
cv2.imshow("camera", frame)
if cv2.waitKey(1) == 27: # ESC(映像ウィンドウがアクティブなとき)
break
cap.release()
cv2.destroyAllWindows()
vis.destroy_window()
ヒント
タイムスタンプは time.perf_counter で得た秒値をミリ秒へ換算した整数を用い、フレームごとに単調増加させる。終了は映像ウィンドウがアクティブな状態で ESC キーを押す。
考察ポイント
被写体の動きと3D骨格・2D重畳がリアルタイムに追従するか、左右の対応が正しいか(被写体の右手を動かすと画面の対応する側が動くか)を確認する。
演習5.カメラ+MediaPipeで円柱メッシュアバターを駆動
手順
姿勢推定モデルは演習4と同じものを ./pose_landmarker_lite.task から用いる(ファイルが無い場合のみダウンロードする)。骨を円柱メッシュ、関節を球メッシュで表す。基準となる円柱と球を1つずつ作成し、ボーン数・関節数だけ複製して各フレームで変換する。円柱は bone_transform で長さ方向(Z軸)のみを骨の長さに合わせて伸縮させ、太さ(半径)は一定に保つ。各フレームの変換は前フレームの累積を避けるため、毎回基準メッシュから計算する(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)。
import os
import time
import urllib.request
import numpy as np
import cv2
import open3d as o3d
import mediapipe as mp
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task"
MODEL_PATH = os.path.join(BASE_DIR, "pose_landmarker_lite.task")
if not os.path.exists(MODEL_PATH):
urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
POSE_CONNECTIONS = [
(11, 12), (11, 13), (13, 15), (12, 14), (14, 16),
(11, 23), (12, 24), (23, 24),
(23, 25), (25, 27), (24, 26), (26, 28),
]
JOINT_IDS = sorted({i for c in POSE_CONNECTIONS for i in c})
def landmarks_to_points(landmarks):
return np.array([[lm.x, -lm.y, -lm.z] for lm in landmarks])
# 始点 p0 から終点 p1 へ向かう円柱の変換行列を作る
# Z軸方向のみ length にスケールし、半径は一定に保つ
def bone_transform(p0, p1):
d = p1 - p0
length = np.linalg.norm(d)
T = np.eye(4)
if length < 1e-6: # 特異点:長さ0なら回転を適用しない
T[:3, 3] = p0
return T
z = d / length
# Z軸を z に合わせる回転を構築
up = np.array([0.0, 0.0, 1.0])
axis = np.cross(up, z)
axis_len = np.linalg.norm(axis)
if axis_len < 1e-6:
R = np.eye(3) if z[2] > 0 else np.diag([1.0, -1.0, -1.0])
else:
axis = axis / axis_len
angle = np.arccos(np.clip(np.dot(up, z), -1.0, 1.0))
c, s = np.cos(angle), np.sin(angle)
K = np.array([[0, -axis[2], axis[1]],
[axis[2], 0, -axis[0]],
[-axis[1], axis[0], 0]])
R = np.eye(3) + s * K + (1 - c) * (K @ K)
S = np.diag([1.0, 1.0, length]) # 長さ方向のみ伸縮、太さ一定
T[:3, :3] = R @ S
T[:3, 3] = (p0 + p1) / 2.0 # 円柱は中点に配置
return T
options = vision.PoseLandmarkerOptions(
base_options=mp_python.BaseOptions(model_asset_path=MODEL_PATH),
running_mode=vision.RunningMode.VIDEO,
)
landmarker = vision.PoseLandmarker.create_from_options(options)
cap = cv2.VideoCapture(0) # カメラが無い場合は "movie.mp4" に変更
# 基準メッシュ(高さ1の円柱を中心原点・Z軸方向に作成)
bone_base = o3d.geometry.TriangleMesh.create_cylinder(radius=0.02, height=1.0)
bone_base.compute_vertex_normals()
joint_base = o3d.geometry.TriangleMesh.create_sphere(radius=0.05)
joint_base.compute_vertex_normals()
bones = [o3d.geometry.TriangleMesh(bone_base) for _ in POSE_CONNECTIONS]
joints = [o3d.geometry.TriangleMesh(joint_base) for _ in JOINT_IDS]
for b in bones:
b.paint_uniform_color([0.4, 0.5, 0.9])
for j in joints:
j.paint_uniform_color([0.8, 0.3, 0.3])
vis = o3d.visualization.Visualizer()
vis.create_window()
opt = vis.get_render_option()
opt.background_color = np.array([0.1, 0.1, 0.1])
opt.light_on = True
opt.mesh_show_back_face = True
added = False
start = time.perf_counter()
running = True
while running:
ok, frame = cap.read()
if not ok:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
timestamp_ms = int((time.perf_counter() - start) * 1000)
result = landmarker.detect_for_video(mp_image, timestamp_ms)
if result.pose_world_landmarks:
pts = landmarks_to_points(result.pose_world_landmarks[0])
# 関節(球)を毎回基準メッシュから配置(累積を避ける)
for k, jid in enumerate(JOINT_IDS):
m = o3d.geometry.TriangleMesh(joint_base)
m.translate(pts[jid])
joints[k].vertices = m.vertices
# 骨(円柱)を毎回基準メッシュから変換
for k, (a, b) in enumerate(POSE_CONNECTIONS):
T = bone_transform(pts[a], pts[b])
m = o3d.geometry.TriangleMesh(bone_base)
m.transform(T)
bones[k].vertices = m.vertices
if not added:
for g in bones + joints:
vis.add_geometry(g)
added = True
for g in bones + joints:
g.compute_vertex_normals()
vis.update_geometry(g)
running = vis.poll_events()
vis.update_renderer()
cv2.imshow("camera", frame)
if cv2.waitKey(1) == 27: # ESC(映像ウィンドウがアクティブなとき)
break
cap.release()
cv2.destroyAllWindows()
vis.destroy_window()
ヒント
描画する関節と接続は JOINT_IDS と POSE_CONNECTIONS で限定する。終了は映像ウィンドウがアクティブな状態で ESC キーを押す。
考察ポイント
円柱の太さが骨の長さによらず一定に保たれるか、関節と骨の接続が破綻しないかを確認する。
演習6.カメラ+MediaPipeでglTF標準アバターをスキニング駆動
手順
glTFアバターを ./avatar.glb へダウンロードする(ファイルが無い場合のみ)。pygltflib でメッシュとスキニングデータ(JOINTS_0、WEIGHTS_0、inverseBindMatrices)を読み込む。ボーンの階層からグローバル変換を計算し、MediaPipeのランドマークに基づいて主要ボーンを回転させ、LBSで頂点位置を更新する。左脚のボーン(ランドマーク 23, 25, 27, 31)はアバターの左側ボーンへ、右脚のボーン(24, 26, 28, 32)は右側ボーンへ対応させる(腕の左右対応と同じ向きにそろえる)。
調整中
ヒント
操作するボーンは主要なもの(腕・脚・胴)に限る(指などの細かいボーンは対応しない)。DRIVEN のボーン名はこのアバター(CesiumMan)固有のため、別のモデルを使う場合は name_to_node で名前を確認して対応付けを修正する。バインド姿勢方向の長さがほぼ0のとき、または可視度が低いときは回転を適用せず直前の姿勢を保つ。アバターの向きがずれる場合は DEBUG を True にして軸の診断表示を有効にする(既定は False)。終了は映像ウィンドウがアクティブな状態で ESC キーを押す。
考察ポイント
被写体の動きにメッシュが追従するか、左右の対応が腕・脚で一致するか、関節付近のメッシュ変形が自然か(ウェイトの効果)を確認する。