AIは双子の顔をどれだけ正確に判別できるのか？（規約違反してしまったので修正の上 再公開いたしました）

# はじめに
このエントリーは元々下記のタイトルで公開しておりましたが規約違反との指摘を拝受しました。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/2a95bc3d-0dd7-300c-43d1-c9eaa7736d3a.png)

確かに当初の内容は下記ガイドラインに明確に違反するものでした。言い訳のしようもありません。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/1da75bce-fb25-0f77-7a87-112aadb3871c.png)

さらにゴーン氏は 2019/09 現在「被疑者」という立場であり、公判前のためいかなる判決も受けておりません。
「有罪」という表現は不適切なものでした。

私の確認と配慮の不足が重なった結果、このような事態を招いたことを、この場を借りてお詫び申し上げます。

## 再公開に至るまで
一方、本来このエントリーでお伝えしたかったことは下記の2点です。

* Face API をはじめとした各種サービスを組み合わせることで、AIを活用した面白い仕組みを簡単に作れる
* Face API は少ない画像数で似ている顔でも高精度で見分けられる完成度の高いサービスである

これらをお伝えしたいという思いは今も変わらず強くあります。
そのため、不適切な部分を修正の上、再度公開することにいたしました。
よろしければお付き合いいただけると幸いです。

## 見分ける顔
幸いにも学習に最適な素材の提供者を見つけることができました。

