scikit-learnとPC内蔵カメラで、機械学習を用いたリアルタイム顔認識をしてみた

#背景
PC内蔵のカメラで学習用写真を自動で簡単に取得できれば、
自分や特定の人の顔識別ができ、様々な応用が展開できると思って作ってみました。

#開発環境
macOS Mojave 10.14.5
MacBook Air (13-inch, Early 2015)
Python 3.6.8 :: Anaconda custom (64-bit)
scikit-learn 0.19.1
OpenCV 4.0.0
#参考にしたサイト
下記のサイトを参考にさせていただきました。

◆OpenCVの顔認識関連
https://qiita.com/hitomatagi/items/04b1b26c1bc2e8081427
https://www.blog.umentu.work/python-opencv3%E3%81%A7%E7%94%BB%E5%83%8F%E3%81%AE%E9%A1%94%E5%88%A4%E5%AE%9A%E3%83%8D%E3%82%BF%E3%81%82%E3%82%8A/

◆機械学習モデルの保存・ロード
https://localab.jp/blog/save-and-load-machine-learning-models-in-python-with-scikit-learn/

#処理の流れ
下記の流れで実装を行いました。
####[STEP1. PC内蔵カメラを使って顔写真の収集](#1-pc内蔵カメラを使って顔写真の収集)
####[STEP2. 学習モデルの作成](#2-学習モデルの作成)
####[STEP３. リアルタイム顔識別の実行](#3-リアルタイム顔識別の実行)


#1. PC内蔵カメラを使って顔写真の収集
ディレクトリ階層は下記のようになっていればよく、この記事ではDesktop上にpythonというフォルダを作成して、下記のcamera_face.pyというスクリプトと、撮影した画像を保管するためのimgフォルダを作っています。
>＜ディレクトリ構成例＞
任意のディレクトリ/
├ camera_face.py
└ img/
　└※ここに写真を格納する 

camera_face.pyスクリプトは、macの内蔵カメラで自分の写真を所望の枚数撮影して（300枚）、顔部のみを指定のサイズ（50×50）の大きさに切り出して保存するものです。
ターミナル上に撮影の進捗を表現するようにしています。

```camera_face.py
import cv2
import glob
import time
import sys
from datetime import  datetime

cap=cv2.VideoCapture(0) #0にするとmacbookのカメラ、1にすると外付けのUSBカメラにできる

# 顔判定で使うxmlファイルを指定する。(opencvのpathを指定)
cascade_path =  '"opencvの格納されているパス"/data/haarcascades/haarcascade_frontalface_alt.xml'
cascade = cv2.CascadeClassifier(cascade_path)

dir = "任意のフォルダパス" # 写真を格納するフォルダを指定

num=300 # 欲しいファイルの数
label = str(input("人を判別するを半角英数3文字でで入力してください ex.slf："))
file_number = len(glob.glob("任意のフォルダパス/*")) #現在のフォルダ内のファイル数
count = 0 #撮影した写真枚数の初期値

#ラベルの文字数を確認
if not len(label) == 3:
    print("半角英数3文字で入力してください")
    sys.exit()

while True:
    #フォルダの中に保存された写真の枚数がnum以下の場合は撮影を続ける
    if count < num:
        time.sleep(0.01) #cap reflesh
        print("あと{}枚です".format(num-count))

        now = datetime.now()#撮影時間
        r, img = cap.read()

        # 結果を保存するための変数を用意しておく
        img_result = img

        # グレースケールに変換
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        #顔判定 minSize で顔判定する際の最小の四角の大きさを指定できる。(小さい値を指定し過ぎると顔っぽい小さなシミのような部分も判定されてしまう。)
        faces=cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=1, minSize=(100, 100))

        # 顔があった場合
        if len(faces) > 0:
            # 複数の顔があった場合、１つずつ四角で囲っていく
            for face in faces:
                #faceには(四角の左上のx座標, 四角の左上のy座標, 四角の横の長さ, 四角の縦の長さ) が格納されている。
                #顔だけ切り出して保存
                x=face[0]
                y=face[1]
                width=face[2]
                height=face[3]
                #50×50の大きさにリサイズ
                roi = cv2.resize(img[y:y + height, x:x + width],(50,50),interpolation=cv2.INTER_LINEAR)
                cv2.imwrite(dir+label+"__"+str(now)+'.jpg', roi)

        #現在の写真枚数から初期値を減産して、今回撮影した写真の枚数をカウント
        count = len(glob.glob("/任意のフォルダパス/*")) - file_number

    #フォルダの中に保存された写真の枚数がnumを満たしたので撮影を終える
    else:
        break

#カメラをOFFにする
cap.release()
```

