MediaPipe BlazePose による人間の無意識の姿勢からの感情予測(ソースコードと実行結果)

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/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install mediapipe opencv-python numpy

MediaPipe BlazePose による人間の無意識の姿勢からの感情予測プログラム

概要

画像から情報を取得し、その内容を自然言語で理解・説明する能力を実現。このプログラムは、画像内容の視覚的理解と自然言語による説明を行う。カメラや動画から取得した画像に含まれる場所、物体、文字情報、状況などを認識し、それらを日本語で記述する。

主要技術

参考文献

[1] Liu, H., Li, C., Li, Y., & Lee, Y. J. (2024). Improved baselines with visual instruction tuning. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 26296-26306).

[2] Radford, A., Kim, J. W., Hallacy, C., Ramesh, A., Goh, G., Agarwal, S., ... & Sutskever, I. (2021). Learning transferable visual models from natural language supervision. In International Conference on Machine Learning (pp. 8748-8763). PMLR.


# MediaPipe BlazePose による人間の無意識の姿勢からの感情予測システム
# 特徴技術名: MediaPipe BlazePose
# 出典: Bazarevsky, V., Grishchenko, I., Raveendran, K., Zhu, T., Grundmann, M., & Kartynnik, Y. (2020). BlazePose: On-device real-time body pose tracking with MediaPipe. Presented at CV4ARVR workshop at CVPR 2020.
# 特徴機能: 単一のRGBビデオフレームから33個の3次元ランドマークをリアルタイムで高精度推定する機能。従来の17ランドマークを大幅に上回る検出点数により,特にフィットネスアプリケーションにおいて詳細な姿勢解析を可能とする。
# 学習済みモデル: MediaPipe BlazePose GHUM 3D - Googleが提供するオープンソースの3D人体姿勢推定モデル。lite,full,heavyの3つのバリエーションを提供し,精度と処理速度のトレードオフに対応。モバイルデバイスでのリアルタイム推論が可能。URL: https://github.com/google-ai-edge/mediapipe
# 方式設計:
#   関連利用技術: OpenCV - 画像・動画処理およびカメラ入力処理, NumPy - 数値計算およびランドマーク座標処理
#   入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用) 出力: リアルタイム3D姿勢推定結果をOpenCV画面で表示,1秒間隔でprint()出力,プログラム終了時にresult.txtに保存
#   処理手順: 1.MediaPipe BlazePoseによる33個3Dランドマーク抽出 2.リアルタイム結果表示
#   前処理,後処理: 前処理: 入力画像の正規化 後処理: 結果のファイル出力
#   追加処理: なし
#   調整を必要とする設定値: detection_confidence: MediaPipe姿勢検出の信頼度閾値(デフォルト0.5), tracking_confidence: MediaPipe追跡の信頼度閾値(デフォルト0.5)
# 将来方策: なし
# その他の重要事項: なし
# 前準備: pip install mediapipe opencv-python numpy

import cv2
import mediapipe as mp
import numpy as np
import tkinter as tk
from tkinter import filedialog
import os
import urllib.request
import time
import math

# MediaPipe設定
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils

# グローバル変数
pose = mp_pose.Pose(
    static_image_mode=False,
    model_complexity=1,
    enable_segmentation=False,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5
)

results_log = []
last_print_time = 0
base_y = None  # 基準Y座標
base_z = None  # 基準Z座標

# 33個の特徴点の定義
POSE_LANDMARKS = {
    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: "右足指先"
}

# 特徴点の色設定(BGR形式)
LANDMARK_COLORS = {
    # 顔関連(赤系)
    0: (0, 0, 255), 1: (0, 50, 255), 2: (0, 100, 255), 3: (0, 150, 255),
    4: (0, 50, 255), 5: (0, 100, 255), 6: (0, 150, 255),
    7: (0, 200, 255), 8: (0, 200, 255), 9: (50, 0, 255), 10: (50, 0, 255),
    # 上半身(緑系)
    11: (0, 255, 0), 12: (0, 255, 0), 13: (50, 255, 0), 14: (50, 255, 0),
    15: (100, 255, 0), 16: (100, 255, 0), 17: (150, 255, 0), 18: (150, 255, 0),
    19: (200, 255, 0), 20: (200, 255, 0), 21: (255, 255, 0), 22: (255, 255, 0),
    # 下半身(青系)
    23: (255, 0, 0), 24: (255, 0, 0), 25: (255, 50, 0), 26: (255, 50, 0),
    27: (255, 100, 0), 28: (255, 100, 0), 29: (255, 150, 0), 30: (255, 150, 0),
    31: (255, 200, 0), 32: (255, 200, 0)
}