フリー素材双子アイドルの **[Mika + Rika](http://mika-rika-free.jp/)** さんです！

[![beauty_1_mikarika.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/24587fea-0604-8fd9-3fd6-bc944fbbab54.jpeg)](http://mika-rika-free.jp/)

このような活動をしている方がいらっしゃることに驚きと感謝が尽きません。
お二人の活動に感謝しつつ、謹んで素材を利用させていただきます。

一卵性の双子ということで難易度が上がりましたが Face API はどこまで健闘を見せてくれるでしょうか？

# 作ったもの
**LINE BOT** のトークに画像を送信すると、どちらの顔なのかを判別し、結果を教えてくれます。

* Mikaさんの画像がポストされた場合
[![Mika Post](https://i.gyazo.com/58bb61c413704897bb0c8165c4dc54b8.gif)](https://i.gyazo.com/bc385c754fcd02fe1ae18fc48e03ddda.gif)

* Rikaさんの画像がポストされた場合
[![Rika Post](https://i.gyazo.com/bc385c754fcd02fe1ae18fc48e03ddda.gif)](https://i.gyazo.com/bc385c754fcd02fe1ae18fc48e03ddda.gif)

* それ以外の人物の画像がポストされた場合
[![Unknown Post](https://i.gyazo.com/9fbda0c5e24f92beafbc87696e9d8591.gif)](https://i.gyazo.com/9fbda0c5e24f92beafbc87696e9d8591.gif)

## 構成図
顔の判別には、 **Cognitive Services** の **Face API** を使いました。
学習に利用した画像は、MikaさんとRikaさん **各5枚ずつ**です。
<img src="https://docs.google.com/drawings/d/e/2PACX-1vQp4Jokt3Wm_OUT8rX8SJq5lds60m25tmSVzslxY9m0v9X-QL5R2y6Q1mfYR0dh_0utrx7Xt13oi2Po/pub?w=1050&amp;h=816">

## 結果
### Mikaさんの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/b6b17ee5-3d53-3fcc-a604-2faa7e0b78a4.png)

### Rikaさんの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/76f17b35-d96d-9f04-a3ff-a2dcc82094e3.png)

### 最終結果

|投稿した画像|全数|Mika|Rika|不明|顔検出されず|
|---|---|---|---|---|---|
|Mika|40|**30**|0|0|10|
|Rika|40|0|**35**|0|5|

**100％ の圧倒的正解率！！！！**(顔検出されない場合を除く)

たった5枚の画像による学習でここまでの精度を出せるなんて、 Face API すごいですね。
Face APIにとっては、一卵性の双子ですら見分けることは朝飯前のようです。

### 顔検出されない場合について
「顔検出されず」の数が少々多いのが気になります。
個別に見ると、はっきり顔が映っているのに顔検出されない場合がありました。Face APIは、顔が斜めに写っている写真は苦手のようです。

* 顔検出されない画像例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154885/fae4fa3d-20ed-967c-5672-7fd1d628bd14.png)


# 作り方
## かかる時間について
この製作に費やした時間は約2日です。
平日フルタイムで働き、帰宅後に作業を開始してこの日数です。
**このエントリーを書くほうが時間がかかっています。**
そのくらい手軽＆簡単に、AI を使ってモノづくりができるよということをお伝えできればと思います。

## 使ったもの
### サービス
* インターフェイス
    * [LINE BOT](https://developers.line.biz/ja/services/messaging-api/)
* 学習用の画像収集
    * [Bing Image Search API](https://azure.microsoft.com/ja-jp/services/cognitive-services/bing-image-search-api/)
* 顔の判別
    * [Cognitive Servises Face API](https://azure.microsoft.com/ja-jp/services/cognitive-services/face/)

### ハードウェア
* 矢印を動かす
    * [サーボモータ](http://akizukidenshi.com/catalog/g/gM-08761/)
* マイコンボード（サーボモータ制御）
    * [obniz Board](https://www.amazon.co.jp/gp/product/B07DD6FK8G/ref=as_li_tl?ie=UTF8&tag=obniz_store-22&camp=247&creative=1211&linkCode=as2&creativeASIN=B07DD6FK8G&linkId=b3746f139ca1e5f997d9a97189638d28)

### 工作道具
* 段ボール
* 両面テープ
* はさみ
* 赤のフェルトペン 

## 画像を収集する
**⚠️ この章は、修正前のオリジナル版作成時に行っていた作業を、その有用性のために残しているものです。<br>修正版では画像はMikaRika公式サイトから手動でDLしました。**

まずはじめに、学習や判定に利用する画像を収集します。
今回は使った枚数が少ないので、手動で集める手もあります。ただし、もっと多くの枚数が要求されるときには自動化しないとつらいです。

画像収集には、Bing の画像検索エンジンを Web API として利用できる **Bing Image Search API** を利用しました。


> [Bingの画像検索APIを使って画像を大量に収集する](https://qiita.com/ysdyt/items/49e99416079546b65dfc)

Google の画像検索は API が廃止されているため、こちらを使いたい場合はスクレイピングするか、サードパーティ製のプログラムを利用しましょう。

### 1. API キーを取得する
先述のエントリーにある通り、Azure ポータルにログインし、**Bing Search v7** のリソースを作成します。

リソースが作成できたら、リソースの管理画面を開き、「リソース管理 - キー」よりアクセスキーを取得します。
これで API を利用する準備が整いました。

### 2. プログラムで API を叩いて画像をローカルに保存する
各種言語向けに API を叩くためのライブラリが提供されているので、それを使って画像を取得していきます。
今回は Python のライブラリを利用しました。

* ライブラリのインストール

``` sh 
pip install azure-cognitiveservices-search-imagesearch
```

実際に使ったプログラムを公開します。

``` bingimagesearch.py
from azure.cognitiveservices.search.imagesearch import ImageSearchAPI
from msrest.authentication import CognitiveServicesCredentials
import os
import requests

subscription_key = "<YOUR API KEY HERE>"
search_term = "<検索キーワード>"
save_dir = "<YOUR DIR PATH HERE>/{0}".format(search_term)
want_count = 400 # 取得したい画像数
# ここから下は変更しない
saved_count = 0
max_count = 150
client = ImageSearchAPI(CognitiveServicesCredentials(subscription_key))


def save_image(_saved_count, content, extension):
    open('{0}/{1}.{2}'.format(save_dir, str(_saved_count), extension), 'wb').write(content)


if not os.path.exists(save_dir):
    os.mkdir(save_dir)

while saved_count < want_count:
    remain_count = want_count - saved_count
    get_count = max_count if remain_count >= max_count else remain_count
    image_results = client.images.search(query=search_term, offset=saved_count, count=get_count)
    print("Searching the web for image of {}".format(search_term))
    if image_results.value:
        for result in image_results.value:
            saved_count += 1
            print(saved_count, result.encoding_format, result.content_url)
            my_file = object()
            try:
                my_file = requests.get(result.content_url, timeout=(3.0, 10.0))
            except requests.exceptions.RequestException :
                print('get thumbnail')
                my_file = requests.get(result.thumbnail_url, timeout=(3.0, 10.0))
            save_image(saved_count, my_file.content, result.encoding_format)


```

API のリクエスト1回で取得できる画像数の上限は150枚です。
それ以上が欲しいときはリクエストを複数回に分けています。

また、URL から画像を取得しようとすると、サーバ側の問題で取得に失敗したり、レスポンスがなかなか返ってこないことがあります。
レスポンス待ちで時間がかかりすぎてはまずいので、リクエストにタイムアウトを設定しました。
その上で、取得に失敗した場合は画像検索エンジンに保持されているサムネイルを取得します。

### 3. 画像を選別する
取得した画像には使えないものが含まれます。下記のいずれかに該当する画像は削除するのが望ましいでしょう。

* 本人の顔が写っていない
* 加工されている
* イラスト

## 顔を学習させる
それでは、集めた画像を使って学習をしていきましょう。

Face API の機能の概要については、下記を参照ください。

> [Azure Face API とは](https://docs.microsoft.com/ja-jp/azure/cognitive-services/face/overview)

今回は一連の機能の中から、**「顔検出」**と**「人物の特定」**を利用しました。

* 顔検出 (Face detection)
    * 画像に含まれる人の顔を検出し、その座標を取得します。
* 人物の特定 (Person identification)
    * 予め登録された顔のデータベースと照合し、検出された顔が誰か特定します。

作業の流れを説明します。
### API キー を発行する
下記URLにアクセスし、"Face" の**API キー**を取得します。
Azure アカウントがあれば、無期限のAPI キーを取得できます。

> [Try Cognitive Services](https://azure.microsoft.com/en-us/try/cognitive-services/)

### 顔のデータベースを作成する
「人物の特定」機能を利用するために、MikaさんとRikaさんの顔データを持つデータベース **(Person Group)** を作成します。

Person Group には複数の **Person** を登録できます。
それぞれの Person に対し、顔写真を複数枚登録して「学習」を行うことで、Person Group の準備が完了します。

「人物の特定」機能とは、ある顔が Person Group に含まれる人物の誰と同一人物なのかを教えてくれる、というものです。

![Person Group](https://docs.microsoft.com/ja-jp/azure/cognitive-services/face/images/group.image.1.jpg)

具体的な作業の流れは、下記に説明があります。

> [Example: Identify faces in images](https://docs.microsoft.com/ja-jp/azure/cognitive-services/face/face-api-how-to-topics/howtoidentifyfacesinimage)

作業を行うための C# のサンプルコードが載っていますが、今回利用する程度のデータ量であれば、**[Cognitive Services の API ポータル](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395244)** を利用したほうが楽です。


実際に行った作業の流れです。

1. [PersonGroup - Create](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395244) で、今回利用するPerson Groupを作成する
    * ここで Request Body の **"recognitionModel"** に指定した値は、あとで利用するため控えておきましょう。より精度の高い **"recognition_02"** が推奨されています。
2. [PersonGroup Person - Create](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f3039523c) で、作成した Person Group にMikaさんと Rikaさんに対応する Person を作成する
    * Person ごとに一意な **"personId"** が発行されます。あとで利用するため控えておきましょう。
3. [PersonGroup Person - Add Face](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f3039523b) で、作成した Person に5枚ずつ顔写真を登録する
    * 顔写真の登録先は "personId" で指定します。
    * ポータルからはローカルファイルのアップロードができないため、**Postman** を利用しました。
4. [PersonGroup - Train](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395249) で、学習をトリガーする
5. [PersonGroup - Get Training Status](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395247) で、学習の完了を確認する
    * 学習にかかった時間は**1分未満**でした。

### 利用してみる

作成した Person Group を使って「人物の特定」機能を試してみましょう。
これには2回の API コールが必要です。

1. [Face - Detect](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395236) で、画像に写っている顔を検出する
    * 検出された顔は Cognitive Services 側に保存され、一意な **"faceId"** が発行されます。これを次のリクエストで利用します。
    * Request Parameter の "recognitionModel" には、PersonGroup - Create の実行時に指定した値と同じものを指定します。そうしないと、次の Face - Identify が失敗します。
2. [Face - Identify](https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395239) で、指定した faceId が Person Group に含まれるどの Person と同一人物か判定する
    * faceId は配列で指定し、結果も配列で返ります。該当する Person が見つからなかった場合は空の配列が返されます。
    * Request Body には "largePersonGroupId" と "personGroupId" のどちらかを指定します。今回は Person Group を利用しているため **"personGroupId"**
 を指定します。

## LINE BOT を作成する
今回の製作の中心となる LINE BOT を作成します。
作成方法は下記を参考にしてください。

> [1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

実際に作成したソースコードを掲載します。

> https://gist.github.com/TakeshiMasukawa/f2ecb70e41b9394fc7a5fe3182d6cdbf

利用するには、ライブラリのインストールと、**.env ファイル**の用意が必要です。

* ライブラリのインストール

``` ps
npm i dotenv @line/bot-sdk obniz express axios
```

* .envファイルを用意して package.json と同じ階層に置く

``` .env
CHANNEL_SECRET=<LINE BOT の Channel Secret>
ACCESS_TOKEN=<LINE BOT の Channel Access Token>
OBNIZ_ID=<obniz ID>
ENDPOINT=<Face API の エンドポイント URL>
SUBSCRIPTION_KEY=<Face API の API キー>
GROUPNAME=<Person Group の名前>
MIKA_PERSON_ID=<Mikaさんの personId>
RIKA_PERSON_ID=<Rikaさんの personId>
```

では、ソースコードの各部を説明していきます。

### LINEアプリで投稿した画像を取得する

> [LINE BOTからNode.jsで画像を受け取って保存する](https://qiita.com/n0bisuke/items/17c795fea4c2b5571ce0)

トークに投稿された画像は LINE BOT バックエンドの Request には含まれておらず、messageId を指定して取りに行く必要があります。
このとき、`responseType: 'arraybuffer'`を指定してください。画像をバイナリデータとして受け取ることができ、そのまま Face API への Request に渡せます。

``` server.js
const fetchPostedImage = async (messageId) => {
  return await axios.get(`/bot/message/${messageId}/content`, {
    baseURL: 'https://api.line.me/v2/',
    responseType: 'arraybuffer',
    headers: {
      Authorization: `Bearer ${process.env.ACCESS_TOKEN}`
    }
  });
```

### Face API の Face - Detect で、画像に含まれる顔を検出する

いよいよ Face API を利用していきます。
トークに投稿された画像の顔を検出し、faceId を取得します。
今回は複数人の顔が写った画像を利用しないため、faceId は1件しか発行されない前提で処理を書いています。

``` server.js
const detectFace = async (data) => {
  return await axios.post('/detect', data, {
    baseURL: process.env.ENDPOINT,
    params: {
      returnFaceId: true,
      returnFaceLandmarks: false,
      recognitionModel: "recognition_02",
      returnRecognitionModel: false,
      detectionModel: "detection_01"
    },
    headers: {
      "Ocp-Apim-Subscription-Key": process.env.SUBSCRIPTION_KEY,
      "Content-Type": "application/octet-stream"
    }
  });
```

### Face API の Face - Identify で、顔が誰のものか判定する

前の手順で取得した faceId を指定し、顔が誰のものであるか判定した結果を取得します。
返ってきた personId がMikaさんのものであれば"Mika"、Rikaさんのものであれば"RIka"。
personId が返されない場合は不明の人物と判定します。

``` server.js

const identifyFace = async (faceIds) => {
  const requestBody = {
    personGroupId: process.env.GROUPNAME,
    faceIds: faceIds,
    maxNumOfCandidatesReturned: 1,
    confidenceThreshold: 0.5
  }
  return await axios.post('/identify', requestBody, {
    baseURL: process.env.ENDPOINT,
    headers: {
      "Ocp-Apim-Subscription-Key": process.env.SUBSCRIPTION_KEY,
      "Content-Type": "application/json"
    }
  })
}
```

### 判定結果を出力する

判定結果をもとに矢印を動かし、「有罪」か「無罪」かを示します。
不明な人物の場合は `shakeServo()` で矢印を揺らし、分からなくてイライラしている感を演出します。
また、`shakeServo()` は、判定の待ち時間にも呼び出し、考えている感を演出しています。

``` server.js

const servoIndicateMika = async () => {
  servo.angle(120.0);
}
const servoIndicateRika = async () => {
  servo.angle(60.0);
}
const shakeServo = async () => {
  for (let i = 0; i < 5; i++) {
    await servo.angle(85.0);
    await obniz.wait(100)
    await servo.angle(95.0);
    await obniz.wait(100)
  }
  await servo.angle(90.0);
}

```

# 感想
* **Face API すごい。**こんなに少ない枚数の画像でこれだけの認識精度の AI アプリをお手軽に作れるなんて。<br>Microsoft が掲げるのミッションのひとつ**「AI の民主化」**は伊達じゃない。
* API を叩いている間はどうしても待ち時間が発生するが、その時間も楽しんでもらう仕掛けとしてサーボモータを揺らしてみたのが、**想定以上にハマっていた**。
* obniz を使うことで、LINE BOT バックエンドのコードの中でハードウェアを制御できる。専用にコードを書いて書き込む必要も、通信用にコードを書く必要もない。<br>**LINE BOT と obniz は極めて相性がいい**。
* **Face API すごい**