29
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-06

こんにちは、Yuiです。

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

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

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

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

機能の紹介

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

判定する表情は顔出しをせずに表情だけを伝えることができるwebアプリを作ったでも書いたのと同じく以下の7パターンです。

image.png

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

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

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

  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に戻してリセットします。
呼び出し部分は以下のような感じ。

  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で管理しています。

  const [stage, setStage] = useState<
    "isNotStart" | "ready" | "start" | "judge" | "finish"
  >("isNotStart");
  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秒以内にその表情をすれば良い感じになってしまってます。それは良くない。

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

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

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

あとがき

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

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

29
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?