AI x WebAR
MediaPipeのハンドトラッキングを使ってみよう
もろもろのダウンロード
http://arfukuoka.lolipop.jp/
lightsaber_web/Sample.zip
自己紹介
氏名:吉永崇(Takashi Yoshinaga)
専門:ARを用いた医療支援や運動計測
Volumetric Video
コミュニティ:ARコンテンツ作成勉強会 主催
ARコンテンツ作成勉強会の紹介
 2013年5月に勉強会をスタート。
 ARコンテンツの作り方をハンズオン形式で学ぶ
 人数は5~10名程度の少人数で実施
 参加条件はAR/VRに興味がある人(知識不要)
 各地で開催 (福岡、熊本、宮崎、長崎、大分、 鹿児島、山口、広島、札幌、関東)
Twitterと勉強会ページで情報を発信しています
@AR_Fukuoka Googleで「AR勉強会」で検索
#AR_Fukuoka
ハッシュタグ
本題に入ります
本日のゴール
MediaPipeのHandsによるハンドトラッキングで遊ぶ
https://youtu.be/pL_q-nHelCU
テンプレートの複製
https://glitch.com/~lightsaber-template
GET STARTED
テンプレートの複製
Remix Your Own
テンプレートの確認
index.htmlをクリックし、コードが表示されることを確認
index.html
index.htmlをクリックし、コードが表示されることを確認
テンプレートの確認
エディタ プレビュー
index.htmlをクリックし、コードが表示されることを確認
テンプレートの確認
プレビューを閉じる
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
テンプレートの確認
Lesson01
テンプレートの確認
ライブラリの
読み込み
MediaPipeや
OpenCVでの
処理を記述
(今日のメイン)
描画領域等
テンプレートの確認
描画領域等
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
テンプレートの確認
ライブラリの
読み込み
ライブラリ読み込みの解説
<!--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
テンプレートの確認
MediaPipeや
OpenCVでの
処理を記述
(今日のメイン)
javascript記述の解説
javascript記述の解説
変数宣言
初期化
描画領域/カメラ/
ハンドトラッキング
認識結果の利用
変数の宣言
変数宣言
初期化に関する記述
初期化
描画領域/カメラ/
ハンドトラッキング
初期化に関する記述
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) {/*手の検出結果を利用する*/ }
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) {/*手の検出結果を利用する*/ }
ハンドトラッキングの初期化
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) {/*手の検出結果を利用する*/ }
詳細は後ほど実装
カメラの初期化と起動
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の映像を
ハンドトラッキング処理に渡す
画像サイズを設定
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
テンプレートの確認
認識結果の利用
カメラ画像の表示
//手の検出結果を利用する
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
動作確認
①Show
②In a New Window
動作確認
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンドトラッキング結果の表示
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();
動作確認
MediaPipe Handsのパラメータをいじろう
• 画像の左右反転
• 認識する手の数の上限の変更
初期化に関する記述
初期化
描画領域/カメラ/
ハンドトラッキング
画像の反転
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
認識する手の数の上限を変更
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に変更
動作確認
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
変数の宣言
変数宣言
画像をスクリプトで扱う準備
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
画像の追加しよう
描画領域等
画像のアップロード
assets
画像のアップロード
Upload an Asset
画像のアップロード
beam.png
画像のアップロード
画像をクリック
画像のURLを取得
Copy
画像のURLを取得
空白をクリック
画像をページ内に追加
<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
Canvas上でライトセイバーを表示
認識結果の利用
rcvResults
手の検出と連動した画像表示
function recvResults(results) {
/*canvasのサイズ指定と画像の描画(スペースの都合により省略)*/
if (results.multiHandLandmarks) {
//見つけた手の数だけ処理を繰り返す
for (const landmarks of results.multiHandLandmarks) {
//骨格を描画
drawConnectors(/*省略*/);
//関節を描画
drawLandmarks(/*省略*/);
drawLightSaber();
}
}
canvasCtx.restore();
}
//ライトセイバーを表示
function drawLightSaber(){
} Lesson08
手の検出と連動した画像表示
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, 横幅, 縦幅
動作確認
(0,0) width
height
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
手の位置・角度の算出に関する考え方
関節点の集まり(多数の点)から、手の位置・角度を計算したい
⎼ OpenCVで図形に近似すると扱いが楽になる。 → 今回は楕円に近似
⎼ 楕円の中心を手の位置、傾きを手の向き、幅を画面上の手のサイズとする
⎼ 手首や親指の付け根は計算には用いないこととする
OpenCV
手の位置・角度の計算
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
手の位置・角度の計算
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
手の関節に対応するインデックス
ここは無視
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
楕円の表示
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
動作確認
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
画像を表示
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);
}
動作確認
右に傾けると上手く行かない
傾きが正しい場合と正しくない場合
140度
60度
120度
楕円の傾きは第2・3象限内(円の左半分)で計算される
角度の補正
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);
}
サイズを大きくしよう
大きさがちょっとショボい
サイズを大きくしよう
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);
動作確認
大きくなったけどズレてる
現状確認
描画の原点=手の位置
画像beamの輪郭
サイズを変える前から
実はそもそもズレていた
画像の位置の補正
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方向に半分ずらす
動作確認
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
ハンズオンの手順
カメラ画像の表示 手の認識結果表示 ライトセイバーの表示
手の位置・角度計算 手の位置・角度に追従 親指の状態に連動
親指の状態の計算
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
親指の状態の計算
4
7
19
 Distance1:
親指(4)から人差し指(7)までの距離
 Distance2:
人差し指(7)から小指(19)までの距離
 親指の状態(ratio):
Distance1 / Distance2
※ratioの大小で親指の状態を評価
親指の状態を計算
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
親指の状態を反映
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();
/*スペースの都合により省略*/
}
親指の立ち具合をかける
完成!

AI x WebAR: MediaPipeのハンドトラッキングを使ってみよう