Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

MediaPipeで手の動作をリアルタイムで検出する

たまたまになるのですが、MediaPipeという手や顔の動きをリアルタイムで検出するライブラリを発見。
面白そうだったので触ってみることにしました。




■MediaPipeとは?

MediaPipeは、Googleが提供しているライブメディアやストリーミングメディア向けのMLソリューションであり、
CPUのみで検出が行えるツールになります。

MediaPipeの特徴として、
 ・エンドツーエンドの高速化:一般的なハードウェアでも高速化された組み込みの高速ML推論と処理
 ・一度構築してどこにでもデプロイ:統合ソリューションはAndroidiOS、デスクトップ/クラウド、ウェブ、IoTで機能します
 ・すぐに使用できるソリューション:フレームワークのフルパワーを実証する最先端のMLソリューション
 ・フリーでオープンソースApache 2.0でのフレームワークとソリューション、完全に拡張可能でカスタマイズ可能
があります。

本ライブラリのソリューションとして、
 ・顔検知
 ・フェイスメッシュ
 ・手検知
 ・ポーズ
 ・物体検出
 ・インスタントモーショントラッキング
などなど、、、
12種類用意されております。
また、MediaPipeは様々なプログラミング言語に対応しております。

対応しているプログラミング言語やソリューションの詳細は下記をご確認ください。
https://google.github.io/mediapipe/

■MediaPipeインストール

今回はpythonを用いて手の動作検知を行っていきます。
そこで、pythonでMediaPipeが利用できるようにMediaPipeをインストールしていきます。
といってもpythonであれば、

pip install mediapipe

or

pip3 install mediapipe

でOKです。
pythonはライブラリインストールが簡単でありがたいです。

自分はインストール済みでしたが、OpenCVが必要になります。
インストールがまだの方は下記を実行してOpenCVをインストールください。

pip install opencv-python
pip install opencv-contrib-python

or 

pip3 install opencv-python
pip3 install opencv-contrib-python

■MediaPipeを用いて手の動作を検知

では早速、てのモーションをリアルタイムで検知していきたいと思います!!
コードは公式が掲載しているコードを少し改変したものを載せます。
https://google.github.io/mediapipe/solutions/hands.html

コードはこちら。

import cv2
import mediapipe as mp

class hands:
    mp_drawing = mp.solutions.drawing_utils
    mp_drawing_styles = mp.solutions.drawing_styles
    mp_hands = mp.solutions.hands
    after_img = ""
    StartFlg = False
    rsp = ""
    FinishFlg = False

    def HandsLoop(self):
        cap = cv2.VideoCapture(0)
        with self.mp_hands.Hands(
            min_detection_confidence=0.5,
            min_tracking_confidence=0.5) as hands:

            while cap.isOpened():
                success, image = cap.read()
                if not success:
                    print("Ignoring empty camera frame.")
                    continue

                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
                before_image = image.copy()

                results = hands.process(image)
                
                if results.multi_hand_landmarks:
                    for hand_landmarks in results.multi_hand_landmarks:
                        self.mp_drawing.draw_landmarks(
                            image,
                            hand_landmarks,
                            self.mp_hands.HAND_CONNECTIONS,
                            self.mp_drawing_styles.get_default_hand_landmarks_style(),
                            self.mp_drawing_styles.get_default_hand_connections_style())

                self.after_img = cv2.flip(image - before_image, 1)

                if __name__ == '__main__':
                    cv2.imshow('MediaPipe Hands', self.after_img)
                    if cv2.waitKey(5) & 0xFF == 27:
                        break

        cap.release()

if __name__ == '__main__':
    m_hands = hands()
    m_hands.HandsLoop()

こちらのコードで手のリアルタイム検知結果がimshowで表示させることが出来ます。
パラメータに関してはとりあえずデフォルト設定で利用しておりますが、
 ・min_detection_confidence:検出が成功したと判断するための最小信頼値(0.0 ~ 1.0の範囲)
 ・min_tracking_confidence:手のランドマークが正常に追跡されたと見なされるためのランドマーク追跡モデルからの最小信頼値(0.0 ~ 1.0の範囲)
を意味しております。


処理フローですが、
まずOpenCVのVideoCaptureによりカメラからのフレーム画像を取得します。
その後、min_detection_confidence、min_tracking_confidenceをそれぞれ0.5のパラメータで
hands.processにてフレーム画像の解析を行い、
self.mp_drawing.draw_landmarksにて解析結果を画像上に描画しているようです。
最後に、imshowにて画像を表示。

この際、手の検知結果のみを表示させるために、
image - before_image
により検出結果の画像から元画像を差し引いています。

こちらのコードを実行するとこちらのような映像が表示されます。
f:id:Elsammit:20220113224901g:plain

■グー、チョキ、パーを認識してみる

手を検知するだけだとサンプルのまんまですので、、、
簡単にグー、チョキ、パーかを判別してみたいと思います。
コードはこちら。

import cv2
import mediapipe as mp
from PIL import ImageFont, ImageDraw, Image
import numpy as np
import time

