MediaPipeを使って領域展開した(Gesture推定/リアルタイム認識)

この記事はGoogle Developer Student Clubs アドベントカレンダー2023 12/22に寄稿するものとして書かれています。
今回の記事のコードは全てここにあります

https://github.com/SuperHotDogCat/DOMAIN_EXPANSION

# なんだか領域展開がしたい
そんな気分の時があるかと思います。というわけで今回は以下のステップに分けて領域展開を実現します。
1. 領域展開するために手の点群データを取得する
2. TensorFlowで領域展開のジェスチャー判別モデルを制作する
3. TensorFlowLiteにConvertしてリアルタイムに領域展開の処理を可能にする

今回はGesture判定のためにとりあえず脳死でDeepLearningを選びましたが、もし他にパターン認識やルールベースでも領域展開できたよ‼️という人はお知らせ下さい

# 1. 領域展開するために手の点群データを取得する

点群データの取得には[MediaPipe](https://developers.google.com/mediapipe)というライブラリを活用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3311006/f6484f8f-75d8-e3c6-955b-605eae71e10f.png)

MediaPipeはGoogle社が提供するオープンソースの画像処理MLライブラリです。非常に高速に動作することで知られています。

今回はMediaPipeのHand landmark detectionを用いてジェスチャーのデータを集めます。
ジェスチャーデータを集める前に、今回のディレクトリ構造と使いまわす関数などをまとめたutils.pyの説明をしましょう。

今回のDirectory構造
```
.
├── DomainExpansion.py
├── GatherGestureData.py
├── ModelConvert.py
├── README.md
├── TrainGesture.py
├── utils.py
├── assets(DomainExpansion.pyで描画するための素材)
│   ├── domain_expansion.mp4
│   └── domain_expansion.wav
├── gesture_data.bin(GatherGestureData.pyで制作)
├── model(TrainGesture.pyで制作)
└── model.tflite(ModelConvert.pyで制作)
```
## utils.py
```python:utils.py
from typing import List
import numpy as np
import cv2
import mediapipe as mp

def preprocessing_train_data(data: List[dict]):
    output = []
    for datum in data:
        left_data = np.array(datum["Left"])
        right_data = np.array(datum["Right"])
        label = np.array(datum["Label"])
        distance_each_data = (left_data - right_data).reshape(-1) #右手左手のkeypointsの差分を取る
        distance_each_data = distance_each_data / np.max(np.abs(distance_each_data)) #絶対値最大値で正規化
        left_data = left_data - left_data[0] #Keypoints 0からの相対ベクトルを求める
        right_data = right_data - right_data[0] #Keypoints 0からの相対ベクトルを求める
        
        left_data = left_data[1:].reshape(-1) #Keypoints 0を除外する
        left_data = left_data / np.max(np.abs(left_data)) #絶対値最大値で正規化
        right_data = right_data[1:].reshape(-1)
        right_data = right_data / np.max(np.abs(right_data)) #最大値で正規化
        label = label.reshape(-1)

        processed_data = np.concatenate([left_data, right_data, distance_each_data, label], axis = 0)
        output.append(processed_data)
    output = np.array(output) #dim: (data_size, 122)
    processed_data, label = output[:,:-1], output[:,[-1]]
    return processed_data, label

def preprocessing_gesture_data(data: List[dict]):
    #gestureのみの処理
    output = []
    for datum in data:
        left_data = np.array(datum["Left"])
        right_data = np.array(datum["Right"])
        distance_each_data = (left_data - right_data).reshape(-1) #右手左手のkeypointsの差分を取る
        distance_each_data = distance_each_data / np.max(np.abs(distance_each_data)) #絶対値最大値で正規化

        left_data = left_data - left_data[0] #Keypoints 0からの相対ベクトルを求める
        right_data = right_data - right_data[0] #Keypoints 0からの相対ベクトルを求める
        
        left_data = left_data[1:].reshape(-1) #Keypoints 0を除外する
        left_data = left_data / np.max(np.abs(left_data)) #絶対値最大値で正規化
        right_data = right_data[1:].reshape(-1)
        right_data = right_data / np.max(np.abs(right_data)) #最大値で正規化

        processed_data = np.concatenate([left_data, right_data, distance_each_data], axis = 0)
        output.append(processed_data)
    output = np.array(output) #dim: (data_size, 122)
    return output

landmark_line_ids = [ 
    (0, 1), (1, 5), (5, 9), (9, 13), (13, 17), (17, 0),  # 掌
    (1, 2), (2, 3), (3, 4),         # 親指
    (5, 6), (6, 7), (7, 8),         # 人差し指
    (9, 10), (10, 11), (11, 12),    # 中指
    (13, 14), (14, 15), (15, 16),   # 薬指
    (17, 18), (18, 19), (19, 20),   # 小指
]


def draw_keypoints_line(results, img, ):
    # 検出した手の数分繰り返し
    img_h, img_w, _ = img.shape     # サイズ取得
    for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
      # landmarkの繋がりをlineで表示
      for line_id in landmark_line_ids:
        # 1点目座標取得
        lm = hand_landmarks.landmark[line_id[0]]
        lm_pos1 = (int(lm.x * img_w), int(lm.y * img_h))
        # 2点目座標取得
        lm = hand_landmarks.landmark[line_id[1]]
        lm_pos2 = (int(lm.x * img_w), int(lm.y * img_h))
        # line描画
        cv2.line(img, lm_pos1, lm_pos2, (128, 0, 0), 1)

                # landmarkをcircleで表示
        z_list = [lm.z for lm in hand_landmarks.landmark]
        z_min = min(z_list)
        z_max = max(z_list)
        for lm in hand_landmarks.landmark:
          lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
          lm_z = int((lm.z - z_min) / (z_max - z_min) * 255)
          cv2.circle(img, lm_pos, 3, (255, lm_z, lm_z), -1)

          # 検出情報をテキスト出力
          # - テキスト情報を作成
          hand_texts = []
          for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
            hand_texts.append("#%d-%d" % (h_id, c_id)) 
            hand_texts.append("- Index:%d" % (hand_class.index))
            hand_texts.append("- Label:%s" % (hand_class.label))
            hand_texts.append("- Score:%3.2f" % (hand_class.score * 100))
                # - テキスト表示に必要な座標など準備
            lm = hand_landmarks.landmark[0]
            lm_x = int(lm.x * img_w) - 50
            lm_y = int(lm.y * img_h) - 10
            lm_c = (64, 0, 0)
            font = cv2.FONT_HERSHEY_SIMPLEX
            # - テキスト出力
            for cnt, text in enumerate(hand_texts):
              cv2.putText(img, text, (lm_x, lm_y + 10 * cnt), font, 0.3, lm_c, 1)
```
はい, いきなり長すぎコードですがしっかり解説していきます。
・preprocessing_train_data
訓練データの整形に使う関数の中にあるkeypointsは手の点群データであり二次元配列で各点の座標が入っています。配列のindexと各点は以下の関係にあります。
<img src="https://developers.google.com/static/mediapipe/images/solutions/hand-landmarks.png">
今回は領域展開を実装したいわけです, そしてその領域展開は今回は伏魔御厨子にしたいと思います。
![E23RG6IVgAQVcU-.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3311006/cb9b876c-0ea9-59e6-7b39-408f275a3a57.jpeg)
↑この手の形を簡単なDNNモデルを作って認識させます。

そこで, コードを再掲載してpreprocessing_train_data関数では何をしているのかというと, 
```python
def preprocessing_train_data(data: List[dict]):
    output = []
    for datum in data:
        left_data = np.array(datum["Left"])
        right_data = np.array(datum["Right"])
        label = np.array(datum["Label"])
        distance_each_data = (left_data - right_data).reshape(-1) #右手左手のkeypointsの差分を取る
        distance_each_data = distance_each_data / np.max(np.abs(distance_each_data)) #絶対値最大値で正規化
        left_data = left_data - left_data[0] #Keypoints 0からの相対ベクトルを求める
        right_data = right_data - right_data[0] #Keypoints 0からの相対ベクトルを求める
        
        left_data = left_data[1:].reshape(-1) #Keypoints 0を除外する
        left_data = left_data / np.max(np.abs(left_data)) #絶対値最大値で正規化
        right_data = right_data[1:].reshape(-1)
        right_data = right_data / np.max(np.abs(right_data)) #最大値で正規化
        label = label.reshape(-1)

        processed_data = np.concatenate([left_data, right_data, distance_each_data, label], axis = 0)
        output.append(processed_data)
    output = np.array(output) #dim: (data_size, 122)
    processed_data, label = output[:,:-1], output[:,[-1]]
    return processed_data, label
```
preprocessing_train_data関数では
```python
datum = {"Left":左手のKeypointsデータ, "Right": 右手のKeypointsデータ, "Label": 伏魔御厨子してるかしてないかのラベル}
```
というものがListでまとめられたデータが与えられます。
Left, Rightのデータはshapeが(20, 2)の座標データで,Labelは1か0のintです。
distance_each_dataではまず左手の座標から右手の座標を引いて, **両手の位置関係を表す変数**を取り出しています。こうすることで伏魔御厨子の手の形の位置関係に両手がなっているかを確認しています。そのあとは変数の値が大きくなりすぎないように絶対値の最大値で正規化しています。

Left, Rightの処理に関しての処理は, **Keypoints0(手首あたりの点)の点の座標からの相対位置**を用いています。これは, Keypoints0からの相対位置にせずに, Keypointsの絶対座標を変数に用いると、ウィンドウのサイズが変わった時に座標の最大値なども変わり, 精度の低いモデルが出来上がるからです。そのあとはKeypoints0の値を除いて, 変数の値が大きくなりすぎないように絶対値の最大値で正規化しています。

**ここで, いくつかの計算ではnumpyというライブラリのブロードキャスト機能を用いて計算されています。これは調べてもらうのが一番早いですが、for文を使わずに配列の要素を全て一気に演算できる機能だと思ってください。**

ここまでのデータ処理を行えばあとはDNNにいれるだけなので, 前処理した配列をflattenして, 次元数が(42+40+40+1)=(123)次元の配列を作っています。このうち1の1次元がラベルなので,関数の返り値としてはKeypointsのデータとラベルを別々に出力しています。

・preprocessing_gesture_data
preprocessing_train_dataとほぼ同じです。

・draw_keypoints_line
mediapipeの手の点群データの取得情報と描画サイズを元にkeypointsを画面上に描画する関数です。今回は本題ではないのでコピペして使ってください。この関数は手が本当にmediapipeに認識されているのかを確認するためだけに使います。

さて, utils.pyの説明を終えたのでトレーニングデータを集めるコードでも書きましょう。
```python:GatherGestureData.py
import cv2
import mediapipe as mp
from time import time, sleep
import pickle
import pygame
from utils import draw_keypoints_line
#https://tama-ud.hatenablog.com/entry/2023/07/09/030155 mediapipe model maker
#https://qiita.com/Kazuhito/items/222999f134b3b27418cdを参考に作った。


hands = mp.solutions.hands.Hands(
    max_num_hands=2,                # 最大検出数
    min_detection_confidence=0.7,   # 検出信頼度
    min_tracking_confidence=0.7    # 追跡信頼度
)

v_cap = cv2.VideoCapture(0)#カメラのIDを選ぶ。映らない場合は番号を変える。


target_fps = 30
# フレームごとの待機時間を計算
clock = pygame.time.Clock()
all_data = []
while v_cap.isOpened():
  success, img = v_cap.read()
  if not success:
    continue
  img = cv2.flip(img, 1)          # 画像を左右反転
  img_h, img_w, _ = img.shape     # サイズ取得
  results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
  if results.multi_hand_landmarks:
    draw_keypoints_line(results, img,)
    #データの追加に関する処理
    data = {}

    for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
        for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
            positions = [0] * 21
            for idx, lm in enumerate(hand_landmarks.landmark):
                lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
                positions[idx] = lm_pos
            data[hand_class.label] = positions

    #これで2つのデータが入った
        # 画像の表示
  cv2.imshow("MediaPipe Hands", img)
  key = cv2.waitKey(5) & 0xFF
  if key == 27:#ESCキーが押されたら終わる
    with open("gesture_data.bin", "wb") as f:
      pickle.dump(all_data, f)
    break
  if key == ord("1"):
    if len(data.keys()) == 2:
      data["Label"] = 1
      all_data.append(data)
      print(len(all_data))
  if key == ord("0"):
    if len(data.keys()) == 2:
      data["Label"] = 0
      all_data.append(data)
      print(len(all_data))
  clock.tick(target_fps)

v_cap.release()
```
このプログラムではFPS制御用のライブラリとしてpygameを呼び出しています。while文一番最後の```clock.tick(target_fps)```で所望のFPS回数分だけ1秒間にwhile文を繰り返していると考えてください。
さて, コードの解説です。
```python
hands = mp.solutions.hands.Hands(
    max_num_hands=2,                # 最大検出数
    min_detection_confidence=0.7,   # 検出信頼度
    min_tracking_confidence=0.7    # 追跡信頼度
)

v_cap = cv2.VideoCapture(0)#カメラのIDを選ぶ。映らない場合は番号を変える。
```
ここでは画像から手の認識を行うmp.solutions.hands.Handsオブジェクトの初期化とOpenCVでカメラの起動をしています。このプログラム全体では以下の流れに沿ってジェスチャーの認識をしています。
1. カメラから映像を取得(実態は1フレームごとの画像)
2. 取得した映像をmediapipeで手の形を認識
3. 手が二つ認識されたら手の形に応じてラベル付けしたデータを作る
こんな感じになっています。ここまで説明した上でwhile文前半の処理を見ていきましょう。
```python
all_data = []
while v_cap.isOpened():
  success, img = v_cap.read()
  if not success:
    continue
  img = cv2.flip(img, 1)          # 画像を左右反転
  img_h, img_w, _ = img.shape     # サイズ取得
  results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
  if results.multi_hand_landmarks:
    draw_keypoints_line(results, img,)
    #データの追加に関する処理
    data = {}

    for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
        for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
            positions = [0] * 21
            for idx, lm in enumerate(hand_landmarks.landmark):
                lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
                positions[idx] = lm_pos
            data[hand_class.label] = positions
```
まず, all_dataというデータを格納するリストを作ります。その後, v_cap.read()で1フレームごとにカメラの映像を撮ってきて映像の取得が成功したか(success)とimgという取得した画像の実態が得られます。
その後, ```results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))```で手の認識を行っています。手が認識されたら, 追加するデータを辞書型で定義```data = {}```し、```hand_landmarks.landmark```の中にある認識された手の座標情報を```positions```の中に格納してデータにして, 右手か左手かを判別する```hand_class.label```のkeyをつけて辞書に位置情報を追加しています。ここで、作られるデータの形はこんな感じです。
```python
data = {"Left":左手のKeypointsデータ, "Right": 右手のKeypointsデータ}
```
ここまで見たら後半のコードを見てみましょう。インデントが重要なため一部前半のコードも含みます。
```python
if results.multi_hand_landmarks:
    draw_keypoints_line(results, img,)
    #データの追加に関する処理
    data = {}

    for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
        for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
            positions = [0] * 21
            for idx, lm in enumerate(hand_landmarks.landmark):
                lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
                positions[idx] = lm_pos
            data[hand_class.label] = positions

    #これで2つのデータが入った
        # 画像の表示
  cv2.imshow("MediaPipe Hands", img)
  key = cv2.waitKey(5) & 0xFF
  if key == 27:#ESCキーが押されたら終わる
    with open("gesture_data.bin", "wb") as f:
      pickle.dump(all_data, f)
    break
  if key == ord("1"):
    if len(data.keys()) == 2:
      data["Label"] = 1
      all_data.append(data)
      print(len(all_data))
  if key == ord("0"):
    if len(data.keys()) == 2:
      data["Label"] = 0
      all_data.append(data)
      print(len(all_data))
  clock.tick(target_fps)

v_cap.release()
```
後半のコードはまず```key = cv2.waitKey(5) & 0xFF```でキー入力を受け付けます。ここで,**伏魔御厨子をしていたら1を, していなかったら0を押してください**この時にdataのkey数が2, つまり両手が認識されていたらデータが追加されます。非常にシンプルなコードです。

**しかし, ここで大きな問題があります。**
![E23RG6IVgAQVcU-.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3311006/cb9b876c-0ea9-59e6-7b39-408f275a3a57.jpeg)
**伏魔御厨子は両手を使うので他の手段でキーボードの0か1を押さなければなりません。**
これに対する策としては、
1. 友達に押してもらう
2. 足で押す

です。今回は大学で作業していたので1の案を取りました。その時の会話がこちらです。

僕「領域展開するからデータ集め手伝って」
友達「は？」

不均衡ラベルデータではDeepLearningモデルはうまく動作しないので、データはラベルが1のもの, 0のものを1:1になるようにとりましょう。今回は50:50で取りました。データが取り終わったらESCキーを押してください。データが保存されます。
```python
  if key == 27:#ESCキーが押されたら終わる
    with open("gesture_data.bin", "wb") as f:
      pickle.dump(all_data, f)
    break
```

実行している間はこんな感じです。
![SnapShot.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3311006/d98ef20a-157b-de00-008e-6d42206a7233.jpeg)


# 2. TensorFlowで領域展開のジェスチャー判別モデルを制作する

データが集め終わったらTensorflow/Kerasで学習しましょう。今回は簡単なMLPモデルでいいです。
Tensorflowがよくわからない人はTrainGesture.pyをコピペして実行してください。

```Python TrainGesture.py
import pickle
import numpy as np
from utils import preprocessing_train_data

from tensorflow import keras
from tensorflow.keras import models, layers, optimizers
from sklearn.model_selection import train_test_split
#https://qiita.com/yohachi/items/434f0da356161e82c242にしたがってモデルを作る

model = models.Sequential([
    layers.Dense(units=40, activation='relu', input_shape=(122,)),
    layers.Dense(units=20, activation='relu'),
    layers.Dense(units=2, activation='softmax'),
]
)

optimizer = optimizers.Adam(lr = 0.001)

model.compile(loss="sparse_categorical_crossentropy",optimizer=optimizer, metrics=['accuracy'])
if __name__ == "__main__":
    data = pickle.load(open("gesture_data.bin","rb"))
    processed_data, label = preprocessing_train_data(data)
    X_train, X_val, y_train, y_val = train_test_split(processed_data, label, test_size=0.2, random_state=42)
    history = model.fit(X_train, y_train, epochs=100, validation_data=(X_val, y_val))
    model.save("model")
```

上のコードを実行して、検証データ(val data)の正解率が96~100%になるまで学習をしてください。なるべく認識精度が良い方が良いです。
このコードを実行すると, 制作したmodelのディレクトリが保存されます。

# TensorFlowLiteにConvertしてリアルタイムに領域展開の処理を可能にする
さて、ここまででモデルの訓練は終わりました。しかし、Tensorflowのモデルをそのまま使うとリアルタイムでの認識は遅くなってしまうかもしれません。そこで、[TensorflowLite](https://www.tensorflow.org/lite?hl=ja)用のモデルにConvertしましょう。こうすればリアルタイムでも早く認識できるモデルが作れます。
```python:ModelConvert.py
import tensorflow as tf

# モデルをコンバートする
# tf.lite.TFLiteConverter.from_saved_modelの引数である
# saved_model_dirは変換したいモデルのディレクトリ
converter = tf.lite.TFLiteConverter.from_saved_model("model") 
tflite_model = converter.convert() #convert

# 変換したモデルを保存する
with open('model.tflite', 'wb') as f:
    f.write(tflite_model)
```
上のコードを実行すると、ディレクトリの構造は最初に見たようになります。
```
.
├── DomainExpansion.py
├── GatherGestureData.py
├── ModelConvert.py
├── README.md
├── TrainGesture.py
├── utils.py
├── assets(DomainExpansion.pyで描画するための素材)
│   ├── domain_expansion.mp4
│   └── domain_expansion.wav
├── gesture_data.bin(GatherGestureData.pyで制作)
├── model(TrainGesture.pyで制作)
└── model.tflite(ModelConvert.pyで制作)
```

さて、ここまできたら領域展開をしましょう。assetsディレクトリに領域展開用の映像とBGMをいれてください。今回は渋谷事変の時の領域展開が好きだったのでその映像を描画するようにします。
```python:DomainExpansion.py
import tensorflow as tf
import numpy as np
import cv2
import mediapipe as mp
import pygame
from utils import preprocessing_gesture_data, draw_keypoints_line

pygame.mixer.init()
pygame.mixer.music.load("assets/domain_expansion.wav")
# TFLiteモデルの読み込み
interpreter = tf.lite.Interpreter(model_path="model.tflite")
# メモリ確保。これはモデル読み込み直後に必須
interpreter.allocate_tensors()
# 学習モデルの入力層・出力層のプロパティをGet.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

hands = mp.solutions.hands.Hands(
    max_num_hands=2,  # 最大検出数
    min_detection_confidence=0.7,  # 検出信頼度
    min_tracking_confidence=0.7,  # 追跡信頼度
)


def infer_gesture(data):
    processed_data = preprocessing_gesture_data([data]).astype(np.float32)
    # indexにテンソルデータのポインタをセット
    interpreter.set_tensor(input_details[0]["index"], processed_data)
    # 推論実行
    interpreter.invoke()
    # 推論結果は、output_detailsのindexに保存されている
    output_data = interpreter.get_tensor(output_details[0]["index"])
    output_data = np.array(output_data[0])
    output_data = np.where(output_data > 0.9)
    return output_data


v_cap = cv2.VideoCapture(0)  # カメラのIDを選ぶ。映らない場合は番号を変える。
movie_cap = cv2.VideoCapture("assets/domain_expansion.mp4")
target_fps = 30
# フレームごとの待機時間を計算

domain_expansion: bool = False  # 領域展開しているか否かを判別する変数
clock = pygame.time.Clock()
while True:
    if not domain_expansion:
        success, img = v_cap.read()
        if not success:
            continue
        img = cv2.flip(img, 1)  # 画像を左右反転
        img_h, img_w, _ = img.shape  # サイズ取得
        results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        if results.multi_hand_landmarks:
            draw_keypoints_line(results,img)

            # データの追加に関する処理
            data = {}
            for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
                for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
                    positions = [0] * 21
                    for idx, lm in enumerate(hand_landmarks.landmark):
                        lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
                        positions[idx] = lm_pos
                    data[hand_class.label] = positions
        else:
            data = {}

        output_data = []
        cv2.imshow("MediaPipe Hands", img)
        if len(data.keys()) == 2:
            output_data = infer_gesture(data)
            if output_data[0] == 1:
                print("領域展開: †伏魔御厨子†")
                pygame.mixer.music.play()
                domain_expansion = True
                v_cap.release()

    elif domain_expansion:
        success, img = movie_cap.read()
        if not success:
            domain_expansion = False
            movie_cap = cv2.VideoCapture("assets/domain_expansion.mp4")
            data = {}
            v_cap = cv2.VideoCapture(0)  # カメラのIDを選ぶ。映らない場合は番号を変える。

        elif success:
            cv2.imshow("MediaPipe Hands", img)

    # FPS制御
    key = cv2.waitKey(5) & 0xFF
    if key == 27:  # ESCキーが押されたら終わる
        break
    clock.tick(target_fps)
```
まず最初のところを解説します。
```python
import tensorflow as tf
import numpy as np
import cv2
import mediapipe as mp
import pygame
from utils import preprocessing_gesture_data, draw_keypoints_line

pygame.mixer.init()
pygame.mixer.music.load("assets/domain_expansion.wav")
# TFLiteモデルの読み込み
interpreter = tf.lite.Interpreter(model_path="model.tflite")
# メモリ確保。これはモデル読み込み直後に必須
interpreter.allocate_tensors()
# 学習モデルの入力層・出力層のプロパティをGet.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
```
今回は実行と並列してBGMを流してFPSを流すためのライブラリとしてpygameを使っています。
最初の二行ではpygameの音響ライブラリを初期化してBGMを読み込んでいます。
その後, tensorflowliteモデルを読み込み, メモリをallocate_tensorsで確保して、入力層と出力層の情報を得ています。このあたりは[公式ドキュメント](https://www.tensorflow.org/api_docs/python/tf/lite/Interpreter)でも詳しく話されています。
次に推論に使う関数を見てみましょう。
```python
def infer_gesture(data):
    processed_data = preprocessing_gesture_data([data]).astype(np.float32)
    # indexにテンソルデータのポインタをセット
    interpreter.set_tensor(input_details[0]["index"], processed_data)
    # 推論実行
    interpreter.invoke()
    # 推論結果は、output_detailsのindexに保存されている
    output_data = interpreter.get_tensor(output_details[0]["index"])
    output_data = np.array(output_data[0])
    output_data = np.where(output_data > 0.9)
    return output_data
```
out_dataの決定まではデータを与えて作成したモデルの出力を出しているだけです。今回のモデルでは**今の手の形が伏魔御厨子の形である確率が0から1の間**で与えられます。普通の分類モデルでは50%が切れ目となりますが、今回は**90%以上伏魔御厨子の手の形である**と判断された時のみ予測ラベルを1にしています。

```python
v_cap = cv2.VideoCapture(0)  # カメラのIDを選ぶ。映らない場合は番号を変える。
movie_cap = cv2.VideoCapture("assets/domain_expansion.mp4")
target_fps = 30
# フレームごとの待機時間を計算

domain_expansion: bool = False  # 領域展開しているか否かを判別する変数
clock = pygame.time.Clock()
```
その後の五行ではOpenCVのカメラをオンにし, 領域展開中に表示する映像もセットしています。

while前の変数やオブジェクトの初期化が終わったら、データを集めた時と同様にMediapipeでの手の認識が始まります。DomainExpansion.pyではwhile文が大きく分けて2つのブロックに分かれています。
```python
while True
    if (領域展開中ではない):
        (取得したデータの処理, 伏魔御厨子の手の形になっているか判定)
        
    elif (領域展開):
        (必中効果範囲内の呪力を帯びたモノには「捌」　無生物には「解」が
        伏魔御厨子が消えるまで絶え間なく浴びせられる。)
```
ではwhile文前半を見てみましょう。
```python
while True:
    if not domain_expansion:
        success, img = v_cap.read()
        if not success:
            continue
        img = cv2.flip(img, 1)  # 画像を左右反転
        img_h, img_w, _ = img.shape  # サイズ取得
        results = hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        if results.multi_hand_landmarks:
            draw_keypoints_line(results,img)

            # データの追加に関する処理
            data = {}
            for h_id, hand_landmarks in enumerate(results.multi_hand_landmarks):
                for c_id, hand_class in enumerate(results.multi_handedness[h_id].classification):
                    positions = [0] * 21
                    for idx, lm in enumerate(hand_landmarks.landmark):
                        lm_pos = (int(lm.x * img_w), int(lm.y * img_h))
                        positions[idx] = lm_pos
                    data[hand_class.label] = positions
        else:
            data = {}

        output_data = []
        cv2.imshow("MediaPipe Hands", img)
        if len(data.keys()) == 2:
            output_data = infer_gesture(data)
            if output_data[0] == 1:
                print("領域展開: †伏魔御厨子†")
                pygame.mixer.music.play()
                domain_expansion = True
                v_cap.release()
```
```cv2.imshow("MediaPipe Hands", img)```までは訓練データを集める時と同じなので割愛します。if文以降のコードでは, まず両手が認識されていることを確認し、確認されたら推論をしています。もし領域展開されていると認識されたら```"領域展開: †伏魔御厨子†"```という, 中学生のXのbioみたいな文字列を標準出力に出し, ```pygame.mixer.music.play()```でBGMを流し, 領域展開しているかしていないかの変数をTrueとし, パソコンのカメラ```v_cap```を一旦```v_cap.release()```で開放しています。domain_expansion = Trueとなったので、elifに移ります。
```python
    elif domain_expansion:
        success, img = movie_cap.read()
        if not success:
            domain_expansion = False
            movie_cap = cv2.VideoCapture("assets/domain_expansion.mp4")
            data = {}
            v_cap = cv2.VideoCapture(0)  # カメラのIDを選ぶ。映らない場合は番号を変える。

        elif success:
            cv2.imshow("MediaPipe Hands", img)

    # FPS制御
    key = cv2.waitKey(5) & 0xFF
    if key == 27:  # ESCキーが押されたら終わる
        break
    clock.tick(target_fps)
```
elifのコードでは, 読み込んだassetsの方の動画を```cv2.imshow```で画面に描画しています。そしてもし動画が終了した場合, ```success = False```となるので、その時に領域展開してない方にwhile文の処理を移すために```domain_expansion = False```とし、カメラv_capを再起動しています。
このコードを実行し, カメラの前で伏魔御厨子のポーズをしてうまく認識されると、画面がこんな感じになります。
<img src="https://pbs.twimg.com/media/GAb9HNsa4AA0emz?format=jpg&name=large">
かっこいいね

# 真面目な話
今回はMediaPipeで簡単な領域展開認識を実装しました。MediaPipeを使えば点群のデータを取得してこのように人間の指の動きに反応するようなシステムが作れます。例えば、手の点群の動きを系列データで取得すれば, 系列データを処理するようなモデルを使うことでSAOのメニュー表示のアレを作ることもできます。
↓これ(画像は[Netflix公式チャンネルより](https://www.youtube.com/watch?v=Uz9FMXcOnXA&t=279s))
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3311006/7df75328-5a7d-6fd5-93c6-8a4453ef4272.png">

また、MediaPipeで手の取得できる点群データは奥行きの情報もあるため, うまく使えば音ゲーを作ることもできます。
みなさんもMediaPipeを使って何が作れるか考えてみてくださいね。
今回は急いで組んだのでCLIアプリしか作れませんでしたが, TensorflowLiteはデバイスに乗せることもできるのでまだ面白いことができそうですね。
今回の記事のコードはここにあります

https://github.com/SuperHotDogCat/DOMAIN_EXPANSION