MediaPipeによるしぐさ検出(ソースコードと実行結果)

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 opencv-python mediapipe numpy scipy pillow 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によるしぐさ検出プログラム
概要
このプログラムは動画像から人物の全身543点のランドマークを検出し、追跡することで身体動作を認識する。まばたき検出にはEAR(Eye Aspect Ratio)アルゴリズム[1]を使用し、手の震えには高周波成分抽出、肩の緊張には変動係数による評価を行う。
主要技術
- MediaPipe Holistic: Googleが開発した機械学習フレームワークである。顔468点、両手42点、体33点の合計543点のランドマークをリアルタイムで検出する[2]。BlazeFace、BlazePalm、BlazePoseの3つのモデルを統合している。
- Eye Aspect Ratio (EAR): 目の開き具合を定量化するアルゴリズムである。6つの目のランドマーク点から垂直距離と水平距離の比率を計算する[1]。まばたき検出の標準的手法として使用されている。
参考文献
- [1] Soukupová, T., & Čech, J. (2016). Real-time eye blink detection using facial landmarks. In 21st computer vision winter workshop (pp. 1-8).
- [2] Lugaresi, C., Tang, J., Nash, H., McClanahan, C., Uboweja, E., Hays, M., Zhang, F., Chang, C. L., Yong, M. G., Lee, J., Chang, W. T., Hua, W., Georg, M., & Grundmann, M. (2019). MediaPipe: A framework for building perception pipelines. arXiv preprint arXiv:1906.08172.
# プログラム名: MediaPipeによるしぐさ検出プログラム
# 特徴技術名: MediaPipe Holistic
# 出典: Lugaresi, C., Tang, J., Nash, H., McClanahan, C., Uboweja, E., Hays, M., Zhang, F., Chang, C. L., Yong, M. G., Lee, J., Chang, W. T., Hua, W., Georg, M., & Grundmann, M. (2019). MediaPipe: A framework for building perception pipelines. arXiv preprint arXiv:1906.08172. https://arxiv.org/abs/1906.08172
# 特徴機能: リアルタイム全身統合ランドマーク検出 - 顔468点、両手42点(各21点)、体33点の合計543点を30FPS以上で同時検出し、時系列追跡により微細動作を検出
# 学習済みモデル: MediaPipe Holistic内蔵モデル(BlazeFace、BlazePalm、BlazePoseの統合モデル)、自動ダウンロード機能付き、https://google.github.io/mediapipe/solutions/holistic
# 方式設計:
# - 関連利用技術: OpenCV(動画処理)、NumPy(数値計算)、SciPy(信号処理)、Pillow(日本語表示)
# - 入力と出力: 入力: 動画(ユーザは「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.時系列追跡 3.微細動作検出 4.確信度計算
# - 前処理、後処理: RGB変換、解像度調整、時系列フィルタリング
# - 追加処理: 移動平均による平滑化、相対座標変換、高周波成分抽出(バターワースフィルタ)
# - 調整を必要とする設定値: BLINK_THRESHOLD(まばたき検出閾値、デフォルト0.21)
# 将来方策: BLINK_THRESHOLDの自動調整機能 - プログラム開始時に個人のまばたきパターンを10秒間学習し、EAR値の分布から最適な閾値を自動設定する機能
# その他の重要事項: TensorFlow警告抑制のため環境変数TF_CPP_MIN_LOG_LEVEL='3'を設定、MediaPipe内部警告をwarnings.filterwarnings('ignore')で抑制
# 前準備:
# - pip install opencv-python mediapipe numpy scipy pillow
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
import cv2
import mediapipe as mp
import numpy as np
from collections import deque
import tkinter as tk
from tkinter import filedialog
from scipy import signal
from PIL import Image, ImageDraw, ImageFont
import warnings
import itertools
import time
import urllib.request
warnings.filterwarnings('ignore')
# 設定値(利用者調整可能)
DETECTION_CONFIDENCE = 0.3 # 人物検出の信頼度閾値
TRACKING_CONFIDENCE = 0.3 # 追跡の信頼度閾値
WINDOW_SIZE = 30 # 動作分析のウィンドウサイズ(フレーム数)
# しぐさ検出の感度設定
BLINK_THRESHOLD = 0.21 # まばたき検出の閾値(将来的に自動調整機能実装予定)
MICRO_MOVEMENT_THRESHOLD = 0.002 # 微細動作検出の閾値
SHOULDER_TENSION_THRESHOLD = 0.01 # 肩の緊張検出の閾値
# 色定義
COLOR_BLUE = (255, 0, 0)
COLOR_GREEN = (0, 255, 0)
COLOR_RED = (0, 0, 255)
COLOR_WHITE = (255, 255, 255)
# ランドマークインデックス
# まばたき検出用(目の周囲12点)
EYE_LANDMARKS = [33, 160, 158, 133, 153, 144, 362, 385, 387, 263, 380, 373]
# 右目の6点
R_EYE = [33, 160, 158, 133, 153, 144]
# 左目の6点
L_EYE = [362, 385, 387, 263, 380, 373]
# 手の震え検出用
HAND_WRIST = 0
HAND_MIDDLE_TIP = 12
# 肩の緊張検出用
POSE_NOSE = 0
POSE_L_SHOULDER = 11
POSE_R_SHOULDER = 12
# フォント設定
FONT_PATH = os.path.join(os.environ.get('WINDIR', 'C:/Windows'), 'Fonts', 'msgothic.ttc')
FONT_SIZE_NORMAL = 24
FONT_SIZE_LARGE = 30
# サンプル動画URL
SAMPLE_VIDEO_URL = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
# 日本語フォント設定
font = ImageFont.truetype(FONT_PATH, FONT_SIZE_NORMAL)
font_large = ImageFont.truetype(FONT_PATH, FONT_SIZE_LARGE)
# MediaPipe初期化(GPU/CPUフォールバック付き)
mp_holistic = mp.solutions.holistic
try:
# GPU使用を試みる
holistic = mp_holistic.Holistic(
min_detection_confidence=DETECTION_CONFIDENCE,
min_tracking_confidence=TRACKING_CONFIDENCE,
model_complexity=2,
refine_face_landmarks=True,
enable_segmentation=False
)
except Exception:
# CPUモードにフォールバック
print('GPU使用不可、CPUモードで実行します')
holistic = mp_holistic.Holistic(
min_detection_confidence=DETECTION_CONFIDENCE,
min_tracking_confidence=TRACKING_CONFIDENCE,
model_complexity=1, # 軽量モデル
refine_face_landmarks=True,
enable_segmentation=False
)
# データ保存用
landmark_history = {
'face': deque(maxlen=WINDOW_SIZE),
'pose': deque(maxlen=WINDOW_SIZE),
'left_hand': deque(maxlen=WINDOW_SIZE),
'right_hand': deque(maxlen=WINDOW_SIZE)
}
# フレームレート計測用
fps_start_time = time.time()
fps_frame_count = 0
current_fps = 30 # 初期値
# 検出結果記録用
detection_log = []
last_output_time = time.time()
def detect_micro_movements(history_data):
"""微細動作の検出"""
gestures = []
# まばたき検出
if len(history_data['face']) >= 10:
ears = []
# dequeから最新10フレームを取得
face_frames = list(itertools.islice(history_data['face'], max(0, len(history_data['face']) - 10), None))
for face_lm in face_frames:
if face_lm:
# EAR(Eye Aspect Ratio)の計算
# 右目の6点
landmarks = [face_lm.landmark[i] for i in R_EYE + L_EYE]
# 信頼度チェック
if all(lm.visibility > 0.5 for lm in landmarks):
# 右目のEAR計算
p1, p2, p3, p4, p5, p6 = [face_lm.landmark[i] for i in R_EYE]
h_dist_r = np.linalg.norm([p1.x - p4.x, p1.y - p4.y])
if h_dist_r > 0:
right_ear = (np.linalg.norm([p2.x - p6.x, p2.y - p6.y]) +
np.linalg.norm([p3.x - p5.x, p3.y - p5.y])) / (2.0 * h_dist_r)
else:
continue
# 左目のEAR計算
p7, p8, p9, p10, p11, p12 = [face_lm.landmark[i] for i in L_EYE]
h_dist_l = np.linalg.norm([p7.x - p10.x, p7.y - p10.y])
if h_dist_l > 0:
left_ear = (np.linalg.norm([p8.x - p12.x, p8.y - p12.y]) +
np.linalg.norm([p9.x - p11.x, p9.y - p11.y])) / (2.0 * h_dist_l)
else:
continue
avg_ear = (right_ear + left_ear) / 2.0
ears.append(avg_ear)
if len(ears) >= 5:
# 移動平均でスムージング
smoothed_ears = np.convolve(ears, np.ones(3)/3, mode='valid')
# まばたきの検出
if len(smoothed_ears) >= 3:
min_val = np.min(smoothed_ears)
max_val = np.max(smoothed_ears)
if max_val > 0 and min_val < BLINK_THRESHOLD * max_val:
min_idx = np.argmin(smoothed_ears)
if 0 < min_idx < len(smoothed_ears) - 1:
confidence = 1.0 - (min_val / (BLINK_THRESHOLD * max_val))
gestures.append(('まばたき', min(confidence, 1.0)))
# 手の微細な震え検出
for hand_type in ['left_hand', 'right_hand']:
if len(history_data[hand_type]) >= WINDOW_SIZE:
hand_data = list(itertools.islice(
(h for h in history_data[hand_type] if h is not None),
WINDOW_SIZE
))
if len(hand_data) >= WINDOW_SIZE // 2:
wrist_positions = []
middle_tip_positions = []
for h in hand_data:
if h.landmark[HAND_WRIST].visibility > 0.5 and h.landmark[HAND_MIDDLE_TIP].visibility > 0.5:
wrist_positions.append([h.landmark[HAND_WRIST].x, h.landmark[HAND_WRIST].y])
middle_tip_positions.append([h.landmark[HAND_MIDDLE_TIP].x, h.landmark[HAND_MIDDLE_TIP].y])
if len(wrist_positions) > 10:
wrist_positions = np.array(wrist_positions)
middle_tip_positions = np.array(middle_tip_positions)
# 相対位置の計算
relative_positions = middle_tip_positions - wrist_positions
movements = np.diff(relative_positions, axis=0)
movement_magnitude = np.linalg.norm(movements, axis=1)
# 高周波成分の抽出
if len(movement_magnitude) > 5:
fs = current_fps
nyquist = fs / 2
cutoff = 4.0 # 4Hzのカットオフ周波数
b, a = signal.butter(4, cutoff / nyquist, 'high')
filtered = signal.filtfilt(b, a, movement_magnitude)
tremor_intensity = np.std(filtered)
if tremor_intensity > MICRO_MOVEMENT_THRESHOLD:
confidence = min(tremor_intensity / (MICRO_MOVEMENT_THRESHOLD * 3), 1.0)
hand_name = '左手' if hand_type == 'left_hand' else '右手'
gestures.append((f'{hand_name}の微細な震え', confidence))
# 肩の緊張検出
if len(history_data['pose']) >= WINDOW_SIZE:
pose_data = [p for p in history_data['pose'] if p is not None]
if len(pose_data) >= WINDOW_SIZE // 2:
shoulder_data = []
for p in pose_data:
if (p.landmark[POSE_L_SHOULDER].visibility > 0.5 and
p.landmark[POSE_R_SHOULDER].visibility > 0.5 and
p.landmark[POSE_NOSE].visibility > 0.5):
left_shoulder_y = p.landmark[POSE_L_SHOULDER].y
right_shoulder_y = p.landmark[POSE_R_SHOULDER].y
nose_y = p.landmark[POSE_NOSE].y
# 首に対する肩の相対高さ
relative_height = ((left_shoulder_y + right_shoulder_y) / 2) - nose_y
shoulder_data.append(relative_height)
if len(shoulder_data) > WINDOW_SIZE // 3:
shoulder_data = np.array(shoulder_data)
mean_val = np.mean(shoulder_data)
if abs(mean_val) > 1e-6: # ゼロ除算回避
cv = np.std(shoulder_data) / abs(mean_val)
if cv > SHOULDER_TENSION_THRESHOLD:
confidence = min(cv / (SHOULDER_TENSION_THRESHOLD * 3), 1.0)
gestures.append(('肩の緊張・上下動', confidence))
return gestures
def draw_landmarks_on_image(frame, results):
"""検出に使用するランドマークを画像上にプロット"""
h, w = frame.shape[:2]
# 顔のランドマーク
if results.face_landmarks:
for idx, landmark in enumerate(results.face_landmarks.landmark):
if landmark.visibility > 0.5:
x = int(landmark.x * w)
y = int(landmark.y * h)
if idx in EYE_LANDMARKS:
cv2.circle(frame, (x, y), 3, COLOR_BLUE, -1) # 青色(使用中)
else:
cv2.circle(frame, (x, y), 1, COLOR_WHITE, -1) # 白色(未使用)
# 手のランドマーク
hand_landmarks_used = [HAND_WRIST, HAND_MIDDLE_TIP]
# 左手
if results.left_hand_landmarks:
for idx, landmark in enumerate(results.left_hand_landmarks.landmark):
if landmark.visibility > 0.5:
x = int(landmark.x * w)
y = int(landmark.y * h)
if idx in hand_landmarks_used:
cv2.circle(frame, (x, y), 4, COLOR_GREEN, -1) # 緑色(使用中)
else:
cv2.circle(frame, (x, y), 2, COLOR_WHITE, -1) # 白色(未使用)
# 右手
if results.right_hand_landmarks:
for idx, landmark in enumerate(results.right_hand_landmarks.landmark):
if landmark.visibility > 0.5:
x = int(landmark.x * w)
y = int(landmark.y * h)
if idx in hand_landmarks_used:
cv2.circle(frame, (x, y), 4, COLOR_GREEN, -1) # 緑色(使用中)
else:
cv2.circle(frame, (x, y), 2, COLOR_WHITE, -1) # 白色(未使用)
# 体のランドマーク
pose_landmarks_used = [POSE_NOSE, POSE_L_SHOULDER, POSE_R_SHOULDER]
if results.pose_landmarks:
for idx, landmark in enumerate(results.pose_landmarks.landmark):
if landmark.visibility > 0.5:
x = int(landmark.x * w)
y = int(landmark.y * h)
if idx in pose_landmarks_used:
cv2.circle(frame, (x, y), 5, COLOR_RED, -1) # 赤色(使用中)
else:
cv2.circle(frame, (x, y), 2, COLOR_WHITE, -1) # 白色(未使用)
return frame
def video_processing(frame):
"""フレームを処理して微細動作を検出"""
global fps_frame_count, fps_start_time, current_fps, last_output_time
h, w = frame.shape[:2]
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = holistic.process(rgb_frame)
# ランドマークの履歴を更新
landmark_history['face'].append(results.face_landmarks)
landmark_history['pose'].append(results.pose_landmarks)
landmark_history['left_hand'].append(results.left_hand_landmarks)
landmark_history['right_hand'].append(results.right_hand_landmarks)
# 微細動作の検出
detected_gestures = detect_micro_movements(landmark_history)
# ランドマークをプロット
frame = draw_landmarks_on_image(frame, results)
# 人物のバウンディングボックス
if results.pose_landmarks:
landmarks = results.pose_landmarks.landmark
x_coords = [lm.x * w for lm in landmarks if lm.visibility > 0.5]
y_coords = [lm.y * h for lm in landmarks if lm.visibility > 0.5]
if x_coords and y_coords:
padding = 20
x_min = max(0, int(min(x_coords)) - padding)
y_min = max(0, int(min(y_coords)) - padding)
x_max = min(w, int(max(x_coords)) + padding)
y_max = min(h, int(max(y_coords)) + padding)
cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), COLOR_GREEN, 2)
# 日本語テキスト描画
if y_min > 35:
img_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
draw.text((x_min, y_min - 35), '検出対象', font=font_large, fill=COLOR_GREEN)
frame = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# 検出結果の表示
if detected_gestures:
img_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
y_offset = 30
for gesture, confidence in detected_gestures:
text = f'{gesture}: {confidence:.1%}'
draw.text((10, y_offset), text, font=font, fill=COLOR_RED)
y_offset += 35
frame = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
cv2.putText(frame, 'Press "q" to quit', (w - 150, h - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, COLOR_WHITE, 1)
# 1秒間隔での出力
current_time = time.time()
if current_time - last_output_time >= 1.0:
if detected_gestures:
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
for gesture, confidence in detected_gestures:
log_entry = f'[{timestamp}] {gesture}: {confidence:.2%}'
print(log_entry)
detection_log.append(log_entry)
last_output_time = current_time
# FPS計測
fps_frame_count += 1
if fps_frame_count >= 30:
elapsed = time.time() - fps_start_time
current_fps = fps_frame_count / elapsed
fps_frame_count = 0
fps_start_time = time.time()
return frame
# プログラム開始時の説明
print('=== MediaPipeによるしぐさ検出プログラム ===')
print('概要: MediaPipe Holisticを使用して微細な人間のしぐさを検出します')
print('検出可能なしぐさ: まばたき、手の震え、肩の緊張')
print('')
print('操作方法:')
print('- 動画再生中: "q"キーで終了')
print('- 検出結果は1秒間隔でコンソールに表示されます')
print('- プログラム終了時に検出結果をresult.txtに保存します')
print('')
# 入力ソースの選択
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':
# サンプル動画ダウンロード
filename = 'vtest.avi'
try:
urllib.request.urlretrieve(SAMPLE_VIDEO_URL, filename)
temp_file = filename
cap = cv2.VideoCapture(filename)
except Exception as e:
print(f'動画のダウンロードに失敗しました: {SAMPLE_VIDEO_URL}')
print(f'エラー: {e}')
exit()
else:
print('無効な選択です')
exit()
print('\n処理を開始します...')
# メイン処理
try:
while True:
cap.grab()
ret, frame = cap.retrieve()
if not ret:
break
processed_frame = video_processing(frame)
cv2.imshow('Micro Gesture Detection', processed_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cap.release()
cv2.destroyAllWindows()
holistic.close()
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
# 検出結果をファイルに保存
if detection_log:
with open('result.txt', 'w', encoding='utf-8') as f:
f.write('=== MediaPipeによるしぐさ検出結果 ===\n')
f.write('\n'.join(detection_log))
print('\nresult.txtに保存しました')