最近スパイスからカレーを作ることにハマっています。
リサーチデベロップメントのたかはしとし( @doushiman_jp ) です。
好きなスパイスはカルダモンです。
先日「body-pixを使ってさっくりバーチャル背景」という記事を書きました。
その時はバーチャル背景ということだったのでbody-pix を使って人体を認識して周囲をボカシてみました。
今回は、顔を隠してみたいと思います。
なぜマスクをかぶるのか
face-api.js というJavascriptモジュールがあります。
これも先日のbody-pix同様、Tensorflow.js を使用しています。
名前の通りface-api.js は「顔」を検出するためのJavascriptライブラリです。
いくつかのモデルが公開されていて、2つの顔の類似性を算出したり、喜怒哀楽といった表情の解析、年齢性別の分析、などが可能です。
その中で今回は「tinyFaceDetector」という、高速に顔の位置などを認識するモデルを使います。
リポジトリをclone して、そこからface-api.min.js とweights ディレクトリを取り出して、自分のワークスペースにコピーします。
基本的に使うのはこれだけです。
Web会議などで、(寝不足だったりして)お顔のコンディションが優れない時、ありますよね。
ビデオ自体をオフにしてしまえば表情は隠せますが、それも少し味気ない。
リアルに覆面を被るのも面倒だ(ちなみに僕は、そんな時もあろうかと自席に仮面が常備されています)
なので、顔を認識した上で、ビデオ画像の顔部分にマスクを貼り付けてみたいと思います。
↓こんなの
*ルチャドールとは:華麗な空中殺法を得意とするメキシカンレスラーのこと。多くが覆面をかぶっています。
まずは顔認識
HTMLはこんな感じです。
face-api.min.jsと、実働部分のjsを読み込むところ、あとはvideo とcanvas くらいです。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="js/face-api.min.js"></script> <script src="js/main.js" defer></script> <title>ルチャドールになれるくん</title> <style> #canvas { position: absolute; top:0; left: 0; } </style> </head> <body> <video id="camera" width="800" height="600" playsinline muted autoplay></video> <canvas id="canvas"></canvas> </body> </html>
学習モデル読み込み
次に、main.jsの初期化部分で、jsディレクトリ下においた学習モデルを読み込みます。
今回は二つ使用しています。
faceapi.nets.tinyFaceDetector.load("js/weights/"), faceapi.nets.faceExpressionNet.load("js/weights/")
次に、カメラの画像をvideoタグに表示します。
setCamera = async () => { const constraints = { audio: false, video: { width: 800, height: 600 facingMode: 'user', } }; await navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { camera.srcObject = stream; camera.onloadedmetadata = (e) => { camera.play() checkFace() }; })
camera はvideo タグを指すオブジェクトです。
ここまででカメラ画像を表示することができたので、次にcheckFace の中身を書いて、顔認識を実装します。
顔認識
chara.src = "./img/mask.png"; checkFace = async () => { let faceData = await faceapi.detectAllFaces( camera, new faceapi.TinyFaceDetectorOptions() ).withFaceExpressions(); if(faceData.length){ const setDetection = () => { let box = faceData[0].detection.box; x = box.x, y = box.y, w = box.width, h = box.height; canvas.getContext('2d').clearRect(0, 0, 800, 600); ctx.drawImage(chara, x, y, w, h); }; setDetection(); } requestAnimationFrame(checkFace); };
faceDataに顔部分を表す矩形が格納されてきます。
そのXYと高さ、幅を元に、canvas にマスク画像をdrawimage します。
ここで重要なのはrequestAnimationFrame で再帰的にcheckFace をコールしているところです。
setIntarval で再描画してしまうと画面のリフレッシュレートに関係なくマスクを描画してしまうので、マスク画像がチラついてしまいます。
requestAnimationFrame だとリフレッシュレートに合わせてcheckFace を実行して、顔の位置に合わせてマスクを再描画してくれるので、チラつくことはありません。
これで、僕もルチャドールになれるはずです。
早速実行してみます。
結果発表
どこからどう見ても、引退してぽっちゃりしたメキシカンレスラーです。とほほ。
動画でお見せできれば良かったのですが、多少顔を動かしてもしっかり追従してくれます。いいね。
まとめ
今回はVideo の上にCanvas を重ねているだけですが、一度Canvas 上で画像をビデオとマスクを合成してからStream を生成すれば、WebRTC にも応用できるのかな〜。
元々全然違う用途でface-api.js を触っていたのですが、ローカルで手軽に顔認識できるということは、色々応用の幅が広がるなあ、と思いました。
あとは、自分の映ったスクリーンショットを見て、もう少し痩せないとなあ、と決意を新たにしたのでした。
ではまた!