###実行結果
imgフォルダ内に顔だけ切り出した300枚の写真が保存できました！
これが学習データになります。
写真の撮影枚数を変えたいときは上記のnum変数を変更すれば良いです。
今回は教師あり学習を行うので、実行時に入力する3文字の英数字は、学習データに紐付けられるラベルになります。
![aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193530/de57640c-9f68-c68b-433e-240c745d165f.png)
取得した画像を見ていると、服の一部が保存されてしまっているもの（赤丸）や、鼻周りのみ抽出されてしまっているもの（青丸）がありました（下図参照）。
これらはちょっと面倒ですが手作業で取り除く必要があります。
![bbb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193530/35efc9e8-b706-7f0e-b6ae-95d2e28d96d0.png)
同じ要領で家族の顔写真を撮影し、imgフォルダ内には自分の顔写真と家族の顔写真を保存しました。自分の写真をdai、妻の写真をyomとラベル付けしています。


#2. 学習モデルの作成
STEP1で撮影した写真を使って学習モデルを作成します。ここで使用するパターン認識モデルはSVMです。
>出典: フリー百科事典『ウィキペディア（Wikipedia）』
サポートベクターマシン（英: support vector machine, SVM）は、教師あり学習を用いるパターン認識モデルの一つである。分類や回帰へ適用できる。
~中略~
サポートベクターマシンは、現在知られている手法の中でも認識性能が優れた学習モデルの一つである。サポートベクターマシンが優れた認識性能を発揮することができる理由は、未学習データに対して高い識別性能を得るための工夫があるためである。

先ほどと同じように、以下のディレクトリ構成となるように下記のML_svm.pyスクリプトを配置します。
>＜ディレクトリ構成例＞
任意のディレクトリ/
├ camera_face.py
├ ML_svm.py
└ img/
　└写真

```ML_svm.py
import cv2
import numpy as np
from PIL import Image
import os
from sklearn import svm
import pickle

#顔判別器の作成
images = []
labels = []
path = "/Users/daisuke/Desktop/python/img"

for f in os.listdir(path):
    #画像のパス
    image_path = os.path.join(path, f)
    #.DS_Storeファイルを読み込まないようにする
    if image_path == "/Users/daisuke/Desktop/python/img/.DS_Store":
        continue
    else:
        #グレースケールで読み込む(convert("L")でグレースケール)
        gray_image = Image.open(image_path).convert("L")
        #numpy配列に格納
        image = np.array(gray_image,"uint8")
        #umageを1次元配列に変換
        image = image.flatten()
        #images[]にimageを格納
        images.append(image)
        #ファイル名からラベルを取得
        labels.append(str(f[0:3]))
#行列に変換
labels = np.array(labels)
images = np.array(images)

#svmの変換器を作成
clf = svm.LinearSVC()
#学習
clf.fit(images,labels)

#学習モデルを保存する
filename = "face_model.sav"
pickle.dump(clf,open(filename,"wb"))

print("モデル保管完了")
```

ML_svm.pyを実行すると、ターミナル上に「モデル保管完了」と表示され、ディレクトリ内にface_model.savファイルが生成されます。
これが学習モデルとなります。

SVMでモデルを作成するときには、複数のラベルが必要になるようです。一つだけだと、分類する対象がないからだと思います、きっと。したがって、今回は自分と家族の写真を対象にして学習させています。

また、、学習させるファイルは全て同じサイズである必要があります。したがって、STEP1で写真をすべて50×50にリサイズして写真を保管しています。
#3. リアルタイム顔識別の実行
ここまできたら、あとはリアルタイム認識です！
以下のスクリプトを同じディレクトリに保管し、実行します。
>＜ディレクトリ構成例＞
任意のディレクトリ/
├ camera_face.py
├ ML_svm.py
├ face_model.sav
├ face_recognizer.py
└ img/
　└写真

