face-api.jsで顔認識

# この記事でやること
今回はface-api.jsで使ってみたかった「顔認識」を試してみます！

# face-api.jsとは
face-api.jsはブラウザで顔検出（face detection：人の顔を自動的に見つける）と
顔認識（face recognition：個人を識別する）ができるJavaScript APIです。
機械学習用のライブラリTensorFlow.jsが利用されています。
[face-api.js](https://justadudewhohacks.github.io/face-api.js/docs/index.html)

・顔検出
・顔認識
・表情検出
・年齢性別推定
ができます。

# 顔認識（face recognition）とは

[wikipedia](https://ja.wikipedia.org/wiki/%E9%A1%94%E8%AA%8D%E8%AD%98%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0)には
>カメラのデジタル画像から、人を自動的に識別するためのコンピュータシステムである。

と書かれています。
この記事では画像に映っている人が「誰か」を判断するものとして話を進めていきます。

# 顔認識のやり方
[Face Recognition by Matching Descriptors](https://justadudewhohacks.github.io/face-api.js/docs/index.html#models-face-recognition)

顔認識を実行するには、faceapi.FaceMatcherを使用して、face descriptors（顔記述子）を比較する。と書いてありました。
学習用の参照画像を用意して顔を検出し、検出された顔記述子と、顔認識を行いたい画像の顔記述子を照合するようです。

[顔認識のチュートリアル](https://itnext.io/face-api-js-javascript-api-for-face-recognition-in-the-browser-with-tensorflow-js-bcc2a6c4cf07)


https://youtu.be/AZ4PdALMqx0

こちらを参考に進めていこうと思います。

# モデルの読み込み
```JavaScript
Promise.all([
   faceapi.loadSsdMobilenetv1Model('./models'),
   faceapi.loadFaceLandmarkModel('./models'),
   faceapi.loadFaceRecognitionModel('/models'), // 顔認識モデル
]).then(()=>{
   start();
});
```
必要な顔モデルを読み込みます。
顔認識を行うのでFaceRecognitionModelと
FaceRecognitionModelを使うために必要なSsdMobilenetv1Model、FaceLandmarkModelの3つのモデルを読み込みます。
すべてのモデルの読み込みが成功したら、startメソッドを呼ぶようにしています

# 認識を行う画像を表示
画像は<a href="https://www.pakutaso.com" title="フリー写真素材ぱくたそ" >フリー写真素材ぱくたそ </a>からお借りしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/48609cb3-28ee-9e54-ffa1-faa107447c72.png)

ボタンを押したら、顔検出するようにしておきます。
```JavaScript
document.getElementById('detect_btn').onclick = () => {
   detectFace();
};

async function detectFace(){
   const img = document.getElementById('result_image');
   const faceData = await faceapi.detectAllFaces(img);
   if(!faceData.length) return;
   faceapi.draw.drawDetections(resultCvs, faceData);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/0c4d5f09-db4f-5f2c-e5fb-db101a8a9149.png)
検出できています◎

# 参照データの読み込み
参照画像を用意します。
今回は1枚ずつですが、1人に対して複数枚の用意しておくと精度がアップするようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/80807935-72ab-e38c-9da2-f6844270883a.png)
適当なラベル付で恐縮ですが、左から
```JavaScript
const labels = ['taro', 'jiro', 'saburo'];
```
とさせていただきました。

```JavaScript
function start(){
   document.getElementById('msg1').innerHTML = "顔モデルを読み込みました<br>参照顔データを読み込んでいます…";
   loadRefImage();
}

const labels = ['taro', 'jiro', 'saburo'];
const refFaceData = new Array(labels.length);

async function loadRefImage() {
   let msgStr = "";
   for (count = 0; count < labels.length; count++) {
      const img = new Image();
      img.src = `./src/${labels[count]}.jpg`;
      refFaceData[count] = await faceapi.detectSingleFace(img).withFaceLandmarks().withFaceDescriptor();
      if (!refFaceData[count]) {
         refFaceData[count] = 'no face';
         msgStr += `<li id="no-face">${labels[count]}さんの顔は見つかりませんでした</font>`;
      }
      else{
         msgStr += `<li>${labels[count]}さんの顔が見つかりました`;
      }
   }
   document.getElementById('msg1').innerHTML = `参照顔データを読み込みました<br>`;
   document.getElementById('result').innerHTML = msgStr;
}
```
躓きポイントですが、
実はtaroさんの参照画像は「顔が検出できなかった」んです。（目をつむってるからかな）

そのことに気が付くまで、なかなかチュートリアルのようにはいかず、？？？となっていました…
参照画像で顔が検出されない場合も想定したコードへ変更したところ動くようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/86b77328-f4a6-f39a-99be-caadd3572c3d.png)

これでjiroさんとsaburoさんの参照顔データ（顔記述子）の取得ができたので次のステップに進めます！

# FaceMatcherの初期化
いよいよFaceMatcherを使います。

>[face-api.js](https://justadudewhohacks.github.io/face-api.js/docs/index.html#models-face-recognition)より引用
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/26cdda85-cd75-7a05-ce62-bc6511291ef8.png)

上記のように、そのままresultsを渡せばOKです。

・taroさんが認識できていない
・labelを任意の値にしたい
という事情があるので、今回は一工夫入れます

loadRefImageメソッド内でラベル付きの参照記述子を作成し、処理終了時に返すようにします。

```diff_JavaScript
+let refLabeledDescriptors;
function start() {
   document.getElementById('msg1').innerHTML = "顔モデルを読み込みました<br>参照顔データを読み込んでいます…";
-   loadRefImage();
+   loadRefImage().then((result) => {
+      refLabeledFaceDescriptors = result;
+   });
}

const labels = ['taro', 'jiro', 'saburo'];
const refFaceData = new Array(labels.length);

async function loadRefImage() {
   let msgStr = "";
+　const labeledDescriptors = [];
   for (count = 0; count < labels.length; count++) {
      const img = new Image();
      img.src = `./src/${labels[count]}.jpg`;
      refFaceData[count] = await faceapi.detectSingleFace(img).withFaceLandmarks().withFaceDescriptor();
      if (!refFaceData[count]) {
         refFaceData[count] = 'no face';
         msgStr += `<li id="no-face">${labels[count]}さんの顔は見つかりませんでした</font>`;
      }
      else{
         msgStr += `<li>${labels[count]}さんの顔が見つかりました`;
+        labeledDescriptors.push(
+           new faceapi.LabeledFaceDescriptors(
+              labels[count],
+              [refFaceData[count].descriptor]
+        ));
      }
   }
   document.getElementById('msg1').innerHTML = `参照顔データを読み込みました<br>`;
   document.getElementById('result').innerHTML = msgStr;
+  return labeledDescriptors;
}
```

FaceMatcherは「顔認識」ボタンが押されたとき、すなわちdetectFaceメソッドの中で初期化しました。
```diff_JavaScript
async function detectFace(){
   const img = document.getElementById('result_image');
   const faceData = await faceapi.detectAllFaces(img);
   if(!faceData.length) return;
   faceapi.draw.drawDetections(resultCvs, faceData);
+  const faceMatcher = new faceapi.FaceMatcher(refLabeledFaceDescriptors, 0.6);
}
```
FaceMatcherのコンストラクタの第2引数は、チュートリアルに倣って0.6としました。

# 顔記述子の比較

いよいよ比較していきます。
```diff_JavaScript
async function detectFace() {
   const img = document.getElementById('result_image');
-  const faceData = await faceapi.detectAllFaces(img);
+  const faceData = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors();
   if (!faceData.length) return;
-  faceapi.draw.drawDetections(resultCvs, faceData);
   const faceMatcher = new faceapi.FaceMatcher(refLabeledFaceDescriptors, 0.6);

+  faceData.forEach(fd => {
+     const bestMatch = faceMatcher.findBestMatch(fd.descriptor);
+     const box = fd.detection.box;
+     const drawBox = new faceapi.draw.DrawBox(box, { label: bestMatch.toString()});
+     drawBox.draw(resultCvs);
+  });
}
```
顔認識を行う画像に対して、FaceDescriptors（顔記述子）を取得します。
そして、faceMatcher.findBestMatchで比較します。
参照データで複数のdescriptorを登録していた場合、もっとも精度の良い結果を選んでくれるとのことです。

const box以降の行は、結果画像にlabelを表示するための処理になります。
以下、実行結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/5f5b3b64-5f66-2440-fb53-bb371794d9cb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/870231e4-6265-cbe4-746a-d5498d3f72e3.png)
顔モデルの読み込みや参照データの読み込みには少し時間がかかります。
サービスで利用するときは、ローディング中が分かる表示があるといいかと思います。
今回は簡易的にメッセージだけ表示しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/34c30db9-d294-be08-daf9-f84fe08d499e.png)
参照データの読み込みが終わったので、「顔認識」ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/e7d6ed03-aa7f-7e10-7a4f-00f4e8dc935a.png)

できた！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/af5a82d6-cf58-8ebc-892a-0427509f136f.png)
labelもきちんと表示されています。


saburoさんなんて学習データ正面顔
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/188d1cc6-6f29-0213-8efa-1eb9a7f913a9.png)
なのに、ほぼ横顔を検出するなんて…すごい。

顔認識させる画像を変えてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/c701daf1-7dac-929f-f1aa-59cd4fdfa931.png)

ばっちりです◎

# 時間はどのくらい？
モデルの読み込み：1300～2250ms
参照データの計算：1600～2700ms　　：1400×933の画像3枚
顔認識：150～270ms　　　　　　　　：2人分のデータについて1400*933の画像1枚

このくらいの時間がかかっていました。
モデルの読み込みや参照データの計算は1度行えばOKですが、
顔認識は毎回実行するのでinputソースが動画になるとちょっと心配です。


# 今後やりたいこと
・小さいサイズのモデルに変更
・[MTCNN顔検出器を使用したリアルタイムJavaScript顔追跡と顔認識](https://itnext.io/realtime-javascript-face-tracking-and-face-recognition-using-face-api-js-mtcnn-face-detector-d924dd8b5740)
・表情検出
・他の顔検出、顔認識系の技術の調査