MediaPipeを使用して誰でも簡単にハンドトラッキングゲームの作成手順

# MediaPipeとは

Googleが提供するライブストリーミングのためのオープンソースのMLソリューションです。このMediaPipeを利用すると高性能なAI画像処理アルゴリズムを利用したARアプリケーション等を簡単に作成できます。

詳細はMediaPipeの[公式サイト](https://ai.google.dev/edge/mediapipe/solutions/guide?hl=ja)でご確認ください。

# 環境構築

Android, Web, Python, iOS	に対応しているそうですが、今回はPythonを使って仮想環境を作って開発していしていきます。

## Pythonのインストール
公式サイトから最新のPythonをダウンロードしてインストールします。
インストール方法は別のサイトで手順を確認してください。
インストール時に「Add Python to PATH」にチェックを入れておいてください。

## 仮想環境の作成

仮想環境とはパソコンの中に仮想的な環境を作ることで、いろんなバージョンのpythonやライブラリを使えるようにすることです。

以下の手順を追ってください。

コマンドプロンプトなどを開いて、自分が作成したいプロジェクトディレクトリにいき、以下のコマンドで仮想環境を作成します。

```bash
python -m venv myenv
```

これでmyenvという名前の仮想環境が構築されます。

## 仮想環境の有効化

同じディレクトリ内にて、以下のコマンドで打って仮想環境を有効化します。
Windows:

```bash
myenv\Scripts\activate
```

macOS/Linux:

```bash
source myenv/bin/activate
```

## 必要なパッケージのインストール

仮想環境を有効化した状態で、以下のコマンドでライブラリをインストールします。
```bash
pip install mediapipe opencv-python
```

これで、mediapipeとopencvのライブラリを仮想環境上にインストールされます。


# ゲームの実装

・ゲームの説明
目的: 右手の人差し指で赤い丸に触れてスコアを稼ぐ。
制限時間: 30秒。
操作:
'f' キーでフルスクリーンとウィンドウモードを切り替え。
'q' キーでゲームを終了。（30秒たつと自動的に終了はします。）

```python
import cv2
import mediapipe as mp
import random
import math
import time

# MediaPipe Handsのセットアップ
mp_hands = mp.solutions.hands
hands = mp_hands.Hands()
mp_drawing = mp.solutions.drawing_utils

# スコアの初期化
score = 0

# ランダムに赤い丸を表示するための初期位置
circle_x = random.randint(100, 500)
circle_y = random.randint(100, 400)
circle_radius = 20

# カメラからの映像をキャプチャ
cap = cv2.VideoCapture(0)

# OpenCVのウィンドウを作成
cv2.namedWindow('Hand Game', cv2.WINDOW_NORMAL)
is_fullscreen = False

# ゲームの制限時間（秒）
game_duration = 30
start_time = time.time()

def is_hand_touching_circle(hand_x, hand_y, circle_x, circle_y, circle_radius):
    distance = math.sqrt((hand_x - circle_x) ** 2 + (hand_y - circle_y) ** 2)
    return distance < circle_radius

def generate_new_circle_position(frame_width, frame_height, circle_radius, exclude_area):
    while True:
        new_x = random.randint(circle_radius, frame_width - circle_radius)
        new_y = random.randint(circle_radius, frame_height - circle_radius)
        if not (exclude_area[0] <= new_x <= exclude_area[2] and exclude_area[1] <= new_y <= exclude_area[3]):
            return new_x, new_y

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # 画像を水平方向に反転
    frame = cv2.flip(frame, 1)

    # 画像をRGBに変換
    image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

    # 手のランドマークの検出
    results = hands.process(image)

    # 画像をBGRに戻す
    frame = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

    # スコアとタイマー表示領域の背景を描画
    cv2.rectangle(frame, (0, 0), (200, 100), (0, 0, 0), -1)

    # 検出結果がある場合、ランドマークを描画し、右手の位置を取得
    if results.multi_hand_landmarks:
        for idx, hand_landmarks in enumerate(results.multi_hand_landmarks):
            mp_drawing.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)

            # ランドマークの座標を取得（例として8番目のランドマーク=右手の人差し指の先端を使用）
            hand_x = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].x * frame.shape[1])
            hand_y = int(hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP].y * frame.shape[0])

            if is_hand_touching_circle(hand_x, hand_y, circle_x, circle_y, circle_radius):
                score += 1
                # 新しい位置に赤い丸を再配置
                circle_x, circle_y = generate_new_circle_position(frame.shape[1], frame.shape[0], circle_radius, (0, 0, 200, 100))

    # 赤い丸を描画
    cv2.circle(frame, (circle_x, circle_y), circle_radius, (0, 0, 255), -1)

    # スコアを表示
    cv2.putText(frame, f'Score: {score}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)

    # 残り時間を計算
    elapsed_time = time.time() - start_time
    remaining_time = max(0, game_duration - int(elapsed_time))
    cv2.putText(frame, f'Time: {remaining_time}', (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)

    # ゲーム終了条件
    if remaining_time <= 0:
        break

    # 画像を表示
    cv2.imshow('Hand Game', frame)

    # キー入力をチェック
    key = cv2.waitKey(10) & 0xFF
    if key == ord('q'):
        break
    elif key == ord('f'):
        is_fullscreen = not is_fullscreen
        if is_fullscreen:
            cv2.setWindowProperty('Hand Game', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
        else:
            cv2.setWindowProperty('Hand Game', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL)

# リソースの解放
cap.release()
cv2.destroyAllWindows()

# ゲーム終了メッセージ
print(f'Game Over! Your score is {score}')
```
取得できるlandmarkは以下の通りです。[参考](https://ai.google.dev/edge/mediapipe/solutions/vision/hand_landmarker?hl=ja)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/4c86908a-078b-1bd4-310a-d45388e69c73.png)


## 動作確認

上記のファイルを仮想環境が作られたディレクトリ内で作成して、仮想環境を使って実行すれば動きます。VScodeではRun Python File in Dedicated Terminalを押せば仮想環境で実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/57814cd7-2e9e-2eb5-b4af-30ed8db8c76d.png)

**結果(動作例)**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3794915/97ea492d-193f-909d-e1ec-6e0ff2062bfe.png)



# 感想
こんなにも簡単に、しかもローカルでハンドトラッキングができることに驚きました。他にも、いろんな機能があるようなので試してみようと思います。

[次回の記事](https://qiita.com/HayatoF/items/1a660ff7008b47b0a20a)でじゃんけんの手の判別をしてみたという記事も書いています。よければ見てみてください。