表情を鍛えることができるアプリを作った

こんにちは、[Yui](https://twitter.com/yui_active)です。

ずっと続く自粛生活、家で一人でいると**表情がなくなりませんか？**
**私はなくなりました。**

というわけで、それに**危機感**を覚えたので、今回は**表情を鍛えることができるアプリ**を作りました。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">家に引きこもってると表情が乏しくなるので、表情を豊かにできるアプリを作りました <a href="https://t.co/TbQxi14jsP">pic.twitter.com/TbQxi14jsP</a></p>&mdash; Yui 🌘 Yuiko Ito (@yui_active) <a href="https://twitter.com/yui_active/status/1434713887388045326?ref_src=twsrc%5Etfw">September 6, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

DEMO: https://face-expression-challenge.vercel.app/
github: https://github.com/yuikoito/face-expression-challenge

**※現在ミニマムの機能しかつけてないので、今後大幅にコードの中身が変わる可能性があります。**

## 機能の紹介
今回の機能としては、スタートを押すとカウントダウンが始まって、その後お題がでるようになっています。
お題はランダムに出すようにしていて、お題のあとに1.5秒後に判定が始まって、その間にしている表情で判定をしています。

判定する表情は[顔出しをせずに表情だけを伝えることができるwebアプリを作った](https://qiita.com/yuikoito/items/a494402993ac4be46d97)でも書いたのと同じく以下の7パターンです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/7e7f0b85-28a7-58fe-64da-728f8ab3e06a.png)

face-api.jsのモデルの導入方法などは上記の記事で書いたままなので省略します。

## お題と表情が一致しているかどうかを確認する

お題とその時の表情が一致しているかどうかを確認するために、動画の表情を認識する関数（faceDetectHandler）にお題を引数として入れました。
そして**0.1秒ごと**に表情を探知しています。

```react
  const faceDetectHandler = (subject: string) => {
    const video = webcamRef.current.video;
    const intervalHandler = setInterval(async () => {
      const detectionsWithExpressions = await faceapi
        .detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
        .withFaceExpressions();
      if (detectionsWithExpressions.length > 0) {
        detectionsWithExpressions.map((detectionsWithExpression) => {
          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];
          if (expression === subject) {
            setIsMatch(true);
          }
        });
      }
    }, 100);
    setIntervalHandler(intervalHandler);
  };

```

そして表情とお題が一致したらsetIsMatch(true)でフラグをtrueにします。

ただ、このままでは一度trueになったら常にisMatchがtrueになってしまいますので、faceDetectHandlerを呼び出す際にfalseに戻してリセットします。
呼び出し部分は以下のような感じ。


```react
  const drawSubject = (expression: string) => {
    // gameカウントが5回になったら処理を止めるために何回お題を描画したかを数える
    setGameCount((gameCount) => gameCount + 1);
    // 一旦falseに戻す
    setIsMatch(false);
    faceDetectHandler(expression);
    const canvas = canvasRef.current;
    // 描画するお題（絵文字）を読み込んで描画する
    const ctx = canvas.getContext("2d");
    const image = document.createElement("img");
    image.onload = () => {
      coverCanvas(ctx, canvas);
      ctx.drawImage(
        image,
        (canvas.width - 300) / 2,
        (canvas.height - 300) / 2,
        300,
        300
      );
    };
    image.src = `/emojis/${expression}.png`;
    // faceDetectHandlerが前に動いていたらその前のintervalは停止しておく
    if (intervalHandler) {
      clearInterval(intervalHandler);
    }
    // お題を出してから1.5秒後にclearRect（webcamの映像が流れる）
    setTimeout(() => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    }, 1500);
    // 更にその1.5秒後に判定する
    setTimeout(() => {
      setStage("judge");
    }, 3000);
  };
```

**ゲーム開始→お題を出す→判定→ゲーム終了の流れ**に関しては状態管理を雑にstageで管理しています。

```react
  const [stage, setStage] = useState<
    "isNotStart" | "ready" | "start" | "judge" | "finish"
  >("isNotStart");
```

```react
  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext("2d");
    if (stage === "judge") {
      judge(ctx, canvas);
    }
    if (stage === "start") {
      const expression =
        ExpressionTypes[Math.floor(Math.random() * ExpressionTypes.length)];
      drawSubject(expression);
    }
    if (stage === "finish") {
      setTimeout(() => {
        coverCanvas(ctx, canvas);
        drawText(ctx, canvas, `${point}／5`);
      }, 1500);
    }
  }, [stage]);
```

## 今後実装予定のこと
今回、問題点はいくつかあるので、正式リリースまでにいくつか修正予定のことがあります。

まず、お題を出した直後に表情認識が走ってしまっているため、UI的にはお題が出る（1.5秒）→判定開始（1.5秒）になりそうなところですが、実際はお題を出した時点で判定が始まっているので、3秒以内にその表情をすれば良い感じになってしまってます。それは良くない。

また、表情判定がかなりザルです。
これは表情判定方法が以下のようにとりあえず現在の表情の中で一番点数が高いものという形でつけているからです。

```react
          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);
```

face-api.jsでは、以下のように全部の表情の点数が取得できるので、以下の例のように一番高いスコアのもの（ここではneutral）がちゃんと1に近い値ならいいのですが、全部0.1〜0.2ぐらいの場合にその表情だと決めつけるのは安易すぎる気がしています。

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

そこで、推定される表情が0.5以上でないと判定しない、というルールを追加してみて様子を見ようかなと思っています。

あとはシェア機能と動的OGPと、最後の判定時になにかメッセージを出せたら面白いなと思っているのでつける予定です。
週一で新しいアプリをリリースする予定が２週かけてリリースになりそうですが、自分にあまくMVPの機能は作り上げたので良しとします。笑

というわけで来週あたりにリリースできればと思ってるので、よければリリース後遊んでください！

## あとがき

これで16週目の週イチ発信となりました。
Tensorflow系はずっとやってることがあって、いい加減なれてきました笑
今回はレイアウト雑にざっくりと作っただけでしたが、思ったより反響があったので、ちゃんとレイアウト整えてシェアボタンやらOGPやら設定しようかなと思います。

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

- [【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)
- [毎週アプリをリリースして3ヶ月経ったので振り返る](https://qiita.com/yuikoito/items/eede538b17d72685ea02)
- [【canvasで塗り絵機能を実装】画像からオリジナル塗り絵を作って塗り絵ができるアプリを作った](https://qiita.com/yuikoito/items/28510dff877e3e222ba5)
- [AR.jsを使って狼を表示してみた【初めてのAR】](https://qiita.com/yuikoito/items/6e489874d589a7d53c80)