手の骨格の検出が出来たら手が開いているか、閉じているかを見てみたくなります。そのためにどのようにしたらいいかを考えましょう。
ml5.jsのHand Pose Detectionでは、手は下図のように⓪~⑳の関節のx、y座標値として検出されます。ここでは、21関節すべてを含む外接四角形の面積Aと、手首と指先関節(⓪、④、⑧、⑫、⑯、⑳)を含む外接四角形の面積Bを比較して、手の開閉を調べる方法を考えます。
下の図に外接四角形(緑の四角)と指先の外接四角形(ピンクの四角)を示します。図のように手を開いているときにはB/Aが1に近く、手を閉じているときには指先は中心に集まるのでB/Aは1よりかなり小さくなります。開き具合をB/Aの変化、0~1によって、閉じているから開いているまでの変化ととらえることができます。よって、例えばB/A>0.95なら手を開いている、B/A≤0.95なら手を閉じているという判断をすることが出来ます。これを使うと、手を開いているか閉じているかなどを判定することが出来るわけです。
面積A、Bはどのようにして求めればいいでしょうか?
各関節位置のx座標値、y座標値をそれぞれxi, yi(iは0~20)とすると、面積Aは
A = {max(xi) - min(xi)} * {max(yi) - min(yi)}
となります。また面積Bは j = 0, 4, 8, 12, 16, 20とすると
B = {max(xj) - min(xj)} * {max(yj) - min(yj)}
となります。
下の図の場合には、
min(xi) = x8, max(xi) = x0, min(yi) = y20, max(yi) = y1
となります。よって面積Aは
A = (x0 - x8)*(y1 - y20)
として計算できます。
それでは最大値・最小値をプログラムで求めてみましょう。プログラムでは配列に入れてからMath.max関数やMath.min関数を使うという手もありますが、ここではforループで1関節ずつ見ていきながら最小値・最大値を求めます。最小値は取り得る可能性のある数値の最大値を初期値として設定し、その値と一つ一つの関節の座標値を比較し、新たな座標値が現在設定されている最小値より小さければ最小値を更新してその座標値と入れ替える。この作業を全関節分行えば、更新された最小値が真の最小値を表していることになります。最大値も同様で、最初に取り得る最小値を設定し、それと順に比べて行って大きい方の値に更新していくことで求まります。
下記にプログラムを示します。このループを終えたminX, maxX, minY, maxYの値がそれぞれの最小値・最大値を与えます。
minX = vw; //関節のx座標の取る可能性のある最大値vw(カメラ画像の幅)を設定
minY = vh; //関節のy座標の取る可能性のある最大値vh(カメラ画像の高さ)を設定
maxX = maxY = 0; //関節のx,y座標の取る可能性のある最小値0を設定
for (let j = 0; j < prediction.landmarks.length; j += 1) { //0から関節の数(prediction.landmarks.length=20)分ループを回す
const keypoint = prediction.landmarks[j]; //各関節の位置(x、y)座標値をkeypointに入れる
if (minX > keypoint[0]) minX = keypoint[0]; //関節位置のx座標の最小値を計算
if (maxX < keypoint[0]) maxX = keypoint[0]; //関節位置のx座標の最大値を計算
if (minY > keypoint[1]) minY = keypoint[1]; //関節位置のy座標の最小値を計算
if (maxY < keypoint[1]) maxY = keypoint[1]; //関節位置のy座標の最大値を計算
}