（ジョークアプリ）laughingman

# Google chromeのFace Detection

以前どこかでGoogle chromeのShape Detection APIが話題になったのを見かけました。この中にFace Detectionがあるらしいので、これを使えばかの有名な笑い男ができるかも？と思い、試してみました。パソコン一台、chrome（＋設定変更）だけで動かせるのが便利ですね。

# 出来上がり

[https://dreamy-turing-428cbe.netlify.com/](https://dreamy-turing-428cbe.netlify.com/)

（[github](https://github.com/cranpun/laughingman)）

開くと「カメラ使っていいですか？」という警告が表示されます。それをOKして、少々お待ちいただければ自動で開始するはず。

確認したのはmacbook＋chrome（最新）。windows+chromeでも大丈夫そうです。2019/8現在だと、chromeのフラグである「Experimental Web Platform features」を有効にしないとダメっぽいです。chrome://flagsから設定できます。
噂によるとAndroidのchromeでも動くらしいんですが、持ってないです。

笑い男の画像を使うのはいかにもまずいので自分のアカウント画像を使ってます。丸わかりとは思いますが、アカウント画像のモチーフです、笑い男。

![laughingman.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23715/563c57d5-4a3c-06b9-9ba9-9d4c98382cfd.gif)

スマフォでyoutubeを再生して、それをPCのwebカメラで撮影しました。
（自分の顔を使うのは動作確認のときにとてもつらい…）
さすがに画像が荒いこともあって百発百中とまではいきませんが、結構な精度と速度です。

基の動画はYoutube-8Mの中から探しました。[こちら](https://youtube.com/watch?v=In4tb4-qHQQ)です。

# 解説

## 1. Face Detection・カメラ周り

今回はwebカメラからリアルタイムで映像を取得する必要があります。なのでFace Detection以外にカメラの起動も必要になります。
大まかな流れはロード完了でカメラを起動、以後、映像読み込み時のフックで「顔検出」「カメラ映像をCanvasに表示」「アイコンをCanvasに表示」です。
カメラは、videoタグ（非表示）のsrcにカメラ映像を設定して、canvasに表示する形で作りました。

### 1. カメラの起動

windowのonloadで以下を実行しました。
映像読み込み時のフック（コード中ではtick関数）なんですが、初回だけ呼び出して、あとは自分自身で再設定するような形です。setTimeoutで周期処理をするようなときのやり方でしょうか。
canvasが２つある理由は後述。

```javascript
// カメラを取得
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// videoタグにカメラを関連付け
const myvideo = document.querySelector("#myvideo");
myvideo.srcObject = stream;

// カメラ周りの初期設定
myvideo.onloadedmetadata = async (e) => {
    // カメラ起動
    myvideo.play();
    // canvasのサイズをカメラサイズに合わせる。vwidth, vheightはキャンバスクリア用の変数です。
    mycanvasicon.width = mycanvasvideo.width = vwidth = myvideo.videoWidth;
    mycanvasicon.height = mycanvasvideo.height = vheight = myvideo.videoHeight;

    // 初回の映像読み込み
    await tick();
};
```

### 2. 映像読み込み時のフック

「顔検出」「アイコンをCanvasに表示」「カメラ映像をCanvasに表示」です。本記事のキモとなる顔検出はなんと一行。

```javascript
// 顔認識のインスタンス
const detector = new FaceDetector();

// 映像読み込み時のフック
const tick = async () => {
    try {
        // アイコン画像が読み込めていたら映像表示開始。
        if (loadedmyicon) {
            // 顔検出。まさかの一行。これだけで写真中の顔全ての情報が配列で取得できます。
            const detects = await detector.detect(myvideo);

            // 以前のアイコンをクリア。これをしないとアイコンが増えていきます。
            myctxicon.clearRect(0, 0, vwidth, vheight);

            // 検出した顔すべてにアイコンを表示。
            for (const detect of detects) {
                const box = detect.boundingBox;
                // boxの大きさが顔より随分小さいので、サイズ拡大する自作関数。
                const pos = fixPosWH(box);
                // アイコン表示
                myctxicon.drawImage(myicon, pos.x, pos.y, pos.wh, pos.wh);
            }
            // カメラの画像を表示。画面いっぱいに上書きするので、前回の表示クリアは不要。
            myctxvideo.drawImage(myvideo, 0, 0, vwidth, vheight);
        }
        // 次回のカメラ映像読み込み準備。
        window.requestAnimationFrame(tick);
    } catch (e) {
        console.error(e);
    }
};
```


## 2. その他

Face Detection以外で困ったことです。

### 1. アイコンが表示されない

カメラの画像のほうが大きいせいか、どうやらアイコンを先に描画してしまい、カメラの画像で上書きされてしまうようでした。

そこで、canvasを２つ用意して```position: fixed```で重ね合わせ、カメラを表示するキャンバスを下に、アイコンを表示するキャンバスを上にすることで解決しました。上に書いたキャンバスが先に描画されます。

```html:index.html
<body>
  <video id="myvideo" autoplay style="visibility: hidden"></video>
    <div id="canvases" style="position: relative">
      <canvas id="mycanvasvideo" class="mycanvas" style="position: fixed; top: 0; left: 0;"></canvas>
      <canvas id="mycanvasicon" class="mycanvas" style="position: fixed; top: 0; left: 0;"></canvas>
    </div>
</body>
```

ちなみに、アイコンの画像の読み込みも待つ必要がありました。

```javascript:main.js
// 顔に表示するアイコンと読み込み待ちフラグ
const myicon = new Image();
myicon.src = "./icon.png";
let loadedmyicon = false;
myicon.onload = () => {
  loadedmyicon = true; // カメラの画像を反映するときにこのフラグを参照
}
```

これ以外に、検知される大きさと顔の大きさがあってないので、アイコンの大きさを調整したりと、微妙な困りごともありました。

# おわりに

意外ときびきび動作するのにびっくりしました。顔隠して動画撮影するときとかいいかもですね。