はじめに
ほとんど手を付けられてなかったMediaPipeでしたが、やる気が少し湧いたのでWindows10で動かしてみました。
この記事はWindows10とAnacondaでMediaPipeを動かしたときの備忘録となっております。
MediaPipe自体についてはこちらから確認いただけるとよいと思います。
公式では15個のソリューションがサンプルとして公開されており、今回はPythonに対応している以下6個のソリューションを実装して試してみました。
(本記事とほぼ同じ内容をGithubにあげておりますので、試してみたい方はこちらからどうぞ)
Face Detection | Face Mesh |
---|---|
Hands | Pose |
Holistic | Objectron |
開発環境
タイトルにもあるように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についてのみ記載します。(以下をコピーしただけでは動きませんので、こちらをご確認ください。)
大まかに、「Main」と「FPS計算」「ML」部分に分けて実装しています。
Main部分
main部分では、引数機能とカメラ制御、描画を担当するようにしています。
また、Face Detection以外も扱えるように、argparseのsubpasersを使ってサブコマンドでソリューションを指定できるように作っています。
そのサブコマンド名自体をML部分のクラス名と同一にしておいて、getattr関数でML処理のインスタンスを作成するような形としています。
# -*- 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計算部分
この処理に関しては、以下ページを参考にさせていただきました。(ありがとうございます!)
# -*- 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
かなり簡単に実装ができて、モデルロード・推論・結果取得がすごく簡単でした。(コメントで★を入れている箇所)
結果取得に関しては、試すソリューションによって少し異なりますが公式に分かりやすく載っているので、非常に良かったです!
# -*- 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を使ったデモを動画として残しております。
おわりに
今回はMediaPipe公式が提供しているPython版ソリューションをWindows+Anacondaで試してみました。
本当はWSL2上で動かしてみたかったのですが、私の環境ではまだWebカメラは認識してくれなかったため断念しました...
今後は提供されたソリューションで遊ぶのではなく、自分でMLパイプラインを構築して試してみたいと考えております。