顔出しをせずに表情だけを伝えることができるwebアプリを作った【Next.js+TypeScriptでface-api.jsを使って表情認識】

最近オンラインでのミーティングが増えていますが、**顔出ししたくないけど表情だけは伝えたい**と思うことってありますよね。

やっぱり人と話す上で表情は大事なので顔出しはしたいけど今メイクしてなくて...といったときに使えるアプリを作ったので、この記事ではその作り方を紹介します。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">顔出しはしたくないけど表情は伝えたい、そんなときに使えるwebアプリ作りました <a href="https://t.co/g0t2svR1Hv">https://t.co/g0t2svR1Hv</a> <a href="https://t.co/1AdkXoWELJ">pic.twitter.com/1AdkXoWELJ</a></p>&mdash; yui ⛅ Yuiko Ito (@yui_active) <a href="https://twitter.com/yui_active/status/1424386546577313796?ref_src=twsrc%5Etfw">August 8, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

もし需要があれば実際のミーティングでも使えるように**Chromeの拡張機能**にしてもいいかなとも思ったんですが、私の表情が乏しいのか、怒った顔や悲しい顔をしてもなかなか認識してくれず、笑顔とびっくり顔しかできませんでした。笑
まあ怒ってても認識されないなら、温和に会議が進む気がするのでそれはそれで良いかもですね！

DEMO→https://face2emoji.vercel.app/
github→https://github.com/yuikoito/face2emoji

ちなみに週一でなにかアプリを作って発信するということを続けていますが、この記事で**12週目**になります。
あとがきにこれまでの記事をまとめておきますので、よければそちらも読んでいただけると嬉しいです！

面白いなと思ったらLGTMしてもらえると励みになります :pray:

## アプリの構成と機能に関して

今回、いつもどおり**Next.js + TypeScript**でアプリを作っています。
表情を認識するのはTensorFlow.jsをもとにして作られたモジュールである[face-api.js](https://github.com/justadudewhohacks/face-api.js/)を使っています。
そしてホスティングはvercelです。

機能はシンプルでアプリを起動すると顔と表情を認識してその上に絵文字をのせるようになっています。
絵文字は[appleの絵文字](https://emojipedia.org/apple/)を利用しており、pngでダウンロードして直接public配下においてます。（雑）

face-api.jsでは表情は**7パターン**で取得できるので、それぞれ以下の通りに表情に対しての絵文字を設定しています。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/07dbee89-d54f-b759-bf2c-bba3de474950.png)

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/96eadd03-fbea-f339-ec8c-553ef3f31a1b.png)

それでは早速このアプリを作り方を説明していきます。

## face-api.jsをインストールして使えるようにする

