【MediaPipe】Landmarkデータから手のポーズを認識するため、”指の状態”を認識する(その1)
カフェチームの山本です。
前回は、Multi Hand Trackingで、プログラムの内部を変更しし、x,y,z軸のスケールが正しいLandmarkデータを取得できました。
今回は、手のポーズを認識するための準備として、各指の状態を認識する方法を考えます。まず、公開されている手法(今回参考にさせて頂いた手法)の概要について触れ、その手法の問題点を解決しながら、手の状態を判定する方法を考えます。
結論としては、指の曲がり角度に閾値を設定することで、映っている角度に関わらず指の状態を2値(open/close)で判定できることがわかりました。
(MediaPipeに関連する記事はこちらにまとめてあります。)
(前提)
このページでは、ポーズとジェスチャについて、(正確な定義かはわかりませんが)以下の様に定義し使用しています。
- ポーズ:画像1枚(1フレーム)だけで判定できる、その瞬間の手の形のこと。(例:グー・パー)
- ジェスチャ:画像の複数枚から判定する、手の動きや形の変化のこと。(例:スライド・ドラッグ)
今回は、ポーズについて考えています。
既存手法
こちらの方がLandmarkを利用したポーズの判定プログラムを公開されています。今回はこの手法を参考手法にさせていただきました。
処理概要
このプログラムの処理は、以下のように2段階になっています。
- 各指の状態を2値(open / close)で判定する。判定は特定のLandmarkの座標の比較で行っている。(例:親指の場合、指先と第一関節のx座標が、根本のx座標よりも左ならopen)
- 判定した各指の状態の組み合わせごとによって、ポーズを認識する(例:人指し指がopen、かつ、それ以外がcloseなら、ONE)
この方法は、カメラに向かって手を見せて明示的にサインを出す、というような状況ならうまく利用できそうです。参考ページの画像からもうまく判定できている様子がわかります。
(ちなみに、このプログラム左手を表から撮影したものを前提としているため、左手を裏から写したり、右手に対しては、そのままは利用できません。v0.7.5で追加された右手左手の判定を利用し、それに応じてx座標の判定を逆にすれば、正しく利用できます。)
問題点
しかし、上の方法は、カフェのように商品を手に撮影している様子を上から撮影している状況だと、以下の点が問題になりそうです。
- 手が真正面から映らない場合、単純な座標の比較では判定できない
例えば、手の横から撮影した画像の場合、単純にx座標を比較しても、指が閉じているかは判定できなさそうです。(下の図では、親指がx座標が根本よりも内側に来ていますが、指を閉じているかというと、そうでもないように見えます)
手法の検討
この問題点を解決するために、以下のように手法を検討しました。
方針
参考手法と上記の問題点を踏まえ、以下のような方針することとしました。1・3番目は参考手法を踏襲し、2番目を追加します。
- 指の状態を2値(open/close)で判定する
- 判定は単純な座標の比較以外で判定する(撮影される方向が変わっても判定できる方法にする)
- 指の状態ごとに組み合わせて判定する
動画を見てみる
方針の2番目について、具体的な方法を考えるため、動画を見てみました。
結果、(動画を見るまでもないような気もしますが)指の角度で判定できそうなことがわかります。角度であれば、撮影の方向が変わっても、同じ判定方法が利用できそうです。
実験:角度を見る(方針1・2)
方法
今回は、簡単のため、各指の「全部の関節の曲がり角度の合計」を利用しました。それぞれの定義は以下のとおりです。
- 全部の関節:Multi Hand Trackingで検出されたLandmarkの点のうち、指ごとに3つある関節。下の画像の1,2,3,5,6,7,9,10,11,13,14,15,17,18,19の点です。親指は他の形と少し形が違うのですが、今回はすべての関節を含めました。
- 曲がり角度:各関節につながる2直線のなす角度。Landmarkは3次元座標で得られているので、通常の3次元空間内のベクトル計算で角度を計算します。以下の様な計算式です。(正確には、これを度数法に変換し、180度から引いたものを曲がり角度としています)
[latex]\theta[rad]=\frac{\vec{10}\cdot\vec{12}}{|\vec{10}||\vec{12}|}=\frac{(x_0-x_1)(x_2-x_1)+(y_0-y_1)(y_2-y_1)+(z_0-z_1)(z_2-z_1)}{\sqrt{(x_0-x_1)^2+(y_0-y_1)^2+(z_0-z_1)^2}\sqrt{(x_2-x_1)^2+(y_2-y_1)^2+(z_2-z_1)^2}}[/latex]
[latex]\vec{ij}はLandmark中のiの点からjの点へのベクトルを、[/latex]
[latex]x_iはiの点のx座標を表しています[/latex]
結果
先程の動画で検出したLandmarkのおける、曲がり角度の変化は以下のグラフのようになりました。
グラフ中の凡例における0~4は、親指から小指までを順に表しています。1フレームごとにプロットし、横軸の刻みは10フレーム(1/3秒)です。グラフの上の方が指が曲がっている、下のほうが伸びていることを表します。
分析
グラフを見ると、以下のことがわかります。
- 指を曲げたときに、きれいに変化が出ている。角度で閾値を設定すれば判定できそう。
- 親指は他の指と比べて角度の変化のスケールが違う。
閾値は以下のように設定できそうです。
- 親指:70度
- それ以外の4指:100度
状態の組み合わせ(方針3)
今回は手話を参考に、上で決まった指の状態の組み合わせに対して、以下のように定義しました。各指が空いているかどうかのタプル(boolean5つのtuple)を受け取り、定義されている形でなければNONEを、定義されていればその結果を返す、という処理です。
from enum import Enum class Pose(Enum): ZERO = 0 ONE = 1 TWO = 2 THREE = 3 FOUR = 4 FIVE = 5 SIX = 6 SEVEN = 7 EIGHT = 8 NINE = 9 NONE = -1 HANDPOSE_JUDGE = { (False, False, False, False, False): Pose.ZERO, (False, True, False, False, False): Pose.ONE, (False, True, True, False, False): Pose.TWO, (False, True, True, True, False): Pose.THREE, (False, True, True, True, True): Pose.FOUR, (True, False, False, False, False): Pose.FIVE, (True, True, False, False, False): Pose.SIX, (True, True, True, False, False): Pose.SEVEN, (True, True, True, True, False): Pose.EIGHT, (True, True, True, True, True): Pose.NINE, } def judge_handpose(fingerIsOpenTuple): if not fingerIsOpenTuple in HANDPOSE_JUDGE: return Pose.NONE else: return HANDPOSE_JUDGE[fingerIsOpenTuple]
結果
上の方針1・2・3にしたがって実装したプログラムの動作を動画で確認しました。
元の映像で確認
正しくNINEとZEROを認識できています。
別動画(0~4、5~9)で確認
こちらもそれぞれの正しく認識できていることがわかります。ポーズが変わる途中でNONEが数フレーム現れていますが、数フレームで均せば問題なさそうです。
指の曲がり角度のグラフをみてみると、親指に関しては、少し閾値を飛び出ている場合がありました(40~50フレーム目)。また、閾値を飛び出ないまでも、かなり近い値になっている場合もみられます(20~30フレーム目)。今回の動画では問題になりませんでしたが、より精度をあげるには、方式を改善した方が良さそうです。
まとめ
今回は、Multi Hand Trackingで検出したLandmarkから、手のポーズを認識を行いました。指の状態の認識として、曲がり角度に閾値を設定して2値で判定し、それらの組み合わせることで、手のポーズを認識できることを確認しました。はっきりとした、簡単なサインに対しては、この方法を利用できそうです。
次回は、もう少し曖昧な手の形に対して、ポーズを認識する方法を検討します。
参考にさせていただいたページ
Simple Hand Gesture Recognition Code - Hand tracking - Mediapipe https://gist.github.com/TheJLifeX/74958cc59db477a91837244ff598ef4a