face-api.jsでニコラ○・ケ○ジになる

顔認識がjsだけでもそれなりに動くのを今更知ったので触ってみた。
顔晒すのに抵抗感がある古い人間なので文中画像少なめです。

# 成果物
GitHub: https://github.com/engabesi/face-nicolas
GitHub Pages: https://engabesi.github.io/face-nicolas/
![2020-02-24_21h23_40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/a5460c31-b84e-253d-9d47-4ec8f5770af2.png)

# やること
- expressでlocalhostを建てる
- face-api.jsで顔の矩形を取る
- 顔に画像を被せる
- GitHub Pagesにdeploy

# face-api.js
今回顔認識に使うライブラリはこちら
https://github.com/justadudewhohacks/face-api.js
jsだけでそれなりの精度とパフォーマンスを出せます。
複数人も認識可能。
ランドマークや表情、顔認証も出来ます。
demoはこちら
https://justadudewhohacks.github.io/face-api.js/face_and_landmark_detection/

## 注意点
`face-api.js`はWebGLを使用しています。
その為ブラウザの設定でハードウェアアクセラレーションをオフにしている場合上手く動作しない可能性があります。

# expressでlocalサーバーを建てる
android等でも気軽にテストするためにまずはlocalhostで動作するようにします。
後にGitHub Pagesも利用したいため、以下の構成にします。

```:構成
root/
├─ app.js
└─ docs/
    ├─ index.html
    ├─ images/
    └─ js/
       ├─ index.js
       └─ lib/
          ├─ face-api.min.js
          └─ models/
             ├─ tiny_face_detector_model-shard1
             └─ tiny_face_detector_model-weights_manifest.json
```

`face-api.min.js`, 及びmodels内ファイルは以下のrepoから取ってきます
https://github.com/justadudewhohacks/face-api.js/tree/master/dist
https://github.com/justadudewhohacks/face-api.js/tree/master/weights

expressを導入します。

```bash:shell
yarn init -y
yarn add express
```

```html:docs\index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script defer src="./js/lib/face-api.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      canvas {
        position: absolute;
      }
    </style>
  </head>
  <body>
    <video id="video" width="720" height="560" autoplay muted></video>
    <script defer src="js/index.js"></script>
  </body>
</html>
```

```javascript:app.js
const express = require("express");
const app = express();

const path = require("path");
app.listen(8080, () => {
  console.log("Running at Port 8080...");
});

app.use(express.static(path.join(__dirname, "docs")));

app.use((req, res) => res.sendStatus(404));
```

これでshellに`node app.js`と打ち、http://localhost:8080/ にアクセスするとページが開きます(今は真っ白)

# webcamera設定

```javascript:docs\index.js
const startVideo = async video => {
  try {
    const constraints = { audio: false, video: {} };
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    video.srcObject = stream;
  } catch (error) {
    console.error(error);
  }
};

(async () => {
  const video = document.querySelector("video");
  await startVideo(video);
})();
```
これでlocalhostを開くとwebcameraの映像が取れているはずです。

# 顔の矩形を取る
まずmodelを読み込みます。

```javascript:docs\index.js
+ const loadModels = async () => {
+   await Promise.all([
+    faceapi.nets.tinyFaceDetector.loadFromUri(`/js/lib/models`)
+  ]);
+ };

  (async () => {
    const video = document.querySelector("video");
+   await loadModels();
    await startVideo(video);
  })();
```

次に顔を認識して矩形描画処理を追加します。

```javascript:docs\index.js
(async () => {
  const video = document.querySelector("video");
  await loadModels();
  await startVideo(video);
// --- ADD ---
  video.addEventListener("play", () => {
    // overlay canvas作成
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      // 検出結果をcanvasのサイズにリサイズ
      const resizedResults = faceapi.resizeResults(results, displaySize);
      // canvasの内容をクリア
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
      // 矩形描画
      faceapi.draw.drawDetections(canvas, resizedResults);
    }, 100);
  });
// --- ADD ---
})();
```
これでscoreが記載された矩形が顔に被さって表示されるようになります。

# 顔に画像を被せる
検出結果から座標を取ってその位置に画像を被せてみます。

```docs\index.js
(async () => {
  const video = document.querySelector("video");
+ // 画像セットアップ
+ const image = new Image();
+ image.src = `/images/cage_neutral.png`;
  await loadModels();
  await startVideo(video);
  video.addEventListener("play", () => {
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);
    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      const resizedResults = faceapi.resizeResults(results, displaySize);
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
-     // faceapi.draw.drawDetections(canvas, resizedResults);
+     resizedResults.forEach(detection => {
+       // 矩形のtopはデコあたりなので調整
+       const marginVal = 0.4;
+       // 矩形の情報はdetection.boxに格納されている
+       const width = detection.box.width;
+       const height = detection.box.height * (1.0 + marginVal);
+       const x = detection.box.x;
+       const y = detection.box.y - detection.box.height * marginVal;

+       canvas.getContext("2d").drawImage(image, x, y, width, height);
+     });
    }, 100);
  });
})();
```
これで自分の顔に画像を貼り付けることが出来ました。
他にもmodelやdetection時に`.with~~`とするだけで表情やランドマーク等の情報を取れます。
詳しくは公式README参照。自分のgitにあげている物にも画像調達に飽きて中途半端になっていますが表情を取るコードも記載してあります。

# GitHub Pages
コードをrepositoryに上げたら
repository > Settings > Options > GitHub Pages > Sourceを
`master branch /docs folder`にすればGitHub Pagesにデプロイすることが出来ます。
だから、ディレクトリ名をdocsにする必要があったんですね。

## 注意点
上記コードでは画像のpathを直打ちしています。
`github.io`にデプロイする場合は問題ありませんが、`github.io/SUB_REPO/`にデプロイした場合、ルートURLが`github.io`扱いとなり、SUB_REPOが飛ばされてしまってpathがおかしくなってしまいます。
`jekyll`の使用をGitHubは推奨していますが試して遊ぶだけの場合ちょっと面倒です。
暫定対策としてpath前に直接SUB_REPO名をくっつけてあげると一応動きます。

# まとめ
たった数分でブラウザ完結の顔認識ができるなんて素晴らしい世の中になりました。
現状のコードだとスマートフォンで見た場合矩形がものすごく横長になってしまっているのでその辺の対応をしだすとヘビーになるかもしれませんがちょっと遊んで見るぐらいだと非常に有用だと思います。

他にも`clmtrackr.js`や`pico.js`、更にはChromeの機能だけでできる`Shape Detection API`等様々なライブラリがありますので是非触って遊んでみてください。