Javascriptの顔認識モデルで激太りのミーシャ・バートンを認識できるのか？

![m1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/724b3555-e16f-9f05-7203-8d585d943e31.jpeg)![m2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/d206c243-3da5-dc42-5aa7-ac330e246c7d.jpeg)

最近のAIの顔認識モデルはすごいんだよ、と妻に話していたら、「激太りしたミーシャ・バートンでもちゃんと認識できるのかね？」と疑問を呈されたので、実装して試して見ることにしました。
左が往年のミーシャ・バートンで、右が激太り期のミーシャ・バートンです。
とても同一人物とは思えませんね。
果たしてJavascriptの顔認識モデルでこのミーシャ・バートンを同一人物として特定できるのでしょうか。

#Javasciptで実装する理由
AIと言えばPython。巷にはPythonの実装例やDockerコンテナが溢れています。
あえてJavascriptで実装する理由は何でしょうか。

理由はJavascriptの方がスマホやタブレット向けのサービスを構築しやすいと考えたためです。
Pythonで顔認識モデルを使う場合、GCPやAWSなどのクラウドサービス上にモデルを展開して、クラウドにデータを送信して推論するか、Jetson、ラズパイ等のIoT機器を使う事になると思います。
画像データや動画を使う場合、都度クラウドにデータを送信していたら、クラウドのコストが跳ね上がってしまいます。
Jetsonやラズパイ等のIoT機器は一般向けサービスとしてはちょっと敷居が高いです。

JavascriptのPWA形式であれば、スマホやタブレット上の写真、動画を使って端末上で推論を行えるため、サービスが展開しやすくなるのではないか、と考えました。

#Javascriptの顔認識モデル

調査したところ、face-api.jsというライブラリが有望そうなので、これを使ってみる事にしました。
https://github.com/justadudewhohacks/face-api.js/

- face detection : 顔の位置特定
- face landmark detection : 顔の輪郭や各パーツの位置特定
- Face Expression Recognition : 感情分析
- Age Estimation & Gender Recognition　: 年齢、性別特定
- Face Recognition　: 人物特定

いろいろなシナリオに対応しており、組み合わせる事で様々なサービスが構築できそうです。
今回はFace Recognitionを利用しますが、これはface detection、face landmark detectionの結果を使って推論しますので、それらのモデルが必要となります。

#ソースコード解説
今回、Vue.jsを使って簡単なwebサービスを構築します。

##導入
face-apiのライブラリをインストールします。

```shell
npm i face-api.js
```
続いて、tensorflowのモデルをダウンロードし、publicフォルダに配置します。
モデルは以下からダウンロードします。
https://github.com/justadudewhohacks/face-api.js/tree/master/weights

利用するサービスによって取捨選択も可能ですが、今回は全部まとめてpublic/weightsに配置しました。
Vueのpublicフォルダは次のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/dc159ab4-29d7-9519-9632-be6c557e4a07.png)

私は最初src配下にモデルを置いて相対パスで読み込もうとしたのですが、face-apiがモデルを認識してくれずハマりました。
src/assetsに置いても駄目でした。
素直にpublicフォルダにweightsというフォルダを切って、置いておけば問題なく読みこんでくれます。
モデルは全部で15.5MBで、サービスの利用都度読み込むのは大変ですが、PWAで一回ダウンロードするだけなら許容範囲かな、と思います。

##face recognitionの処理を行うクラス

画像認識の処理全般を`MyFaceRecognizer`クラスとして定義しています。

まずはface-apiのライブラリを読み込みます。

```src/faceProcessing.js

import * as faceapi from "face-api.js";
```

`MyFaceRecgnizer`クラスを定義します。
クラス変数の`labeledDescriptors`には、教師データとしてラベル付けされた顔の特徴量の情報（float32の配列）を保持しておきます。


```src/faceProcessing.js
export class MyFaceRecognizer {
  /**@type {Array<faceapi.LabeledFaceDescriptors>} */
  labeledDescriptors = [];
```