```face_recognizer.py
import cv2
import numpy as np
import pickle
from sklearn import svm

def resize_image(image, height, width):
    # 元々のサイズを取得
    org_height, org_width = image.shape[:2]
    # 大きい方のサイズに合わせて縮小
    if float(height)/org_height > float(width)/org_width:
        ratio = float(height)/org_height
    else:
        ratio = float(width)/org_width
    # リサイズ
    resized = cv2.resize(image,(int(org_height*ratio),int(org_width*ratio)))
    return resized

def mag_change(bai,img):
    #グレースケール変換
    frame_gray =cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    #顔認識処理を早くするために、画像の解像度を下げる。画像サイズを取得し、サイズを変更する
    orgHeight, orgWidth = frame_gray.shape[:2]
    frame_gray_size = (int(orgWidth/bai), int(orgHeight/bai))
    #リサイズする
    frame_gray_resize = cv2.resize(frame_gray, frame_gray_size,interpolation = cv2.INTER_AREA)
    return frame_gray_resize

if __name__ == "__main__":
    # 内蔵カメラを起動
    cap = cv2.VideoCapture(0)

    # OpenCVに用意されている顔認識するためのxmlファイルのパス
    cascade_path = "/Users/daisuke/opencv/data/haarcascades/haarcascade_frontalface_alt.xml"
    # カスケード分類器の特徴量を取得する
    cascade = cv2.CascadeClassifier(cascade_path)

    #STEP2で保存したモデルのロード
    clf = pickle.load(open("face_model.sav","rb"))

    while True:
        # 内蔵カメラから読み込んだキャプチャデータを取得
        ret, frame = cap.read()

        # 結果を保存するための変数を用意しておく
        result =frame

        # 顔認識を低解像度で実施するための準備
        mag = 3 #倍率
        resized = mag_change(mag,frame)

        # 顔認識の実行
        facerect = cascade.detectMultiScale(resized, scaleFactor=1.2, minNeighbors=3, minSize=(10,10))

        # 顔が見つかったらfacerectには(四角の左上のx座標, 四角の左上のy座標, 四角の横の長さ, 四角の縦の長さ) が格納されている。
        if len(facerect) > 0:
            for x,y,w,h in facerect:
                x=int(x*mag)
                y=int(y*mag)
                w=int(w*mag)
                h=int(h*mag)

                #顔の部分だけ切り抜いてモザイク処理をする
                cut_img = result[y:y+h,x:x+w]
                cut_face = cut_img.shape[:2][::-1]

                #顔判別用の箱を作る
                recog =[]
                #リアルタイムに認識した顔を50×50の解像度（clf作成時の解像度）に合わせる
                recog_img = cv2.resize(cut_img,(50,50),interpolation=cv2.INTER_LINEAR)
                #グレースケールに変換
                recog_gray =cv2.cvtColor(recog_img, cv2.COLOR_BGR2GRAY)
                #1次元の行列に変換
                recog_gray = np.array(recog_gray,"uint8").flatten()
                #顔認識用の箱に入れる
                recog.append(recog_gray)
                #行列に変換
                recog=np.array(recog)

                #予測実行
                pred = clf.predict(recog)
                print(pred)

                #予測の結果、画像上に文字を表示
                if pred == "dai":
                    name = "daiarg"
                    cv2.putText(result,name,(int(x),y-int(h/5)),cv2.FONT_HERSHEY_PLAIN, int(w/50),(0,255,0),5,cv2.LINE_AA)
                elif pred == "yom":
                    name = "yome"
                    cv2.putText(result,name,(int(x),y-int(h/5)),cv2.FONT_HERSHEY_PLAIN, int(w/50),(0,255,0),5,cv2.LINE_AA)
                else:
                    result = result

        cv2.imshow("image", result)

        # qキーを押すとループ終了
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # 内蔵カメラを終了
    cap.release()
    cv2.destroyAllWindows()
```

###実行結果
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193530/e3d2e908-5415-1cc9-b88a-3b682f475794.png)
できました！
顔をちゃんと認識して、顔の上に名前を重ねて表示することができました。
11fpsくらいで処理ができています。カクカクはそこまで目立たないレベル。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193530/9f2433ce-8723-c19c-0287-f1fe30dd205c.png)
二人同時表示もできました！

私の写真をdai、妻の写真をyomとそれぞれラベル付けしているため、このようなスクリプトになっています。写真撮影時につけるラベル名と、表示したい名前は適宜変更が必要になります。

#まとめ
自分専用の学習データを取得して、学習モデルを作成し、リアルタイム顔識別を行いました。
ラズパイなどに展開できれば、LINEの連携、リモコンのON/OFFなど応用が広がりそうです！そちらも作ったらまた投稿したいと思います。