280.ラズパイ無双[20 MediaPipe Hands]
初回:2022/10/12
Raspberry Pi (ラズベリーパイ、通称"ラズパイ")で何か作ってみようという新シリーズです。前回『PINTO model zoo』を紹介しましたが、サンプルのハンドトラッキングをしたいと思ったのですが、Pythonじゃなかったので、『MediaPipe』を、ご紹介したいと思います。
P子「紹介シリーズに変更したの?」※1
しかし、本当にすごいと思います。インストールして、実行するだけで、サクサクっと動きました。それもラズパイ上で、それなりの速度が出ます。びっくりです。
P子「あなたが世間知らずなだけでしょ」
本当にそう思います。こういう技術を知ってる人、さらに先に行ってる人には物足りないかもしれませんが、私のようにラズパイでこんなことができるんだという事をまだ知らない人に、知ってもらえれば幸いです。
1.MediaPipe
≪参考1≫
https://google.github.io/mediapipe/
MediaPipe
『MediaPipeは、ライブおよびストリーミング メディア向けのクロスプラットフォームでカスタマイズ可能な ML ソリューションを提供します。』とのことです。
P子「Google翻訳 丸コピーじゃないの?」
『MediaPipe offers cross-platform, customizable ML solutions for live and streaming media.』が原本です。
ここには、16種類のサンプルが置いてあります。その中の Python で実行可能なサンプルは、7種類あります。
Face Detection | 顔検出 |
Face Mesh | フェイスメッシュ |
Hands | 手 |
Pose | ポーズ |
Holistic | ホリスティック(人間のポーズ、顔のランドマーク、ハンド トラッキング) |
Selfie Segmentation | 人物区分 |
Objectron | オブジェクト検出 |
ラズパイに、openCV はインストール済みで、カメラの動作確認も出来ているという前提です。
$ pip3 install mediapipe
準備は、たったこれだけです。
2.Hands
実は、これら 7つのサンプルをすべて実行してみました。ここでは、一番動作させたかった『Hands』を例に、動かしてみましょう。
https://google.github.io/mediapipe/solutions/hands
この中から、Python のサンプルソースをコピーしてきます。オリジナルは、静止画とカメラの動画などのサンプルが混在しているので、もう少し単純化してみます。
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_hands = mp.solutions.hands
# For webcam input:
cap = cv2.VideoCapture(0)
with mp_hands.Hands() as hands:
while True:
_, image = cap.read()
imgRGB = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(imgRGB)
image = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2BGR)
if results.multi_hand_landmarks:
# 左右の手が見えている場合、cnt==2
cnt = len(results.multi_hand_landmarks)
# ①
for idx in range(cnt) :
hand_landmarks = results.multi_hand_landmarks[idx]
# ②
# ③
# ④
mp_drawing.draw_landmarks(
image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS)
# ⑤
cv2.imshow('MediaPipe Hands', image) # cv2.flip(image, 1) しません。
k = cv2.waitKey(5)
if k == ord('q') or k == 27: # q または、ESC で終了
break
cap.release()
3.ハンドサイン
さて、手の形状認識が出来たという事で、折角なので手の形状に応じたデータ登録をしてみたいと思います。今回は、複雑な事をせず、単純に数字を入力してみましょう。
その前に、手のランドマークを見てみます。
Fig 2. 21 hand landmarks.
results.multi_hand_landmarks で、手のランドマークを取得できます。
mp_hands.Hands() のコンストラクタで、各種パラメータを指定できますが、max_num_hands=2 が初期値で、最大2つの手を認識できます。つまり、左右の手を認識可能です。認識順に配列にセットされるようです。
数字を入力するハンドサインは、これらのランドマークから計算します。親指以外は、単純に関節の位置関係から求めます。ランドマークは、x,y,z 軸のデータを持っているので、縦方向のy軸で立っているかどうかの判定を行います。親指は横に出るので、x軸で判定します。
この、x軸やy軸は、0~1の範囲で正規化されていますので、実際の指関節座標は、画像サイズを掛ける必要があります。デモなので指関節の番号を、表示させる際に、実際の座標を計算しています。
数字入力のカウントは、左手の親指以外の指を立てると、『1』、親指を伸ばすと、『5』を加算する事で、片手で0~9までの数字を表すことにします。次に、右手の場合は、指1本を、『10』にして、親指が『50』で、両手で 0~99までの数字を入力できるようにしてみます。
そして、右手か左手かの判別は、results.multi_handedness で行います。これの、classification[0] に、indexの0,1 labelのLeft,Right、利き手の推定確率のscore を持っています。この label で返される値は、左右逆転しています。Android アプリで、左右を反転した映像に対して手を認識しているそうで、ここでは、index の 0 と 1 に、実際の左右を割り当てています。手のランドマークと、左右判定を行うために、配列番号でアクセスするように for 文を変更しています。
このランドマークと左右判別のために、オリジナルの for hand_landmarks in results.multi_hand_landmarks: ではなく、for idx in range(cnt) : で配列を処理しています。
先のコードの ① ~ ⑤ の箇所に、今回追加する処理を入れてみます。
if results.multi_hand_landmarks:
# 左右の手が見えている場合、cnt==2
cnt = len(results.multi_hand_landmarks)
# ① 変数の初期化
height, width, channel = image.shape # ④ 各指関節座標を求めるのに使用
sum = 0
msg = ''
for idx in range(cnt) :
hand_landmarks = results.multi_hand_landmarks[idx]
# ② ハンドサインで指の数を数える
cmnd = handSign(hand_landmarks.landmark)
# ③ 右手、左手の判定
hndness = results.multi_handedness[idx]
clsf = hndness.classification[0] # indexの0,1 labelのLeft,Right
if clsf.index == 0: # indexの0 は、本物の右手
sum += cmnd * 10 # 右手は 10の位
msg += f'Right: {cmnd} '
elif clsf.index == 1:
sum += cmnd # 左手は 1の位
msg += f'Left: {cmnd} '
# ④ 各指関節(x,y座標(0~1の範囲)から、実座標を求める)
for i, lm in enumerate(hand_landmarks.landmark):
cx, cy = int(lm.x * width), int(lm.y * height)
cv2.putText(image,str(i),(cx+10,cy+10),
cv2.FONT_HERSHEY_PLAIN,1,(255,255,255),1,cv2.LINE_AA)
# cv2.circle(image,(cx,cy),2,(255,0,255),cv2.FILLED)
mp_drawing.draw_landmarks(
image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS)
# ⑤ ハンドサインの計算結果を表示する
msg += f' = {sum} '
cv2.putText(image,msg,(5,30),
cv2.FONT_HERSHEY_PLAIN,1.5,(255,255,255),1,cv2.LINE_AA)
cv2.imshow('MediaPipe Hands', image) # cv2.flip(image, 1) しません。
② で、片手分のランドマークから指の数字を計算する関数を呼び出しています。以下に、その関数を示します。
# ② ハンドサインで指の数を数える
def handSign(hlm) : # hand_landmarks.landmark
cmnd=0
# 人差し指が立っている場合(座標では、下が大きい)
if hlm[8].y < hlm[7].y < hlm[6].y < hlm[0].y:
cmnd += 1
# 中指が立っている場合
if hlm[12].y < hlm[11].y < hlm[10].y < hlm[0].y:
cmnd += 1
# 薬指が立っている場合
if hlm[16].y < hlm[15].y < hlm[14].y < hlm[0].y:
cmnd += 1
# 小指が立っている場合
if hlm[20].y < hlm[19].y < hlm[18].y < hlm[0].y:
cmnd += 1
# 親指の左右方向で、手のひら根元との位置関係
if (hlm[4].x < hlm[3].x < hlm[5].x < hlm[17].x or # 親指が左(手の甲)
hlm[4].x > hlm[3].x > hlm[5].x > hlm[17].x ): # 親指が右(手のひら)
cmnd += 5
return cmnd
4.おまけ
おまけって言うほどの事はありませんが、もし、同じ現象で苦労されている場合の手助けになればと思い、覚書として書いておきます。
Objectron のサンプルですが、そのまま動かすと
urllib.error.HTTPError: HTTP Error 404: Not Found
というエラーが発生しました。
~/.local/lib/python3.9/site-packages/mediapipe/python/solutions/download_utils.py
の中の、_OSS_URL_PREFIX = 'https://github.com/google/mediapipe/raw/master/' が存在しません。
最新の master は、https://github.com/google/mediapipe/tree/master 以下にあり、その中の、/mediapipe/python/solutions/download_utils.py の中は、_GCS_URL_PREFIX = 'https://storage.googleapis.com/mediapipe-assets/' という記述に代わっていました。
とりあえず、download_utils.py を取得して、既存のファイルと置き換えると動きました。本来は、最新版をインストールしてやれば、問題ないのかもしれません。
P子「他のも新しくなってるかもよ」
そうですね。日々進化している技術なので、先のサンプルも、動かなくなる可能性がありますが、その時はその時です。
5.まとめ
とりあえず、単純に指関節の上下で判断していますが、きちんと手をカメラに向ければ、正しい計算結果が得られます。例えば、左手だけで、0~9までの数字、左手+右手を3本使えば、A~Z 、4本目は記号などと割り当て、右手親指を下向きで1文字確定、上向きでリターン、左に倒して1文字削除などにすれば、文字列を入力できるかもしれません。
P子「薬指が、うまく伸びない...」
実現は難しそうですが、画面上にキーボードを出して、指で押す感じを検出してキー入力するとか、指の動きでスワイプとか、いろいろできそうです。
他のも時間があれば、色々と試してみたいと思います。
ほな、さいなら
======= <<注釈>>=======
※1 P子「紹介シリーズに変更したの?」
P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。