def print_feature_explanation():
    """3つの主要特徴量と計算式の説明を表示"""
    explanation = """MediaPipeポーズ推定における3つの主要特徴量と計算式
特徴量1: 肩の位置・距離(MediaPipe 11番・12番の距離): 緊張・ストレス・自信との関連
MediaPipeでは11番が左肩、12番が右肩として定義されています。肩関節は可動性の高い関節であり、肩を上げることは緊張やストレスを示し、肩を後ろに引くことは自信を表現します。
計算式:
肩を上げる動作(緊張・ストレス)の検出
肩の上昇度 = (y₁₁ + y₁₂) / 2 - 基準Y座標

肩を後ろに引く動作(自信)の検出
肩の後退度 = 基準Z座標 - (z₁₁ + z₁₂) / 2

肩間距離
肩間距離 = √[(x₁₂ - x₁₁)² + (y₁₂ - y₁₁)² + (z₁₂ - z₁₁)²]

where:
* x₁₁, y₁₁, z₁₁ = 左肩(11番)の3D座標
* x₁₂, y₁₂, z₁₂ = 右肩(12番)の3D座標
* 基準座標 = リラックス状態での肩位置
特徴量2: 肩-手首間距離(MediaPipe 11番・15番, 12番・16番の距離): 開放性・防御性との関連
MediaPipeでは15番が左手首、16番が右手首として定義されています。腕を胸の前で組むことは防御的な身体言語として解釈され、不安感、苛立ち、または閉鎖性を示します。身体の開放性や手の位置は感情の知覚に影響を与えます。
計算式:
防御性の検出(腕を胸の前で組む)
体中心X = (x₁₁ + x₁₂) / 2
左手首の体中心接近度 = |x₁₅ - 体中心X|
右手首の体中心接近度 = |x₁₆ - 体中心X|
防御度 = 1 / (左手首の体中心接近度 + 右手首の体中心接近度 + ε)
※ ε は0除算を防ぐための小さな定数

開放性の検出
左肩-左手首距離 = √[(x₁₅ - x₁₁)² + (y₁₅ - y₁₁)² + (z₁₅ - z₁₁)²]
右肩-右手首距離 = √[(x₁₆ - x₁₂)² + (y₁₆ - y₁₂)² + (z₁₆ - z₁₂)²]
特徴量3: 頭部-肩間距離(MediaPipe 0番・11番, 0番・12番の距離): 注意・関心・疲労との関連
MediaPipeでは0番が鼻として定義されており、頭部を代表する特徴点として使用されます。片手で頭を支えることは関心を示し、両手で頭を支えることは退屈や疲労を示唆します。頭を上に傾けることは優越感情(自信、誇り、軽蔑)を示し、下に傾けることは劣等感情(恥、恥ずかしさ、敬意)を示します。
計算式:
頭を支える動作の検出
頭部-左手首距離 = √[(x₁₅ - x₀)² + (y₁₅ - y₀)² + (z₁₅ - z₀)²]
頭部-右手首距離 = √[(x₁₆ - x₀)² + (y₁₆ - y₀)² + (z₁₆ - z₀)²]

頭の傾きの検出
頭部傾斜角 = arctan((y₀ - (y₁₁ + y₁₂)/2) / (z₀ - (z₁₁ + z₁₂)/2))

参考:頭部-肩間距離
頭部-左肩距離 = √[(x₁₁ - x₀)² + (y₁₁ - y₀)² + (z₁₁ - y₀)² + (z₁₂ - z₀)²]
頭部-右肩距離 = √[(x₁₂ - x₀)² + (y₁₂ - y₀)² + (z₁₂ - y₀)² + (z₁₂ - z₀)²]"""
    print(explanation)

