手のジェスチャーで3D空間にエフェクトを発生させる
Webカメラで手を検出して、3D空間にエフェクトを表示するシステムを作った。
手を開くとパーティクルが飛び出す。グーにすると爆発エフェクト。指の形でビームが発射される。
深度マップもAI推論で生成してる。単眼カメラから奥行き情報を取得。これで立体的な表現が可能になった。
手の21個のランドマークをMediaPipe Handsで検出。指の開き具合や角度から、どのジェスチャーかを判定してる。
なぜ作ったか
手の動きでインタラクティブに操作できるシステムが欲しかった。
マウスやキーボードじゃなくて、自然な動作で操作。写真展示で来場者が体験できるインターフェースを想定してた。
深度マップも一緒に扱うことで、実空間と仮想空間の境界を曖昧にしたかった。カメラの前の人が、自分の手で3D空間に影響を与える。そういう体験。
前にmidori271で手の検出を試したけど、あれはパターン認識が想定が甘かった。今回は精度を上げて、複数のジェスチャーに対応させた。
システムの構成
システムは4つのパーツで構成されてる。
1. Webカメラ管理:映像ストリーム取得、カメラ切り替え
2. 深度マップ生成:ONNX Runtimeで AI推論(WebGPU/WebGL)
3. 手の検出:MediaPipe Handsで21ランドマーク検出
4. エフェクト生成:Three.jsで3D表示、ジェスチャーに応じたエフェクト
深度推定の実装
深度マップは、ONNXモデルをWebGPUで実行して生成してる。
最初、CPUで実行したら推論が遅かった。1フレーム500msくらい。2fpsしか出ない。実用は難しい。
WebGLに変えたら、150msまで改善。6-7fps。まだ足りない。
WebGPUに変えた。50ms。20fps出た。これなら実用的。
// WebGPU対応のONNXセッション作成
async loadModel() {
if (this.isWebGPUSupported) {
this.session = await ort.InferenceSession.create(
this.modelPath,
{ executionProviders: ["webgpu"] }
);
} else {
// フォールバック
this.session = await ort.InferenceSession.create(
this.modelPath,
{ executionProviders: ["webgl", "wasm"] }
);
}
}
WebGPUが使えるブラウザは、Chrome 113以降とかEdge 113以降。古いブラウザではWebGLにフォールバックする仕組み。
入力画像は512×512にリサイズしてから推論。大きすぎると重い、小さすぎると精度が落ちる。512×512がバランス良かった。
深度マップは0-255のグレースケールで表現。明るいほど近い、暗いほど遠い。この情報を3Dメッシュの高さに変換すると、立体的に見える。
手の検出
MediaPipe Handsで手の21個のランドマークを検出。
手のひら、指の関節、指先。全部で21箇所。これを毎フレーム追跡。
最初、minDetectionConfidenceを0.7に設定してた。検出が安定しない。手を動かすと、すぐに見失う。
0.5に下げたら、安定した。検出率90%以上。誤検出もほとんどない。
// MediaPipe Hands の設定
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 // 追跡の閾値
});
modelComplexityは0、1、2の3段階。0は軽量で速い、2は高精度だけど負荷が高かった。1がちょうど良かった。
検出したランドマークから、指の開き具合を計算。親指から小指まで、各指が開いているかを判定。
ジェスチャー認識
手の形からジェスチャーを認識する処理。
親指と人差し指の距離が近いと「ピンチ」。全指が開いてると「パー」。全指が閉じてると「グー」。
最初、単純な閾値で判定してた。でも、手のサイズや距離で精度がブレる。
ランドマーク間の比率を使うようにした。手のひらの大きさに対する指の長さの比率。これで距離に依存しなくなった。
// パー判定(全指が開いている)
function isOpenHand(landmarks) {
// 手のひらサイズを計算
const palmSize = calculatePalmSize(landmarks);
// 各指先と手のひらの距離を計算
for (let i = 0; i < 5; i++) {
const fingerTip = landmarks[4 + i * 4]; // 指先
const palmCenter = landmarks[9]; // 手のひら中心
const distance = calculateDistance(fingerTip, palmCenter);
// 比率で判定
if (distance / palmSize < 0.6) {
return false; // 指が閉じている
}
}
return true; // 全指が開いている
}
ジェスチャーごとにエフェクトを切り替え。パーならパーティクル放出、グーなら爆発エフェクト、ピンチならビーム発射。
エフェクトの生成
Three.jsでエフェクトを作ってる。
パーティクルエフェクトは、手の位置から放射状に粒子を放出。速度と色はランダム。重力で下に落ちる。
最初、1000個のパーティクルを生成したら重かった。30fpsまで落ちた。
500個に減らした。60fps維持。見た目はほとんど変わらない。
// パーティクルエフェクト生成
function createParticleEffect(position, color, scale = 1.0) {
const particleCount = Math.floor(50 * scale);
for (let i = 0; i < particleCount; i++) {
const particle = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 8, 8),
new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.8
})
);
// ランダムな方向に初速度を設定
particle.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 10,
Math.random() * 5 + 5,
(Math.random() - 0.5) * 10
);
particle.position.copy(position);
scene.add(particle);
// アニメーション処理
particles.push(particle);
}
}
ビームエフェクトは、円柱ジオメトリを指の方向に配置。グローエフェクトで光ってる感じを出してる。
ハマったところ
ブラウザ対応
WebGPUは新しい技術で、対応ブラウザが限られる。Chrome 113以降、Edge 113以降。Firefox、Safariは未対応(2025年2月時点)。
フォールバック処理が必須。WebGLも試して、それもダメならCPU。3段階のフォールバック。
深度マップの遅延
深度推論は50msかかる。手の検出は30ms。合計80ms。12fpsが限界。
並列処理にした。深度推論と手の検出を同時実行。WebWorkerで別スレッド化。
これで30fpsまで改善。ただし、CPUコアが2つ以上必要。
手の検出精度
暗い環境だと検出率が落ちる。50%くらいまで下がった。
明るさが重要。照明を追加したら、90%に改善。カメラの前に照明を設置。
背景が複雑だと誤検出が増える。シンプルな背景推奨。
パフォーマンスの最適化
最終的な処理時間:
- Webカメラ取得:5ms
- 深度推論(WebGPU):50ms
- 手の検出:30ms
- エフェクト描画:10ms
- 合計:95ms(約10fps)
並列化で30fpsに改善。深度推論と手の検出を別スレッドで実行。
メモリ使用量:
- 初期:約500MB
- 深度マップ生成後:約800MB
- エフェクト大量発生時:約1.2GB
パーティクルの数を制限して、メモリを800MB以下に抑えた。
実際に試した結果
パターン1:パーの動き
手を開くと、パーティクルが放射状に飛び出す。勢いよく開くと、粒子の速度も上がる。手の動きに連動して、エフェクトが変化。予想以上にダイナミック。
パターン2:グーの動き
手を閉じると、爆発エフェクト。中心から波紋が広がる。色も赤→オレンジ→黄色と変化。迫力ある。
パターン3:ピンチの動き
親指と人差し指をくっつけると、ビームが発射される。指の向きにビームが伸びる。狙った方向に撃てる。遊べる。
使ってみて
実際に試してみると、手の動きで操作できるのは直感的で楽しい。マウスを持たずに、空中で手を動かすだけ。
ポイントは3つ:
- 深度推論はWebGPUで50ms(WebGLだと150ms、CPUだと500ms)
- 手の検出はminDetectionConfidence: 0.5で安定(0.7だと頻繁に見失う)
- パーティクル数を500個以下に制限してメモリ800MB以内
写真展示で使う予定だったけど、まだ安定性に課題がある。暗い環境だと精度が落ちるので、照明の工夫が必要。
同じようなジェスチャー認識システムを作りたい方の参考になれば嬉しいです。
まとめ
今回作ったのは、手のジェスチャーで3D空間にエフェクトを発生させるシステムです。
ポイントは以下の4つ:
- ONNX Runtime + WebGPUで深度マップをリアルタイム生成(50ms/フレーム)
- MediaPipe Handsで手の21ランドマークを高精度検出(minDetectionConfidence: 0.5)
- ランドマーク間の比率でジェスチャー認識(距離に依存しない)
- パーティクル数とメモリを制限してパフォーマンス維持(800MB以下、30fps)
深度推論と手の検出を並列化することで、30fpsを実現できました。
WebGPUの威力を実感できるプロジェクトでした。同じような技術に興味がある方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。