MediaPipe Hands による手指ジェスチャー分析システム(ソースコードと実行結果)

プログラム利用ガイド
1. このプログラムの利用シーン
このツールは、カメラまたは動画ファイルから手指のジェスチャーをリアルタイムで認識・分析するためのシステムである。手話研究、ヒューマンコンピュータインタラクション開発、ジェスチャー制御インターフェースの構築、手指動作の定量的解析に適用される。医療・リハビリテーション分野では手指機能評価の補助ツールとして活用できる。
2. 主な機能
- リアルタイム手指ランドマーク検出: 21点の3次元座標を高精度で追跡
- 6種類の基本ジェスチャー分類: OKサイン、サムズアップ、指差し、ピースサイン、握りこぶし、開いた手
- 定量的特徴量計算: 指の曲がり具合(curl)、手の開き具合、親指分離度、手のサイズ
- 時系列データ可視化: 特徴量の変化をリアルタイムグラフで表示
- データ記録機能: CSV形式での計測ログ保存、JSON形式での詳細データ出力
- 複数入力対応: ウェブカメラ、動画ファイル、サンプル動画
3. 基本的な使い方
プログラム起動後、入力選択画面で以下を選択する:
- 0: 動画ファイル(ファイルダイアログで選択)
- 1: カメラ映像(リアルタイム処理)
- 2: サンプル動画(自動ダウンロード)
処理画面では、左右の手それぞれについて、ジェスチャー名称、指ごとの曲がり具合バー、各種特徴量の数値、時系列グラフが表示される。qキーでプログラム終了、sキーで現在の特徴量データをJSONファイルに保存する。プログラム終了時に全計測データがresult.txtにCSV形式で自動保存される。
4. 便利な機能
- 移動平均スムージング: ノイズ除去による安定した特徴量計算
- 個人差対応: 手のスケール正規化による体格差の自動補正
- 両手同時処理: 左右の手を独立して追跡・分析
- 詳細ログ機能: フレーム単位での計測値記録とタイムスタンプ付きデータ出力
- 日本語表示対応: 結果表示とファイル出力における日本語文字の完全サポート
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 mediapipe opencv-python numpy pillow
手指ジェスチャー分析システム
概要
このプログラムは、MediaPipe Handsによる手指ジェスチャー認識システムである。カメラ映像または動画ファイルから手の21点の3次元ランドマークを検出し、距離比率に基づく定量的特徴量算出により、6種類の基本ジェスチャーを分類する[1]。
主要技術
MediaPipe Handsは、GoogleがAR/VR用途向けに開発した手指検出・追跡フレームワークである[1]。パーム検出モデルと手指ランドマーク検出モデルの2段階パイプラインで構成され、単一のRGBカメラから21点の3次元手指ランドマークをリアルタイムで推定する。
距離比率計算によるcurl値算出では、関節の直線距離(base→tip)と関節経由距離(base→PIP→DIP→tip)の比率から指の屈曲度を0〜1の範囲で定量化する。重み付き移動平均によるスムージングにより、時系列データの平滑化を実現している。
技術的特徴
手のスケール正規化を行っている。親指の最大屈曲時のcurl値は0.3、他指の全屈曲時は0.5〜0.6として、ジェスチャー判定の閾値を調整している。
左右の手それぞれについて特徴量の時系列グラフの表示とCSV形式での計測ログ出力を実現している。
実装の特色
サムズアップ判定において、他4指のcurl値に対して下限のみの制約(≥0.25、上限なし)を設けている。ルールベース分類では、OKサイン、サムズアップ、指差し、ピースサイン、握りこぶし、開いた手の6種類を多段階しきい値により判定する。
MediaPipe Frameworkの計算グラフ機能を活用し、パーム検出とランドマーク推定をパイプライン化している[2]。
参考文献
[1] Zhang, F., Bazarevsky, V., Vakunov, A., Tkachenka, A., Sung, G., Chang, C. L., & Grundmann, M. (2020). MediaPipe Hands: On-device Real-time Hand Tracking. arXiv preprint arXiv:2006.10214. https://arxiv.org/abs/2006.10214
[2] Lugaresi, C., Tang, J., Nash, H., McClanahan, C., Uboweja, E., Hays, M., ... & Grundmann, M. (2019). MediaPipe: A Framework for Building Perception Pipelines. arXiv preprint arXiv:1906.08172. https://arxiv.org/abs/1906.08172
ソースコード
# 手指ジェスチャー分析システム
# 特徴技術名: MediaPipe Hands
# 出典: Zhang, F., Bazarevsky, V., Vakunov, A., Tkachenka, A., Sung, G., Chang, C. L., & Grundmann, M. (2020).
# MediaPipe Hands: On-device Real-time Hand Tracking. arXiv:2006.10214.
# 公式情報: https://google.github.io/mediapipe/solutions/hands
#
# 特徴機能: 単一RGB画像から21点の3次元手指ランドマークをリアルタイム推定し,
# 正規化画像座標(x,y)に基づく相対距離比で指の状態と手の形状を定量化する。
#
# 学習済みモデル: MediaPipe Hands内蔵モデルを使用(Python Solutions APIではモデルファイルを直接指定しない)。
# model_complexityによりバリアントを選択(0:軽量版,1:標準版)。
# 参考: https://google.github.io/mediapipe/solutions/hands
# https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark
#
# 方式設計:
# - 関連利用技術:
# - OpenCV: カメラ入力と画像表示(BGR画像処理)
# - NumPy: 距離比率計算(正規化座標での演算)
# - Pillow: 日本語テキスト描画(TrueTypeフォント,Meiryo)
# - tkinter: 動画ファイル選択ダイアログ
# - urllib.request: サンプル動画のダウンロード
# - csv: 計測ログのCSV出力(result.txt)
# - json: 任意の特徴量ログ保存(sキー)
#
# - 入出力:
# 入力: 0=動画ファイル(tkinterで選択),1=カメラ,2=サンプル動画(vtest.aviをDL)。
# 出力(画面): OpenCVウィンドウにリアルタイム描画。
# タイトル,フレーム番号/検出手数,認識結果の要約(右手/左手),手ごとのジェスチャー種別,
# 指ごとの曲がり具合バー(数値併記),手の開き具合,親指分離度,全体的曲がり,手のサイズ。
# 履歴が十分な場合,左右手それぞれに overall_curl と hand_openness の時系列グラフを描画(画面表示のみ)。
# 出力(コンソール):
# カメラ時: タイムスタンプと手ごとのジェスチャー要約。
# 動画/サンプル時: フレーム番号と手ごとのジェスチャー要約。
# 出力(ファイル):
# プログラム終了時に計測ログをCSV形式で result.txt に保存(UTF-8)。
# sキー押下で gesture_log_YYYYMMDD_HHMMSS.json を保存(任意)。
#
# - 処理手順:
# 1) カメラ/動画から画像取得
# 2) MediaPipe Handsで手検出と21点ランドマーク推定
# 3) 距離比率による指ごとの曲がり具合(curl)計算
# 4) 手の開き具合と親指分離度の計算
# 5) ルールベースでジェスチャー分類
# 6) 特徴量の可視化(オーバレイと簡易グラフ)
#
# - 前処理・後処理:
# 前処理: ランドマークの移動平均スムージング(重み [0.2, 0.3, 0.5],直近最大3フレーム)。
# 後処理: 計測ログのCSV保存(result.txt),任意で特徴量JSON保存(sキー)。
#
# - 追加処理:
# MediaPipeの正規化座標系(x,y)をそのまま用い,手のサイズ正規化で個人差を低減。
#
# - 調整を要する設定値(主要):
# HAND_CONFIDENCE(検出信頼度閾値,既定0.7,範囲0.0–1.0)
# TRACKING_CONFIDENCE(追跡信頼度閾値,既定0.5)
# MAX_NUM_HANDS(最大検出手数,既定2)
# SMOOTHING_FRAMES(スムージングに用いる履歴長,既定3)
# MODEL_COMPLEXITY(0:軽量,1:標準;Handsでは0/1を使用)
#
# ジェスチャー定義(現行ロジックの概要):
# しきい値(実測に合わせた暫定値):
# FINGER_CURL_LOW=0.25(他指の伸展境界)/ FINGER_CURL_HIGH=0.50(他指の屈曲境界)
# THUMB_CURL_EXT=0.20(親指が伸展)/ THUMB_CURL_BENT=0.35(親指が屈曲扱い)
# OPEN_HAND_HIGH=0.55(手の開きが大きい)
# THUMB_SEP_OK=0.30(OKサインの親指-人差し指距離)/ THUMB_SEP_UP=0.50(サムズアップの親指分離度)
# 判定順と要点:
# - OKサイン: 親指-人差し指が近い(thumb_separation < THUMB_SEP_OK)かつ
# indexはやや曲げ,ring/pinkyは伸展寄り
# - サムズアップ: 親指が伸展(thumb_curl < THUMB_CURL_EXT)かつ親指分離度 > THUMB_SEP_UP,
# 他4指のcurlは下限のみ要求(>=0.25,上限なし)
# - 指差し: indexが伸展(<FINGER_CURL_LOW),middle〜pinkyが屈曲(≥FINGER_CURL_HIGH)
# - ピースサイン: index/middleが伸展,ring/pinkyが屈曲
# - 握りこぶし: 他指が屈曲,開きは大きすぎない
# - 開いた手: 開きが大きく,他指の多くが伸展
#
# 計測量の定義:
# - curl(0〜1): direct=直線距離(base→tip),chain=関節経由距離(base→PIP→DIP→tip)。
# r=direct/chain,curl=clip(1−r)を1.5倍して0〜1にクリップ(大きいほど屈曲)。
# 親指は 1→2→3→4,他指は MCP→PIP→DIP→TIP。
# - openness(0〜1): 隣接指先間と親指-人差し指/中指を手スケールで正規化した重み付き平均。
# - thumb_separation(0〜1): 親指先端(4)-人差し指先端(8)距離の手スケール正規化。
# - overall_curl: 5指curlの平均。描画・ログ用の補助量。
# - hand_scale: 手首(0)-中指MCP(9)距離。
# - 座標系: 2次元の正規化画像座標(x,y)を使用。zは描画時の点サイズ調整に使用。
#
# 画面に表示されるもの(リアルタイム):
# - タイトル,フレーム番号/検出手数
# - 認識結果の要約(右手/左手)
# - 手ごとのジェスチャー種別
# - 指ごとのcurlバーと数値,openness,thumb_separation,overall_curl,hand_scale
# - 簡易グラフ(左右手のoverall_curl/opennessの時系列;画面表示のみ)
#
# 記録(ファイル出力):
# - result.txt(CSV, UTF-8, 集計なし)
# カメラ: timestamp, hand, gesture, overall_curl, hand_openness, thumb_separation, hand_scale,
# curl_thumb, curl_index, curl_middle, curl_ring, curl_pinky
# 動画/サンプル: frame, hand, gesture, overall_curl, hand_openness, thumb_separation, hand_scale,
# curl_thumb, curl_index, curl_middle, curl_ring, curl_pinky
# - gesture_log_*.json(sキー任意保存): 上記各種の詳細レコード
#
# 実測観測(本環境での傾向):
# - 親指: 完全に曲げてもcurlは約0.3
# - 他指: すべて曲げると0.5〜0.6,いくつかのみ曲げた場合は0.2〜0.4
# これらの観測に合わせてしきい値を設計している(サムズアップの他4指は下限0.25のみ要求,上限なし)。
import cv2
import numpy as np
import mediapipe as mp
import math
from PIL import Image, ImageDraw, ImageFont
import json
from datetime import datetime
import tkinter as tk
from tkinter import filedialog
import time
import os
import urllib.request
import csv # CSV出力
# 調整可能な設定値
HAND_CONFIDENCE = 0.7 # 手検出の信頼度閾値(0.0-1.0)
TRACKING_CONFIDENCE = 0.5 # トラッキングの信頼度閾値(0.0-1.0)
MAX_NUM_HANDS = 2 # 検出する手の最大数
SMOOTHING_FRAMES = 3 # スムージングに使用するフレーム数
MODEL_COMPLEXITY = 1 # モデルの複雑度(0:軽量、1:標準)※Handsでは0/1
# 実測観測に基づくしきい値
FINGER_CURL_LOW = 0.25 # 他指の伸展境界
FINGER_CURL_HIGH = 0.50 # 他指の屈曲境界
THUMB_CURL_EXT = 0.20 # 親指が伸展
THUMB_CURL_BENT = 0.35 # 親指が屈曲扱い
OPEN_HAND_HIGH = 0.55 # 手の開きが大きい
THUMB_SEP_OK = 0.30 # OKサインの親指-人差し指距離
THUMB_SEP_UP = 0.50 # サムズアップの親指分離度
# UI設定
FONT_SIZES = {
'large': 30,
'medium': 20,
'small': 16,
'tiny': 12
}
GRAPH_WIDTH = 200
GRAPH_HEIGHT = 100
HISTORY_MAX_LENGTH = 100
# スムージング重み
SMOOTHING_WEIGHTS = np.array([0.2, 0.3, 0.5])
# 色定義(BGR)
COLORS = {
'thumb': (255, 0, 0),
'index': (0, 255, 0),
'middle': (0, 0, 255),
'ring': (255, 255, 0),
'pinky': (255, 0, 255),
'palm': (0, 255, 255),
'wrist': (128, 128, 128)
}
# MediaPipeランドマーク定義
FINGER_TIPS = [4, 8, 12, 16, 20]
FINGER_MCPS = [1, 5, 9, 13, 17]
FINGER_NAMES = ['親指', '人差し指', '中指', '薬指', '小指']
# 日本語フォント(Windows)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
font_large = ImageFont.truetype(FONT_PATH, FONT_SIZES['large'])
font_medium = ImageFont.truetype(FONT_PATH, FONT_SIZES['medium'])
font_small = ImageFont.truetype(FONT_PATH, FONT_SIZES['small'])
font_tiny = ImageFont.truetype(FONT_PATH, FONT_SIZES['tiny'])
def draw_japanese_text(frame, text, position, font, color):
img_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
draw.text(position, text, font=font, fill=color)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
def simple_distance_2d(p1, p2):
return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
def calculate_hand_scale(landmarks):
wrist = landmarks.landmark[0]
middle_mcp = landmarks.landmark[9]
scale = simple_distance_2d(wrist, middle_mcp)
return max(scale, 0.01)
def calculate_finger_curl_simple(landmarks, finger_idx):
# 実測: 親指は完全屈曲でも≈0.3,他指は全屈曲で≈0.5〜0.6,部分屈曲で≈0.2〜0.4の傾向
if finger_idx == 0: # 親指: 1(CMC), 2(MCP), 3(IP), 4(TIP)
cmc = landmarks.landmark[1]
mcp = landmarks.landmark[2]
ip = landmarks.landmark[3]
tip = landmarks.landmark[4]
direct_distance = simple_distance_2d(cmc, tip)
joint_distance = (simple_distance_2d(cmc, mcp) +
simple_distance_2d(mcp, ip) +
simple_distance_2d(ip, tip))
else: # 他指: MCP→PIP→DIP→TIP
base_idx = FINGER_MCPS[finger_idx]
pip_idx = base_idx + 1
dip_idx = base_idx + 2
tip_idx = FINGER_TIPS[finger_idx]
base = landmarks.landmark[base_idx]
pip = landmarks.landmark[pip_idx]
dip = landmarks.landmark[dip_idx]
tip = landmarks.landmark[tip_idx]
direct_distance = simple_distance_2d(base, tip)
joint_distance = (simple_distance_2d(base, pip) +
simple_distance_2d(pip, dip) +
simple_distance_2d(dip, tip))
if joint_distance < 0.001:
return 0.0
distance_ratio = min(direct_distance / joint_distance, 1.0)
curl_ratio = max(0.0, 1.0 - distance_ratio)
return min(1.0, curl_ratio * 1.5)
def calculate_hand_openness(landmarks):
finger_tips = [landmarks.landmark[tip_idx] for tip_idx in FINGER_TIPS]
distances = []
for i in range(len(finger_tips) - 1):
distances.append(simple_distance_2d(finger_tips[i], finger_tips[i + 1]))
thumb_tip = finger_tips[0]
index_tip = finger_tips[1]
middle_tip = finger_tips[2]
distances.extend([
simple_distance_2d(thumb_tip, index_tip),
simple_distance_2d(thumb_tip, middle_tip)
])
hand_scale = calculate_hand_scale(landmarks)
if hand_scale < 0.001:
return 0.0
normalized = [d / hand_scale for d in distances]
weights = [1.0, 1.0, 1.0, 1.0, 0.8, 0.6]
weighted_sum = sum(v * w for v, w in zip(normalized, weights))
avg_openness = weighted_sum / sum(weights)
return min(1.0, avg_openness * 0.5)
def calculate_thumb_separation(landmarks):
thumb_tip = landmarks.landmark[4]
index_tip = landmarks.landmark[8]
separation = simple_distance_2d(thumb_tip, index_tip)
hand_scale = calculate_hand_scale(landmarks)
return min(1.0, (separation / hand_scale) * 0.8)
def classify_gesture_simple(finger_curls, hand_openness, thumb_separation):
# 実測レンジに基づく再設計ルール
thumb = finger_curls[0]
idx, mid, rng, pky = finger_curls[1], finger_curls[2], finger_curls[3], finger_curls[4]
def curled_other(v): # 他指の屈曲
return v >= FINGER_CURL_HIGH
def extended_other(v): # 他指の伸展
return v < FINGER_CURL_LOW
def other_min_bend(v): # 他指の下限のみ(サムズアップ用)
return v >= FINGER_CURL_LOW
def thumb_extended(v): # 親指の伸展
return v < THUMB_CURL_EXT
def thumb_curled(v): # 親指の屈曲扱い
return v >= THUMB_CURL_BENT
# 1) OKサイン(親指-人差し指が近い。indexやや曲げ、他指は伸展寄り)
if thumb_separation < THUMB_SEP_OK and idx >= 0.25 and mid < 0.45 and rng < 0.35 and pky < 0.35:
return "OKサイン"
# 2) サムズアップ(親指が伸展・分離。他4指は下限のみ要求 >=0.25,上限なし)
if thumb_extended(thumb) and thumb_separation > THUMB_SEP_UP and all(other_min_bend(v) for v in [idx, mid, rng, pky]):
return "サムズアップ"
# 3) 指差し(indexが伸展,他3指は屈曲)
if extended_other(idx) and all(curled_other(v) for v in [mid, rng, pky]):
return "指差し"
# 4) ピースサイン(index/middleが伸展,ring/pinkyが屈曲)
if extended_other(idx) and extended_other(mid) and curled_other(rng) and curled_other(pky):
return "ピースサイン"
# 5) 握りこぶし(他指が屈曲,開きは大きすぎない)
if all(curled_other(v) for v in [idx, mid, rng, pky]) and hand_openness < OPEN_HAND_HIGH:
return "握りこぶし"
# 6) 開いた手(開きが大きく,他指の多くが伸展)
if hand_openness > OPEN_HAND_HIGH and sum(extended_other(v) for v in [idx, mid, rng, pky]) >= 3:
return "開いた手"
return "その他"
def simple_smoothing(history, weights):
if len(history) == 0:
return []
actual_length = min(len(history), len(weights))
if actual_length == 1:
return history[-1]
recent_history = history[-actual_length:]
used_weights = weights[-actual_length:]
weight_sum = used_weights.sum()
if weight_sum > 0:
used_weights = used_weights / weight_sum
else:
used_weights = np.ones(actual_length) / actual_length
smoothed_landmarks = []
for landmark_idx in range(21):
smooth_x = 0.0
smooth_y = 0.0
smooth_z = 0.0
for i in range(actual_length):
weight = used_weights[i]
landmark = recent_history[i].landmark[landmark_idx]
smooth_x += landmark.x * weight
smooth_y += landmark.y * weight
smooth_z += landmark.z * weight
class SimpleLandmark:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
smoothed_landmarks.append(SimpleLandmark(smooth_x, smooth_y, smooth_z))
class SimpleHandLandmarks:
def __init__(self, landmarks):
self.landmark = landmarks
return SimpleHandLandmarks(smoothed_landmarks)
def update_feature_history_safe(feature_history, hand_side, overall_curl, hand_openness, frame_count):
feature_history[hand_side]['overall_curl'].append(overall_curl)
feature_history[hand_side]['hand_openness'].append(hand_openness)
feature_history[hand_side]['frame_numbers'].append(frame_count)
max_len = max(len(feature_history[hand_side]['overall_curl']),
len(feature_history[hand_side]['hand_openness']),
len(feature_history[hand_side]['frame_numbers']))
if max_len > HISTORY_MAX_LENGTH:
excess = max_len - HISTORY_MAX_LENGTH
for _ in range(excess):
if feature_history[hand_side]['overall_curl']:
feature_history[hand_side]['overall_curl'].pop(0)
if feature_history[hand_side]['hand_openness']:
feature_history[hand_side]['hand_openness'].pop(0)
if feature_history[hand_side]['frame_numbers']:
feature_history[hand_side]['frame_numbers'].pop(0)
frame_count = 0
results_log = []
def video_frame_processing(frame, hands, landmark_history, feature_history, gesture_log, frame_count):
current_time = time.time()
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = hands.process(rgb_frame)
h, w, _ = frame.shape
# タイトル
frame = draw_japanese_text(frame, "手指ジェスチャー認識", (10, 10), font_large, (0, 255, 0))
detected_hands = 0
result_text = ""
if results.multi_hand_landmarks:
for hand_landmarks, handedness in zip(results.multi_hand_landmarks, results.multi_handedness):
detected_hands += 1
hand_label = "右手" if handedness.classification[0].label == "Right" else "左手"
hand_side = handedness.classification[0].label
# スムージング
landmark_history[hand_side].append(hand_landmarks)
if len(landmark_history[hand_side]) > SMOOTHING_FRAMES:
landmark_history[hand_side].pop(0)
smoothed_landmarks = simple_smoothing(landmark_history[hand_side], SMOOTHING_WEIGHTS)
# 特徴量
finger_curls = [calculate_finger_curl_simple(smoothed_landmarks, i) for i in range(5)]
hand_openness = calculate_hand_openness(smoothed_landmarks)
thumb_separation = calculate_thumb_separation(smoothed_landmarks)
hand_scale = calculate_hand_scale(smoothed_landmarks)
# 判定
gesture_type = classify_gesture_simple(finger_curls, hand_openness, thumb_separation)
overall_curl = sum(finger_curls) / len(finger_curls)
# 履歴更新
update_feature_history_safe(feature_history, hand_side, overall_curl, hand_openness, frame_count)
finger_curl_dict = {FINGER_NAMES[i]: finger_curls[i] for i in range(5)}
# ランドマーク描画
for i, landmark in enumerate(hand_landmarks.landmark):
x = int(landmark.x * w)
y = int(landmark.y * h)
z = landmark.z
radius = max(2, int(8 * (1 - min(abs(z), 1))))
if i == 0:
color = COLORS['wrist']
elif 1 <= i <= 4:
color = COLORS['thumb']
elif 5 <= i <= 8:
color = COLORS['index']
elif 9 <= i <= 12:
color = COLORS['middle']
elif 13 <= i <= 16:
color = COLORS['ring']
elif 17 <= i <= 20:
color = COLORS['pinky']
else:
color = COLORS['palm']
cv2.circle(frame, (x, y), radius, color, -1)
# 接続線描画
mp_drawing.draw_landmarks(
frame, hand_landmarks, mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style()
)
# 手ごとの表示
x_offset = 10 if hand_side == "Left" else w // 2 + 10
y_offset = 50
frame = draw_japanese_text(frame, f"{hand_label} - {gesture_type}", (x_offset, y_offset), font_medium, (255, 255, 0))
y_offset += 30
frame = draw_japanese_text(frame, "指の曲がり具合:", (x_offset, y_offset), font_small, (255, 255, 255))
y_offset += 20
for finger_name in FINGER_NAMES:
curl_value = finger_curl_dict[finger_name]
bar_length = int(curl_value * 100)
frame = draw_japanese_text(frame, f"{finger_name}:", (x_offset, y_offset), font_tiny, (200, 200, 200))
cv2.rectangle(frame, (x_offset + 70, y_offset), (x_offset + 70 + bar_length, y_offset + 10),
(0, 255, 0) if curl_value < 0.5 else (255, 255, 0), -1)
frame = draw_japanese_text(frame, f"{curl_value:.2f}", (x_offset + 175, y_offset), font_tiny, (200, 200, 200))
y_offset += 15
y_offset += 10
frame = draw_japanese_text(frame, f"手の開き具合: {hand_openness:.3f}", (x_offset, y_offset), font_tiny, (200, 200, 200))
y_offset += 15
frame = draw_japanese_text(frame, f"親指分離度: {thumb_separation:.3f}", (x_offset, y_offset), font_tiny, (200, 200, 200))
y_offset += 15
frame = draw_japanese_text(frame, f"全体的曲がり: {overall_curl:.2f}", (x_offset, y_offset), font_tiny, (200, 200, 200))
y_offset += 15
frame = draw_japanese_text(frame, f"手のサイズ: {hand_scale:.3f}", (x_offset, y_offset), font_tiny, (200, 200, 200))
# グラフ(表示のみ)
if len(feature_history[hand_side]['overall_curl']) > 5:
graph_x = 10 if hand_side == "Left" else w - GRAPH_WIDTH - 20
graph_y = h - 250
cv2.rectangle(frame, (graph_x, graph_y), (graph_x + GRAPH_WIDTH, graph_y + GRAPH_HEIGHT), (50, 50, 50), -1)
cv2.rectangle(frame, (graph_x, graph_y), (graph_x + GRAPH_WIDTH, graph_y + GRAPH_HEIGHT), (255, 255, 255), 1)
curl_data = feature_history[hand_side]['overall_curl']
frames = feature_history[hand_side]['frame_numbers']
min_length = min(len(curl_data), len(frames))
if min_length > 1:
curl_data = curl_data[-min_length:]
frames = frames[-min_length:]
frame_range = frames[-1] - frames[0]
if frame_range > 0:
for i in range(1, min_length):
x1 = graph_x + int((frames[i-1] - frames[0]) * GRAPH_WIDTH / frame_range)
y1 = graph_y + GRAPH_HEIGHT - int(curl_data[i-1] * GRAPH_HEIGHT)
x2 = graph_x + int((frames[i] - frames[0]) * GRAPH_WIDTH / frame_range)
y2 = graph_y + GRAPH_HEIGHT - int(curl_data[i] * GRAPH_HEIGHT)
x1 = max(graph_x, min(x1, graph_x + GRAPH_WIDTH))
x2 = max(graph_x, min(x2, graph_x + GRAPH_WIDTH))
y1 = max(graph_y, min(y1, graph_y + GRAPH_HEIGHT))
y2 = max(graph_y, min(y2, graph_y + GRAPH_HEIGHT))
cv2.line(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
graph2_y = graph_y + GRAPH_HEIGHT + 30
cv2.rectangle(frame, (graph_x, graph2_y), (graph_x + GRAPH_WIDTH, graph2_y + GRAPH_HEIGHT), (50, 50, 50), -1)
cv2.rectangle(frame, (graph_x, graph2_y), (graph_x + GRAPH_WIDTH, graph2_y + GRAPH_HEIGHT), (255, 255, 255), 1)
openness_data = feature_history[hand_side]['hand_openness']
min_length_2 = min(len(openness_data), len(frames))
if min_length_2 > 1 and (frames[-1] - frames[0]) > 0:
openness_data = openness_data[-min_length_2:]
frame_range = frames[-1] - frames[0]
for i in range(1, min_length_2):
x1 = graph_x + int((frames[i-1] - frames[0]) * GRAPH_WIDTH / frame_range)
y1 = graph2_y + GRAPH_HEIGHT - int(openness_data[i-1] * GRAPH_HEIGHT)
x2 = graph_x + int((frames[i] - frames[0]) * GRAPH_WIDTH / frame_range)
y2 = graph2_y + GRAPH_HEIGHT - int(openness_data[i] * GRAPH_HEIGHT)
x1 = max(graph_x, min(x1, graph_x + GRAPH_WIDTH))
x2 = max(graph_x, min(x2, graph_x + GRAPH_WIDTH))
y1 = max(graph2_y, min(y1, graph2_y + GRAPH_HEIGHT))
y2 = max(graph2_y, min(y2, graph2_y + GRAPH_HEIGHT))
cv2.line(frame, (x1, y1), (x2, y2), (255, 255, 0), 2)
frame = draw_japanese_text(frame, f"{hand_label} - 曲がり", (graph_x, graph_y - 15), font_tiny, (255, 255, 255))
frame = draw_japanese_text(frame, f"{hand_label} - 開き", (graph_x, graph2_y - 15), font_tiny, (255, 255, 255))
# ログ(CSVは終了時にgesture_logから出力)
gesture_log.append({
'frame': frame_count,
'hand': hand_label,
'gesture_type': gesture_type,
'finger_curls': finger_curl_dict,
'overall_curl': overall_curl,
'hand_openness': hand_openness,
'thumb_separation': thumb_separation,
'hand_scale': hand_scale,
'timestamp': datetime.now().isoformat()
})
# 要約テキスト構築
if result_text:
result_text += " | "
result_text += f"{hand_label}:{gesture_type}"
if not result_text:
result_text = "手が検出されていません"
# フレーム番号/検出数と、認識結果の要約を表示(両手分)
frame = draw_japanese_text(frame, f"フレーム: {frame_count} | 検出: {detected_hands}手", (10, 30), font_small, (0, 255, 255))
frame = draw_japanese_text(frame, f"認識結果: {result_text}", (10, 45), font_small, (0, 200, 255))
return frame, result_text, current_time
# 起動時の説明
print("=== 手指ジェスチャー分析システム ===")
print("概要: MediaPipe Handsで手指21点を検出し、距離比率によりcurl/openness等を算出してルール判定を行う。")
print("\n定義:")
print("- curl(0〜1): base→tipの直線距離とbase→PIP→DIP→tipの経路長の比から算出(大きいほど屈曲)。")
print("- openness(0〜1): 指先間距離の手スケール正規化の重み付き平均。")
print("- thumb_separation(0〜1): 親指先端-人差し指先端の距離を手スケールで正規化。")
print("- overall_curl: 5指curlの平均。")
print("\n実測観測(しきい値設計の根拠):")
print("- 親指: 最大屈曲でも curl ≈ 0.3")
print("- 他指: 全屈曲で curl ≈ 0.5〜0.6、部分屈曲で 0.2〜0.4")
print("\nしきい値(暫定):")
print(f"- FINGER_CURL_LOW={FINGER_CURL_LOW}, FINGER_CURL_HIGH={FINGER_CURL_HIGH}")
print(f"- THUMB_CURL_EXT={THUMB_CURL_EXT}, THUMB_CURL_BENT={THUMB_CURL_BENT}")
print(f"- OPEN_HAND_HIGH={OPEN_HAND_HIGH}, THUMB_SEP_OK={THUMB_SEP_OK}, THUMB_SEP_UP={THUMB_SEP_UP}")
print("\nジェスチャー判定ルール(簡約):")
print("- OKサイン: thumb_separation < THUMB_SEP_OK ほか")
print("- サムズアップ: 親指伸展・分離 + 他4指のcurlは下限のみ要求(>=0.25,上限なし)")
print("- 指差し: index伸展 他3指屈曲")
print("- ピースサイン: index/middle伸展 ring/pinky屈曲")
print("- 握りこぶし: 他指屈曲かつ開きが大きすぎない")
print("- 開いた手: 開きが大きく 他指の多くが伸展")
print("\n操作方法:")
print("- 'q'キー: プログラム終了")
print("- 's'キー: 特徴量をJSONファイルに保存")
print("\n出力:")
print("- 画面: 認識結果の要約(右手/左手)と各特徴量・グラフ(表示のみ)")
print("- ファイル: 終了時にresult.txtへCSVで計測ログのみ保存(集計なし)\n")
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")
choice = input("選択: ")
# MediaPipe初期化
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
hands = mp_hands.Hands(
static_image_mode=False,
max_num_hands=MAX_NUM_HANDS,
min_detection_confidence=HAND_CONFIDENCE,
min_tracking_confidence=TRACKING_CONFIDENCE,
model_complexity=MODEL_COMPLEXITY
)
# 履歴管理
landmark_history = {'Left': [], 'Right': []}
feature_history = {
'Left': {'overall_curl': [], 'hand_openness': [], 'frame_numbers': []},
'Right': {'overall_curl': [], 'hand_openness': [], 'frame_numbers': []}
}
gesture_log = []
# 入力ソース
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)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
else:
SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
SAMPLE_FILE = 'vtest.avi'
urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
cap = cv2.VideoCapture(SAMPLE_FILE)
if not cap.isOpened():
print('動画ファイル・カメラを開けませんでした')
exit()
# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法: q=終了 / s=JSON保存')
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
MAIN_FUNC_DESC = "手指ジェスチャー認識"
processed_frame, result, current_time = video_frame_processing(frame, hands, landmark_history, feature_history, gesture_log, frame_count)
cv2.imshow(MAIN_FUNC_DESC, processed_frame)
if choice == '1':
print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
else:
print(frame_count, result)
results_log.append(result)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s'):
filename = f"gesture_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(gesture_log, f, ensure_ascii=False, indent=2)
print(f"\n特徴量を {filename} に保存しました")
results_log.append(f"特徴量を {filename} に保存しました")
finally:
print('\n=== プログラム終了 ===')
cap.release()
cv2.destroyAllWindows()
hands.close()
# 計測値のCSVをresult.txtに書き出し(UTF-8, カメラ=timestamp、動画=frame)
header_camera = ['timestamp', 'hand', 'gesture', 'overall_curl', 'hand_openness', 'thumb_separation', 'hand_scale',
'curl_thumb', 'curl_index', 'curl_middle', 'curl_ring', 'curl_pinky']
header_video = ['frame', 'hand', 'gesture', 'overall_curl', 'hand_openness', 'thumb_separation', 'hand_scale',
'curl_thumb', 'curl_index', 'curl_middle', 'curl_ring', 'curl_pinky']
with open('result.txt', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
if choice == '1':
writer.writerow(header_camera)
else:
writer.writerow(header_video)
for rec in gesture_log:
curls = rec['finger_curls']
if choice == '1':
row = [
rec['timestamp'],
rec['hand'],
rec['gesture_type'],
rec['overall_curl'],
rec['hand_openness'],
rec['thumb_separation'],
rec['hand_scale'],
curls['親指'],
curls['人差し指'],
curls['中指'],
curls['薬指'],
curls['小指'],
]
else:
row = [
rec['frame'],
rec['hand'],
rec['gesture_type'],
rec['overall_curl'],
rec['hand_openness'],
rec['thumb_separation'],
rec['hand_scale'],
curls['親指'],
curls['人差し指'],
curls['中指'],
curls['薬指'],
curls['小指'],
]
writer.writerow(row)
print('\n計測結果をresult.txtに保存しました')