def print_program_info():
    """プログラムの機能と特徴点情報を表示"""
    print('=' * 80)
    print('プログラムの機能:')
    print('- MediaPipe BlazePoseを使用した3D人体姿勢推定')
    print('- 33個の身体特徴点をリアルタイムで検出・追跡')
    print('- 各特徴点の3次元座標(x, y, z)を1秒間隔で表示・記録')
    print('- 検出結果をresult.txtファイルに保存')
    print('=' * 80)
    print('\n33個の特徴点の対応表:')
    print('-' * 40)
    for id, name in POSE_LANDMARKS.items():
        print(f'ID {id:2d}: {name}')
    print('-' * 40)
    print()

def calculate_features(landmarks):
    """11個の特徴量を計算"""
    global base_y, base_z

    # 必要な座標を取得
    x0, y0, z0 = landmarks[0].x, landmarks[0].y, landmarks[0].z  # 鼻
    x11, y11, z11 = landmarks[11].x, landmarks[11].y, landmarks[11].z  # 左肩
    x12, y12, z12 = landmarks[12].x, landmarks[12].y, landmarks[12].z  # 右肩
    x15, y15, z15 = landmarks[15].x, landmarks[15].y, landmarks[15].z  # 左手首
    x16, y16, z16 = landmarks[16].x, landmarks[16].y, landmarks[16].z  # 右手首

    # 基準座標の初期化(最初のフレームの値を使用)
    if base_y is None:
        base_y = (y11 + y12) / 2
    if base_z is None:
        base_z = (z11 + z12) / 2

    # 特徴量1: 肩の位置・距離
    shoulder_elevation = (y11 + y12) / 2 - base_y  # 肩の上昇度
    shoulder_retraction = base_z - (z11 + z12) / 2  # 肩の後退度
    shoulder_distance = math.sqrt((x12 - x11)**2 + (y12 - y11)**2 + (z12 - z11)**2)  # 肩間距離

    # 特徴量2: 肩-手首間距離
    body_center_x = (x11 + x12) / 2
    left_wrist_approach = abs(x15 - body_center_x)
    right_wrist_approach = abs(x16 - body_center_x)
    epsilon = 0.001  # 0除算防止
    defensiveness = 1 / (left_wrist_approach + right_wrist_approach + epsilon)  # 防御度

    left_shoulder_wrist_distance = math.sqrt((x15 - x11)**2 + (y15 - y11)**2 + (z15 - z11)**2)  # 左肩-左手首距離
    right_shoulder_wrist_distance = math.sqrt((x16 - x12)**2 + (y16 - y12)**2 + (z16 - z12)**2)  # 右肩-右手首距離

    # 特徴量3: 頭部-肩間距離
    head_left_wrist_distance = math.sqrt((x15 - x0)**2 + (y15 - y0)**2 + (z15 - z0)**2)  # 頭部-左手首距離
    head_right_wrist_distance = math.sqrt((x16 - x0)**2 + (y16 - y0)**2 + (z16 - z0)**2)  # 頭部-右手首距離

    # 頭部傾斜角
    shoulder_center_y = (y11 + y12) / 2
    shoulder_center_z = (z11 + z12) / 2
    if abs(z0 - shoulder_center_z) > epsilon:
        head_tilt_angle = math.atan((y0 - shoulder_center_y) / (z0 - shoulder_center_z))
        head_tilt_angle = math.degrees(head_tilt_angle)  # ラジアンから度に変換
    else:
        head_tilt_angle = 90.0 if (y0 - shoulder_center_y) > 0 else -90.0

    head_left_shoulder_distance = math.sqrt((x11 - x0)**2 + (y11 - y0)**2 + (z11 - z0)**2)  # 頭部-左肩距離
    head_right_shoulder_distance = math.sqrt((x12 - x0)**2 + (y12 - y0)**2 + (z12 - z0)**2)  # 頭部-右肩距離

    return {
        "肩の上昇度": shoulder_elevation,
        "肩の後退度": shoulder_retraction,
        "肩間距離": shoulder_distance,
        "防御度": defensiveness,
        "左肩-左手首距離": left_shoulder_wrist_distance,
        "右肩-右手首距離": right_shoulder_wrist_distance,
        "頭部-左手首距離": head_left_wrist_distance,
        "頭部-右手首距離": head_right_wrist_distance,
        "頭部傾斜角": head_tilt_angle,
        "頭部-左肩距離": head_left_shoulder_distance,
        "頭部-右肩距離": head_right_shoulder_distance
    }

