📝

MediaPipe・OpenCV・matplotlibによる指の先端の推論・結果のプロット・軌跡のグラフ化

2023/08/12に公開
1

この記事はQiitaの記事の転載です!!

元記事はこちら

https://qiita.com/blueman/items/66ef82e3bf03dfca62bb


目次

はじめに
実行環境
指を推論させる方法
推論結果のプロット方法
指の動きのグラフ化の方法
ソースコード
結果
特徴
考察
まとめ

はじめに

今回は、Googleの機械学習ライブラリである MediaPipe を用いて指の先端の推論ができました。
また、推論結果から OpenCV を用いて指の先端をマーカーでプロットし、 Pillow を用いてマーカーの説明を日本語で表記させられました。
また、推論結果から matplotlib を用いて指の先端の軌跡をグラフ化 できたので紹介したいと思います。

公式ドキュメントはこちらです

https://developers.google.com/mediapipe/solutions/vision/hand_landmarker

今回は、主に下の公式GitHubを参照して実装しました。

公式GitHub


マイページについて

https://qiita.com/blueman


X(Twitter)について

https://twitter.com/0ca00118726208m


Qiitaについて

実行環境

実行環境は次の通りです。

実行環境
  • 環境

    • Windows 10
    • Python 3.10.5
  • ライブラリ

    • MediaPipe 0.10.2
    • OpenCV 4.8.0
    • Pillow 9.2.0
    • numpy 1.22.4
    • matplotlib 3.6.1

処理内容ライブラリの対応を分かりやすく表にすると次の通りです。

説明
処理内容 ライブラリ
指の推論 MediaPipe
推論結果のプロット
画面への出力
OpenCV
マーカーの説明 Pillow
pillowのデータをOpenCVで読み込めるように変換 numpy
推論結果のグラフ化 matplotlib

指を推論させる方法

公式GitHubを見ると、 mediapipe.solutions.handsMediapipe Hands を読み込み Hands インスタンスで手の最大数手の検出精度手の追跡精度などを設定できるそうです。

そうして得られた推論結果には次の出力結果が含まれているそうです。

出力内容
名前 内容
multi_hand_landmarks 手の各ランドマークの位置

出力は(x,y,z)のリストで返ってくる

そうです。
また、xyzはそれぞれ

  • xyはそれぞれ画像の幅と高さで正規化されている値
  • zは手首の奥行きを原点として値が大きくなるほどカメラから遠ざかり値が小さくなるほど近づく値

らしいです。

名前 内容
multi_hand_world_landmarks 手の各ランドマークの3D座標(m単位)での位置

原点は手の近似幾何中心

らしいです。

名前 内容
multi_handedness 利き手の推論結果("Right"か"Left"の文字列として返ってくる)

利き手は入力データがミラーリングされていると仮定して出力される

らしいです。


推論結果のプロット方法

推論結果のプロットでは次のような処理を行っています。

処理内容
  1. multi_hand_landmarks の出力結果から手のランドマークのID画像の高さと幅でそれぞれ正規化されたxy座標を取得
  2. webカメラの高さを取得
  3. multi_hand_landmarks で得られた x座標webカメラの幅multi_hand_landmarks で得られた y座標webカメラの高さをそれぞれ計算(xy座標をwebカメラの映像で補正している)
  4. それぞれの指の先端の座標を取得
  5. IDごとに OpenCV を用いて結果をマーカー形式でプロット
  6. IDごとで Pillow を用いてそれぞれの指の文字列を日本語で表示

IDの対応は次の通りです。

対応表
ID番号 どの指の先端か
4 親指
8 人差し指
12 中指
16 薬指
20 小指

ちなみに、他の手のランドマークのIDは次の通りだそうです。
MediaPipe_ランドマーク.png
参照元
https://github.com/google/mediapipe/blob/master/docs/solutions/hands.md


指の動きのグラフ化の方法

指の動きのグラフ化では次のような処理を行っています。

