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

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

手指ジェスチャー分析システム

概要

カメラから取得した画像データから手の形状を認識する機能を実装している。本プログラムは、リアルタイム画像認識によって人間の手指を認識する。カメラ映像から手の21個の特徴点(ランドマーク)を3次元座標として検出し、これらの点から指の曲がり具合、手のひらの曲率、親指の開き角度などの特徴量を計算する。これらの特徴量を組み合わせることで、「開いた手」「握りこぶし」「指差し」「ピースサイン」といったジェスチャーを分類する。

主要技術

参考文献

[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.

[2] Rautaray, S. S., & Agrawal, A. (2015). Vision based hand gesture recognition for human computer interaction: a survey. Artificial Intelligence Review, 43(1), 1-54.


# 手指ジェスチャー分析システム
# 特徴技術名: 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 preprint arXiv:2006.10214.
# 特徴機能: 単一RGB画像から21点の3次元手指ランドマークをリアルタイム推定し、指の曲がり具合と手のひらの曲率を定量化
# 学習済みモデル: hand_landmark_lite.tflite(軽量版、約2MB)- 21点の手指ランドマーク検出用CNNモデル、https://github.com/google/mediapipe/tree/master/mediapipe/modules/hand_landmark
# 方式設計:
#   - 関連利用技術:
#     - OpenCV: カメラ入力と画像表示(BGR形式の画像処理)
#     - NumPy: ベクトル演算と角度計算(3次元座標の数値処理)
#     - Pillow: 日本語テキスト描画(TrueTypeフォント対応)
#   - 入力と出力: 入力: 動画(ユーザは「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)MediaPipeで手検出と21点推定 3)3D座標から指の屈曲角度計算 4)手のひら平面からの偏差計算 5)ジェスチャー分類 6)特徴量の可視化
#   - 前処理、後処理: 前処理: 時系列スムージング(過去3フレームの移動平均)、後処理: 特徴量のJSON形式での保存
#   - 追加処理: 指の曲がり具合の0-1正規化(角度を0度=0、180度=1に変換)、手のひら曲率の算出(MCP関節群の平面からの距離計算)
#   - 調整を必要とする設定値: HAND_CONFIDENCE(手検出の信頼度閾値、デフォルト0.7、範囲0.0-1.0)- 低すぎると誤検出増加、高すぎると検出失敗増加
# 将来方策: HAND_CONFIDENCE(手検出の信頼度閾値)の最適値を自動探索する機能。検出失敗が続いた場合に0.7から0.1刻みで低下させ、検出成功率が最も高い値を記録・推奨する機能の実装
# その他の重要事項: z座標は画像幅で正規化されており、x,y座標と同じスケール
# 前準備: pip install mediapipe opencv-python numpy pillow

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

# 調整可能な設定値
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:標準、2:高精度)

# 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)   # 手首 - グレー
}

# 指の関節チェーン定義
FINGER_CHAINS = [
    [1, 2, 3, 4],      # 親指
    [5, 6, 7, 8],      # 人差し指
    [9, 10, 11, 12],   # 中指
    [13, 14, 15, 16],  # 薬指
    [17, 18, 19, 20]   # 小指
]

FINGER_NAMES = ['親指', '人差し指', '中指', '薬指', '小指']

# 日本語フォント設定
font_large = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", FONT_SIZES['large'])
font_medium = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", FONT_SIZES['medium'])
font_small = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", FONT_SIZES['small'])
font_tiny = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", 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)

# プログラム開始時の説明
print("=== 手指ジェスチャー分析システム ===")
print("概要: MediaPipe Handsを使用して手指の21点ランドマークを検出し、")
print("      指の曲がり具合と手のひらの曲率を定量化します。")
print("\n操作方法:")
print("- 'q'キー: プログラム終了")
print("- 's'キー: 特徴量をJSONファイルに保存")
print("\n処理結果は1秒ごとにコンソールに表示され、")
print("終了時にresult.txtに保存されます。\n")

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':
    # サンプル動画ダウンロード
    url = "https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi"
    filename = "vtest.avi"
    try:
        print("サンプル動画をダウンロード中...")
        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_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  # 精度向上のため1に設定
)

# 履歴管理(左右の手別)
landmark_history = {'Left': [], 'Right': []}
feature_history = {
    'Left': {'overall_curl': [], 'thumb_spread': [], 'frame_numbers': []},
    'Right': {'overall_curl': [], 'thumb_spread': [], 'frame_numbers': []}
}
gesture_log = []
frame_count = 0
start_time = time.time()
last_print_time = start_time
console_output = []  # コンソール出力を保存

print("\n処理を開始します...")
print("'q'キーで終了、's'キーで特徴量を保存\n")

