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 による人間の無意識の姿勢からの感情予測プログラム
概要
画像から情報を取得し、その内容を自然言語で理解・説明する能力を実現。このプログラムは、画像内容の視覚的理解と自然言語による説明を行う。カメラや動画から取得した画像に含まれる場所、物体、文字情報、状況などを認識し、それらを日本語で記述する。
主要技術
- Visual Instruction Tuning(視覚指示調整)[1]
GPT-4を用いて生成したマルチモーダル指示追従データによる学習手法である。視覚エンコーダと大規模言語モデルを線形投影層で接続し、画像理解タスクに対する指示追従能力を獲得する。 - CLIP(Contrastive Language-Image Pre-training)[2]
OpenAIが開発した視覚エンコーダであり、対照学習により画像とテキストの共通表現空間を学習する。画像を576個の視覚トークンに変換し、言語モデルが処理可能な形式に変換する役割を担う。
参考文献
[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()