処理内容
  1. 処理の最初でそれぞれの指の先端の座標のデータを格納する空の配列を宣言
  2. 推論結果のプロット方法指の先端の座標を取得できたので、それらの値を空の配列に順次代入
  3. 最終的に取得した座標のデータから matplotlib を用いてグラフ化して表示

ソースコード

下にソースコードを示します。おそらく実行環境で示した環境では動くはず。

ソースコード
hand_tracking.py
import numpy as np #Pillowの出力をOpenCVで扱えるようにするためにインポート
from PIL import Image, ImageDraw, ImageFont #結果の出力の文字列を日本語にするためにインポート
import cv2 #出力用にインポート
import mediapipe as mp #Googleの機械学習ライブラリであるmediapipeをインポート
import matplotlib.pyplot as plt #指の先端の軌跡をグラフ化するためにインポート

#親指の先端のx,y座標を格納する配列
cx_thumb = []
cy_thumb = []

#人差し指の先端のx,y座標を格納する配列
cx_index = []
cy_index = []

#中指の先端のx,y座標を格納する配列
cx_middle = []
cy_middle = []

#薬指の先端のx,y座標を格納する配列
cx_ring = []
cy_ring = []

#小指の先端のx,y座標を格納する配列
cx_pinky = []
cy_pinky = []

fontpath ='C:\Windows\Fonts\MSGOTHIC.TTC' #日本語フォントのパス(コードでは、MSゴシック)
font = ImageFont.truetype(fontpath, 30) #フォントの設定(文字の大きさを30にしている)

cap = cv2.VideoCapture(0) #webカメラの設定

mpHands = mp.solutions.hands #mediapipe handsの読み込み
hands = mpHands.Hands() #Handsインスタンスを生成
mpDraw = mp.solutions.drawing_utils #mediapipe drawing_utilsの読み込み

