TensorFlow.jsを土台として作成されたml5.jsライブラリを利用すると、APIを通じてブラウザ上で機械学習のアルゴリズムとモデルを使うことができます。今回は顔のパーツを認識してその部分に別の画像を表示させます。顔を隠したいときに使えるかもしれません。
Contents
静止画と動画から顔のパーツを検出する
最初に静止画の顔検出をしてみることにします。
以下のコードはサーバー上でないとうまく動きません。
HTML部分
まずHTML部分を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>静止画から顔のパーツを検出する</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://unpkg.com/ml5@0.4.3/dist/ml5.min.js"></script> </head> <body> <img src="./image.png" id = "image" /><br> <!-- canvasに画像を描画して目と鼻と口がある部分に「目」「鼻」「口」と書かれた画像を表示する --> <canvas id="can"></canvas><br> <img src="./eye.png" id = "eye"> <img src="./nose.png" id = "nose"> <img src="./mouth.png" id = "mouth"><br> <div id = "message"></div> <script src="./index.js"></script> </body> </html> |
画像の読み込み
JavaScript部分を示します。
1 2 3 4 5 6 |
let $eye = document.getElementById('eye'); let $nose = document.getElementById('nose'); let $mouth = document.getElementById('mouth'); let $can = document.getElementById('can'); let $message = document.getElementById('message'); |
getFaceImageAsync関数は画像ファイルのパスを引数とし、image要素を返します。
1 2 3 4 5 6 7 8 9 |
async function getFaceImageAsync(path){ let image = new Image(); image.src = path; await new Promise(resolve => { image.onload = () => resolve('') }); return image; } |
faceAPIの呼び出し
faceAPIDetect関数はgetFaceImageAsync関数が返した要素を引数として、faceAPIを呼び出します。そしてその結果を返します。また処理に時間がかかるので「処理中」とかどこまで処理が完了したのかを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
async function faceAPIDetect($face){ $message.innerText = '処理中'; // オプションの設定 const detectionOptions = { withLandmarks: true, withDescriptors: false }; let faceAPI; let ret = await new Promise(resolve => { // faceAPIの利用 faceAPI = ml5.faceApi(detectionOptions, () => resolve('done ml5.faceApi')); }); $message.innerText = '処理中 ' + ret; let detectResults; let detectError; ret = await new Promise(resolve => { faceAPI.detect($face, (err, results) => { detectResults = results; detectError = err; resolve('done faceAPI.detect'); }) }); $message.innerText = '処理中 ' + ret; return detectResults; } |
検出結果の表示
faceAPIDetect関数が返したオブジェクトから各パーツの座標を抜き出すには以下のようにします。
1 2 3 4 5 6 7 8 9 10 |
// results はfaceAPIDetect関数の戻り値 for(let i=0; i < results.length; i++){ let arr = results[i].parts.nose; // 鼻の座標の配列 for(let i = 0; i < arr.length; i++){ let x = arr[i]._x; let y = arr[i]._y; } } |
getMeanOfX関数、getMeanOfY関数は座標の配列からX座標とY座標の平均値をそれぞれ取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function getMeanOfX(arr){ if(arr.length == 0) return -1; let sum = 0; for(let i = 0; i < arr.length; i++) sum += arr[i]._x; return sum / arr.length; } function getMeanOfY(arr){ if(arr.length == 0) return -1; let sum = 0; for(let i = 0; i < arr.length; i++) sum += arr[i]._y; return sum / arr.length; } |
ページが読み込まれたらこれらの関数を用いて目と鼻と口がある座標を取得してそれらの中心に画像を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
window.onload = async () => { // canvasに画像を描画するが、処理が完了するまで非表示 $can.style.display = 'none'; let $face = await getFaceImageAsync('./image.png'); $can.width = $face.width; $can.height = $face.height; ctx = $can.getContext('2d'); ctx.drawImage($face, 0, 0); let results = await faceAPIDetect($face); console.log(results); for(let i=0; i<results.length; i++){ // 鼻があるだいたいの位置を取得 let arr = results[i].parts.nose; let x = getMeanOfX(arr); let y = getMeanOfY(arr); ctx.drawImage($nose, x-8, y-8, 16, 16); // 右目があるだいたいの位置を取得 arr = results[i].parts.rightEye; x = getMeanOfX(arr); y = getMeanOfY(arr); ctx.drawImage($eye, x-8, y-8, 16, 16); // 左目があるだいたいの位置を取得 arr = results[i].parts.leftEye; x = getMeanOfX(arr); y = getMeanOfY(arr); ctx.drawImage($eye, x-8, y-8, 16, 16); // 口があるだいたいの位置を取得 arr = results[i].parts.mouth; x = getMeanOfX(arr); y = getMeanOfY(arr); ctx.drawImage($mouth, x-8, y-8, 16, 16); } // 処理が完了したのでcanvasを表示する $can.style.display = 'block'; $message.innerText = ''; } |
動画から顔のパーツを検出する
動画としてカメラを使います。カメラの使用を許可するとカメラで撮影されたあなたの顔が表示されます。目がある部分には「目」と書かれた画像が表示されます。急に顔を動かすと画像も少しおくれてついていきます。
今回はml5.jsが提供している機能の中でも、体の部位の座標を取得してくれる poseNet を使います。カメラをつかった処理はp5.jsを使います。そのためml5以外に上の2行が必要です。
1 2 3 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script> <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script> |
p5.jsのカメラをつかった描画
p5.jsでカメラをつかった描画は以下のようにおこないます。
setup関数とdraw関数は自動的に実行されます。最初にcreateCanvas関数でcanvasを生成し、loadImage関数で画像ファイルを読み込み、カメラで撮影されたものが描画できるようにしておきます。あとはdraw関数によって指定された位置に描画処理がおこなわれます。
loadImage関数はサーバー上でないとエラーが発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>p5の描画処理</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script> <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script> </head> <body> <script> let video; const width = 500, height = 400; let eyeImage; function setup() { eyeImage = loadImage('./eye.png'); createCanvas(width, height); // キャプチャされたものをそのまま表示するのではなく、canvasに表示させたい video = createCapture(VIDEO); video.size(width, height); video.hide(); // こっちは非表示にする } function draw() { // キャプチャされたものをcanvasに表示させる image(video, 0, 0, width, height); // 画像ファイル(eye.png)を座標(100, 100)幅と高さ 32 でcanvasに表示させる image(eyeImage, 100, 100, 32, 32); } </script> </body> </html> |
HTML部分
カメラで撮影された画像から目を検出する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>カメラで撮影されたものから目を検出する</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script> <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script> </head> <body> <div id="is-ready"></div> <script src="./video.js"></script> </body> </html> |
JavaScript部分
定数とグローバル変数を示します。両目のX座標とY座標を格納する変数には-100(見えない場所)を格納しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 |
// canvasの幅と高さ const width = 500, height = 400; let video; let eyeImageLoaded = false; let eyeImage; // 両目のX座標とY座標 let eyelX = -100; let eyelY = -100; let eyerX = -100; let eyerY = -100; |
初期化の処理でml5.poseNet関数を呼び出し、引数に対象のVideo Element とロード後の処理を記述しています。poseNet.on(‘pose’, gotPoses);とすることで新しいポーズが検出されるたびにgotPoses関数が実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function setup() { let $isReady = document.getElementById('is-ready'); poseNet = ml5.poseNet(video, () => $isReady.innerText = '準備中'); eyeImage = loadImage('./eye.png'); createCanvas(width, height); video = createCapture(VIDEO); video.size(width, height); video.hide(); poseNet = ml5.poseNet(video, () => $isReady.innerText = '準備完了'); poseNet.on('pose', gotPoses); } |
gotPoses関数では目が検出された場合はグローバル変数にその座標の値を格納しています。そうでない場合は-100を格納して「目」と書かれた画像が描画されない(実際には見えない位置に描画)ようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function gotPoses(poses) { if (poses.length > 0) { let pose = poses[0].pose; eyelX = pose.leftEye.x; eyelY = pose.leftEye.y; eyerX = pose.rightEye.x; eyerY = pose.rightEye.y; } else { eyelX = -100; eyelY = -100; eyerX = -100; eyerY = -100; } } |
描画の処理ではカメラで撮影された画像と目が検出された座標に「目」と書かれた画像を表示させます。
1 2 3 4 5 6 |
function draw() { image(video, 0, 0, width, height); image(eyeImage, eyelX-16,eyelY-16,32,32); image(eyeImage, eyerX-16,eyerY-16,32,32); } |