mediapipe pose landmarker による3次元姿勢推定
【概要】MediaPipe Pose Landmarkerは単一カメラから33個の人体ランドマーク点を2D/3D座標で推定するGoogleの機械学習技術。BlazePose GHUMアーキテクチャ,リアルタイム処理(10-30ms)が特徴である。正規化画像座標と世界座標を出力する。関節角度計算が可能。フィットネス、姿勢分析、モーション解析に応用可能。

目次
第1章 基礎理論
1.1 概要
MediaPipe Pose Landmarkerは、Googleが開発したMediaPipeフレームワークにおける人体姿勢推定技術である。この技術は、単一のカメラ映像から人体の33個のランドマーク点を2次元画像座標と3次元世界座標で推定する機械学習ソリューションである。
1.2 基本仕様
- 入力形式: 単一カメラからの2次元映像(静止画、動画、リアルタイム映像)
- 出力形式: 33個のランドマーク(正規化画像座標と世界座標)
- 世界座標系: 人物の腰部中央を原点とする3次元座標(メートル単位)
1.3 技術仕様
- 姿勢検出器入力サイズ: 224×224×3
- 姿勢ランドマーク入力サイズ: 256×256×3
- アーキテクチャ: MobileNetV2ベースの軽量CNN(畳み込みニューラルネットワーク)
1.4 技術的優位点
- 2つの座標系: 正規化画像座標とメートル単位の世界座標を同時出力
- リアルタイム処理: 最適化されたBlazePoseアーキテクチャによる高速処理
- 詳細な姿勢情報: 33個のランドマーク点による包括的な人体表現
- 統合処理: 人体検出と姿勢推定の一体化アーキテクチャ
第2章 技術詳細
2.1 アーキテクチャ
MediaPipe Pose Landmarkerは、BlazePose GHUM(Generative Human Model:生成的人体モデル)アーキテクチャ(Bazarevsky et al., 2020; Grishchenko et al., 2022)に基づいて構築されている。システムは以下の2段階で動作する。
2.2 姿勢検出モデル
- 機能: 画像フレーム内の人体の存在を検出
- 入力サイズ: 224×224×3
- 処理内容: 主要な姿勢ランドマーク点の特定
2.3 姿勢ランドマークモデル
- 機能: 完全な姿勢マッピングの実行
- 入力サイズ: 256×256×3
- 処理内容: 33個の姿勢ランドマークの推定
BlazePose処理フロー
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 入力画像 │───→│ 第1段階:姿勢検出 │───→│ 人体領域抽出 │ │ (任意サイズ) │ │ (224×224×3) │ │ │ └─────────────┘ └──────────────────┘ └─────────────────┘ │ ▼ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 出力結果 │←───│ 第2段階:ランドマーク │←───│ 領域の正規化 │ │ 33個の座標 │ │ (256×256×3) │ │ │ │ ・正規化座標 │ │ │ │ │ │ ・世界座標 │ │ │ │ │ │ ・可視性 │ │ │ │ │ └─────────────┘ └──────────────────┘ └─────────────────┘ 処理時間:約10-30ms(リアルタイム処理可能)
2.4 座標出力
- 正規化画像座標: 画像内の相対位置(0.0-1.0の範囲)
- 世界座標: メートル単位の3次元座標(腰部中央を原点とする実世界座標)
2.5 ランドマーク属性
各ランドマークには以下の属性が含まれる:
- x, y, z座標: 位置情報
- visibility: フレーム内での可視性確率
- presence: フレーム内での存在確率
2.6 Z座標の特性
世界座標のZ軸は、GHUM(3次元人体形状モデル)による合成データを用いて推定される。腰部を基準とし、カメラ方向が負の値、背面方向が正の値となる。
図2:33個のランドマーク点配置
0(鼻) / \ 1(左目内) 2(左目) 3(左目外) 4(右目内) 5(右目) 6(右目外) 7(左耳) 8(右耳) 9(口左) 10(口右) 11(左肩) 12(右肩) | | 13(左肘) 14(右肘) | | 15(左手首) 16(右手首) | | 17(左小指) 19(左人差指) 18(右小指) 20(右人差指) | | | | 21(左親指) 22(右親指) 23(左腰) 24(右腰) | | 25(左膝) 26(右膝) | | 27(左足首) 28(右足首) | | 29(左かかと) 31(左足指) 30(右かかと) 32(右足指)
図3:座標系の比較
【正規化画像座標】 【世界座標】 (0,0)────────(1,0) Z軸(奥行き) │ │ ↗ │ ●(0.5,0.3) │ │ Y軸(上下) │ 肩位置 │ │ ↗ │ │ │/ │ ●(0.5,0.6) │ └────→ X軸(左右) │ 腰位置 │ 原点(0,0,0):腰部中央 │ │ (0,1)────────(1,1) 例:肩位置(0.0, 0.3, -0.1)m 腰位置(0.0, 0.0, 0.0)m 特徴: 特徴: - 0.0~1.0の範囲 - メートル単位 - 画像サイズに依存しない - 実世界の距離を表現 - 左上が原点 - 腰部中央が原点
第3章 Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
3.1 必要なライブラリのインストール,設定
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd
と入力 > 右クリック > 「管理者として実行」)。し、以下を実行する。
pip install opencv-python mediapipe matplotlib japanize-matplotlib numpy mkdir "C:\Program Files\Python312\Lib\site-packages\mediapipe\modules" mkdir "C:\Program Files\Python312\Lib\site-packages\mediapipe\modules\pose_landmark" icacls "C:\Program Files\Python312\Lib\site-packages\mediapipe\modules" /grant "%USERNAME%:(OI)(CI)F" /T icacls "C:\Program Files\Python312\Lib\site-packages\mediapipe\modules\pose_landmark" /grant "%USERNAME%:(OI)(CI)F" /T
MediaPipeを使用したリアルタイム3次元姿勢推定プログラムである。カメラまたは動画ファイルから人体の33個のランドマーク点を検出し、3次元世界座標と関節角度をリアルタイムで計算・表示する。
第4章 MediaPipe 3次元人体姿勢推定プログラム
4.1 概要
このプログラムは、2次元の動画像から人体の3次元姿勢を推定する。通常のカメラで撮影された映像から、33個の人体ランドマーク(関節や特徴点)の3次元座標を推定し、各点の可視性と存在確率を算出する。これにより、人間の姿勢や動作を立体的に理解し、関節角度などの身体情報を抽出できる。
4.2 主要技術
- MediaPipe Pose
GoogleのMediaPipeフレームワークに含まれる人体姿勢推定技術である。BlazePoseアーキテクチャを基盤とし、単一のRGB画像から33個の3Dランドマークを検出する。処理は人体領域の検出とランドマーク推定の2段階で構成され、モバイルデバイスでもリアルタイム動作が可能である [1]。
- BlazePose
MediaPipe Poseの中核となる深層学習モデルである。MobileNetV2をベースとしたエンコーダ・デコーダ構造を採用し、ヒートマップ回帰と座標回帰を組み合わせて高精度な推定を実現する。学習には合成データと実データを組み合わせた大規模データセットを使用している [2]。
4.3 参考文献
[1] C. Lugaresi et al., "MediaPipe: A Framework for Building Perception Pipelines," arXiv:1906.08172, 2019.
[2] V. Bazarevsky et al., "BlazePose: On-device Real-time Body Pose tracking," arXiv:2006.10204, 2020.
# MediaPipe 3次元人体姿勢推定プログラム
# 特徴技術名: MediaPipe Pose
# 出典: V. Bazarevsky et al., "BlazePose: On-device Real-time Body Pose tracking," arXiv:2006.10204, 2020.
# 特徴機能: 33個の3D人体ランドマークをリアルタイムで推定し、各ランドマークのvisibility(画像上での可視性)とworld座標(メートル単位の3D座標)を提供する機能
# 学習済みモデル: BlazePose(lite/full/heavy)、人体姿勢推定用事前学習済みモデル、33個の3Dランドマーク検出、MediaPipeに組み込み
# 方式設計:
# - 関連利用技術: OpenCV(動画入力処理、リアルタイム表示)、Matplotlib(3D座標プロット)、NumPy(ベクトル演算、角度計算)
# - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用)、出力: 処理結果が画像化できる場合にはOpenCV画面でリアルタイムに表示.OpenCV画面内に処理結果をテキストで表示.さらに,1秒間隔で,print()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存」したことをprint()で表示.プログラム開始時に,プログラムの概要,ユーザが行う必要がある操作(もしあれば)をprint()で表示.
# - 処理手順: 1.動画/カメラから画像フレーム取得、2.BGR→RGB色空間変換、3.MediaPipe Poseで33個のランドマーク検出、4.3D世界座標とvisibility取得、5.関節角度計算、6.骨格と角度の描画
# - 前処理、後処理: 前処理: BGR→RGB色空間変換(MediaPipeの入力要件)、後処理: 世界座標系での3D座標正規化(ヒップ中心を原点とした相対座標)
# - 追加処理: DirectShowバックエンド(CAP_DSHOW)によるWindows環境でのカメラ遅延削減、バッファサイズ1設定による最新フレーム取得
# - 調整を必要とする設定値: MIN_DETECTION_CONFIDENCE(0.0-1.0):初期検出の信頼度閾値、人物検出の感度を制御。MIN_TRACKING_CONFIDENCE(0.0-1.0):トラッキングの信頼度閾値、動作の滑らかさを制御
# 将来方策: 検出された人物の動きの速さを分析し、動的にMIN_TRACKING_CONFIDENCEを調整する機能(速い動きでは低く、遅い動きでは高く設定)
# その他の重要事項: Windows環境専用(CAP_DSHOW使用)、's'キーで3Dプロット表示、'q'キーで終了
# 前準備:
# - pip install opencv-python mediapipe matplotlib japanize-matplotlib numpy
import os
import urllib.request
import shutil
# MediaPipeモデルファイルの事前ダウンロード
def download_mediapipe_models():
"""MediaPipeのモデルファイルをローカルにダウンロード"""
models_dir = os.path.join(os.getcwd(), 'mediapipe_models')
pose_dir = os.path.join(models_dir, 'pose_landmark')
# ディレクトリ作成
os.makedirs(pose_dir, exist_ok=True)
# モデルファイルのURL
model_urls = {
'pose_landmark_lite.tflite': 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task',
'pose_landmark_full.tflite': 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/1/pose_landmarker_full.task',
'pose_landmark_heavy.tflite': 'https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task'
}
# MediaPipeの内部モデルパスを確認
try:
import mediapipe as mp
mediapipe_path = os.path.dirname(mp.__file__)
target_dir = os.path.join(mediapipe_path, 'modules', 'pose_landmark')
# 既存のモデルファイルをコピー
for model_name in ['pose_landmark_lite.tflite', 'pose_landmark_full.tflite', 'pose_landmark_heavy.tflite']:
src_path = os.path.join(target_dir, model_name)
dst_path = os.path.join(pose_dir, model_name)
if os.path.exists(src_path) and not os.path.exists(dst_path):
shutil.copy2(src_path, dst_path)
print(f'モデルファイルをコピーしました: {model_name}')
elif not os.path.exists(dst_path):
# ダウンロードを試みる
print(f'モデルファイルをダウンロード中: {model_name}')
try:
# 簡易的なダウンロード(実際のURLは異なる可能性があります)
url = f'https://storage.googleapis.com/mediapipe-assets/pose_landmark_{model_name.split("_")[2].split(".")[0]}.tflite'
urllib.request.urlretrieve(url, dst_path)
print(f'ダウンロード完了: {model_name}')
except:
print(f'ダウンロードに失敗しました: {model_name}')
except Exception as e:
print(f'モデルファイルの準備中にエラーが発生しました: {e}')
return models_dir
# モデルファイルの準備
print('MediaPipeモデルファイルを準備中...')
models_dir = download_mediapipe_models()
import cv2
import tkinter as tk
from tkinter import filedialog
import mediapipe as mp
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from mpl_toolkits.mplot3d import Axes3D
import time
import sys
import io
# Windows文字エンコーディング設定
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)
# 定数定義
MIN_DETECTION_CONFIDENCE = 0.7 # 検出信頼度閾値
MIN_TRACKING_CONFIDENCE = 0.5 # トラッキング信頼度閾値
# 表示設定
ARROW_LENGTH = 30 # 画面外ランドマーク矢印の長さ(ピクセル)
FONT_SIZE = 0.6 # 角度表示のフォントサイズ
FONT_THICKNESS = 2 # フォントの太さ
OUTPUT_INTERVAL = 1.0 # コンソール出力間隔(秒)
# 3Dプロット設定
PLOT_RANGE = 0.5 # 3Dプロットの表示範囲(メートル)
PLOT_ELEV = 10 # 3Dプロットの仰角
PLOT_AZIM = -90 # 3Dプロットの方位角
# 関節角度定義(名前、始点、頂点、終点のランドマークインデックス)
JOINT_ANGLES = [
('左肘', 11, 13, 15),
('右肘', 12, 14, 16),
('左膝', 23, 25, 27),
('右膝', 24, 26, 28)
]
# 主要ランドマーク定義(コンソール出力用)
MAIN_LANDMARKS = [
(11, '左肩'),
(12, '右肩'),
(15, '左手首'),
(16, '右手首')
]
KEYPOINTS = {
0: '鼻', 1: '左目内側', 2: '左目', 3: '左目外側', 4: '右目内側', 5: '右目', 6: '右目外側',
7: '左耳', 8: '右耳', 9: '口左', 10: '口右', 11: '左肩', 12: '右肩', 13: '左肘', 14: '右肘',
15: '左手首', 16: '右手首', 17: '左小指', 18: '右小指', 19: '左指先', 20: '右指先',
21: '左親指', 22: '右親指', 23: '左腰', 24: '右腰', 25: '左膝', 26: '右膝',
27: '左足首', 28: '右足首', 29: '左踵', 30: '右踵', 31: '左足先', 32: '右足先'
}
# グローバル変数
output_results = []
last_output_time = 0
start_time = 0
frame_num = 0
pose = None
show_3d_plot = False
def calculate_angle_3d(p1, p2, p3):
"""3D座標から角度を計算(p2が頂点)"""
v1 = np.array([p1.x - p2.x, p1.y - p2.y, p1.z - p2.z])
v2 = np.array([p3.x - p2.x, p3.y - p2.y, p3.z - p2.z])
if np.linalg.norm(v1) == 0 or np.linalg.norm(v2) == 0:
return 0.0
cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
return np.degrees(angle)
def video_processing(frame):
"""動画フレーム処理関数"""
global last_output_time, output_results, frame_num, pose, show_3d_plot
frame_num += 1
current_time = time.time()
# MediaPipe処理
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = pose.process(rgb_frame)
# 指定間隔での出力
if current_time - last_output_time >= OUTPUT_INTERVAL and results.pose_world_landmarks:
last_output_time = current_time
landmarks_world = results.pose_world_landmarks.landmark
output_text = []
output_text.append('=' * 70)
output_text.append(f'【時刻 {current_time - start_time:.1f}秒】フレーム {frame_num}')
output_text.append('【3D世界座標(メートル単位)】')
output_text.append('-' * 70)
# 主要ランドマーク座標出力
for idx, name in MAIN_LANDMARKS:
landmark = landmarks_world[idx]
output_text.append(f'{idx}:{name:>6}: X={landmark.x:>7.3f}, Y={landmark.y:>7.3f}, Z={landmark.z:>7.3f}')
# 関節角度計算・出力
output_text.append('-' * 70)
output_text.append('【関節角度】')
for joint_name, idx1, idx2, idx3 in JOINT_ANGLES:
angle = calculate_angle_3d(landmarks_world[idx1], landmarks_world[idx2], landmarks_world[idx3])
output_text.append(f'{joint_name}: {angle:>6.1f}度')
for line in output_text:
print(line)
output_results.extend(output_text)
# 描画処理
if results.pose_landmarks:
landmarks_2d = results.pose_landmarks.landmark
h, w = frame.shape[:2]
# 画面外ランドマークの矢印表示
for idx, landmark in enumerate(landmarks_2d):
if landmark.x < 0 or landmark.x > 1 or landmark.y < 0 or landmark.y > 1:
x_clipped = int(np.clip(landmark.x, 0, 1) * w)
y_clipped = int(np.clip(landmark.y, 0, 1) * h)
# 矢印の始点を計算
if landmark.y > 1: # 下
arrow_start = (x_clipped, y_clipped - ARROW_LENGTH)
elif landmark.y < 0: # 上
arrow_start = (x_clipped, y_clipped + ARROW_LENGTH)
elif landmark.x > 1: # 右
arrow_start = (x_clipped - ARROW_LENGTH, y_clipped)
else: # 左
arrow_start = (x_clipped + ARROW_LENGTH, y_clipped)
cv2.arrowedLine(frame, arrow_start, (x_clipped, y_clipped), (0, 0, 255), 3)
# 関節角度の表示
if results.pose_world_landmarks:
landmarks_world = results.pose_world_landmarks.landmark
# 肘の角度表示(左右)
for side, elbow_idx, shoulder_idx, wrist_idx in [('左', 13, 11, 15), ('右', 14, 12, 16)]:
if landmarks_2d[elbow_idx].visibility > 0.5:
elbow_x = int(landmarks_2d[elbow_idx].x * w)
elbow_y = int(landmarks_2d[elbow_idx].y * h)
angle = calculate_angle_3d(
landmarks_world[shoulder_idx],
landmarks_world[elbow_idx],
landmarks_world[wrist_idx]
)
text_x = min(max(elbow_x + 10, 50), w - 50)
text_y = min(max(elbow_y - 10, 30), h - 30)
cv2.putText(frame, f'{angle:.0f}', (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, FONT_SIZE, (0, 255, 0), FONT_THICKNESS)
# 骨格描画
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_drawing.draw_landmarks(
frame,
results.pose_landmarks,
mp.solutions.pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style()
)
# 3Dプロット表示
if show_3d_plot and results.pose_world_landmarks:
show_3d_plot = False
landmarks_world = results.pose_world_landmarks.landmark
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(111, projection='3d')
x_coords = []
y_coords = []
z_coords = []
colors = []
sizes = []
for i in range(len(landmarks_world)):
x = landmarks_world[i].x
y = landmarks_world[i].z # Z軸を表示用Y軸に
z = -landmarks_world[i].y # Y軸を表示用Z軸に(反転)
x_coords.append(x)
y_coords.append(y)
z_coords.append(z)
visibility = getattr(landmarks_world[i], 'visibility', 0.0)
if visibility > 0.7:
colors.append('red')
sizes.append(60)
elif visibility > 0.3:
colors.append('orange')
sizes.append(40)
else:
colors.append('gray')
sizes.append(20)
ax.scatter(x_coords, y_coords, z_coords, c=colors, s=sizes, alpha=0.8)
# 接続線の描画
connections = mp.solutions.pose.POSE_CONNECTIONS
for connection in connections:
start_idx, end_idx = connection
if start_idx < len(landmarks_world) and end_idx < len(landmarks_world):
start_vis = getattr(landmarks_world[start_idx], 'visibility', 0.0)
end_vis = getattr(landmarks_world[end_idx], 'visibility', 0.0)
if start_vis > 0.3 and end_vis > 0.3:
ax.plot([x_coords[start_idx], x_coords[end_idx]],
[y_coords[start_idx], y_coords[end_idx]],
[z_coords[start_idx], z_coords[end_idx]],
'b-', linewidth=1, alpha=0.7)
ax.set_xlabel('X軸(左右)[m]')
ax.set_ylabel('Y軸(奥行き)[m]')
ax.set_zlabel('Z軸(上下)[m]')
ax.set_title('3次元姿勢推定結果(世界座標系)')
ax.set_xlim([-PLOT_RANGE, PLOT_RANGE])
ax.set_ylim([-PLOT_RANGE, PLOT_RANGE])
ax.set_zlim([-PLOT_RANGE, PLOT_RANGE])
ax.view_init(elev=PLOT_ELEV, azim=PLOT_AZIM)
plt.show()
return frame
# プログラム開始
print('=' * 60)
print('MediaPipe 3次元人体姿勢推定プログラム')
print('=' * 60)
print('【プログラム概要】')
print('MediaPipe Poseを使用して33個の人体ランドマークを検出し、')
print('3D座標と関節角度を計算・表示します。')
print('')
print('【操作方法】')
print("- 'q'キー: プログラム終了")
print("- 's'キー: 3Dプロット表示")
print('-' * 60)
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')
choice = input('選択: ')
temp_file = None
if choice == '0':
root = tk.Tk()
root.withdraw()
path = filedialog.askopenfilename()
if not path:
exit()
cap = cv2.VideoCapture(path)
elif choice == '1':
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
elif choice == '2':
# サンプル動画ダウンロード
url = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
filename = 'vtest.avi'
try:
urllib.request.urlretrieve(url, filename)
temp_file = filename
cap = cv2.VideoCapture(filename)
except Exception as e:
print(f'動画のダウンロードに失敗しました: {url}')
print(f'エラー: {e}')
exit()
else:
print('無効な選択です')
exit()
# MediaPipe初期化
mp_pose = mp.solutions.pose
# MODEL_COMPLEXITYの自動選択
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(cap.get(cv2.CAP_PROP_FPS))
if frame_width * frame_height > 1920 * 1080 or fps > 60:
MODEL_COMPLEXITY = 2 # heavy
elif frame_width * frame_height > 1280 * 720 or fps > 30:
MODEL_COMPLEXITY = 1 # full
else:
MODEL_COMPLEXITY = 0 # lite
print(f'MODEL_COMPLEXITY: {MODEL_COMPLEXITY} (解像度: {frame_width}x{frame_height}, FPS: {fps})')
# MediaPipe Poseの初期化を試みる
try:
pose = mp_pose.Pose(
static_image_mode=False,
model_complexity=MODEL_COMPLEXITY,
smooth_landmarks=True,
enable_segmentation=False,
min_detection_confidence=MIN_DETECTION_CONFIDENCE,
min_tracking_confidence=MIN_TRACKING_CONFIDENCE
)
except PermissionError as e:
cap.release()
if temp_file:
os.remove(temp_file)
exit()
start_time = time.time()
# メイン処理
try:
while True:
cap.grab()
ret, frame = cap.retrieve()
if not ret:
break
processed_frame = video_processing(frame)
cv2.imshow('Video', processed_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s'):
show_3d_plot = True
finally:
cap.release()
cv2.destroyAllWindows()
pose.close()
if temp_file:
os.remove(temp_file)
# 結果保存
if output_results:
with open('result.txt', 'w', encoding='utf-8') as f:
for line in output_results:
f.write(line + '\n')
print('result.txtに保存しました')
4.4 使用方法
- プログラムを実行すると入力ソース選択画面が表示される
- カメラ(1)または動画ファイル(2)を選択する
- 動画ファイル選択時はファイル選択ダイアログが開く
- 人物がフレーム内に映ると自動的に姿勢検出が開始される
- コンソールに3次元座標と関節角度がリアルタイム表示される
- 'q'キーで終了、動画の場合はスペースキーで一時停止・再開
第5章 応用・実験
5.1 実験手法
モデル複雑度による比較実験
プログラム内のMODEL_COMPLEXITY
定数を変更することで、3つの異なる精度レベルを比較できる:
MODEL_COMPLEXITY = 0
:軽量版(高速、基本精度)MODEL_COMPLEXITY = 1
:標準版(バランス型)MODEL_COMPLEXITY = 2
:高精度版(最高精度、やや低速)
検出感度による実験
以下の定数を調整して検出性能を比較できる:
MIN_DETECTION_CONFIDENCE
:初回検出の信頼度閾値(0.3-0.9)MIN_TRACKING_CONFIDENCE
:追跡継続の信頼度閾値(0.3-0.9)
図4:関節角度計算の幾何学
【肘関節角度の計算例】 P1(肩) ● │\ │ \ vector1 = P1 - P2 │ \ │ \ │ ●P2(肘) ← 角度計算の中心点 │ / │ / vector2 = P3 - P2 │ / │/ ● P3(手首) 計算式: 1. vector1 = P1 - P2 (肩から肘へのベクトル) 2. vector2 = P3 - P2 (手首から肘へのベクトル) 3. cos(θ) = (vector1 · vector2) / (|vector1| × |vector2|) 4. θ = arccos(cos(θ)) × 180/π [度] 例: P1(肩) = (0.0, 0.3, -0.1) P2(肘) = (0.2, 0.1, -0.1) P3(手首) = (0.3, 0.0, -0.1) → 肘関節角度 = 約90度
5.2 姿勢・動作分析実験
- 静止姿勢の分析:直立、座位、横臥など異なる姿勢での座標値変化を観察
- 動作の数値化:腕の上げ下げ、膝の屈伸で角度変化をリアルタイム確認
- 左右対称性の検証:左右の関節角度を比較して身体バランスを評価
- 運動解析:スクワット、腕立て伏せなどの運動フォームを角度で定量評価
- 姿勢矯正:正しい姿勢と悪い姿勢の座標・角度の違いを数値で比較
- 動画解析:スポーツ動作や舞踊の動画を読み込んで詳細な動作分析を実施
- 複数人比較:異なる人物の同一動作における座標・角度パターンの違いを調査
5.3 発見できる現象
- 人間が意識していない微細な身体の揺れが座標値に現れる現象
- 関節角度の変化パターンから個人の癖や特徴を発見
- 3次元座標のZ値(奥行き)による立体的な姿勢把握
- リアルタイム処理による即座のフィードバック
参考文献
原論文
- Bazarevsky, V., Grishchenko, I., Raveendran, K., Zhu, T., Zhang, F., & Grundmann, M. (2020). BlazePose: On-device Real-time Body Pose tracking. arXiv preprint arXiv:2006.10204.
- Grishchenko, I., Bazarevsky, V., Bazavan, E. G., Li, N., Zanfir, A., Zanfir, M., ... & Sminchisescu, C. (2022). BlazePose GHUM Holistic: Real-time 3D Human Landmarks and Pose Estimation. arXiv preprint arXiv:2206.11678.
公式ドキュメント
- MediaPipe Solutions: https://developers.google.com/mediapipe/solutions
- MediaPipe Pose Landmarker: https://developers.google.com/mediapipe/solutions/vision/pose_landmarker
発展的学習資料
- OpenCV公式ドキュメント(コンピュータビジョン基礎): https://docs.opencv.org/
- NumPy公式ドキュメント(数値計算): https://numpy.org/doc/
- 3次元座標変換の数学的基礎に関する線形代数教材
- コンピュータビジョン:アルゴリズムと応用(Richard Szeliski著)
専門用語集
ランドマーク点(Landmark Point)
人体の特定部位を表す座標点。MediaPipe Pose Landmarkerでは33個の点で人体の主要関節と顔の特徴点を表現する。
正規化座標(Normalized Coordinates)
画像サイズに依存しない相対座標系。画像の幅と高さを1.0として、0.0から1.0の範囲で位置を表現する座標系。
BlazePoseアーキテクチャ(BlazePose Architecture)
Googleが開発した軽量な人体姿勢推定アーキテクチャ。2段階の処理(人体検出→姿勢推定)により高速かつ高精度な推定を実現する。
GHUM(Generative Human Model)
3次元人体形状モデルの一種。統計的人体モデルとして、多様な人体形状と姿勢を数学的に表現するモデル。
世界座標(World Coordinates)
実世界の3次元空間における座標系。MediaPipeでは人物の腰部中央を原点とし、メートル単位で表現される。
可視性(Visibility)
ランドマーク点がカメラから見えている確率を示す値。0.0(完全に隠れている)から1.0(完全に見えている)の範囲で表現される。
存在確率(Presence)
ランドマーク点がフレーム内に存在する確率を示す値。画像範囲外にある場合は低い値となる。
高度な機能実装
Pythonプログラム
# MediaPipe 3D姿勢推定プログラム
# Webカメラ・動画ファイル入力による3D姿勢推定
# 論文: "BlazePose: On-device Real-time Body Pose tracking" (CVPR 2020)
# GitHub: https://github.com/google/mediapipe
# 特徴: MediaPipeは、リアルタイム3D姿勢推定を実現するGoogleのライブラリ
# 軽量で高速(30FPS)、33個の3D関節点を検出、豊富な関節角度計算、Windows環境対応
# 学習済みモデル: BlazePoseモデル - モバイル向け最適化された軽量モデル、高精度姿勢推定
# 前準備: pip install mediapipe opencv-python numpy matplotlib japanize-matplotlib
# (管理者権限のコマンドプロンプトで実行)
import math
import tkinter as tk
from tkinter import filedialog
import cv2
import mediapipe as mp
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from mpl_toolkits.mplot3d import Axes3D
# 定数定義
MODEL_COMPLEXITY = 2
MIN_DETECTION_CONFIDENCE = 0.7
MIN_TRACKING_CONFIDENCE = 0.5
VISIBILITY_THRESHOLD = 0.7
# 全33個のランドマーク定義(MediaPipe公式準拠)
KEYPOINTS = {
0: "鼻", 1: "左目内側", 2: "左目", 3: "左目外側", 4: "右目内側", 5: "右目", 6: "右目外側",
7: "左耳", 8: "右耳", 9: "口左", 10: "口右", 11: "左肩", 12: "右肩", 13: "左肘", 14: "右肘",
15: "左手首", 16: "右手首", 17: "左小指", 18: "右小指", 19: "左指先", 20: "右指先",
21: "左親指", 22: "右親指", 23: "左腰", 24: "右腰", 25: "左膝", 26: "右膝",
27: "左足首", 28: "右足首", 29: "左踵", 30: "右踵", 31: "左足先", 32: "右足先"
}
# 主要ランドマークインデックス
IMPORTANT_LANDMARKS = [0, 11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28]
# 初期化処理
# MediaPipe初期化
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
# 入力ソース選択処理
print("=" * 50)
print("MediaPipe 3次元姿勢推定")
print("=" * 50)
print("入力ソース:")
print("1. カメラ")
print("2. 動画ファイル")
print("-" * 50)
while True:
choice = input("選択 (1 or 2): ").strip()
if choice in ["1", "2"]:
input_source = "camera" if choice == "1" else "video"
break
print("1 または 2 を入力してください")
# ビデオキャプチャ設定処理
if input_source == "camera":
# カメラ初期化(DirectShowバックエンド使用)
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("カメラを起動中...")
else:
root = tk.Tk()
root.withdraw()
video_path = filedialog.askopenfilename(
title="動画ファイルを選択",
filetypes=[("動画ファイル", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv")]
)
root.destroy()
cap = cv2.VideoCapture(video_path)
print(f"動画読み込み: {video_path}")
# 動画情報表示処理
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if input_source == "video":
print(f"FPS: {fps:.1f}, フレーム数: {frame_count}フレーム")
print("操作: 'q'=終了, 's'=3Dプロット表示, スペース=一時停止/再開")
print("-" * 50)
# Pose初期化処理
pose = mp_pose.Pose(
static_image_mode=False,
model_complexity=MODEL_COMPLEXITY,
enable_segmentation=False,
min_detection_confidence=MIN_DETECTION_CONFIDENCE,
min_tracking_confidence=MIN_TRACKING_CONFIDENCE
)
frame_num = 0
# メイン処理
while cap.isOpened():
if input_source == "camera":
# バッファをクリア(最新フレームのみ取得)
cap.grab()
ret, frame = cap.retrieve()
else:
ret, frame = cap.read()
if not ret:
if input_source == "video":
print("動画終了")
break
frame_num += 1
# RGB変換と姿勢推定
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
rgb_frame.flags.writeable = False
results = pose.process(rgb_frame)
frame.flags.writeable = True
# 結果出力
# 3D座標・関節角度表示
if results.pose_world_landmarks:
landmarks = results.pose_world_landmarks.landmark
# 座標表示(主要ランドマーク)
print("=" * 60)
if input_source == "video":
print(f"【フレーム {frame_num}】")
print("【3次元座標(主要ランドマーク)】")
for idx in IMPORTANT_LANDMARKS:
name = KEYPOINTS[idx]
x, y, z = landmarks[idx].x, landmarks[idx].y, landmarks[idx].z
visibility = getattr(landmarks[idx], 'visibility', 0.0)
print(f"{name:>6}: X={x:>7.3f}m, Y={y:>7.3f}m, Z={z:>7.3f}m (vis:{visibility:.3f})")
# 関節角度計算
print("\n【関節角度】")
angles = {}
# 各関節の角度計算
joint_configs = [
([11, 13, 15], "左肘"),
([12, 14, 16], "右肘"),
([23, 25, 27], "左膝"),
([24, 26, 28], "右膝")
]
for indices, name in joint_configs:
visibility_ok = all(getattr(landmarks[idx], 'visibility', 0.0) > VISIBILITY_THRESHOLD for idx in indices)
if visibility_ok:
p1 = np.array([landmarks[indices[0]].x, landmarks[indices[0]].y, landmarks[indices[0]].z])
p2 = np.array([landmarks[indices[1]].x, landmarks[indices[1]].y, landmarks[indices[1]].z])
p3 = np.array([landmarks[indices[2]].x, landmarks[indices[2]].y, landmarks[indices[2]].z])
v1 = p1 - p2
v2 = p3 - p2
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 > 0 and norm2 > 0:
cos_angle = np.dot(v1, v2) / (norm1 * norm2)
cos_angle = np.clip(cos_angle, -1.0, 1.0)
angle = math.degrees(math.acos(cos_angle))
angles[name] = angle
print(f"{name}: {angle:.1f}度")
# 肩角度計算(胴体中心基準)
left_hip_vis = getattr(landmarks[23], 'visibility', 0.0) > VISIBILITY_THRESHOLD
right_hip_vis = getattr(landmarks[24], 'visibility', 0.0) > VISIBILITY_THRESHOLD
if left_hip_vis and right_hip_vis:
left_hip = np.array([landmarks[23].x, landmarks[23].y, landmarks[23].z])
right_hip = np.array([landmarks[24].x, landmarks[24].y, landmarks[24].z])
body_center = (left_hip + right_hip) / 2
shoulder_configs = [
([13, 11], "左肩"),
([14, 12], "右肩")
]
for indices, name in shoulder_configs:
visibility_ok = all(getattr(landmarks[idx], 'visibility', 0.0) > VISIBILITY_THRESHOLD for idx in indices)
if visibility_ok:
p1 = np.array([landmarks[indices[0]].x, landmarks[indices[0]].y, landmarks[indices[0]].z])
p2 = np.array([landmarks[indices[1]].x, landmarks[indices[1]].y, landmarks[indices[1]].z])
v1 = p1 - p2
v2 = body_center - p2
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 > 0 and norm2 > 0:
cos_angle = np.dot(v1, v2) / (norm1 * norm2)
cos_angle = np.clip(cos_angle, -1.0, 1.0)
angle = math.degrees(math.acos(cos_angle))
angles[name] = angle
print(f"{name}: {angle:.1f}度")
# 結果解釈表示
if angles:
print("\n【解釈】")
print("・肘角度: 180度=完全伸展, 0度=完全屈曲")
print("・膝角度: 180度=完全伸展, 0度=完全屈曲")
print("・肩角度: 肘と胴体中心の角度(腕の挙上度)")
print(f"・信頼度閾値: {VISIBILITY_THRESHOLD} 以上で角度計算")
else:
print("検出精度不足のため角度計算を実行できませんでした")
# 画像表示処理
# 骨格描画
if results.pose_landmarks:
mp_drawing.draw_landmarks(
frame,
results.pose_landmarks,
mp_pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style()
)
# 結果表示
title = f"MediaPipe 3D姿勢推定 ({'カメラ' if input_source == 'camera' else '動画'})"
if input_source == "video":
title += f" - {frame_num}/{frame_count}"
cv2.imshow(title, frame)
# キー入力処理
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s') and results.pose_world_landmarks:
# 3次元プロット作成
landmarks = results.pose_world_landmarks.landmark
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(111, projection='3d')
# 座標とスタイル情報を準備
x_coords, y_coords, z_coords, colors, sizes = [], [], [], [], []
for i in range(len(landmarks)):
# 座標変換:直感的な軸配置
x = landmarks[i].x # X軸(左右)
y = landmarks[i].z # Y軸(奥行き)MediaPipeのZ座標を使用
z = -landmarks[i].y # Z軸(上下)MediaPipeのY座標を使用
x_coords.append(x)
y_coords.append(y)
z_coords.append(z)
# 可視性に応じた色分けとサイズ設定
visibility = getattr(landmarks[i], 'visibility', 0.0)
if visibility > 0.7:
colors.append('red')
sizes.append(60)
elif visibility > 0.3:
colors.append('orange')
sizes.append(40)
else:
colors.append('gray')
sizes.append(20)
# 座標プロット
ax.scatter(x_coords, y_coords, z_coords, c=colors, s=sizes, alpha=0.8)
# MediaPipe標準接続線を描画
for connection in mp_pose.POSE_CONNECTIONS:
start_idx, end_idx = connection
if start_idx < len(landmarks) and end_idx < len(landmarks):
start_vis = getattr(landmarks[start_idx], 'visibility', 0.0)
end_vis = getattr(landmarks[end_idx], 'visibility', 0.0)
if start_vis > 0.3 and end_vis > 0.3:
ax.plot([x_coords[start_idx], x_coords[end_idx]],
[y_coords[start_idx], y_coords[end_idx]],
[z_coords[start_idx], z_coords[end_idx]],
'b-', linewidth=1, alpha=0.7)
# 軸設定
ax.set_xlabel('X軸(左右)')
ax.set_ylabel('Y軸(奥行き)')
ax.set_zlabel('Z軸(上下)')
ax.set_title('3次元姿勢推定結果(直感的な軸配置)')
# 表示範囲と視点設定
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.view_init(elev=10, azim=-90)
plt.show()
elif key == ord(' ') and input_source == "video":
print("一時停止 (スペース=再開, q=終了)")
while True:
key = cv2.waitKey(0) & 0xFF
if key == ord(' '):
break
elif key == ord('q'):
cap.release()
cv2.destroyAllWindows()
pose.close()
exit()
# リソース解放処理
cap.release()
cv2.destroyAllWindows()
pose.close()
概要
MediaPipeを使用した3次元姿勢推定プログラムである。リアルタイムでカメラ映像から人体の3D座標を取得し、関節角度の計算と3D可視化を行う。AI技術による人体認識の精度と、実際の3次元空間での姿勢分析を体験できる。
事前準備
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd
と入力 > 右クリック > 「管理者として実行」)し、以下を実行する:
winget install --scope machine --id Python.Python.3.12 -e --silent
pip install opencv-python mediapipe numpy matplotlib
使用方法
- プログラムを実行すると、Webカメラが起動し、リアルタイムで姿勢推定が開始される
- 10フレームごとに主要関節の3D座標がコンソールに表示される
- 50フレームごとに3D可視化画像が自動保存される
- 'q'キーで終了する
モデル選択・実験要素
model_complexity設定:
- 0: 軽量版(高速処理)
- 1: 標準版(バランス型)
- 2: 高精度版(処理重い)
検出精度調整:
- min_detection_confidence: 0.5~0.9(初回検出の閾値)
- min_tracking_confidence: 0.5~0.9(追跡継続の閾値)
実験のアイデア
3D座標の活用実験:
- 異なるポーズでの3D座標変化の観察
- 関節角度の変化による動作分析
- 左右の対称性比較
精度検証実験:
- model_complexityの違いによる精度比較
- 照明条件変更での認識精度評価
- 複数人同時認識の限界確認
応用実験:
- 運動フォームの3D分析
- リハビリ動作の角度測定
- ダンス動作の3次元記録