昨今は非接触の需要やVTuberの広がりにより顔・手・ポーズのトラッキングを目にすることが増えてきました。
そうなると自分でも実装したくなるのが世の常ということで…今回はMediaPipeというフレームワークを使ってWebカメラでのお手軽なハンドトラッキングをやってみようと思います。
MediaPipeはGoogleが提供しているメディア向けのフレームワークで、CPUでも高速かつ様々なプラットフォームに対応しているオープンソースプロジェクトです。
2019年に発表されているので最新のソリューションというわけではないですが、今なお手軽なトラッキング手段として使いやすいと思います。
準備
MediaPipeはC++やPythonなどにも対応していますが、今回はWebページとして実装したいのでJavaScriptで試してみようと思います。
JavaScriptで実装する場合はnpmでインストールする方法もありますが、今回は簡単のためCDNから読み込む方向で実装します。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> </head> <body> <div class="container"> <video class="input_video"></video> <canvas class="output_canvas" width="1280px" height="720px"></canvas> </div> </body> </html>
上記のコードでCDNからMediaPipeを読み込み、合わせて検出結果表示用のvideo, canvasタグを用意しています。
今回はハンドトラッキングを行うため、手の検出モデルを追加で読み込んでいます。他にも顔や体全体など別の検出モデルが公式サイトに掲載されているため、興味のある方はそちらをご確認ください。
本体の読み込みができたら早速手を検出して画面上に表示してみます。下記コードでWebカメラの呼び出しと検出結果の画面出力まで行うことができます。
const videoElement = document.getElementsByClassName("input_video_01")[0] const canvasElement = document.getElementsByClassName("output_canvas_01")[0] const canvasCtx = canvasElement.getContext("2d") const onResults = (results) => { canvasCtx.save() canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height) canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height) if (results.multiHandLandmarks) { for (const landmarks of results.multiHandLandmarks) { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, { color: "#00FF00", lineWidth: 5, }) drawLandmarks(canvasCtx, landmarks, { color: "#FF0000", lineWidth: 2, }) } } canvasCtx.restore() } 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, }) hands.onResults(onResults) const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({ image: videoElement }) }, width: 1280, height: 720, }) camera.start()
こちらも公式サイトの各モデルの紹介ページに対応するサンプルコードが掲載されているため、ご自身の使用したい環境に合わせたコードを使用してください。
コードを書いてページを開いてみると下記の画像のような結果が得られます。ちなみにこちらの動作サンプルはWebカメラの撮影結果は非表示にしています。非表示にする際はvideoタグ自体を消してしまうとその後の処理に使えなくなるため、display:none;
を使用しています。
これだけのコードで、Webカメラからハンドトラッキングを実現することができました。
活用方法を考える
さて、簡単にハンドトラッキングをすることができましたが、せっかくなので簡単な活用方法を載せておきます。
今回はWebページで動作することを想定しているので、特定のポーズを取ったらページ遷移する処理を追加したいと思います。
特定のポーズを判定するためにはMediaPipeで検出された21点のランドマークが使えます。ランドマークは下記の図のように片手に21点あり、上記サンプルコード内onResults()
ではresults.multiHandLandmarks
に格納されています。今回使用しているハンドトラッキングのモデルでは左右の区別はされないため、左右の判定が必要な場合は体全体の検出のモデルを使用してください。
今回は、ETのポーズを取るとページ遷移をするようにします。簡単のため「人差し指同士が一定の距離未満になっている」を条件にして、他の指の状態は考えないものとします。
そして実装したのが下記のコードです (上記のサンプルコードで書かれている部分は省略しています)。
const onResults = (results) =& gt; { ... if (results.multiHandLandmarks) { ... if (results.multiHandLandmarks.length == 2) { // 両手(2つの手)のトラッキングができているときは指の位置でETチェックを行う // 2つの手の人差し指先端の値を用意 const hand1IndexFingerTop = results.multiHandLandmarks[0][8] const hand2IndexFingerTop = results.multiHandLandmarks[1][8] // 2点間の距離を算出 const IndexFingerDistance = Math.sqrt( Math.pow(hand1IndexFingerTop.x * 1280 - hand2IndexFingerTop.x * 1280, 2) + Math.pow(hand1IndexFingerTop.y * 720 - hand2IndexFingerTop.y * 720, 2) + Math.pow(hand1IndexFingerTop.z - hand2IndexFingerTop.z, 2) ) // 2点間の距離がしきい値以下ならカウンターを増やす // ※ 一瞬の誤検知は度々するのである程度同じポーズであることを確認しています if (IndexFingerDistance & lt;= 20) { action1Counter += 1 // カウンターが一定値を超えた場合ページ遷移を行う if (action1Counter == 100) { if (window.confirm("ET...?")) { location.href = 'https://nbcuni.co.jp/et-goods/' } } } else { // 該当条件を見対していない場合はカウンターを初期化しておく action1Counter = 0 } } } }
こちらのコードを掲載して下記の画像のように人差し指を近づけると、ETのグッズ販売ページに遷移されたかと思います。
ハマりそうな点はコード内にコメントにも掲載しましたが、一瞬の誤検知はどうしてがあるので、ポーズを取っているかの判定にはある程度のマージンが必要になります。
今回掲載したコードではポーズを取っていると判定してからカウンターを増やしていき一定数を超えたタイミングで正式にETポーズを取ったと判定しています。
おわりに
とうわけで簡単にハンドトラッキングを試してみましたが、難しいところはすべてMediaPipeが補ってくれているのでとても簡単でした。
ただ、今回ようなシンプルな手の形状なら容易ですが、これが複雑な形や動きを伴うものになるとポーズの判定が難しくなるため別のアプローチが必要になってくると思います。
とはいえ簡単にハンドトラッキングを試すことができるのでアイディア次第で色々できると思いますのでぜひ一度試してみてください!