`initializeFaceRecognizer`で、処理に必要なモデルをpublic/weightsから読み込みます。
数MBあるので、読み込みにはそれなりに時間がかかります。特に初回でWEBから読み込む場合はそれなりに待つことになるでしょう。
読み込んでいるモデルは、顔の位置を認識するためのssdMobilenetv1と、顔のパーツ位置を特定するためのfaceLandMark68Net、顔認識のためのfaceRecognitionNetです。
顔認識では、顔の位置と顔のパーツ位置情報を元に認識するため、前処理としてこれらのモデルが必要となります。

```src/faceProcessing.js
  /**
   * 初期化処理
   * 処理に必要なモデルを読み込む。
   */
  async initializeFaceRecognizer() {
    await faceapi.nets.ssdMobilenetv1.loadFromUri("/weights");
    await faceapi.nets.faceLandmark68Net.loadFromUri("/weights");
    await faceapi.nets.faceRecognitionNet.loadFromUri("/weights");
  }

```

`checkDescriptors`処理では、推論のための画像データを元に、教師データから最もマッチする画像のラベルを返します。

まず画像ファイルを元にモデルを使って顔の位置を特定(`detectSingleFace`)し、顔のパーツ位置を特定(`withFaceLandmarks`)し、最後に FaceDescriptorと呼ばれる顔の特徴量情報（float32の配列）を取得します(`withFaceDescriptor`)。

次に、`FaceMatcher`に、教師データとしてのラベル済みの顔の特徴量情報を読み込ませます。
その`FaceMatcher`に推論させたい画像のdescriptor(特徴量情報)を読みこませてベストマッチを特定します。
そのベストマッチの`.toString()`で画像のラベルが返されます。

```src/faceProcessing.js
  /**
   * fileが過去の画像に存在しているか確認し、存在していればそのテキストを返す。
   * 存在していなければ空文字列を返す
   * @param {HTMLImageElement} img
   * @returns {Promise<String>}
   */
  async checkDescriptors(img) {
    const result = await faceapi
      .detectSingleFace(img)
      .withFaceLandmarks()
      .withFaceDescriptor();

    const faceMatcher = new faceapi.FaceMatcher(this.labeledDescriptors);
    const bestMatch = faceMatcher.findBestMatch(result.descriptor);
    return bestMatch.toString();
  }
```

`addDescriptor`処理は、教師データのラベルとその特徴量情報（descriptor）をクラスのメンバー変数の`labeledDescriptors`に追加していきます。推論ではこのメンバー変数を利用します。

```src/faceProcessing.js
  /**
   * 顔一覧に画像を登録する
   * @param {String} label
   * @param { HTMLImageElement} img
   */
  async addDescriptor(label, img) {
    const result = await faceapi
      .detectSingleFace(img)
      .withFaceLandmarks()
      .withFaceDescriptor();
    this.labeledDescriptors.push(
      new faceapi.LabeledFaceDescriptors(label, [result.descriptor])
    );
  }
}

```

## Vueのtemplate部

UIのVueファイルを解説していきます。
UIパーツにはVuetifyを利用しています。
上から次の通りです。

- 教師部
    - `v-text-filed` 教師用画像のラベルを入力するテキストフィールド
    - `v-file-input` 画像のファイル選択
    - `v-img` 選択した画像を表示
- 推論部
    - `v-file-input` 推論用の画像ファイル選択
    - `v-img` 推論用に選択した画像ファイルの表示
    - 推定結果：画像の顔認識結果を表示。教師部で指定したファイルと同じ顔があればそのラベルを表示します。
 

```Home.vue
<template>
  <div id="app">
    <v-container>
      <v-col>
        <v-row>
          <v-text-field v-model="label" placeholder="画像のラベルを設定" />
          <v-file-input @change="uploadImage" placeholder="画像を選択" />
        </v-row>
        <v-img max-height="300" contain :src="imgSrc" />
      </v-col>
      <v-divider />
      <v-col>
        <v-file-input @change="predictImage" />
        <v-img max-height="300" contain :src="imgOut" />
        推定結果: {{ predictedLabel }}
      </v-col>
    </v-container>
  </div>
</template>
```