while True:
	success,img = cap.read() #webカメラの映像を読み込み
	img = cv2.flip(img,1) #webカメラの映像を左右反転(動かしている手を一致させるため)
	imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB) #BGR画像をRGB画像に変換(OpenCVの画像はBGR画像であり推論するためにはRGB画像にする必要があるため)
	results = hands.process(imgRGB) #手の推論
	
	if results.multi_hand_landmarks: #手のランドマークが検出されたらTrue(if文の中を実行する)、検出されなかったらFalse(if文の中を実行しない)
		for handLms in results.multi_hand_landmarks: #変数handLmsが手のランドマークの配列の間、実行
			for id,lm in enumerate(handLms.landmark): #手のランドマークのリスト(handLms.landmark)の中のすべてのインデックスと要素を取得
				h,w,_ = img.shape #webカメラ画像の幅と高さを取得(チャネルは取得しないので3つ目を_にする)
				cx,cy = int(lm.x*w),int(lm.y*h) #手のランドマークを補正(lm.xは画像の幅、lm.yは画像の高さで正規化されているのでwebカメラ画像の幅と高さをそれぞれかけることで位置を補正させている)
				if id == 4: #ランドマークのインデックスが4(親指の先端)の場合
					cx_thumb.append(cx) #親指の先端のx座標を順次cx_thumbに格納
					cy_thumb.append(cy) #親指の先端のy座標を順次cy_thumbに格納
					cv2.drawMarker(img,(cx,cy),(255,0,0),markerType=cv2.MARKER_CROSS,markerSize=10,thickness=10) #webカメラの画像の(cx,cy)の座標に青(255,0,0)のサイズが10px、太さが10pxの十字のマーカー(cv2.MARKER_CROSS)をプロット
					img_pil = Image.fromarray(img) #webカメラの映像をPillowで扱える形式に変換
					draw = ImageDraw.Draw(img_pil) #ImageDrawオブジェクトをDraw()メソッドに渡すことで描画の準備をする
					draw.text((10, 5), '親指',  fill=(255, 0, 0), font=font, stroke_width=1, stroke_fill=(255, 0, 0)) #「親指」という文字列を(10,5)の座標に出力(枠線の太さが1px、色が文字色と同一(太字を表現))
					img = np.array(img_pil) #OpenCVで扱える形式に変換
				if id == 8: #ランドマークのインデックスが8(人差し指の先端)の場合
					cx_index.append(cx) #人差し指の先端のx座標を順次cx_indexに格納
					cy_index.append(cy) #人差し指の先端のy座標を順次cy_indexに格納
					cv2.drawMarker(img,(cx,cy),(0,255,0),markerType=cv2.MARKER_CROSS,markerSize=10,thickness=10) #webカメラの画像の(cx,cy)の座標に緑(0,255,0)のサイズが10px、太さが10pxの十字のマーカー(cv2.MARKER_CROSS)をプロット
					img_pil = Image.fromarray(img) #webカメラの映像をPillowで扱える形式に変換
					draw = ImageDraw.Draw(img_pil) #ImageDrawオブジェクトをDraw()メソッドに渡すことで描画の準備をする
					draw.text((10, 40), '人差し指',  fill=(0, 255, 0), font=font,  stroke_width=1, stroke_fill=(0, 255, 0)) #「人差し指」という文字列を(10,40)の座標に出力(枠線の太さが1px、色が文字色と同一(太字を表現))
					img = np.array(img_pil) #OpenCVで扱える形式に変換
				if id == 12: #ランドマークのインデックスが12(中指の先端)の場合
					cx_middle.append(cx) #中指の先端のx座標を順次cx_middleに格納
					cy_middle.append(cy) #中指の先端のy座標を順次cy_middleに格納
					cv2.drawMarker(img,(cx,cy),(0,0,255),markerType=cv2.MARKER_CROSS,markerSize=10,thickness=10) #webカメラの画像の(cx,cy)の座標に赤(0,0,255)のサイズが10px、太さが10pxの十字のマーカー(cv2.MARKER_CROSS)をプロット
					img_pil = Image.fromarray(img) #webカメラの映像をPillowで扱える形式に変換
					draw = ImageDraw.Draw(img_pil) #ImageDrawオブジェクトをDraw()メソッドに渡すことで描画の準備をする
					draw.text((10, 75), '中指',  fill=(0, 0, 255), font=font,  stroke_width=1, stroke_fill=(0, 0, 255)) #「中指」という文字列を(10,75)の座標に出力(枠線の太さが1px、色が文字色と同一(太字を表現))
					img = np.array(img_pil) #OpenCVで扱える形式に変換
				if id == 16: #ランドマークのインデックスが16(薬指の先端)の場合
					cx_ring.append(cx) #薬指の先端のx座標を順次cx_ringに格納
					cy_ring.append(cy) #薬指の先端のy座標を順次cy_ringに格納
					cv2.drawMarker(img,(cx,cy),(255,255,0),markerType=cv2.MARKER_CROSS,markerSize=10,thickness=10) #webカメラの画像の(cx,cy)の座標に水色(255,255,0)のサイズが10px、太さが10pxの十字のマーカー(cv2.MARKER_CROSS)をプロット
					img_pil = Image.fromarray(img) #webカメラの映像をPillowで扱える形式に変換
					draw = ImageDraw.Draw(img_pil) #ImageDrawオブジェクトをDraw()メソッドに渡すことで描画の準備をする
					draw.text((10, 110), '薬指',  fill=(255, 255, 0), font=font,  stroke_width=1, stroke_fill=(255, 255, 0)) #「薬指」という文字列を(10,110)の座標に出力(枠線の太さが1px、色が文字色と同一(太字を表現))
					img = np.array(img_pil) #OpenCVで扱える形式に変換
				if id == 20: #ランドマークのインデックスが20(小指の先端)の場合
					cx_pinky.append(cx) #小指の先端のx座標を順次cx_pinkyに格納
					cy_pinky.append(cy) #小指の先端のy座標を順次cy_pinkyに格納
					cv2.drawMarker(img,(cx,cy),(0,255,255),markerType=cv2.MARKER_CROSS,markerSize=10,thickness=10) #webカメラの画像の(cx,cy)の座標に黄色(0,255,255)のサイズが10px、太さが10pxの十字のマーカー(cv2.MARKER_CROSS)をプロット
					img_pil = Image.fromarray(img) #webカメラの映像をPillowで扱える形式に変換
					draw = ImageDraw.Draw(img_pil) #ImageDrawオブジェクトをDraw()メソッドに渡すことで描画の準備をする
					draw.text((10, 145), '小指',  fill=(0, 255, 255), font=font,  stroke_width=1, stroke_fill=(0, 255, 255)) #「小指」という文字列を(10,145)の座標に出力(枠線の太さが1px、色が文字色と同一(太字を表現))
					img = np.array(img_pil) #OpenCVで扱える形式に変換
					
	cv2.imshow("Image",img) #結果の出力
	if cv2.waitKey(1) & 0xFF == ord('c'): #キーボードで「c」キーが押されたら
		 cv2.destroyWindow('Image') #出力結果の画面を閉じる
		 break #手の推論と結果の出力の処理を終了

