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 基本仕様

1.3 技術仕様

1.4 技術的優位点

第2章 技術詳細

2.1 アーキテクチャ

MediaPipe Pose Landmarkerは、BlazePose GHUM(Generative Human Model:生成的人体モデル)アーキテクチャ(Bazarevsky et al., 2020; Grishchenko et al., 2022)に基づいて構築されている。システムは以下の2段階で動作する。

2.2 姿勢検出モデル

2.3 姿勢ランドマークモデル

BlazePose処理フロー

┌─────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  入力画像   │───→│  第1段階:姿勢検出  │───→│   人体領域抽出   │
│ (任意サイズ) │    │    (224×224×3)    │    │                │
└─────────────┘    └──────────────────┘    └─────────────────┘
                                                      │
                                                      ▼
┌─────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   出力結果   │←───│ 第2段階:ランドマーク │←───│   領域の正規化   │
│  33個の座標  │    │    (256×256×3)    │    │                │
│ ・正規化座標  │    │                  │    │                │
│ ・世界座標   │    │                  │    │                │
│ ・可視性     │    │                  │    │                │
└─────────────┘    └──────────────────┘    └─────────────────┘

処理時間:約10-30ms(リアルタイム処理可能)

2.4 座標出力

2.5 ランドマーク属性

各ランドマークには以下の属性が含まれる:

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 主要技術

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. カメラ(1)または動画ファイル(2)を選択する
  3. 動画ファイル選択時はファイル選択ダイアログが開く
  4. 人物がフレーム内に映ると自動的に姿勢検出が開始される
  5. コンソールに3次元座標と関節角度がリアルタイム表示される
  6. 'q'キーで終了、動画の場合はスペースキーで一時停止・再開

第5章 応用・実験

5.1 実験手法

モデル複雑度による比較実験

プログラム内のMODEL_COMPLEXITY定数を変更することで、3つの異なる精度レベルを比較できる:

検出感度による実験

以下の定数を調整して検出性能を比較できる:

図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 発見できる現象

参考文献

原論文

公式ドキュメント

発展的学習資料

専門用語集

ランドマーク点(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

使用方法

  1. プログラムを実行すると、Webカメラが起動し、リアルタイムで姿勢推定が開始される
  2. 10フレームごとに主要関節の3D座標がコンソールに表示される
  3. 50フレームごとに3D可視化画像が自動保存される
  4. 'q'キーで終了する

モデル選択・実験要素

model_complexity設定:

検出精度調整:

実験のアイデア

3D座標の活用実験:

精度検証実験:

応用実験: