Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MediaPipeのハンドトラッキングで作るARライトセイバー

TakashiYoshinaga
March 05, 2022
300

 MediaPipeのハンドトラッキングで作るARライトセイバー

2022/03/05のXRワークショップでの資料

TakashiYoshinaga

March 05, 2022
Tweet

Transcript

  1. HTMLの記述の解説 <!--Webカメラの映像を取得(不可視に設定済み)--> <video id="input_video" style="position:absolute; display:none;"></video> <!--ライトセーバー的な画像(不可視に設定済み)--> <img id="beam" src="画像のURL"

    style="position:absolute; display:none;"> <!--カメラ画像と⼿の認識結果を合成して表⽰--> <canvas id="output_canvas" style="position:absolute;"></canvas> input_video beam input_video output_canvas input_video
  2. ライブラリ読み込みの解説 <!--media pipe: 手の骨格取得や認識結果の描画に使用--> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/ camera_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> <!--opencv.js: 手の傾きや中心位置を計算するために使用--> <script src="https://docs.opencv.org/3.4.1/opencv.js"></script> OpenCV Camera Utils Hands + drawing utils
  3. 初期化に関する記述 window.onload = function() { let videoElement = document.getElementById('input_video'); canvasElement

    = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ maxNumHands: 2, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合は1 }); //結果を処理する関数を登録 hands.onResults(recvResults); //カメラの初期化 const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({image: videoElement}); }, width: 1280, height: 720 }); camera.start(); //カメラの動作開始 }; function recvResults(results) {/*⼿の検出結果を利⽤する*/ }
  4. HTMLの要素との関連付け window.onload = function() { let videoElement = document.getElementById('input_video'); //ビデオ要素の取得

    canvasElement = document.getElementById('output_canvas'); //表⽰⽤のCanvasを取得 canvasCtx = canvasElement.getContext('2d'); //Canvas描画に関する情報にアクセス //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ maxNumHands: 2, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合は1 }); //結果を処理する関数を登録 hands.onResults(recvResults); //カメラの初期化 const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({image: videoElement}); }, width: 1280, height: 720 }); camera.start(); }; function recvResults(results) {/*⼿の検出結果を利⽤する*/ } input_video output_canvas input_video
  5. ハンドトラッキングの初期化 window.onload = function() { let videoElement = document.getElementById('input_video'); //ビデオ要素の取得

    canvasElement = document.getElementById('output_canvas'); //表⽰⽤のCanvasを取得 canvasCtx = canvasElement.getContext('2d'); //Canvas描画に関する情報にアクセス //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ maxNumHands: 2, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合はtrue }); //結果を処理する関数を登録 hands.onResults(recvResults); //カメラの初期化 const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({image: videoElement}); }, width: 1280, height: 720 }); camera.start(); //カメラの動作開始 }; function recvResults(results) {/*⼿の検出結果を利⽤する*/ } 詳細は後ほど実装
  6. カメラの初期化と起動 window.onload = function() { let videoElement = document.getElementById('input_video'); canvasElement

    = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ maxNumHands: 2, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合は1 }); //結果を処理する関数を登録 hands.onResults(recvResults); //カメラの初期化 const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({image: videoElement}); }, width: 1280, height: 720 }); camera.start(); //カメラの動作開始 }; function recvResults(results) {/*⼿の検出結果を利⽤する*/ } videoElementの映像を ハンドトラッキング処理に渡す 画像サイズを設定
  7. カメラ画像の表⽰ //⼿の検出結果を利⽤する function recvResults(results) { let width=results.image.width; let height=results.image.height; //画像のサイズとcanvasのサイズが異なる場合はサイズを調整

    if(width!=canvasElement.width){ //⼊⼒画像と同じサイズのcanvas(描画領域)を⽤意 canvasElement.width=width; canvasElement.height=height; } //以下、canvasへの描画に関する記述 canvasCtx.save(); //画像を表⽰ canvasCtx.drawImage(results.image, 0, 0, width, height); canvasCtx.restore(); } Lesson02 (0,0) width height
  8. ハンドトラッキング結果の表⽰ Lesson03 canvasCtx.save(); //画像を表⽰ canvasCtx.drawImage(results.image, 0, 0, width, height); //⼿を検出したならばtrue

    if (results.multiHandLandmarks) { //⾒つけた⼿の数だけ処理を繰り返す for (const landmarks of results.multiHandLandmarks) { //⾻格を描画 drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#00FF00', lineWidth: 2}); //関節を描画 drawLandmarks(canvasCtx, landmarks, { color: '#FF0000', lineWidth: 1,radius:2}); } } canvasCtx.restore();
  9. 画像の反転 window.onload = function() { let videoElement = document.getElementById('input_video'); canvasElement

    = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ selfieMode:true, //画像を左右反転 maxNumHands: 2, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合は1 }); /*以下省略*/ Lesson04
  10. 認識する⼿の数の上限を変更 window.onload = function() { let videoElement = document.getElementById('input_video'); canvasElement

    = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ selfieMode:true, //画像を左右反転 maxNumHands: 1, //認識可能な⼿の最⼤数 modelComplexity: 1,//精度に関する設定(0~1) minDetectionConfidence: 0.5,//⼿検出の信頼度 minTrackingConfidence: 0.5,//⼿追跡の信頼度 useCpuInference: false, //M1 MacのSafariの場合は1 }); /*以下省略*/ Lesson05 maxNumHandsを1に変更
  11. 画像をスクリプトで扱う準備 let canvasElement; let canvasCtx; let beam; //ライトセイバー的な画像 //初期化 window.onload

    = function() { //画像の読み込み beam = document.getElementById("beam"); let videoElement = document.getElementById('input_videoʼ); canvasElement = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ /*省略*/ }); Lesson06
  12. 画像をページ内に追加 <body> <video id="input_video" style="position:absolute; display:none;"></video> <img id="beam" src="画像のURL" style="position:absolute;

    display:none;"> <canvas id="output_canvas" style="position:absolute;"></canvas> </body> URLを貼り付ける ⾮表⽰Offの場合、カメラ画像が出る前に⼀瞬表⽰される display:none;を⼀旦削除(動作確認後は戻す) beam input_video(⾮表⽰) Lesson07
  13. ⼿の検出と連動した画像表⽰ function recvResults(results) { /*canvasのサイズ指定と画像の描画(スペースの都合により省略)*/ if (results.multiHandLandmarks) { //⾒つけた⼿の数だけ処理を繰り返す for

    (const landmarks of results.multiHandLandmarks) { //⾻格を描画 drawConnectors(/*省略*/); //関節を描画 drawLandmarks(/*省略*/); drawLightSaber(); } } canvasCtx.restore(); } //ライトセイバーを表⽰ function drawLightSaber(){ } Lesson08
  14. ⼿の検出と連動した画像表⽰ function recvResults(results) { /*canvasのサイズ指定と画像の描画(スペースの都合により省略)*/ if (results.multiHandLandmarks) { for (const

    landmarks of results.multiHandLandmarks) { //⾻格を描画 drawConnectors(/*省略*/); //関節を描画 drawLandmarks(/*省略*/); drawLightSaber(); } } canvasCtx.restore(); } //ライトセイバーを表⽰ function drawLightSaber(){ canvasCtx.drawImage(beam, 0, 0, beam.width, beam.height); } Lesson09 画像, 位置X, 位置Y, 横幅, 縦幅
  15. ⼿の位置・⾓度の計算 let canvasElement; let canvasCtx; let beam; //ライトセイバー的な画像 let ell;

    //⼿の位置や傾きを表す楕円 //初期化 window.onload = function() { //画像の読み込み beam = loadImage('画像のURL'); let videoElement = document.getElementById('input_videoʼ); canvasElement = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); //⼿の認識に関するオプション hands.setOptions({ /*省略*/ }); Lesson10
  16. ⼿の位置・⾓度の計算 function recvResults(results) { /*描画領域のサイズ設定など(スペースの都合上省略)*/ if (results.multiHandLandmarks) { //⾒つけた⼿の数だけ処理を繰り返す for

    (const landmarks of results.multiHandLandmarks) { drawConnectors(/*略*/); drawLandmarks(canvasCtx, landmarks, {/*略*/); cvFunction(landmarks,width,height); drawLightSaber(); } } canvasCtx.restore(); } //⼿の中⼼や傾きを計算 function cvFunction(landmarks,width,height){ } Lesson11
  17. OpenCVを⽤いた楕円近似 //⼿の中⼼や傾きを計算 function cvFunction(landmarks,width,height){ //⼿の関節を保持する配列 let points = []; //⼿のひらや親指の付け根付近以外の関節を取得

    for(var i=2;i<21;i++){ //0~1で表現されたx,yを画像のサイズに変換 points.push(landmarks[i].x*width); points.push(landmarks[i].y*height); } //点の集まりをOpenCVで扱えるデータフォーマットに変換 let mat = cv.matFromArray( points.length/2, 1, cv.CV_32SC2, points); //点の集まりにフィットする楕円を計算 ell = cv.fitEllipse(mat); //メモリの解放 mat.delete(); } Lesson12
  18. 楕円の表⽰ function drawLightSaber(){ //楕円の⾓度 let angle = ell.angle; //位置指定 canvasCtx.translate(ell.center.x,

    ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, //位置 ell.size.width/2.0, ell.size.height/2.0, //半径 0, 0, 2 * Math.PI); //⾓度と表⽰の開始・終了 canvasCtx.stroke(); //⼀旦コメント化 //canvasCtx.drawImage(beam, 0, 0, beam.width, beam.height); } Lesson13
  19. 画像を表⽰ Lesson14 function drawLightSaber(){ //楕円の⾓度 let angle = ell.angle; //位置指定

    canvasCtx.translate(ell.center.x, ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, //位置 ell.size.width/2.0, ell.size.height/2.0, //半径 0, 0, 2 * Math.PI); //⾓度と表⽰の開始・終了 canvasCtx.stroke(); //再度有効化 canvasCtx.drawImage(beam, 0, 0, beam.width, beam.height); }
  20. ⾓度の補正 Lesson15 function drawLightSaber(){ //楕円の⾓度 let angle = ell.angle; //ライトセイバーの向きを反転

    if(angle<90){ angle=angle-180; } //位置指定 canvasCtx.translate(ell.center.x, ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, ell.size.width/2.0, ell.size.height/2.0, 0, 0, 2 * Math.PI); canvasCtx.stroke(); canvasCtx.drawImage(beam, 0, 0, beam.width, beam.height); }
  21. サイズを⼤きくしよう Lesson16 //楕円の⾓度 let angle = ell.angle; //ライトセイバーの向きを反転 if(angle<90){ angle=angle-180;

    } //デフォルトサイズを元画像の2倍くらいにしたい。 let mul = (ell.size.width*2.0)/beam.width; //位置指定 canvasCtx.translate(ell.center.x, ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, ell.size.width/2.0, ell.size.height/2.0, 0, 0, 2 * Math.PI); canvasCtx.stroke(); //デフォルトサイズに倍数をかける canvasCtx.scale(mul, mul); canvasCtx.drawImage(beam, 0, 0, beam.width, beam.height);
  22. 画像の位置の補正 Lesson17 //楕円の⾓度 let angle = ell.angle; //ライトセイバーの向きを反転 if(angle<90){ angle=angle-180;

    } //デフォルトサイズを元画像の2倍くらいにしたい。 let mul = (ell.size.width*2.0)/beam.width; //位置指定 canvasCtx.translate(ell.center.x, ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, ell.size.width/2.0, ell.size.height/2.0, 0, 0, 2 * Math.PI); canvasCtx.stroke(); //デフォルトサイズに倍数をかける canvasCtx.scale(mul, mul); canvasCtx.drawImage(beam, -beam.width/2.0, 0, beam.width, beam.height); あらかじめx⽅向に半分ずらす
  23. 親指の状態の計算 let canvasElement; let canvasCtx; let beam; //ライトセイバー的な画像 let ell;

    //⼿の位置や傾きを表す楕円 let ratio; //親指の⽴ち具合を保持 //初期化 window.onload = function() { //画像の読み込み beam = loadImage('画像のURL'); let videoElement = document.getElementById('input_videoʼ); canvasElement = document.getElementById('output_canvas'); canvasCtx = canvasElement.getContext('2d'); //HandTrackingを使⽤するための関連ファイルの取得と初期化 const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); /*以下省略*/ Lesson18
  24. 親指の状態計算 function cvFunction(landmarks,width,height){ /*⼿を楕円に近似(スペースの都合上、省略)*/ //メモリの解放 mat.delete(); //親指と⼈差し指までの距離 let dx=(landmarks[7].x-landmarks[4].x)*width; let

    dy=(landmarks[7].y-landmarks[4].y)*height; let distance1 = Math.sqrt(dx*dx + dy*dy); //⼈差し指から⼩指までの距離 dx=(landmarks[7].x-landmarks[19].x)*width; dy=(landmarks[7].y-landmarks[19].y)*height; let distance2 = Math.sqrt(dx*dx + dy*dy); //⽐率の計算 ratio=distance1/distance2; //0.9:close, 1.3:sumb upとして0.9~1.3を0~1に正規化 let close=0.9; let up=1.3; ratio=(Math.max(close,Math.min(up,ratio))-close)/(up-close); } Lesson19
  25. 親指の状態を反映 Lesson19 function drawLightSaber(){ //楕円の⾓度 let angle = ell.angle; //ライトセイバーの向きを反転

    if(angle<90){ angle=angle-180; } //デフォルトサイズを元画像の2倍くらいにしたい。 let mul = ratio * (ell.size.width*2.0)/beam.width; //位置指定 canvasCtx.translate(ell.center.x, ell.center.y); //⾓度指定 canvasCtx.rotate(angle * Math.PI /180.0); //楕円を描画 canvasCtx.beginPath(); canvasCtx.ellipse(0, 0, ell.size.width/2.0, ell.size.height/2.0, 0, 0, 2 * Math.PI); canvasCtx.stroke(); /*スペースの都合により省略*/ } 親指の⽴ち具合をかける