##VueのScript部
続いてVueのスクリプト部を解説していきます。

### Import

faceapiのライブラリを読み込みます。
また、上記で定義した`faceProcessing.js`から`MyFaceRecognizer`クラスをImportします。

```js
import * as faceapi from "face-api.js";
import { MyFaceRecognizer } from "../scripts/faceProcessing.js";
```

###データ部

Home.vueのローカル変数を定義しています。
`recognizer`に、`MyFaceRecognizer`クラスの新しいインスタンスを保持しています。

```js
export default {
  name: "Home",
  // データ定義
  data() {
    return {
      label: "",
      predictedLabel: "",
      imgSrc: "",
      imgOut: "",
      uploadImageUrl: "",
      recognizer: new MyFaceRecognizer()
    };
  },
```
Home.vue起動時に、`MyFaceRecognizer`クラスの初期化処理（モデルの読み込み）を行います。
非同期処理で時間がかかる処理のため、実際にはモデルの読み込みが終わるまで教師データの登録や推論処理が開始されないように制御する必要があると思います。

```js
  async mounted() {
    await this.recognizer.initializeFaceRecognizer();
  },
```

`uploadImage`処理では、ローカルで読み込んだ画像ファイルとラベルをrecognizerに渡します。
`faceapi.bufferToImage()`という便利関数を使うと、バイナリのデータから扱いやすいHTMLImageElementのデータに変換できます。

```js
  // メソッド定義
  methods: {
    async uploadImage(file) {
      // create an HTMLImageElement from a Blob
      const img = await faceapi.bufferToImage(file);
      this.imgSrc = img.src;
      this.recognizer.addDescriptor(this.label, img);
    },
```

`predictImage`は推論処理です。recognizerで推論した結果を画面に出力します。

```js
    async predictImage(file) {
      // create an HTMLImageElement from a Blob
      const img = await faceapi.bufferToImage(file);
      this.imgOut = img.src;
      this.predictedLabel = await this.recognizer.checkDescriptors(img);
    }
  }
};
```

#実験
では実際に推論処理がうまくいくか試してみましょう。
ブラッド・ピット、レオナルド・ディカプリオ、ミーシャ・バートンの画像を使って実験してみます。

##教師データの登録
ブラッド・ピット、レオナルド・ディカプリオ、ミーシャ・バートンの画像を１枚づつラベルをつけて保存していきます。

①ブラッド・ピットの教師データ登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/8e5cc797-ea66-f088-de59-5df61276487c.png)


②レオナルド・ディカプリオの教師データ登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/3dcd2903-709b-7815-b663-182a0d2dba0c.png)

③往年のミーシャ・バートンの教師データ登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/cbbafe36-a0f8-44ec-67bd-22e844071f36.png)

##推論の実験
教師データを使って別の画像で推論していきます。

①まずはブラッド・ピットの別画像から

ちょっと斜めの顔でもきっちり認識してくれました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/3b088e2c-d26a-357d-e00f-0582cb364e3c.png)

②レオナルド・ディカプリオの別画像から

すごく若い時の写真でもちゃんと認識してくれました。凄いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/b76ef4d2-492c-97dc-cfc6-3995b9990aa1.png)

③ミーシャ・バートンの激太り写真

果たして識別してくれるでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/1a8aee92-a028-69fc-5fcd-543379f4533e.png)

出ました！ミーシャ・バートン。太ってても全然問題なしです。

一応教師データのないレディーガガも推論してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/39c1546e-421e-9704-a0a1-0ca19dd1b88b.png)
ご覧の通り、教師データが存在しないため、unknownとなりました。

顔認証モデル、精度や推論スピードを求めるならFaceNet(モデルだけで100MB近くあるようです）とかを使った方が良いのでしょうが、ライトにWebサービス等で使うのであればface-api.jsでも十分に実用できそうです。

#この記事の投稿者について

Twitterで顔認識を活用したアプリ開発についてつぶやいています。
https://twitter.com/studiothere2

noteにアプリ開発の日記を連載しています。
https://note.com/there2