def extract_landmarks(results):
    """MediaPoseの結果から特徴量を抽出"""
    if results.pose_landmarks:
        landmarks = []
        for landmark in results.pose_landmarks.landmark:
            landmarks.extend([landmark.x, landmark.y, landmark.z, landmark.visibility])
        return np.array(landmarks)
    else:
        return np.zeros(132)  # 33 landmarks * 4 features

def draw_landmarks_with_colors(image, landmarks):
    """特徴点を色付きで描画"""
    height, width = image.shape[:2]

    for idx, landmark in enumerate(landmarks.landmark):
        x = int(landmark.x * width)
        y = int(landmark.y * height)

        # 画像範囲内かチェック
        if 0 <= x < width and 0 <= y < height:
            color = LANDMARK_COLORS.get(idx, (255, 255, 255))
            cv2.circle(image, (x, y), 5, color, -1)
            cv2.circle(image, (x, y), 7, color, 2)

            # ID番号を表示
            cv2.putText(image, str(idx), (x + 10, y - 5),
                       cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1)

def video_processing(frame):
    """メインの動画処理関数"""
    global last_print_time, results_log

    current_time = time.time()
    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

    # MediaPipe姿勢推定
    results = pose.process(rgb_frame)

    if results.pose_landmarks:
        # カスタム描画(色付き特徴点)
        draw_landmarks_with_colors(frame, results.pose_landmarks)

        # 接続線の描画
        mp_drawing.draw_landmarks(
            frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
            landmark_drawing_spec=None,
            connection_drawing_spec=mp_drawing.DrawingSpec(color=(200, 200, 200), thickness=1))

        # 特徴量抽出
        features = extract_landmarks(results)

        # 1秒間隔でのログ出力
        if current_time - last_print_time >= 1.0:
            print(f'\n時刻: {current_time:.1f}s')
            print('ID, x, y, z:')
            for idx, landmark in enumerate(results.pose_landmarks.landmark):
                print(f'{idx:2d}, {landmark.x:.4f}, {landmark.y:.4f}, {landmark.z:.4f}')

            # 11個の特徴量を計算して表示
            print('\n特徴量:')
            feature_values = calculate_features(results.pose_landmarks.landmark)
            for name, value in feature_values.items():
                print(f'{name}: {value:.4f}')

            # ログに保存
            log_entry = f'時刻: {current_time:.1f}s\n'
            log_entry += 'ID, x, y, z:\n'
            for idx, landmark in enumerate(results.pose_landmarks.landmark):
                log_entry += f'{idx:2d}, {landmark.x:.4f}, {landmark.y:.4f}, {landmark.z:.4f}\n'
            log_entry += '\n特徴量:\n'
            for name, value in feature_values.items():
                log_entry += f'{name}: {value:.4f}\n'
            results_log.append(log_entry)

            last_print_time = current_time

    return frame

def save_results():
    """結果をファイルに保存"""
    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('3D姿勢推定システム実行結果\n')
            f.write('=' * 40 + '\n')
            f.write('33個の特徴点の対応表:\n')
            f.write('-' * 40 + '\n')
            for id, name in POSE_LANDMARKS.items():
                f.write(f'ID {id:2d}: {name}\n')
            f.write('-' * 40 + '\n\n')
            f.write('特徴点座標データと特徴量\n')
            f.write('=' * 40 + '\n\n')
            for result in results_log:
                f.write(result + '\n')
        print('\nresult.txtに保存しました')

# プログラム開始時の説明
print('人間の無意識行動からの感情予測システム')
print('MediaPipe BlazePoseによる3D姿勢推定を使用')
print('操作: qキーで終了')

# プログラムの機能と特徴点情報を表示
print_program_info()

# 3つの主要特徴量と計算式の説明を表示
print_feature_explanation()

print('=' * 50)
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()

# メイン処理
try:
    while True:
        cap.grab()
        ret, frame = cap.retrieve()
        if not ret:
            break

        processed_frame = video_processing(frame)
        cv2.imshow('3D Pose Estimation', processed_frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
finally:
    cap.release()
    cv2.destroyAllWindows()
    if temp_file:
        os.remove(temp_file)
    save_results()