plt.plot(cx_thumb,cy_thumb,linestyle = "solid",color=(0,0,1),label="親指") #親指の先端の座標の軌跡をグラフ化
plt.plot(cx_index,cy_index,linestyle = "dashed",color=(0,1,0),label="人差し指") #人差し指の先端の座標の軌跡をグラフ化
plt.plot(cx_middle,cy_middle,linestyle = "dashdot",color=(1,0,0),label="中指") #中指の先端の座標の軌跡をグラフ化
plt.plot(cx_ring,cy_ring,linestyle = "dotted",color=(0,1,1),label="薬指") #薬指の先端の座標の軌跡をグラフ化
plt.plot(cx_pinky,cy_pinky,color=(1,1,0),label="小指") #小指の先端の座標の軌跡をグラフ化
plt.legend(prop={"family":"MS Gothic","weight":"bold"}) #matplotlibで日本語が使えるように設定
plt.show() #グラフの表示

結果

下に結果を示します。
(動画はクリック再生/停止を切り替えられます)

出力動画

下の動画のようにも手を動かしてみました ↓

違う手の動き

このときの指の先端の軌跡のグラフは次の通りです。

グラフ

hand_tracking.png
指の先端の軌跡のグラフ


特徴

結果を見て感じた特徴は次の通りです。

特徴
  • 出力動画では手に物を持っていても(持っているのはリモコンですw)うまく指の先端を推論できています
  • 出力動画では手を閉じていてもうまく指の先端を推論できています
  • 指の先端の軌跡のグラフでは動きの終わりでグラフの形が乱れています
  • 指の先端の軌跡のグラフでは x軸の値が最大値まで動くと戻っていっています
    (二次関数を横に倒したような形になっています)

考察

特徴で示した特徴からの考察は次の通りです。

考察
特徴からの疑問点 考察
動きの終わりでグラフの形が乱れているのはなぜ? 指を推論させる方法 で紹介した Handsインスタンスの手の追跡精度か検出精度の値を変更すれば抑制されると思われます。
x軸の値が最大値まで動くと戻っていっている(二次関数を横に倒したような形)になっているのはなぜ? 手が左右に揺れている場合にはこのような形になると思われます。
おそらく手を上下に振るy軸の値が最大値まで動くと戻る(二次関数のような形)になると思われます。

まとめ

今回は、指の先端の推論推論結果からのプロット・各指の名前の日本語表示指の先端の軌跡のグラフ化を行いました。
この記事が実際に役に立つかどうかは分かりませんが、誰かの役に立ってくれると嬉しいです。
記事を執筆する余力があれば、次回も記事を投稿する予定です。
次回の予定としては、X(Twitter)でのアンケートで上位だったファイル操作についての記事を投稿予定です。
具体的には、tkcalendarを用いて月と日を指定すると対象のフォルダ内に指定した月と日を_で繋げたフォルダを作成して対象のフォルダ内のすべてのファイルの更新日時を参照作成したフォルダ(月と日を_で繋げたフォルダ)に指定した月と日以下の更新日時のファイルを格納できるプログラムを作成できたのでそれに関する記事を投稿する予定です。

Discussion