face-api.jsでルチャドールになりたい
最近スパイスからカレーを作ることにハマっています。
リサーチデベロップメントのたかはしとし( @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 を触っていたのですが、ローカルで手軽に顔認識できるということは、色々応用の幅が広がるなあ、と思いました。
あとは、自分の映ったスクリーンショットを見て、もう少し痩せないとなあ、と決意を新たにしたのでした。
ではまた!