手のジェスチャーでUI操作 - MediaPipeによるパターン認識
カメラの前で手を動かすだけで、動画を再生したり画像を切り替えたりできるシステムを作った。
MediaPipeで手の21個のランドマークを検出。そのパターンから、グー、チョキ、パー、サムズアップなど10種類以上のジェスチャーを認識する。
作ったもの
Webカメラで手を映すと、AIがリアルタイムでジェスチャーを認識する。
グーで動画再生。パーで画像表示。チョキで一時停止や切り替え。手のポーズだけでメディアをコントロールできる仕組み。
認識できるジェスチャーは10種類以上。それぞれに異なる機能を割り当ててある。
なぜ作ったか
midori278のハンドジェスチャーシステムがベースにある。それの拡張版。
最初は3つのジェスチャー(グー、チョキ、パー)だけだった。でも、それだと機能が少ない。もっと色々な操作をしたい。
手のポーズは無限にある。サムズアップ、OKサイン、ロックサイン。それぞれに機能を割り当てれば、キーボードやマウスに触れずに操作できる。これだ。
MediaPipeによる手の検出
MediaPipeのHands APIを使っている。
const hands = new Hands({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}
});
hands.setOptions({
maxNumHands: 2,
modelComplexity: 1,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
手のランドマークは21個。指先、関節、手首の位置を3D座標(x, y, z)で取得する。
最初はminDetectionConfidence: 0.7に設定していた。でも、検出が不安定だった。少し手を傾けただけで検出が途切れる。
0.5に下げたら、安定した。検出の頻度が上がって、ジェスチャー認識が滑らかになった。
ジェスチャー認識のロジック
21個のランドマークから、ジェスチャーを判定する。
function recognizeGesture(landmarks) {
const thumbTip = landmarks[4]; // 親指の先
const indexTip = landmarks[8]; // 人差し指の先
const middleTip = landmarks[12]; // 中指の先
const ringTip = landmarks[16]; // 薬指の先
const pinkyTip = landmarks[20]; // 小指の先
// 各指が伸びているかどうかを判定
const isIndexStraight = indexTip.y < indexMcp.y - 0.03;
const isMiddleStraight = middleTip.y < middleMcp.y - 0.03;
// ... 他の指も同様に判定
}
判定の基本は、指先と付け根のy座標の差。差が大きければ指は伸びている、小さければ曲がっている。
閾値0.03。最初は0.05だったが、誤検出が多かった。0.03に下げたら精度が上がった。
パターンマッチングの実装
各ジェスチャーを条件分岐で判定する。
グー(👊)の判定:
const isRockGesture = allFingertipsBelowBases && fingersClose;
全ての指先が付け根より下で、指先同士が近い。これがグーの条件。
パー(✋)の判定:
const isPaperGesture = isIndexStraight &&
isMiddleStraight &&
isRingStraight &&
isPinkyStraight &&
isThumbStraight &&
fingersSpread;
全部の指が伸びていて、指が広がっている。これがパー。
チョキ(✌️)の判定:
const isScissorsGesture = isIndexStraight &&
isMiddleStraight &&
!isRingStraight &&
!isPinkyStraight &&
isThumbBent;
人差し指と中指だけ伸びている。
この単純な条件分岐だけ。でも、意外と精度が高い。照明条件が良ければ、90%以上の認識率になった。
複雑なジェスチャーの判定
OKサイン(👌)やロックサイン(🤘)も実装した。
OKサインの判定:
const isOkSign = thumbToIndexDist < 0.05 &&
isMiddleStraight &&
isRingStraight &&
isPinkyStraight;
親指と人差し指の距離が近くて、他の指が伸びている。距離の閾値は0.05。
最初は0.03で厳しすぎた。ちゃんとOKサインを作っても認識されない。0.05に緩めたら、認識率が上がった。
サムズアップ(👍)の判定:
const isThumbsUp = thumbDirection === "up" &&
!isIndexStraight &&
!isMiddleStraight &&
!isRingStraight &&
!isPinkyStraight;
親指が上を向いていて、他の指は全部曲がっている。
親指の方向判定は別の関数で実装。親指の先と付け根の座標差から、上下左右を判定する仕組み。
ジェスチャーに対応する機能
各ジェスチャーにハンドラ関数を割り当てる。
const gestureHandlers = {
"グー": handleRockGesture,
"パー": handlePaperGesture,
"チョキ": handleScissorsGesture,
"指差し": handlePointingGesture,
"サムズアップ": handleThumbsUp,
"サムズダウン": handleThumbsDown,
"OK": handleOkSign,
"ロックサイン": handleRockSign,
// ... その他のジェスチャー
};
グーで動画を再生。パーで画像を表示。チョキで一時停止や切り替え。
動画はThree.jsのVideoTextureとして3D空間に表示される。画像も同様。手のポーズだけでメディアをコントロールできる。
誤検出の防止
連続して同じジェスチャーが検出される問題があった。
グーをすると、動画が何度も再生開始される。これは課題。
クールダウン時間を設けた。一度ジェスチャーを認識したら、1秒間は次の認識をスキップする。
let lastGestureTime = 0;
const cooldownTime = 1000; // 1秒
if (Date.now() - lastGestureTime < cooldownTime) {
return; // クールダウン中
}
lastGestureTime = Date.now();
これで、誤検出が減った。意図しない連続操作がなくなって、使いやすくなった。
認識精度の課題
照明条件で精度が変わる。
明るい部屋だと認識率90%以上。暗い部屋だと50%くらいに落ちる。これは大きな課題。
手の角度も影響する。正面から見た手は認識しやすい。斜めから見ると、ランドマークの位置がズレる。判定ロジックの閾値を調整する必要がある。
現状は「開発途中」。まだ改良の余地がたくさんある。閾値の自動調整や、複数フレームの平均を取るなど、試したいことがある。
試してみたジェスチャー
グーで動画再生は安定している。パーで画像表示も問題なし。
サムズアップは、親指の角度によって認識率が変わった。真っ直ぐ上に立てると認識しやすい。斜めだと認識されない時がある。
OKサインは、親指と人差し指の輪の大きさで認識率が変わる。小さい輪だと認識されやすい。大きい輪だと、距離が離れすぎて認識されない。
使ってみて
手のジェスチャー認識システムとして、基本機能は動作した。
ポイントは以下の3つ:
- MediaPipeで手の21個のランドマークを検出(minDetectionConfidence: 0.5)
- 指先と付け根のy座標差で伸びているか判定(閾値0.03)
- クールダウン時間1秒で誤検出を防止
開発途中だが、手のポーズでUIを操作する面白さは体験できた。照明条件や手の角度の影響を改善すれば、もっと実用的になると思う。
まとめ
今回は、MediaPipeを使った手のジェスチャー認識システムを作りました。
ポイントは以下の4つ:
- 手の21個のランドマークを使ってジェスチャーを判定
- 10種類以上のジェスチャーパターンに対応
- クールダウン時間で連続検出を防止
- 照明条件が良ければ90%以上の認識率
単純な条件分岐だけで、意外と高精度に認識できた。開発途中だが、ジェスチャーベースのインターフェースの可能性を感じた。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。