Windows10 + AnacondaでMediaPipe動かしてみた

# はじめに

ほとんど手を付けられてなかったMediaPipeでしたが、やる気が少し湧いたのでWindows10で動かしてみました。

この記事はWindows10とAnacondaでMediaPipeを動かしたときの備忘録となっております。

MediaPipe自体については[こちら](https://mediapipe.dev/)から確認いただけるとよいと思います。

[公式](https://github.com/google/mediapipe)では15個のソリューションがサンプルとして公開されており、今回はPythonに対応している以下6個のソリューションを実装して試してみました。

（本記事とほぼ同じ内容をGithubにあげておりますので、試してみたい方は[こちら](https://github.com/T-Sumida/mediapipe_python4windows)からどうぞ）

|  Face Detection  |  Face Mesh  |
| :----: | :----: | 
| ![face_detect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/5706ea0f-59f5-8130-19cb-f254f482f4ff.jpeg) | ![face_mesh.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/a694a6dc-3ada-adbe-9047-9688257c2997.jpeg) |
|  **Hands**  |  **Pose**  |
| ![hand.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/00f2b60a-6ec5-d5ca-98dd-e4bd73328d04.jpeg) |  ![pose.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/2b16427c-e62e-44fa-fd24-778290842cf0.jpeg) |
|  **Holistic**  |  **Objectron**  |
| ![holistic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/2d1e073d-3899-3495-8f9d-6d2a6f7f815a.jpeg) | ![objectron.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130155/7386fc75-362d-4616-0fc8-75511abaa8c3.png) |

# 開発環境

タイトルにもあるようにWindows10上で開発を行いました。

- Windows 10 home Insider Preview 10.0.21327 build 221327
    - Core(TM) i7-9700K CPU @ 3.60GHz
- Anaconda 4.8.2
- Web Camera

# 環境構築

Anacondaで仮想環境を構築し、その中にMediaPipeをインストールして開発します。

**注意）あまりよくないのですがcondaとpipを使ってインストールしています。現状は特に問題は出ていませんが、実施する場合は自己責任でお願いいたします。**

```
(base) $ git clone https://github.com/T-Sumida/mediapipe_python4windows.git
(base) $ conda create -n mediapipe python=3.7
(base) $ conda activate mediapipe
(mediapipe) $ conda install requests
(mediapipe) $ pip install mediapipe, loguru
```

# 実装

この記事ではHandsについてのみ記載します。（以下をコピーしただけでは動きませんので、[こちら](https://github.com/T-Sumida/mediapipe_python4windows)をご確認ください。）

大まかに、「Main」と「FPS計算」「ML」部分に分けて実装しています。

## Main部分
main部分では、引数機能とカメラ制御、描画を担当するようにしています。

また、Face Detection以外も扱えるように、argparseのsubpasersを使ってサブコマンドでソリューションを指定できるように作っています。
そのサブコマンド名自体をML部分のクラス名と同一にしておいて、getattr関数でML処理のインスタンスを作成するような形としています。

```python
# -*- coding:utf-8 -*-
import copy
import argparse

import cv2
import numpy as np
from loguru import logger

import models
from utils import FpsCalculator


def get_args() -> argparse.Namespace:
    """引数取得

    Returns:
        argparse.Namespace: 取得結果
    """
    parser = argparse.ArgumentParser()

    parser.add_argument("--device", help='device id', type=int, default=0)
    parser.add_argument("--width", help='capture width', type=int, default=960)
    parser.add_argument(
        "--height", help='capture height', type=int, default=540
    )

    subparsers = parser.add_subparsers(dest="model")

    # hand_tracker command parser
    parser_ht = subparsers.add_parser(
        'HandTracker', help='', description='HandTracker'
    )
    parser_ht.add_argument(
        '--max_num_hands', type=int, default=2, help='最大検出手数'
    )
    parser_ht.add_argument(
        '--min_detection_confidence', type=float, default=0.7,
        help='手検出モデルの最小信頼値 [0.0, 1.0]'
    )
    parser_ht.add_argument(
        '--min_tracking_confidence', type=float, default=0.5,
        help='ランドマーク追跡モデルの最小信頼値 [0.0, 1.0]'
    )

    args = parser.parse_args()
    return args


def draw_fps(image: np.ndarray, fps: int) -> np.ndarray:
    """fpsを描画する

    Args:
        image (np.ndarray): ベースイメージ
        fps (int): FPS

    Returns:
        np.ndarray: 描画済みイメージ
    """
    width = image.shape[1]

    cv2.rectangle(image, (width-80, 0), (width, 20), (0, 0, 0), -1)
    cv2.putText(image, "FPS: " + str(fps), (width-75, 15), cv2.FONT_HERSHEY_SIMPLEX,
                0.5, (255, 255, 255), 1)
    return image


def main() -> None:
    """メインループ"""
    args = get_args().__dict__

    # setting camera device
    cap = cv2.VideoCapture(args['device'])
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, args['width'])
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args['height'])
    del args['device'], args['width'], args['height']

    # setting detector
    try:
        model_name = args['model']
        del args['model']
        detector = getattr(models, model_name)(**args)
    except Exception as e:
        logger.error(e)
        exit(1)

    # setting fps calculator
    calculator = FpsCalculator()

    # main loop
    while cap.isOpened():
        ret, image = cap.read()
        if not ret:
            break

        image = cv2.flip(image, 1)
        tmp_image = copy.deepcopy(image)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        if detector.detect(image):
            tmp_image = detector.draw(tmp_image)

        fps = calculator.calc()
        tmp_image = draw_fps(tmp_image, fps)
        cv2.imshow(model_name, tmp_image)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    return


if __name__ == "__main__":
    main()

```

## FPS計算部分
この処理に関しては、以下ページを参考にさせていただきました。（ありがとうございます！）
>> https://dev.classmethod.jp/articles/opencv-disp-fps/

```python
# -*- coding:utf-8 -*-
from timeit import default_timer as timer


class FpsCalculator():
    def __init__(self) -> None:
        """Initial"""

        self.frame_count = 0

        self.accum_time = 0
        self.curr_fps = 0
        self.prev_time = timer()
        self.result_fps = 0

    def calc(self) -> int:
        """calc fps

        Returns:
            int: current fps
        """
        # update frame count
        self.frame_count += 1

        # update fps
        self.__curr_time = timer()
        self.__exec_time = self.__curr_time - self.prev_time
        self.prev_time = self.__curr_time
        self.accum_time = self.accum_time + self.__exec_time
        self.curr_fps = self.curr_fps + 1
        if self.accum_time > 1:
            self.accum_time = self.accum_time - 1
            self.result_fps = self.curr_fps
            self.curr_fps = 0

        return self.result_fps

```

## ML部分
以下のようなクラス設計にし、他のソリューションを追加した場合も簡単に拡張できるようにしております。

Python版MediaPipeの扱い方に関しては以下を参考にしております。
>> https://google.github.io/mediapipe/solutions/face_detection.html

かなり簡単に実装ができて、モデルロード・推論・結果取得がすごく簡単でした。（コメントで★を入れている箇所）

結果取得に関しては、試すソリューションによって少し異なりますが公式に分かりやすく載っているので、非常に良かったです！


```python
# -*- coding:utf-8
import cv2
import numpy as np
import mediapipe as mp
from loguru import logger

from .abst_detector import AbstDetector


class HandTracker(AbstDetector):
    def __init__(self, max_num_hands: int, min_detection_confidence: float, min_tracking_confidence: float) -> None:
        """初期化処理

        Args:
            max_num_hands (int): 最大検出手数
            min_detection_confidence (float): 手検出モデルの最小信頼値
            min_tracking_confidence (float): ランドマーク追跡モデルからの最小信頼値
        """
        # モデルロード★
        self.tracker = mp.solutions.hands.Hands(
            max_num_hands=max_num_hands,
            min_detection_confidence=min_detection_confidence,
            min_tracking_confidence=min_tracking_confidence,
        )

    def detect(self, image: np.ndarray) -> bool:
        """手検出処理

        Args:
            image (np.ndarray): 入力イメージ

        Returns:
            bool: 手が検出できたか
        """
        try:
            # 推論処理★
            self.results = self.tracker.process(image)
        except Exception as e:
            logger.error(e)
        return True if self.results.multi_hand_landmarks is not None else False

    def draw(self, image: np.ndarray) -> np.ndarray:
        """処理結果を描画する

        Args:
            image (np.ndarray): ベースイメージ

        Returns:
            np.ndarray: 描画済みイメージ
        """
        base_width, base_height = image.shape[1], image.shape[0]
        for hand_landmarks, handedness in zip(self.results.multi_hand_landmarks, self.results.multi_handedness):

            landmark_buf = []

            # 結果取得★
            # keypoint
            for landmark in hand_landmarks.landmark:
                x = min(int(landmark.x * base_width), base_width - 1)
                y = min(int(landmark.y * base_height), base_height - 1)
                landmark_buf.append((x, y))
                cv2.circle(image, (x, y), 3, (255, 0, 0), 5)

            # connection line
            for con_pair in mp.solutions.hands.HAND_CONNECTIONS:
                cv2.line(image, landmark_buf[con_pair[0].value],
                         landmark_buf[con_pair[1].value], (255, 0, 0), 2)

        return image


```


# デモ

実際にHandsを使ったデモを動画として残しております。

<iframe width="560" height="315" src="https://www.youtube.com/embed/xuJvxxlT4QY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# おわりに
今回はMediaPipe公式が提供しているPython版ソリューションをWindows+Anacondaで試してみました。

本当はWSL2上で動かしてみたかったのですが、私の環境ではまだWebカメラは認識してくれなかったため断念しました...

今後は提供されたソリューションで遊ぶのではなく、自分でMLパイプラインを構築して試してみたいと考えております。