60
38

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.

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

Last updated at Posted at 2021-08-09

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

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

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

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

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

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

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

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

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

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

6.png

7.png

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

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

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

$ yarn add face-api.js

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

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

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

  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だともっと精度は良くなりますが、かなり重くなるので、動かない端末が出てきます。(特にスマホではほとんど固まると思います。)

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

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

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

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
}
    const Array = Object.entries(detectionsWithExpression.expressions);
    const scoresArray = Array.map((i) => i[1]);
    const expressionsArray = Array.map((i) => i[0]);

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

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

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に報告されてました。

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

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

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

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

あとがき

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

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

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

60
38
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
60
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?