PHONE APPLI Engineer blog

エンジニアブログ

face-api.jsでルチャドールになりたい

最近スパイスからカレーを作ることにハマっています。

リサーチデベロップメントのたかはしとし( @doushiman_jp ) です。

好きなスパイスはカルダモンです。

先日「body-pixを使ってさっくりバーチャル背景」という記事を書きました。

phoneappli.hatenablog.com

その時はバーチャル背景ということだったのでbody-pix を使って人体を認識して周囲をボカシてみました。

今回は、を隠してみたいと思います。

なぜマスクをかぶるのか

face-api.js というJavascriptモジュールがあります。

これも先日のbody-pix同様、Tensorflow.js を使用しています。

名前の通りface-api.js は「顔」を検出するためのJavascriptライブラリです。

いくつかのモデルが公開されていて、2つの顔の類似性を算出したり、喜怒哀楽といった表情の解析、年齢性別の分析、などが可能です。

その中で今回は「tinyFaceDetector」という、高速に顔の位置などを認識するモデルを使います。

リポジトリをclone して、そこからface-api.min.js とweights ディレクトリを取り出して、自分のワークスペースにコピーします。

基本的に使うのはこれだけです。

Web会議などで、(寝不足だったりして)お顔のコンディションが優れない時、ありますよね。

ビデオ自体をオフにしてしまえば表情は隠せますが、それも少し味気ない。

リアルに覆面を被るのも面倒だ(ちなみに僕は、そんな時もあろうかと自席に仮面が常備されています)

f:id:doushiman_jp:20210720134238j:plain

なので、顔を認識した上で、ビデオ画像の顔部分にマスクを貼り付けてみたいと思います。

↓こんなの

f:id:doushiman_jp:20210720134221p:plain

*ルチャドールとは:華麗な空中殺法を得意とするメキシカンレスラーのこと。多くが覆面をかぶっています。

まずは顔認識

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 を実行して、顔の位置に合わせてマスクを再描画してくれるので、チラつくことはありません。

これで、僕もルチャドールになれるはずです。

早速実行してみます。

結果発表

強そう。

f:id:doushiman_jp:20210720142748p:plain

どこからどう見ても、引退してぽっちゃりしたメキシカンレスラーです。とほほ。

動画でお見せできれば良かったのですが、多少顔を動かしてもしっかり追従してくれます。いいね。

まとめ

今回はVideo の上にCanvas を重ねているだけですが、一度Canvas 上で画像をビデオとマスクを合成してからStream を生成すれば、WebRTC にも応用できるのかな〜。

元々全然違う用途でface-api.js を触っていたのですが、ローカルで手軽に顔認識できるということは、色々応用の幅が広がるなあ、と思いました。

あとは、自分の映ったスクリーンショットを見て、もう少し痩せないとなあ、と決意を新たにしたのでした。

ではまた!


PHONE APPLIについて

phoneappli.net
phoneappli.net