※Next.js+TypeScriptのセットアップ及びwebcamの設定に関しては[前に書いた記事](https://qiita.com/yuikoito/items/bb9c40bbb673e4f71abe)と同じなので必要であればその手順を参考にしてください。

```
$ yarn add face-api.js
```

でface-api.jsをインストールします。

ただ、face-api.jsではREADMEにある通りモデルを読み込む必要があるので、[face-api.jsのgithub](https://github.com/justadudewhohacks/face-api.js/)から**weightsフォルダ**をコピーしてきて、public配下に置きます。
名前はweights→modelsに変更します。

ここまでできたらモデルを読み込みます。


```react
  const loadModels = async () => {
    const MODEL_URL = "/models";
    await Promise.all([
      faceapi.nets.tinyFaceDetector.load(MODEL_URL),
      faceapi.nets.faceExpressionNet.load(MODEL_URL),
    ]);
  };
```

これでloadModelsの実行後はface-api.jsで顔認識と表情認識ができるようになりました。
ちなみに今回顔認識部分ではtinyFaceDetectorを利用しています。ssdMobilenetv1だともっと精度は良くなりますが、かなり重くなるので、動かない端末が出てきます。（特にスマホではほとんど固まると思います。）

顔認識と表情認識ができるようになったので、まずはログが正しく出力されるか確認します。
以下のように一旦ボタンクリックで顔認識するようにして、ログを見てみます。

```react:pages/index.tsx
import * as faceapi from "face-api.js";
import { useRef } from "react";
import Webcam from "react-webcam";

export default function Home() {
  const webcamRef = useRef<Webcam>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const loadModels = async () => {
    const MODEL_URL = "/models";
    await Promise.all([
      faceapi.nets.tinyFaceDetector.load(MODEL_URL),
      faceapi.nets.faceExpressionNet.load(MODEL_URL),
    ]);
  };

  const faceDetectHandler = async () => {
    await loadModels();
    if (webcamRef.current && canvasRef.current) {
      const webcam = webcamRef.current.video as HTMLVideoElement;
      const canvas = canvasRef.current;
      webcam.width = webcam.videoWidth;
      webcam.height = webcam.videoHeight;
      canvas.width = webcam.videoWidth;
      canvas.height = webcam.videoHeight;
      const video = webcamRef.current.video;
      const detectionsWithExpressions = await faceapi
        .detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
        .withFaceExpressions();
      console.log(detectionsWithExpressions);
    }
  };

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <Webcam audio={false} ref={webcamRef} className={styles.video} />
        <canvas ref={canvasRef} className={styles.video} />
        <button onClick={faceDetectHandler}>顔認識</button>
      </main>
    </div>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/aa676f1f-677e-3b58-7e21-6f8df185e55a.png)

こんなふうにログが出ていれば成功です。

detectionの部分が顔認識の部分で座標などが入っています。expressionsが表情で、それぞれの表情のスコアが入っています。

ここまでできれば後はdetectionの部分で顔の場所を確認して、expressionsの中で一番高いスコアの表情に基づく絵文字を描画するだけです。


## 表情に応じて絵文字を変更する

描画部分は長くなるので、別のファイルにロジックをまとめます。
先程出力したdetectionsWithExpressionsと描画するcanvasをindex.tsxから受け取るようにします。

まずは、表情に関しては以下のように**表情名: スコア**の形で出力されるため、計算しやすい形に変えます。

```
{
  angry: 0.00012402892753016204
  disgusted: 0.00000494607138534775
  fearful: 2.4963259193100384e-7
  happy: 0.00011926032311748713
  neutral: 0.9996343851089478
  sad: 0.00010264792217640206
  surprised: 0.000014418363207369111
}
```


```react
    const Array = Object.entries(detectionsWithExpression.expressions);
    const scoresArray = Array.map((i) => i[1]);
    const expressionsArray = Array.map((i) => i[0]);
```

これでスコアのみが入った配列と表情の名前のみが入った配列が分かれるので、スコアを計算して一番大きいスコアを求め、そのスコアに一致する表情を出したらOKです。

すべてまとめると以下になります。

```react:utils/drawEmoji.ts
import {
  WithFaceExpressions,
  FaceDetection,
  FaceExpressions,
} from "face-api.js";

export const drawEmoji = async (
  detectionsWithExpressions: WithFaceExpressions<{
    detection: FaceDetection;
    expressions: FaceExpressions;
  }>[],
  canvas: HTMLCanvasElement
) => {
  detectionsWithExpressions.map((detectionsWithExpression) => {
    const ctx = canvas.getContext("2d");
    const Array = Object.entries(detectionsWithExpression.expressions);
    const scoresArray = Array.map((i) => i[1]);
    const expressionsArray = Array.map((i) => i[0]);
    const max = Math.max.apply(null, scoresArray);
    const index = scoresArray.findIndex((score) => score === max);
    const expression = expressionsArray[index];
    const image = document.createElement("img");
    image.onload = () => {
      const width = detectionsWithExpression.detection.box.height * 1.2;
      const height = detectionsWithExpression.detection.box.height * 1.2;
      const x = detectionsWithExpression.detection.box.x - width * 0.1;
      const y = detectionsWithExpression.detection.box.y - height * 0.2;
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(image, x, y, width, height);
    };
    image.src = `/emojis/${expression}.png`;
  });
};

```

ちなみに今回複数人検出できるようになってるので、detectionsWithExpressionsが複数になってるのですが、``ctx.clearRect(0, 0, canvas.width, canvas.height);``を上記の場所に置くとmapが順番に流れる関係で複数人数がいた場合一人を検出して描画したら他の人の描画がされない..という感じになります。
そのため、複数人対応する場合なら、仮のcanvas要素を用意してそこに全員分描画してから、メインのcanvasに描画する、というような処理が必要になるとは思いますが、今回は面倒なのでそこまでしてません。

ともかく、これでロジック部分ができたので、あとはindex.tsxで上記の関数を呼び出せばOKです。

## 【デプロイ時の注意】 Module not found: Can't resolve 'fs'の対応

face-api.jsはnode.jsでも利用できるようになっているため、一部でfsを利用しています。ただ、ブラウザで動かす場合はもちろんfsは不要なので、この部分でデプロイ時にエラーがでます。
そのため、ブラウザで動かす場合はfsを使わないということを明示的に書く必要があります。

この部分は以下のissueに報告されてました。

https://github.com/justadudewhohacks/face-api.js/issues/154


issueにかかれてあるように今回はnext.config.jsを以下のように書き換えます。

```js:next.config.js
module.exports = {
  reactStrictMode: true,
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        fs: false,
      };
    }
    return config;
  },
};

