いまさらだけど、Chainerで顔認識をしてみよう(推論フェーズ編)

###概要
[いまさらだけど、Chainerで顔認識をしてみよう(訓練フェーズ編)](https://qiita.com/hima_zin331/items/fa93c7c546da0c3ac31a)の続きで、今回は推論フェーズです。

USB接続式ウェブカメラを用いて顔の認識を行ってみます。

###環境
**-Software-**
Windows 10 Home
Anaconda3 64-bit(Python3.7)
Spyder
**-Library-**
Chainer 7.0.0
opencv-python 4.1.2.30
**-Hardware-**
CPU: Intel core i9 9900K
GPU: NVIDIA GeForce RTX2080ti
RAM: 16GB 3200MHz
(PCでもウェブカメラがあれば実行可能)

###参考
**書籍**
Pythonで始めるOpenCV4プログラミング 北山 直洋 (著)
([Amazonページ](https://www.amazon.co.jp/Python%E3%81%A7%E5%A7%8B%E3%82%81%E3%82%8BOpenCV-4%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E5%8C%97%E5%B1%B1-%E7%9B%B4%E6%B4%8B/dp/4877834613))
**サイト**
[Chainer APIリファレンス](https://docs.chainer.org/en/stable/reference/index.html)

###プログラム
一応、Githubに上げておきます。
https://github.com/himazin331/Face-Recognition-Chainer-
リポジトリには訓練フェーズ、推論フェーズ、データ加工プログラム、Haar-Cascadeが含まれています。

###前提
本プログラムの動作にはHaar-Like特徴量のCascadeファイルが必須です。
今回はOpenCVのHaar-Cascadeを使用します。
なお、Cascadeはリポジトリに含まれるので別途用意する必要はありません。

###ソースコード
**コードが汚いのはご了承ください...**

```Python:face_recog_CH.py
from PIL import Image
import numpy as np
import cv2

import sys
import os
import argparse as arg

import chainer
import chainer.links as L
import chainer.functions as F
import chainer.serializers as S


# ==================================== face_recog_train_CH.pyと同じネットワーク構成　====================================
class CNN(chainer.Chain):
    def __init__(self, n_out):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(1, 16, 5, 1, 0),
            conv2=L.Convolution2D(16, 32, 5, 1, 0),
            conv3=L.Convolution2D(32, 64, 5, 1, 0),
            link=L.Linear(None, 1024),
            link_class=L.Linear(None, n_out),
        )

    def __call__(self, x):
        h1 = F.max_pooling_2d(F.relu(self.conv1(x)), ksize=2)
        h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), ksize=2)
        h3 = F.relu(self.conv3(h2))
        h4 = F.relu(self.link(h3))
        return self.link_class(h4)
# ================================================================================================================


def main():
    # コマンドラインオプション引数
    parser = arg.ArgumentParser(description='RealTime Face Recognition Program(Chainer)')
    parser.add_argument('--param', '-p', type=str, default=None,
                        help='学習済みパラメータの指定(未指定ならエラー)')
    parser.add_argument('--cascade', '-c', type=str, default=os.path.dirname(os.path.abspath(__file__)) + '/haar_cascade.xml'.replace('/', os.sep),
                        help='Haar-cascadeの指定(デフォルト値=./haar_cascade.xml)')
    parser.add_argument('--device', '-d', type=int, default=0,
                        help='カメラデバイスIDの指定(デフォルト値=0)')
    args = parser.parse_args()

    # パラメータファイル未指定時->例外
    if args.param is None:
        print("\nException: Trained Parameter-File not specified.\n")
        sys.exit()
    # 存在しないパラメータファイル指定時->例外
    if os.path.exists(args.param) is False:
        print("\nException: Trained Parameter-File {} is not found.\n".format(args.param))
        sys.exit()
    # 存在しないHaar-cascade指定時->例外
    if os.path.exists(args.cascade) is False:
        print("\nException: Haar-cascade {} is not found.\n".format(args.cascade))
        sys.exit()

    # 設定情報出力
    print("=== Setting information ===")
    print("# Trained Prameter-File: {}".format(os.path.abspath(args.param)))
    print("# Haar-cascade: {}".format(args.cascade))
    print("# Camera device: {}".format(args.device))
    print("===========================")

    # カメラインスタンス生成
    cap = cv2.VideoCapture(args.device)
    # FPS値の設定
    cap.set(cv2.CAP_PROP_FPS, 60)

    # 顔検出器のセット
    detector = cv2.CascadeClassifier(args.cascade)

    # 学習済みパラメータの読み込み
    model = L.Classifier(CNN(2))
    S.load_npz(args.param, model)

    red = (0, 0, 255)
    green = (0, 255, 0)
    p = (10, 30)

    while True:
        # フレーム取得
        _, frame = cap.read()

        # カメラ認識不可->例外
        if _ is False:
            print("\nException: Camera read failure.\n")
            sys.exit()

        # 顔検出
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = detector.detectMultiScale(gray)

        # 顔未検出->continue
        if len(faces) == 0:
            cv2.putText(frame, "face is not found",
                        p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, red, thickness=2)
            cv2.imshow("frame", frame)
            
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
            continue

        # 顔検出時
        for (x, y, h, w) in faces:
            # 顔領域表示
            cv2.rectangle(frame, (x, y), (x + w, y + h), red, thickness=2)

            # 顔が小さすぎればスルー
            if h < 50 and w < 50:
                cv2.putText(frame, "detected face is too small",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, red, thickness=2)
                cv2.imshow("frame", frame)
                break

            # 検出した顔を表示
            cv2.imshow("gray", cv2.resize(gray[y:y + h, x:x + w], (250, 250)))

            # 画像処理
            face = gray[y:y + h, x:x + w]
            face = Image.fromarray(face)
            face = np.asarray(face.resize((32, 32)), dtype=np.float32)
            recog_img = face[np.newaxis, :, :]

            # 顔識別
            y = model.predictor(chainer.Variable(np.array([recog_img])))
            c = F.softmax(y).data.argmax()

            # 分類したいクラスの分だけ記述してください
            if c == 0:
                cv2.putText(frame, "hoge",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, green, thickness=2)
            elif c == 1:
                cv2.putText(frame, "fuga",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, green, thickness=2)
            cv2.imshow("frame", frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # リソース解放
    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()
```

###実行結果
今回は、安倍晋三さんと麻生太郎さんを識別してみました。
学習データ件数はどちらも100枚です。
![abe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/45c492c4-ba32-cc38-6b8e-0353d6a43b6d.png "安倍を認識")
![aso.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/0dcfecc5-fa7e-f6f6-0f80-55f13f055deb.png
"麻生を認識")

**コマンド**

`python face_recog_CH.py -p <パラメータファイル> -c <cascade> (-d <カメラデバイスID>)`

###説明
予測フェーズということで、カメラを用いて顔の識別をするプログラムになります。
####ネットワークモデル
CNNクラスですが、学習フェーズ(face_recog_train_CH.py)のネットワークモデルと全く同じものを
そのまま記述してください。構造がちょっとでも違うと動きません。
ハイパーパラメータや層が違ければ、重みなどのパラメータの個数も異なるため、学習により最適化されたパラメータを
適用することができません。

```python:CNNクラス
# ==================================== face_recog_train_CH.pyと同じネットワーク構成　====================================
class CNN(chainer.Chain):
    def __init__(self, n_out):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(1, 16, 5, 1, 0),
            conv2=L.Convolution2D(16, 32, 5, 1, 0),
            conv3=L.Convolution2D(32, 64, 5, 1, 0),
            link=L.Linear(None, 1024),
            link_class=L.Linear(None, n_out),
        )

    def __call__(self, x):
        h1 = F.max_pooling_2d(F.relu(self.conv1(x)), ksize=2)
        h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), ksize=2)
        h3 = F.relu(self.conv3(h2))
        h4 = F.relu(self.link(h3))
        return self.link_class(h4)
# ================================================================================================================
```

---

####セットアップ
カメラインスタンスの生成やcascadeの読み込み、パラメータの取り込みを行います。

```python
    # カメラインスタンス生成
    cap = cv2.VideoCapture(args.device)
    # FPS値の設定
    cap.set(cv2.CAP_PROP_FPS, 60)

    # 顔検出器のセット
    detector = cv2.CascadeClassifier(args.cascade)
```
`chainer.serializers.load_npz()`でネットワークモデルにパラメータを適用します。
注意として、学習フェーズで`L.Classifier()`でモデルをラップしてインスタンスを生成しましたが、
予測フェーズでも同じようにモデルを`L.Classifier()`でラップする必要があります。

```python
    # 学習済みパラメータの読み込み
    model = L.Classifier(CNN(2))
    S.load_npz(args.param, model)
```

---

####顔認識

まず、カメラで撮影を行います。
`cap.read()`で撮影を行えます。
1回`cap.read()`を実行させたら1枚の静画像が得られます。
while文やfor文を使って逐次`cap.read()`を実行させ、得られる静画像を連続して出力することで動いてるように見せます。

`cap.read()`は2つの値を返します。
1つめは撮影ができているか否かのフラグ(コード上では`_`)。
2つめに実際に撮影した静画像(コード上では`frame`)。

以下、静画像をフレームと表記します。

```python
    while True:
        # フレーム取得
        _, frame = cap.read()

        # カメラ認識不可->例外
        if _ is False:
            print("\nException: Camera read failure.\n")
            sys.exit()

        # 顔検出
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = detector.detectMultiScale(gray)

        # 顔未検出->continue
        if len(faces) == 0:
            cv2.putText(frame, "face is not found",
                        p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, red, thickness=2)
            cv2.imshow("frame", frame)
            
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
            continue
```
フレーム取得後、フレームをグレースケール化し、Haar-Like特徴量のCascadeを用いて顔検出を行います。
`detector.detectMultiScale()`は顔を検出した場合、検出位置の情報(座標と幅高さ)を返し、検出できなかった場合は、
なにも返しません。

顔を検出できなかった時、ウィンドウ上に「face is not found」と出力し、continueします。

---

顔を検出した時の処理を説明します。

返却される検出箇所のx座標とy座標、幅と高さを用いて、画像処理を施します。

```python
        # 顔検出時
        for (x, y, h, w) in faces:
            # 顔領域表示
            cv2.rectangle(frame, (x, y), (x + w, y + h), red, thickness=2)

            # 顔が小さすぎればスルー
            if h < 50 and w < 50:
                cv2.putText(frame, "detected face is too small",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, red, thickness=2)
                cv2.imshow("frame", frame)
                break

            # 検出した顔を表示
            cv2.imshow("gray", cv2.resize(gray[y:y + h, x:x + w], (250, 250)))

            # 画像処理
            face = gray[y:y + h, x:x + w]
            face = Image.fromarray(face)
            face = np.asarray(face.resize((32, 32)), dtype=np.float32)
            recog_img = face[np.newaxis, :, :]

            # 顔識別
            y = model.predictor(chainer.Variable(np.array([recog_img])))
            c = F.softmax(y).data.argmax()

            # 分類したいクラスの分だけ記述してください
            if c == 0:
                cv2.putText(frame, "hoge",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, green, thickness=2)
            elif c == 1:
                cv2.putText(frame, "fuga",
                            p, cv2.FONT_HERSHEY_SIMPLEX, 1.0, green, thickness=2)
            cv2.imshow("frame", frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
```
画像処理は具体的に、
①フレームから顔領域の切り出し
②顔領域をリサイズするために、一度配列(array)から画像に変換
③32px×32pxにリサイズ
④配列の次元を追加(チャンネル数の追加, [チャンネル数, 高さ, 幅])
を行ってます。

さて、識別できる形にデータを加工できたら、いよいよ顔の認識です。
`y = model.predictor(chainer.Variable(np.array([recog_img])))`
で予測を開始します。`chainer.Variable()`はデータを連鎖律に対応付けさせる関数です。
次に、`c = F.softmax(y).data.argmax()`で予測結果をソフトマックス関数に通した後、argmaxで
もっとも大きい要素(インデックス)を返却します。

そして最後にif文を使って、要素(インデックス)に対応したクラス名を出力してやります。

今回は安倍晋三と麻生太郎の2クラス分類ですが、どちらでもない顔を学習させて、
安倍晋三でも麻生太郎でもない顔を入力させたとき、「どちらでもない」なんていう出力をすることも可能です。

###おわりに
もともと、これらのプログラムは高校の課題研究(卒業研究)で開発したものなのでコードとか適当です。
分類するクラス数を変えるのは学習データさえあれば容易にできますので、好きなようにしていただければなと思います。