FONT = ImageFont.truetype('C:\Windows\Fonts\meiryo.ttc' , 32)
class hands:
    mp_drawing = mp.solutions.drawing_utils
    mp_drawing_styles = mp.solutions.drawing_styles
    mp_hands = mp.solutions.hands
    after_img = ""
    StartFlg = False
    rsp = ""
    FinishFlg = False

    def HandShape(self, hand_landmarks, image):
        image_width, image_height = image.shape[1], image.shape[0]
        index_finger = [
            int(hand_landmarks.landmark[8].y * image_height),
            int(hand_landmarks.landmark[6].y * image_height)
        ]

        index_middle = [
            int(hand_landmarks.landmark[12].y * image_height),
            int(hand_landmarks.landmark[10].y * image_height)
        ]

        index_ring = [
            int(hand_landmarks.landmark[16].y * image_height),
            int(hand_landmarks.landmark[14].y * image_height)
        ]

        index_pinky = [
            int(hand_landmarks.landmark[20].y * image_height),
            int(hand_landmarks.landmark[18].y * image_height) 
        ]        

        fingersList = [index_finger, index_middle, index_ring, index_pinky]

        return fingersList

    def JudgeRPS(self, fingersList):
        hand_ret = ""
        if fingersList[0][0] > fingersList[0][1] and fingersList[1][0] > fingersList[1][1] and \
            fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]:
            hand_ret = "グー"
        elif fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]:
            hand_ret = "チョキ"
        else:
            hand_ret = "パー"

        return hand_ret

    def HandsLoop(self):
        cap = cv2.VideoCapture(0)
        with self.mp_hands.Hands(
            min_detection_confidence=0.5,
            min_tracking_confidence=0.5) as hands:

            while cap.isOpened():
                success, image = cap.read()
                if not success:
                    print("Ignoring empty camera frame.")
                    continue

                image.flags.writeable = True
                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
                before_image = image.copy()

                results = hands.process(image)
                
                if results.multi_hand_landmarks:
                    for hand_landmarks in results.multi_hand_landmarks:
                        self.mp_drawing.draw_landmarks(
                            image,
                            hand_landmarks,
                            self.mp_hands.HAND_CONNECTIONS,
                            self.mp_drawing_styles.get_default_hand_landmarks_style(),
                            self.mp_drawing_styles.get_default_hand_connections_style())
                        fingerList = self.HandShape(hand_landmarks, image)
                        self.rsp = self.JudgeRPS(fingerList)
                        
                self.after_img = cv2.flip(image - before_image, 1)
                img_pil = Image.fromarray(self.after_img)
                draw = ImageDraw.Draw(img_pil)
                draw.text((10, 350), self.rsp, font = FONT, fill = (255,0,255))
                self.after_img = np.array(img_pil)

                cv2.imshow('MediaPipe Hands', self.after_img)
                 if cv2.waitKey(5) & 0xFF == 27:
                    break

        cap.release()

if __name__ == '__main__':
    m_hands = hands()
    m_hands.HandsLoop()

先ほど解析しませんでしたが、
hand_landmarks
に検知した手の節?の座標が格納されております。
hand_landmarksは{x,y}構造体の配列になっておりそれぞれ下記画像の通り割り当てられております。
f:id:Elsammit:20220111221902p:plain

こちらの座標情報を元に、
手の形状解析を行っております。
解析のコードはこちら。

    def HandShape(self, hand_landmarks, image):
        image_width, image_height = image.shape[1], image.shape[0]
        index_finger = [
            int(hand_landmarks.landmark[8].y * image_height),
            int(hand_landmarks.landmark[6].y * image_height)
        ]

        index_middle = [
            int(hand_landmarks.landmark[12].y * image_height),
            int(hand_landmarks.landmark[10].y * image_height)
        ]

        index_ring = [
            int(hand_landmarks.landmark[16].y * image_height),
            int(hand_landmarks.landmark[14].y * image_height)
        ]

        index_pinky = [
            int(hand_landmarks.landmark[20].y * image_height),
            int(hand_landmarks.landmark[18].y * image_height) 
        ]        

        fingersList = [index_finger, index_middle, index_ring, index_pinky]

        return fingersList

解析の結果から、グー、チョキ、パーの解析を行います。
コードはこちら。
今回は簡単のために、y座標のみをチェックかつ先端(TIP)がDIPの位置以下になった場合に指が曲がったと判断し、
その曲がった指をチェックすることでグー、チョキ、パーを解析しています。

    def JudgeRPS(self, fingersList):
        hand_ret = ""
        if fingersList[0][0] > fingersList[0][1] and fingersList[1][0] > fingersList[1][1] and \
            fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]:
            hand_ret = "グー"
        elif fingersList[2][0] > fingersList[2][1] and fingersList[3][0] > fingersList[3][1]:
            hand_ret = "チョキ"
        else:
            hand_ret = "パー"

        return hand_ret

後は解析した結果を画像に重畳させてimshowで表示。

self.after_img = cv2.flip(image - before_image, 1)
img_pil = Image.fromarray(self.after_img)
draw = ImageDraw.Draw(img_pil)
draw.text((10, 350), self.rsp, font = FONT, fill = (255,0,255))
self.after_img = np.array(img_pil)
cv2.imshow('MediaPipe Hands', self.after_img)

こちらのコードを実行した結果は下記に掲載しました。
よろしければご覧ください。
youtu.be

■最後

MediaPipeですが、結構検出精度が高く使っていてとても楽しかったです。
もう少し使ってみたいと思います。
また、何か出来たら掲載していきたいと思います。