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

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

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

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

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

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

[**公式GitHub**](https://github.com/google/mediapipe/blob/master/docs/solutions/hands.md)

--------

よろしければこちらもどうぞ!!

<details><summary>マイページについて</summary>

:::note info
主に<ins>**Pythonについての記事**</ins>を投稿してます!!
:::

[<font color="blue"><ins>**マイページはこちら**</ins></font>](https://qiita.com/blueman,"マイページ")

</details>

<details><summary>X(Twitter)について</summary>

:::note info
<ins>**Qiitaの新着記事の通知**</ins>や<ins>**おススメの理工学書の紹介**</ins>をツイートしてます!!
:::
[<font color="blue"><ins>**X(Twitter)はこちら**</ins></font>](https://twitter.com/0ca00118726208m,"X(Twitter)")

</details>

<details><summary>Zennについて</summary>

:::note info
主に<ins>**自分の書いたQiitaの記事の転載**</ins>と<ins>**Pythonを使っていて遭遇したエラーの対処法のような記事**</ins>を投稿していく予定です!!
:::
[<font color="blue"><ins>**Zennはこちら**</ins></font>](https://zenn.dev/blueman,"zenn")

</details>

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

<details><summary>実行環境</summary>

- **環境**

    -  **`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`**

</details>

**処理内容**と**ライブラリ**の対応を分かりやすく表にすると次の通りです。
<details><summary>説明</summary>

| <font color="red">処理内容</font> | <font color="red">ライブラリ</font> |
|:-:|:-:|
|**指の推論**|<font color="red"> **`MediaPipe`** </font>|
|**推論結果のプロット**<br>**画面への出力**|<font color="red"> **`OpenCV`** </font>|
|**マーカーの説明**|<font color="red"> **`Pillow`** </font>|
|**pillowのデータをOpenCVで読み込める<br>ように変換**|<font color="red"> **`numpy`** </font>|
|**推論結果のグラフ化**|<font color="red"> **`matplotlib`** </font>|

</details>


# 指を推論させる方法
[**公式GitHub**](https://github.com/google/mediapipe/blob/master/docs/solutions/hands.md)を見ると、 **`mediapipe.solutions.hands`** で **`Mediapipe Hands`** を読み込み **`Hands`** インスタンスで<font color="red">**手の最大数**</font>や<font color="red">**手の検出精度**</font>、<font color="red">**手の追跡精度**</font>などを設定できるそうです。

:::note
**`OpenCV`の色空間**は<font color="red">**BGR色空間**</font>であるのに対し **`MediaPipe`で推論させるのに必要なデータの色空間**は<font color="red">**RGB色空間**</font>だそうです。なので、`OpenCV`で読み込んだデータを<font color="red">**RGB色空間に変換する必要がある**</font>みたいです。
:::

そうして得られた**推論結果**には次の<font color="red">**出力結果**</font>が含まれているそうです。

<details><summary>出力内容</summary>

|名前|内容|
| :---: | :----: |
|<font color="red"> **`multi_hand_landmarks`** </font>|**手の各ランドマークの位置**|

> **出力は<font color="red">($x$,$y$,$z$)のリスト</font>で返ってくる**

そうです。

また、$x$、$y$、$z$はそれぞれ
> - **<font color="red">$x$と$y$</font>はそれぞれ<font color="red">画像の幅と高さで正規化</font>されている値**
> - **<font color="red">$z$</font>は<font color="red">手首の奥行きを原点</font>として<font color="red"><ins>値が大きくなる</ins>ほどカメラから<ins>遠ざかり</ins>値が<ins>小さくなる</ins>ほど<ins>近づく</ins>値**</font>

らしいです。

|名前|内容|
|:-:|:-:|
|<font color="red"> **`multi_hand_world_landmarks`**  </font>|**手の各ランドマークの<br>3D座標<br> ($m$単位での位置)**|

> **<font color="red">原点</font>は<font color="red">手の近似幾何中心</font>**

らしいです。

|名前|内容|
|:-:|:-:|
|<font color="red"> **`multi_handedness`** </font>|**利き手の推論結果<br>("Right"か"Left"の文字列として<br>返ってくる)**|

> **利き手は<font color="red">入力データ</font>が<font color="red">ミラーリングされていると仮定</font>して出力される**

らしいです。

</details>

# 推論結果のプロット方法
<font color="red">**推論結果のプロット**</font>では次のような処理を行っています。
<details><summary>処理内容</summary>

1. <font color="red"> **`multi_hand_landmarks`** </font>の出力結果から<font color="red">**手のランドマークのID**</font>と<font color="red">**画像の高さと幅でそれぞれ正規化された$x$と$y$座標**</font>を取得
1. webカメラの<font color="red">**幅と高さ**</font>を取得
1. <font color="red"> **`multi_hand_landmarks`** </font>で得られた<font color="red">**$x$座標**</font>とwebカメラの<font color="red">**幅**</font>の<ins>**積**</ins>と<font color="red"> **`multi_hand_landmarks`** </font>で得られた<font color="red">**$y$座標**</font>とwebカメラの<font color="red">**高さ**</font>の<ins>**積**</ins>をそれぞれ計算
($x$、$y$座標をwebカメラの映像で補正している)
1. それぞれの<font color="red">**指の先端の座標**</font>を取得
1. IDごとに<font color="red"> **`OpenCV`** </font>を用いて結果を<font color="red">**マーカー形式でプロット**</font>
1. IDごとで<font color="red"> **`Pillow`** </font>を用いて<font color="red">**それぞれの指の文字列を日本語で表示**</font>

-------

</details>

<font color="red">**ID**</font>と<font color="red">**指**</font>の対応は次の通りです。
<details><summary>対応表</summary>

|**ID番号**|**どの指の先端か**|
| :------: | :------------: |
|**4**|**親指**|
|**8**|**人差し指**|
|**12**|**中指**|
|**16**|**薬指**|
|**20**|**小指**|

ちなみに、他の手のランドマークのIDは次の通りだそうです。
![MediaPipe_ランドマーク.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3198998/8244b020-8269-7f0b-4ec3-eb3a796c5693.png)
参照元
https://github.com/google/mediapipe/blob/master/docs/solutions/hands.md

---------------------

</details>

# 指の動きのグラフ化の方法
<font color="red">**指の動きのグラフ化**</font>では次のような処理を行っています。

<details><summary>処理内容</summary>

1. 処理の最初で<font color="red">**それぞれの指の先端の座標のデータを格納する空の配列を宣言**</font>
1. [推論結果のプロット方法](#推論結果のプロット方法)で<font color="red">**指の先端の座標**</font>を取得できたので、<font color="red">**それらの値を空の配列に順次代入**</font>
1. <font color="red">**最終的に取得した座標のデータ**</font>から***matplotlib***を用いて<font color="red">**グラフ化して表示**</font>

--------------------------------

</details>

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

<details><summary>ソースコード</summary>

```python: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() #グラフの表示
```

-------------------------------

</details>

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

<details><summary>出力動画</summary>

<iframe width="560" height=315" src="https://www.youtube.com/embed/k5MKSpjrVeM?mute=1&controls=0&rel=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope;" allowfullscreen></iframe>

----------------------------------

</details>

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

<details><summary>違う手の動き</summary>

<iframe width="560" height="315" src="https://www.youtube.com/embed/S-SHXTuRclg?mute=1&controls=0&rel=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

-----------------------------

</details>

このときの<ins>**指の先端の軌跡のグラフ**</ins>は次の通りです。

<details><summary>グラフ</summary>

![hand_tracking.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3198998/89842404-7c02-1831-f4dc-0f6bd65965d0.png)

------------------------------

</details>

# 特徴
[結果](#結果)を見て感じた特徴は次の通りです。

<details><summary>特徴</summary>

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

-------------------------------------------

</details>

# 考察
[特徴](#特徴)で示した特徴からの考察は次の通りです。

<details><summary>考察</summary>

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

--------------------------------------

</details>

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