【React + Typescriptで顔認識】TensorFlowを使って画像にマスクをかけるアプリを作った

こんにちは、[Yui](https://twitter.com/yui_active)です。
最近暇になってきたので、週イチ発信を自主的に始めることにしました。

とりあえずは仕事でめちゃくちゃ忙しくなるか、飽きるか、ネタがなくなるまでは続けることにします。
というわけで今回はwebcameraとTensorFlowを使って、画像にマスクをリアルタイムでつけるアプリを作ったのでその手順を公開します。

完成はこちら

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">マスクを描画するアプリを作って遊ぶなどした <a href="https://t.co/qT230RNZJQ">https://t.co/qT230RNZJQ</a> <a href="https://t.co/xwgIGj2U3d">pic.twitter.com/xwgIGj2U3d</a></p>&mdash; Yui 🌔 Yuiko Ito (@yui_active) <a href="https://twitter.com/yui_active/status/1396480098421850113?ref_src=twsrc%5Etfw">May 23, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

DEMO→https://mask-app-one.vercel.app/

** 追記: エラーハンドリングが不十分なため、動作が少し不安定です。もし1〜2秒待ってマスクが表示されない場合は、リロードしてください..。随時修正していきます。


## react-webcamをインストール

雛形を作ります。

```
$ npx create-react-app face-mask-app --template typescript
```

```
yarn add react-webcam @types/react-webcam
```

[公式](https://www.npmjs.com/package/react-webcam)を参考に動かしてみます。
スタイルはとりあえずはベタ書きしておきます。

```react:App.tsx
import { useRef } from "react";
import "./App.css";
import Webcam from "react-webcam";

const App = () => {
  const webcam = useRef<Webcam>(null);

  return (
    <div className="App">
      <header className="header">
        <div className="title">face mask App</div>
      </header>
      <Webcam
        audio={false}
        ref={webcam}
      />
    </div>
  );
}

export default App;

```

デザインはともかく、起動できました。

![pic2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/c37627a1-82c5-80ae-3fda-bdc85cce15b8.png)


それでは次にTensorflowで顔認識をします。

良さげなモデルを見つけたので、以下を使います。
https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection

```
yarn add @tensorflow-models/face-landmarks-detection @tensorflow/tfjs-core @tensorflow/tfjs-converter @tensorflow/tfjs-backend-webgl
```

[公式](https://www.npmjs.com/package/@tensorflow-models/face-landmarks-detection)を参考に書いていきます。

```react:App.tsx
// 最初にインポートが必要
import "@tensorflow/tfjs-core";
import "@tensorflow/tfjs-converter";
import "@tensorflow/tfjs-backend-webgl";
import * as faceLandmarksDetection from "@tensorflow-models/face-landmarks-detection";
import { MediaPipeFaceMesh } from "@tensorflow-models/face-landmarks-detection/dist/types";

const App = () => {
  const webcam = useRef<Webcam>(null);
  const canvas = useRef<HTMLCanvasElement>(null);

  const runFaceDetect = async () => {
    const model = await faceLandmarksDetection.load(
      faceLandmarksDetection.SupportedPackages.mediapipeFacemesh
    );
    detect(model);
  };

  const detect = async (model: MediaPipeFaceMesh) => {
    if (webcam.current) {
      const webcamCurrent = webcam.current as any;
      // ビデオのロードが完了するのを確認してから次のステップに進む
      if (webcamCurrent.video.readyState === 4) {
        const video = webcamCurrent.video;
        const predictions = await model.estimateFaces({
          input: video,
        });
        if (predictions.length) {
          console.log(predictions);
        }
      }
    };
  };

  useEffect(() => {
    runFaceDetect();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [webcam.current?.video?.readyState])

```

:::note warn
補足
最新版では、``faceLandmarksDetection.SupportedPackages.mediapipeFacemesh``の部分が以下に変更になっているとの連絡を頂きました。@kurikurisan さんありがとうございます！

```javascript
const model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
const detectorConfig = {
  runtime: 'mediapipe', // or 'tfjs'
  solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh',
}
const detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
```
:::

とりあえずconsole.logで顔認識ができているか確認しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/99846b3a-3a76-a5cf-c0c8-6c9c1763f24c.png)

## canvasで動画に画像を重ねる

それでは次に動画に画像を重ねていきます。
canvasを使いたいので、Webcamタグの下にcanvasタグを追加します。

```react
  const webcam = useRef<Webcam>(null);
  const canvas = useRef<HTMLCanvasElement>(null);

// 省略 //
      <Webcam
        audio={false}
        ref={webcam}
      />
      <canvas
        ref={canvas}
      />
```

canvasで描画するサイズをvideoと合わせたいので、

```react
    const videoWidth = webcamCurrent.video.videoWidth;
    const videoHeight = webcamCurrent.video.videoHeight;
    canvas.current.width = videoWidth;
    canvas.current.height = videoHeight;
```

で、どこを描画するかですが、そこは[公式のマップ](https://github.com/tensorflow/tfjs-models/blob/master/face-landmarks-detection/mesh_map.jpg)を見ながらめちゃくちゃ頑張って座標を確認しました。（正直この作業が一番大変だったｗ）

マップによると、鼻上らへんが195番目だったので、そこを支点として、canvasのbeginPath()〜closePath()を利用して書くことにします。
ちょっと描画部分のロジックが長くなってしまったので、mask.tsでファイルを切り分けます。

```react:mask.ts
import { AnnotatedPrediction } from "@tensorflow-models/face-landmarks-detection/dist/mediapipe-facemesh";
import {
  Coord2D,
  Coords3D,
} from "@tensorflow-models/face-landmarks-detection/dist/mediapipe-facemesh/util";

const drawMask = (
  ctx: CanvasRenderingContext2D,
  keypoints: Coords3D,
  distance: number
) => {
  const points = [
    93,
    132,
    58,
    172,
    136,
    150,
    149,
    176,
    148,
    152,
    377,
    400,
    378,
    379,
    365,
    397,
    288,
    361,
    323,
  ];

  ctx.moveTo(keypoints[195][0], keypoints[195][1]);
  for (let i = 0; i < points.length; i++) {
    if (i < points.length / 2) {
      ctx.lineTo(
        keypoints[points[i]][0] - distance,
        keypoints[points[i]][1] + distance
      );
    } else {
      ctx.lineTo(
        keypoints[points[i]][0] + distance,
        keypoints[points[i]][1] + distance
      );
    }
  }
};

export const draw = (
  predictions: AnnotatedPrediction[],
  ctx: CanvasRenderingContext2D,
  width: number,
  height: number
) => {
  if (predictions.length > 0) {
    predictions.forEach((prediction: AnnotatedPrediction) => {
      const keypoints = prediction.scaledMesh;
      const boundingBox = prediction.boundingBox;
      const bottomRight = boundingBox.bottomRight as Coord2D;
      const topLeft = boundingBox.topLeft as Coord2D;
      // 顔のサイズを参考に、マスクを少しだけ大きくする
      const distance =
        Math.sqrt(
          Math.pow(bottomRight[0] - topLeft[0], 2) +
            Math.pow(topLeft[1] - topLeft[1], 2)
        ) * 0.02;
      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = "black";
      ctx.save();
      ctx.beginPath();
      drawMask(ctx, keypoints as Coords3D, distance);
      ctx.closePath();
      ctx.fill();
      ctx.restore();
    });
  }
};

```

後は上記の関数をApp.tsx内で呼び出すだけ

```react:App.tsx
  const runFaceDetect = async () => {
    const model = await faceLandmarksDetection.load(
      faceLandmarksDetection.SupportedPackages.mediapipeFacemesh
    );
    detect(model);
  };

  const detect = async (model: MediaPipeFaceMesh) => {
    if (webcam.current && canvas.current) {
      const webcamCurrent = webcam.current as any;
      if (webcamCurrent.video.readyState === 4) {
        const video = webcamCurrent.video;
        const videoWidth = webcamCurrent.video.videoWidth;
        const videoHeight = webcamCurrent.video.videoHeight;
        canvas.current.width = videoWidth;
        canvas.current.height = videoHeight;
        const predictions = await model.estimateFaces({
          input: video,
        });
        const ctx = canvas.current.getContext("2d") as CanvasRenderingContext2D;
        requestAnimationFrame(() => {
          draw(predictions, ctx, videoWidth, videoHeight);
        });
        detect(model);
      }
    }
  };

```

## 制作時間
制作時間は構想からデプロイまでトータルで10時間ぐらいかかりました。
デザインもほぼないようなアプリなのに時間かけすぎた感があります。
遊びで作ってるMVPアプリなので、もっとサクサク作れるようになりたいもんですねー。

## あとがき
最初[face-api.js](https://github.com/justadudewhohacks/face-api.js)を使おうとして、モデルの読み込みができずに5時間ぐらい溶かした挙げ句、結局face-api.jsの元になったtensorflowで作成することにしました。

[issue報告](https://github.com/justadudewhohacks/face-api.js/issues/598)にも上がっているものですが、このエラーが解消できずに、もうわけがわからなくなったのでtensorflowに逃げました。笑
分かる人教えてください..。

今回の全体のコードはこちら→https://github.com/YuikoIto/mask-app

## 参考
- [Canvasでよく使う描画テクまとめ](https://y-stream.blogspot.com/2013/02/jscanvas.html)
- [tfjs-models](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection)
- [Iris landmark tracking in the browser with MediaPipe and TensorFlow.js](https://blog.tensorflow.org/2020/11/iris-landmark-tracking-in-browser-with-MediaPipe-and-TensorFlowJS.html)