try:
    while True:
        cap.grab()
        ret, frame = cap.retrieve()
        if not ret:
            break

        frame_count += 1
        current_time = time.time()
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = hands.process(rgb_frame)

        # タイトル描画
        frame = draw_japanese_text(frame, "手指ジェスチャー認識", (10, 10), font_large, (0, 255, 0))

        detected_hands = 0
        current_features = []

        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)

                points_3d = []
                if len(landmark_history[hand_side]) >= SMOOTHING_FRAMES:
                    # 重み付き移動平均
                    weights = SMOOTHING_WEIGHTS[-len(landmark_history[hand_side]):]
                    weights = weights / weights.sum()  # 正規化

                    for i in range(21):
                        avg_x = sum(lm.landmark[i].x * w for lm, w in zip(landmark_history[hand_side], weights))
                        avg_y = sum(lm.landmark[i].y * w for lm, w in zip(landmark_history[hand_side], weights))
                        avg_z = sum(lm.landmark[i].z * w for lm, w in zip(landmark_history[hand_side], weights))
                        points_3d.append([avg_x, avg_y, avg_z])
                else:
                    for lm in hand_landmarks.landmark:
                        points_3d.append([lm.x, lm.y, lm.z])

                # 指の曲がり具合計算
                finger_curls = {}
                for i, finger_name in enumerate(FINGER_NAMES):
                    chain = FINGER_CHAINS[i]
                    base = np.array(points_3d[chain[0]])
                    middle = np.array(points_3d[chain[1]])
                    tip = np.array(points_3d[chain[3]])

                    v1 = middle - base
                    v2 = tip - middle

                    norm1 = np.linalg.norm(v1)
                    norm2 = np.linalg.norm(v2)

                    if norm1 > 0 and norm2 > 0:
                        v1_norm = v1 / norm1
                        v2_norm = v2 / norm2
                        dot_product = np.clip(np.dot(v1_norm, v2_norm), -1.0, 1.0)
                        angle_rad = np.arccos(dot_product)
                        finger_curls[finger_name] = float(angle_rad / np.pi)
                    else:
                        finger_curls[finger_name] = 0.0

                # 手のひらの曲率計算
                wrist = np.array(points_3d[0])
                thumb_mcp = np.array(points_3d[2])
                index_mcp = np.array(points_3d[5])
                middle_mcp = np.array(points_3d[9])
                ring_mcp = np.array(points_3d[13])
                pinky_mcp = np.array(points_3d[17])

                mcp_points = [index_mcp, middle_mcp, ring_mcp, pinky_mcp]
                mcp_center = np.mean(mcp_points, axis=0)

                palm_normal_v1 = mcp_center - wrist
                palm_normal_v2 = pinky_mcp - index_mcp
                palm_normal = np.cross(palm_normal_v1, palm_normal_v2)

                norm = np.linalg.norm(palm_normal)
                if norm > 0:
                    palm_normal = palm_normal / norm

                distances = []
                for mcp in mcp_points:
                    vec_to_mcp = mcp - wrist
                    distance = abs(np.dot(vec_to_mcp, palm_normal))
                    distances.append(distance)

                mean_curvature = float(np.mean(distances))

                # 親指の開き角度
                thumb_vec = thumb_mcp - wrist
                index_vec = index_mcp - wrist

                thumb_norm = np.linalg.norm(thumb_vec)
                index_norm = np.linalg.norm(index_vec)

                thumb_spread_angle = 0.0
                if thumb_norm > 0 and index_norm > 0:
                    thumb_vec_norm = thumb_vec / thumb_norm
                    index_vec_norm = index_vec / index_norm
                    dot = np.clip(np.dot(thumb_vec_norm, index_vec_norm), -1.0, 1.0)
                    thumb_spread_angle = float(math.degrees(np.arccos(dot)))

                # ジェスチャー分類
                overall_curl = float(np.mean(list(finger_curls.values())))

                if overall_curl < 0.2:
                    gesture_type = "開いた手"
                elif overall_curl > 0.7:
                    gesture_type = "握りこぶし"
                elif finger_curls['人差し指'] < 0.3 and overall_curl > 0.5:
                    gesture_type = "指差し"
                elif finger_curls['親指'] < 0.3 and finger_curls['人差し指'] < 0.3 and overall_curl > 0.5:
                    gesture_type = "ピースサイン"
                else:
                    gesture_type = "その他"

                # 特徴量履歴更新
                feature_history[hand_side]['overall_curl'].append(overall_curl)
                feature_history[hand_side]['thumb_spread'].append(thumb_spread_angle)
                feature_history[hand_side]['frame_numbers'].append(frame_count)

                if len(feature_history[hand_side]['overall_curl']) > HISTORY_MAX_LENGTH:
                    feature_history[hand_side]['overall_curl'].pop(0)
                    feature_history[hand_side]['thumb_spread'].pop(0)
                    feature_history[hand_side]['frame_numbers'].pop(0)

                # 現在の特徴量を保存
                current_features.append({
                    'hand': hand_label,
                    'gesture': gesture_type,
                    'overall_curl': overall_curl,
                    'palm_curvature': mean_curvature,
                    'thumb_spread': thumb_spread_angle,
                    'finger_curls': finger_curls
                })

                # ランドマーク描画
                h, w, _ = frame.shape
                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()
                )

                # 特徴量表示
                # 左手は左側、右手は右側に表示
                if hand_side == "Left":
                    x_offset = 10
                else:
                    x_offset = 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, curl_value in finger_curls.items():
                    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"手のひら曲率: {mean_curvature:.3f}", (x_offset, y_offset), font_tiny, (200, 200, 200))
                y_offset += 15
                frame = draw_japanese_text(frame, f"親指の開き: {thumb_spread_angle:.1f}°", (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))

                # グラフ描画(左右別)
                if len(feature_history[hand_side]['overall_curl']) > 10:
                    # グラフ位置(左手は左下、右手は右下)
                    if hand_side == "Left":
                        graph_x = 10
                    else:
                        graph_x = w - GRAPH_WIDTH - 20

                    # グラフ1: 全体的な曲がり
                    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']

                    if len(frames) > 1:
                        frame_range = frames[-1] - frames[0]
                        if frame_range > 0:
                            for i in range(1, len(curl_data)):
                                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)
                                cv2.line(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)

                    # グラフ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)

                    thumb_data = feature_history[hand_side]['thumb_spread']
                    if len(frames) > 1 and frame_range > 0:
                        for i in range(1, len(thumb_data)):
                            x1 = graph_x + int((frames[i-1] - frames[0]) * GRAPH_WIDTH / frame_range)
                            y1 = graph2_y + GRAPH_HEIGHT - int((thumb_data[i-1] / 180.0) * GRAPH_HEIGHT)
                            x2 = graph_x + int((frames[i] - frames[0]) * GRAPH_WIDTH / frame_range)
                            y2 = graph2_y + GRAPH_HEIGHT - int((thumb_data[i] / 180.0) * 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))

                    if len(frames) > 0:
                        frame = draw_japanese_text(frame, f"F:{frames[0]}", (graph_x, graph_y + GRAPH_HEIGHT + 5), font_tiny, (200, 200, 200))
                        frame = draw_japanese_text(frame, f"F:{frames[-1]}", (graph_x + GRAPH_WIDTH - 40, graph_y + GRAPH_HEIGHT + 5), font_tiny, (200, 200, 200))

                # ログ記録
                gesture_log.append({
                    'frame': frame_count,
                    'hand': hand_label,
                    'finger_curls': finger_curls,
                    'palm_curvature': mean_curvature,
                    'thumb_spread_angle': thumb_spread_angle,
                    'overall_curl': overall_curl,
                    'gesture_type': gesture_type,
                    'timestamp': datetime.now().isoformat()
                })

        # 1秒ごとのコンソール出力
        if current_time - last_print_time >= 1.0:
            elapsed_time = current_time - start_time
            output_text = f"\n=== 経過時間: {elapsed_time:.1f}秒 (フレーム {frame_count}) ==="
            print(output_text)
            console_output.append(output_text)

            if detected_hands > 0:
                output_text = f"検出された手: {detected_hands}個"
                print(output_text)
                console_output.append(output_text)

                for feature in current_features:
                    output_text = f"\n{feature['hand']}:"
                    print(output_text)
                    console_output.append(output_text)

                    output_text = f"  ジェスチャー: {feature['gesture']}"
                    print(output_text)
                    console_output.append(output_text)

                    output_text = f"  全体的な曲がり: {feature['overall_curl']:.3f}"
                    print(output_text)
                    console_output.append(output_text)

                    output_text = f"  手のひら曲率: {feature['palm_curvature']:.3f}"
                    print(output_text)
                    console_output.append(output_text)

                    output_text = f"  親指の開き: {feature['thumb_spread']:.1f}°"
                    print(output_text)
                    console_output.append(output_text)

                    output_text = "  指の曲がり具合:"
                    print(output_text)
                    console_output.append(output_text)

                    for finger, curl in feature['finger_curls'].items():
                        output_text = f"    {finger}: {curl:.3f}"
                        print(output_text)
                        console_output.append(output_text)
            else:
                output_text = "手が検出されていません"
                print(output_text)
                console_output.append(output_text)

            last_print_time = current_time

        # フレーム番号と検出数表示
        frame = draw_japanese_text(frame, f"フレーム: {frame_count} | 検出: {detected_hands}手", (10, 30), font_small, (0, 255, 255))

        cv2.imshow('手指ジェスチャー認識', frame)

        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} に保存しました")
            console_output.append(f"\n特徴量を {filename} に保存しました")

finally:
    cap.release()
    cv2.destroyAllWindows()
    hands.close()

    # result.txtに保存
    with open('result.txt', 'w', encoding='utf-8') as f:
        f.write('\n'.join(console_output))
    print("\nresult.txtに保存しました")

    if gesture_log:
        filename = f"gesture_log_final_{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"最終ログを {filename} に保存しました")

    # 一時ファイルの削除
    if temp_file and os.path.exists(temp_file):
        os.remove(temp_file)

    print("\nプログラムを終了しました")