目次
はじめに
実行環境
指を推論させる方法
推論結果のプロット方法
指の動きのグラフ化の方法
ソースコード
結果
特徴
考察
まとめ
はじめに
今回は、Googleの機械学習ライブラリである MediaPipe
を用いて 指の先端の推論ができました。
また、推論結果から OpenCV
を用いて指の先端をマーカーでプロットし、 Pillow
を用いてマーカーの説明を日本語で表記させられました。
また、推論結果から matplotlib
を用いて指の先端の軌跡をグラフ化 できたので紹介したいと思います。
公式ドキュメントはこちらです
今回は、主に下の公式GitHubを参照して実装しました。
よろしければこちらもどうぞ!!
実行環境
実行環境は次の通りです。
実行環境
-
環境
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.hands
で Mediapipe Hands
を読み込み Hands
インスタンスで手の最大数や手の検出精度、手の追跡精度などを設定できるそうです。
OpenCV
の色空間はBGR色空間であるのに対し MediaPipe
で推論させるのに必要なデータの色空間はRGB色空間だそうです。なので、OpenCV
で読み込んだデータをRGB色空間に変換する必要があるみたいです。
そうして得られた推論結果には次の出力結果が含まれているそうです。
出力内容
名前 | 内容 |
---|---|
multi_hand_landmarks |
手の各ランドマークの位置 |
出力は($x$,$y$,$z$)のリストで返ってくる
そうです。
また、$x$、$y$、$z$はそれぞれ
- $x$と$y$はそれぞれ画像の幅と高さで正規化されている値
- $z$は手首の奥行きを原点として値が大きくなるほどカメラから遠ざかり値が小さくなるほど近づく値
らしいです。
名前 | 内容 |
---|---|
multi_hand_world_landmarks |
手の各ランドマークの 3D座標 ($m$単位での位置) |
原点は手の近似幾何中心
らしいです。
名前 | 内容 |
---|---|
multi_handedness |
利き手の推論結果 ("Right"か"Left"の文字列として 返ってくる) |
利き手は入力データがミラーリングされていると仮定して出力される
らしいです。
推論結果のプロット方法
推論結果のプロットでは次のような処理を行っています。
処理内容
-
multi_hand_landmarks
の出力結果から手のランドマークのIDと画像の高さと幅でそれぞれ正規化された$x$と$y$座標を取得 - webカメラの幅と高さを取得
-
multi_hand_landmarks
で得られた$x$座標とwebカメラの幅の積とmulti_hand_landmarks
で得られた$y$座標とwebカメラの高さの積をそれぞれ計算
($x$、$y$座標をwebカメラの映像で補正している) - それぞれの指の先端の座標を取得
- IDごとに
OpenCV
を用いて結果をマーカー形式でプロット - IDごとで
Pillow
を用いてそれぞれの指の文字列を日本語で表示
IDと指の対応は次の通りです。
対応表
ID番号 | どの指の先端か |
---|---|
4 | 親指 |
8 | 人差し指 |
12 | 中指 |
16 | 薬指 |
20 | 小指 |
ちなみに、他の手のランドマークのIDは次の通りだそうです。
参照元
https://github.com/google/mediapipe/blob/master/docs/solutions/hands.md
指の動きのグラフ化の方法
指の動きのグラフ化では次のような処理を行っています。
処理内容
- 処理の最初でそれぞれの指の先端の座標のデータを格納する空の配列を宣言
- 推論結果のプロット方法で指の先端の座標を取得できたので、それらの値を空の配列に順次代入
- 最終的に取得した座標のデータからmatplotlibを用いてグラフ化して表示
ソースコード
下にソースコードを示します。おそらく実行環境で示した環境では動くはず。
ソースコード
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() #グラフの表示
結果
下に結果を示します。
(動画はクリックで再生/停止を切り替えられます)
出力動画
下の動画のようにも手を動かしてみました ↓
違う手の動き
このときの指の先端の軌跡のグラフは次の通りです。
特徴
結果を見て感じた特徴は次の通りです。
特徴
- 出力動画では手に物を持っていても(持っているのはリモコンですw)うまく指の先端を推論できています
- 出力動画では手を閉じていてもうまく指の先端を推論できています
- 指の先端の軌跡のグラフでは動きの終わりでグラフの形が乱れています
- 指の先端の軌跡のグラフでは$x$軸の値が最大値まで動くと戻っていっています
(二次関数を横に倒したような形になっています)
考察
特徴で示した特徴からの考察は次の通りです。
考察
特徴からの疑問点 | 考察 |
---|---|
動きの終わりでグラフの形が乱れているのはなぜ? |
指を推論させる方法 で紹介したHands インスタンスの手の追跡精度か検出精度の値を変更すれば抑制されると思われます。 |
$x$軸の値が最大値まで動くと戻っていっている(二次関数を横に倒したような形)になっているのはなぜ? | 手が左右に揺れている場合にはこのような形になると思われます。 おそらく手を上下に振ると$y$軸の値が最大値まで動くと戻る(二次関数のような形)になると思われます。 |
まとめ
今回は、指の先端の推論、推論結果からのプロット・各指の名前の日本語表示、指の先端の軌跡のグラフ化を行いました。
この記事が実際に役に立つかどうかは分かりませんが、誰かの役に立ってくれると嬉しいです。
記事を執筆する余力があれば、次回も記事を投稿する予定です。
次回の予定としては、X(Twitter)でのアンケートで上位だったファイル操作についての記事を投稿予定です。
具体的には、tkcalendar
を用いて月と日を指定すると対象のフォルダ内に指定した月と日をアンダーバーで繋げたフォルダを作成して対象のフォルダ内のすべてのファイルの更新日時を参照し作成したフォルダ(月と日をアンダーバーで繋げたフォルダ)に指定した月と日以下の更新日時のファイルを格納できるプログラムを作成できたのでそれに関する記事を投稿する予定です。