手の検出と深度推定を同時に実行した
MediaPipe Handsで手のランドマーク検出と、MiDaSで深度推定を同時に実行するシステムを作った。
手の特徴点と深度情報、2つのAI推論を並行して処理。指のパターンを検出して、あるパターンの時に画像を表示する。
やりたかったこと
手のジェスチャー認識だけじゃなくて、深度情報も同時に取りたかった。
手がカメラからどれくらい離れてるか。その情報を使えば、もっと面白いインターフェースが作れるんじゃないか。
2つのAI推論を同時に動かす。負荷が心配だったけど、やってみたかった。
2つのAI推論の並行実行
MediaPipe Handsと MiDaS深度推定を同時に実行する。
// 手のランドマーク検出(MediaPipe Hands)
const handsInference = async () => {
await hands.send({ image: videoElement });
};
// 深度推定(MiDaS)
const depthInference = async () => {
const result = await inferenceManager.runInference(videoElement);
renderDepthMap(result.outputData, result.width, result.height);
};
// 並行実行
async function updateFrame() {
await Promise.all([
handsInference(),
depthInference()
]);
requestAnimationFrame(updateFrame);
}
Promise.allで並行実行してる。
最初は、順次実行で試した。
// NG: 順次実行
await handsInference(); // 約15ms
await depthInference(); // 約20ms
// 合計: 約35ms
順次だと、合計35ms。約28fpsになった。遅い。
並行実行に変えたら、約20ms(遅い方に合わせる)。約50fpsに改善した。
深度情報を活用した表示
手までの距離を深度マップから取得して、画像のサイズを変える。
function getHandDepth(landmarks, depthData, depthWidth, depthHeight) {
// 手の中心座標を計算
let avgX = 0, avgY = 0;
landmarks.forEach(lm => {
avgX += lm.x;
avgY += lm.y;
});
avgX /= landmarks.length;
avgY /= landmarks.length;
// 深度マップから深度値を取得
const depthX = Math.floor(avgX * depthWidth);
const depthY = Math.floor(avgY * depthHeight);
const depthIndex = depthY * depthWidth + depthX;
const depth = depthData[depthIndex];
return depth;
}
// 深度に応じて画像サイズを変更
const handDepth = getHandDepth(landmarks, depthData, depthWidth, depthHeight);
const scale = 1.0 - handDepth; // 深度値を反転(近い=1.0、遠い=0.0)
const imageSize = 100 + scale * 100; // 100〜200px
imageElement.style.width = imageSize + 'px';
imageElement.style.height = imageSize + 'px';
手が近いと画像が大きく、遠いと小さく表示される。
最初は、深度を考慮してなかった。手が遠くても近くても、画像のサイズが同じ。違和感があった。
深度を使ったら、自然になった。手を前に出すと画像が大きくなる。直感的。
パターン認識の実装
指のパターンを分岐で判定する。
function detectPattern(landmarks) {
const indexExtended = landmarks[8].y < landmarks[6].y;
const middleExtended = landmarks[12].y < landmarks[10].y;
const thumbExtended = landmarks[4].x > landmarks[3].x + 0.05;
// ピースサイン
if (indexExtended && middleExtended && !thumbExtended) {
return "ピースサイン";
}
return null;
}
if (pattern === "ピースサイン") {
imageElement.style.display = 'block';
} else {
imageElement.style.display = 'none';
}
ピースサインの時だけ画像を表示。シンプルな実装。
最初は、複数のパターンを実装しようとした。コードが複雑になった。
ピースサイン1つだけに絞ったら、シンプルで分かりやすくなった。midori270で増やせばいい。
2つの推論の負荷
MediaPipe Handsと深度推定を同時に動かすと、負荷が高い。
| 推論 | 時間 |
|------|------|
| MediaPipe Hands | 約15ms |
| MiDaS深度推定(512×512) | 約20ms |
| 並行実行の合計 | 約20ms(遅い方) |
並行実行なので、合計時間は遅い方に合わせる。約20ms。
これに描画処理(約5ms)を加えると、合計約25ms。約40fps。
最初は、深度推定も1024×1024でやろうとした。約50msかかって、合計55ms(約18fps)になった。厳しかった。
512×512に下げたら、約20msになった。fpsが倍になった。
ハマったところ
推論の同時実行エラー
最初、同じWEBカメラ映像で、2つの推論を同時に実行したら、エラーが出た。
// NG: 同じビデオ要素を同時に使用
await Promise.all([
hands.send({ image: videoElement }),
inferenceManager.runInference(videoElement)
]);
// → エラー: Cannot read videoElement simultaneously
ビデオ要素を複数の推論で同時に読み込むと、エラーになることがあった。
解決策は、フレームを一度キャンバスに描画してから、それぞれの推論に渡す。
// OK: キャンバスに描画してから使用
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
await Promise.all([
hands.send({ image: canvas }),
inferenceManager.runInference(canvas)
]);
これで、エラーが出なくなった。
深度マップの座標変換
MediaPipeのランドマークは 0〜1 の正規化座標。深度マップは 512×512 のピクセル座標。座標変換が必要。
const depthX = Math.floor(landmark.x * depthWidth);
const depthY = Math.floor(landmark.y * depthHeight);
最初は、変換を忘れてた。深度値が全部0になった。
座標をピクセル単位に変換したら、正しい深度値が取れた。
メモリ使用量
2つの推論を同時に実行すると、メモリ使用量が増える。
長時間実行してると、ブラウザが重くなった。
定期的にガベージコレクションを呼び出すようにした。
let frameCount = 0;
function updateFrame() {
frameCount++;
// 100フレームごとにガベージコレクション
if (frameCount % 100 === 0) {
if (window.gc) window.gc();
}
// 推論実行...
}
これで、長時間実行しても安定した。
パフォーマンス
PCでの動作を測定した。
| 処理 | 時間 |
|------|------|
| MediaPipe Hands推論 | 約15ms |
| MiDaS深度推定(512×512) | 約20ms |
| 並行実行の合計 | 約20ms |
| 描画処理 | 約5ms |
| 合計 | 約25ms |
約40fpsで動作する。60fpsには届かないけど、2つの推論を同時に実行してることを考えれば、まあまあ。
スマホだと、約15fps。ギリギリ使える。
結果
手のランドマーク検出と深度推定を同時に実行できた。
- MediaPipe Handsで手のランドマーク検出(約15ms)
- MiDaS深度推定(512×512、約20ms)
- Promise.allで並行実行(合計約20ms)
- 深度に応じて画像サイズを変更
- ピースサインで画像を表示
2つの推論を並行実行することで、パフォーマンスを確保した。順次実行より約15ms速い。
深度情報を使った表示サイズの調整は、予想以上に自然で良かった。
推論のスピードを比較:
パフォーマンスの詳細:
全体の処理フロー:
まとめ
今回は、手のランドマーク検出と深度推定を同時に実行する実験を行いました。
ポイントは以下の3つ:
- 2つのAI推論を並行実行(MediaPipe Hands + MiDaS)
- Promise.allで並行処理、合計約20ms(約40fps)
- 深度情報で画像サイズを調整(手が近いと大きく、遠いと小さく)
並行実行することで、順次実行より約15ms高速化できた。
複数のAI推論を組み合わせる実験を考えている方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。