```

これで無事デプロイもできるようになりました。

全体のコードはgithubにのせているので、必要であれば参照してください。

https://github.com/yuikoito/face2emoji

## あとがき

これで12週目の週イチ発信となりました。
最初[一番最初の記事](https://qiita.com/yuikoito/items/b8062b9f89f72c6865ed)ではモデルの読み込みができずにface-api.jsを使うことを断念しましたが、今回できるようになっていて我ながらびっくりしました。

すべてREADMEに書いてあることをそのままやってるだけなんですが、TensorFlow.jsとちょっとだけ仲良くなったことで読解力が上がった気がします。機械学習全くわからない人間でも用意されてるモデルを使うだけで色々できて楽しいですね。

良ければこれまでの週イチ発信も見て下さい！
ではでは〜。

- [【React + Typescriptで顔認識】tensorflowを使って画像にマスクをかけるアプリを作った](https://qiita.com/Yui_active/items/b8062b9f89f72c6865ed)
- [【React + Typescript】ボタン一つでコンポーネントのscssをコピーできるサイトを作った](https://qiita.com/Yui_active/items/1288fed0dcc24c71e159)
- [【アップデート】ui-componentsに18個のコンポーネントを追加した](https://qiita.com/yuiko_ito/items/c1d0599e1e7da7fa42e0)
- [【Nuxt.js × Tailwind CSS】ボタン一つで有名絵画風の画像にできるサービスをリリースした！](https://qiita.com/yuikoito/items/126b0e2d36a707bfd36e)
- [【GASでLINE Bot作成】現在地の近くのおすすめのごはん屋さんを教えてくれるLINE Botを作った](https://qiita.com/yuikoito/items/c1aef147cef4458f9d2e)
- [【動的OGP】Next.js + TypeScript + Vercelデプロイで動的OGPを実現する](https://qiita.com/yuikoito/items/619120c592d99f9d3053)
- [【LambdaでOpenCVを利用】AWSとOpenCVを利用してポケモン画像でアスキーアート風に変換するAPIを作った](https://qiita.com/yuikoito/items/7dcf1deeed46f46b2f2e)
- [ポケモン画像でアスキーアート風に変換するwebアプリを作った](https://qiita.com/yuikoito/items/da5954a5d78da9de6a4e)
- [ボタン一つで漫画風の画像にできるサービスを作った](https://qiita.com/yuikoito/items/a76ea5bf05f04a44df8d)
- [【Next.js + TensorFlowでweb cameraにバーチャル背景をつける】バーチャル旅行を体験できるアプリを作った](https://qiita.com/yuikoito/items/bb9c40bbb673e4f71abe)
- [オリンピックを盛り上げるためにピクトグラムさんになれるアプリを作った【Next.js+TypeScriptでTensorFlow.jsを使って姿勢検出】](https://qiita.com/yuikoito/items/274f926b5c5e59b43ce9)