[深層学習]webカメラで顔認識して、LINEbotで報告するシステムをつくった。





#はじめに
私が所属しているサークルでは、部屋の出入りをいちいちLINEとslackで報告しなくてはならず、これがめんどい。。。。
ならば、監視カメラ的な物をつくってLINEbotとslackbotに代わりに報告してもらおう！というのがコレを作った動機です。

流れは、
１．webカメラを起動する
２．カメラで顔を認識したら、顔の部分だけをトリミングして保存
３．保存した画像を学習済みモデルに通して分類する
４．分類結果をLINE,slackbotからメッセージとして送信する
という感じです。

サークルメンバー10人の顔を見分けたい！！

#目次
1.環境
2.画像収集
3.画像の水増し
3.転移学習
4.webカメラの起動・LINE,slackとの連携
5.終わりに




#環境

・python 3.6.5
・opencv-python 3.4.4.19
・Keras 2.2.4
・Flask 1.0.2
・line-bot-sdk 1.8.0
・slackbot 0.5.3

#画像収集
画像はLINEグループのアルバムから丸ごともってきました。
学習データ用に、これらの画像から顔だけを切り出していきます！
学習済みの検出器を[こちら](https://github.com/opencv/opencv/tree/master/data/haarcascades)から持ってきて、`face_cut.py`と同じ階層においておきました。


```python:face_cut.py
import cv2
import glob
import numpy as np

model = "./models/haarcascade_frontalface_alt.xml"
faceCascade = cv2.CascadeClassifier(model)#顔検出器を生成

PHOTOS_PATH = "./input_done"
SAVE_PATH = "./detected_faces"
files = glob.glob(PHOTOS_PATH + "/*.jpg") 
detect_count= 0
undetect_count= 0
read_count = 0

for i,file in enumerate(files):
    img = cv2.imread(file)#第二引数に0指定しろよ
    img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    h, w = img.shape[:2]
    img = cv2.resize(img, (w,h))#結局リサイズしてない。
    face = faceCascade.detectMultiScale(img)#顔として認識した正方形の左上の座標がx,yに、横、縦幅がw,hに格納されている

    read_count += 1
    if len(face)>0:#顔を認識したら、      
        for  rect in face:#認識したすべての顔を切り出す
            cv2.imwrite(SAVE_PATH + "/" +str(read_count) + "_" + str(detect_count) +".jpg", img[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]])
            detect_count += 1
            print("detect!",detect_count)
    else:
        undetect_count += 1
        print("undetect...", undetect_count)

print("detect_count", detect_count)
print("undetect_count", undetect_count)

```
`PHOTOS_PATH`で指定したフォルダの画像を読み込み、切り出した画像は`SAVE_PATH`で指定したフォルダに保存します。



この後、画像を使いやすいようにクラス（人物）ごとにフォルダに分類しました。この作業で6時間くらいかかった。。。


......さあ、各クラス150枚準備できました！


#画像の水増し
さっそく学習！...といきたいのですが、これだけでは足りないのでトレーニングデータを水増ししていきます！
トレーニングデータには120枚をあてました。

それぞれの画像に対して左右反転、コントラスト調整、平滑化、γ変換をして2^4倍に増やします。
この際、水増しをするのはトレーニングデータのみにします。

```python:face_aug.py
import cv2
import os,glob
import numpy as np
from sklearn import model_selection

#クラスごとのフォルダ名をリストにまとめる
classes = ["member1","menber2","member3","member4","member5","member6","member7","member8","member9","member10","others"]
num_classes = len(classes)
image_size = 50

X_train = []
X_test = []
y_train = []
y_test = []

#-----------------------------------------------------------
#~水増しする関数のみなさん~

#左右反転
def flip(img):
    #第二引数を正でｙ軸対象
    flip_images  = cv2.flip(img, 1)
    return flip_images
#コントラスト
def cont(img):
    #ルックアップテーブルの生成
    min_table=50
    max_table=205
    diff_table=max_table - min_table

    LUT_HC = np.arange(256, dtype = "uint8")

    #ハイコントラストLUT作成
    for i in range(0, min_table):
        LUT_HC[i] = 0
    for i in range(min_table, max_table):
        LUT_HC[i] = 255 * (i - min_table) / diff_table
    for i in range(max_table, 255):
        LUT_HC[i] = 255

    #変換
    #cv2.LUTで適用
    high_cont_imgs = cv2.LUT(img, LUT_HC)
    return high_cont_imgs

#ぼかし
def blur(img):
    blur_images = cv2.GaussianBlur(img, (5, 5), 0)
    return blur_images
#γ変換
def gamma(img):
    # ガンマ変換ルックアップテーブル
    gamma1 = 0.75
    LUT_G1 = np.arange(256, dtype="uint8")
    for i in range(256):
        LUT_G1[i] = 255 * pow(float(i) / 255, 1.0 / gamma1)
    #変換
    gamma1_images = cv2.LUT(img, LUT_G1)
    return gamma1_images
#-----------------------------------------------------------------
for index, classlabel in enumerate(classes): 
    photos_dir = "./" + classlabel
    files = glob.glob(photos_dir + "/*.jpg") 
    for i,file in enumerate(files):
        if i >= 150: #ラベルごとの画像の枚数をそろえる
            break
        img = cv2.imread(file)
        img = cv2.resize(img, (image_size,image_size))

        if i < 30: #この番号以下の写真のデータはテスト用になる
            data = np.asarray(img)
            X_test.append(data)
            y_test.append(index)
        else: #それ以外はトレーニング用になる
            #1枚の画像ずつ処理する
            images = [img]
            images.extend(list(map(flip, images)))#倍倍に増えていく~
            images.extend(list(map(cont, images)))
            images.extend(list(map(blur, images)))
            images.extend(list(map(gamma, images)))
            
            for _ in range(len(images)):#imagesの枚数分だけ正解ラベルを作成
                y_train.append(index)
             #処理した全ての画像を格納する
            X_train.extend(images)
    print(classlabel , "done!")

X_train = np.array(X_train)#nparrayに変換する
X_test = np.array(X_test)
y_train = np.array(y_train)
y_test = np.array(y_test)
xy = (X_train,X_test,y_train,y_test)#これらをxyにまとめ、
np.save("./face_aug.npy", xy)#画像データを保存する

#ちゃんとデータができているか確認した
X_train, X_test, y_train, y_test = np.load("face_aug.npy") 
print(len(X_train),len(X_test),len(y_train),len(y_test))

for i in range(len(classes)):
    count = 0
    for j in range(len(y_train)):
        if y_train[j] == i:
            count += 1
    print(count)
```
各クラス約2000枚のデータがそろったので、いよいよ転移学習させていきましょう💪

#VGG16で転移学習させる

```python:vgg16.py
from keras import optimizers
from keras.applications.vgg16 import VGG16
from keras.datasets import cifar10
from keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization, Activation
from keras.models import Model, Sequential
from keras.callbacks import EarlyStopping
from keras.utils import plot_model, np_utils
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

classes = ["member1","menber2","member3","member4","member5","member6","member7","member8","member9","member10","others"]
num_classes = len(classes)
epochs=20
batch_size=64


X_train, X_test, y_train, y_test = np.load("face_aug.npy") 
X_train = X_train.astype("float") / 255 #X_trainの各チャンネルの値は0~255だがそれを255で割ることですべての値を0~1にする(正規化)
X_test = X_test.astype("float") / 255
y_train = np_utils.to_categorical(y_train, num_classes) #one_hotベクトルに変換
y_test = np_utils.to_categorical(y_test, num_classes) 

input_tensor = Input(shape=(50, 50, 1))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))

#top_model.add(Dense(128))
#top_model.add(BatchNormalization())
#top_model.add(Activation('sigmoid'))
#top_model.add(Dropout(0.5))

#top_model.add(Dense(64))
#top_model.add(BatchNormalization())
#top_model.add(Activation('sigmoid'))
#top_model.add(Dropout(0.5))

top_model.add(Dense(32))
#top_model.add(BatchNormalization())
top_model.add(Activation('sigmoid'))
#top_model.add(Dropout(0.5))

top_model.add(Dense(num_classes, activation="softmax"))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:6]:#VGGの層の重みの固定を調整する
  layer.trainable = False
    
model.compile(optimizer=optimizers.SGD (lr=1e-4, momentum=0.9),loss='categorical_crossentropy', metrics=['accuracy'])

es = EarlyStopping(monitor='val_loss', patience=0, verbose=0, mode='min')#バリデーションデータ（テストデータ）の損失関数の値が大きくなると早期終了する

history = model.fit(X_train, y_train, validation_data=(X_test, y_test), batch_size=batch_size, epochs=epochs, callbacks=[es])

model.save("faces.h5")

#モデルの評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss: ', scores[0])
print('Test accuracy: ', scores[1])
print()

#学習曲線の表示
plt.plot(history.history['acc'], label='acc', ls='-')
plt.plot(history.history['val_acc'], label='val_acc', ls='-')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(loc='best')
plt.show()
```
結果は75%くらい。
グラフ見た感じもっと精度あげられそう。
実用化するなら95%くらい欲しいけど、妥協しました。。
いろんなサイズのモデルで学習させてみましたが、１番精度が良かったのはごく小さなモデルでした。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/288721/aae3e507-fadc-c6cb-fe17-84d90750e070.png)

######考察してみた
・今回は50x50にリサイズしたが、元画像がそれよりも小さい画像があった。そのため、かなりぼやけてしまい上手く学習できなかったのではないか。

・今回はサークルメンバー以外の人も分類できるように、othersクラスを設けた。このクラスだけ異なる多数の人の顔で学習させたので、上手く重みの調整ができなかったのかもしれない。

・重みを固定するVGGの層を減らした方が精度がよかった。初期の層でよりプリミティブな特徴を捉え、残りの層で今回扱ったデータ特有の特徴を捉えたためだろう。こういう結果が出たのは、imagenetが人の顔を含んでいないためだろうか。



#webカメラの起動・LINEとslackとの連携
いよいよ、先ほど作成したモデルと、LINEとslackとの連携部分を書いていきます！
長々とディープラーニングについて書いてきましたが、メインはこれからですね！

`came.py`でwebカメラを起動して、顔を認識したら`detect_push`モジュールを呼び出してLINEbotとslackbotからメッセージを送っています。

（以前は`subprocess.check_call()`を使っていました。モジュール化すると見やすいですね。）






```python:came.py
import cv2
import numpy as np
from keras.models import load_model
from detect_push import detectFace, pushToLine, pushToSlack
import time

SAVE_PATH = "./input/face.jpg"
model = load_model('faces.h5')
face_cascade = cv2.CascadeClassifier('./models/haarcascade_frontalface_default.xml')

cap = cv2.VideoCapture(0)#webカメラをキャプチャーする
while True:
    ret, img = cap.read()
    img = cv2.flip(img, 1)#反転
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)#グレースケール化
    cv2.imshow("img",img)
    faces = face_cascade.detectMultiScale(gray, 1.9, 5)
    if len(face)>0:      
        for rect in face:
            cv2.imwrite(SAVE_PATH, img[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]])
            print("recognize face !!")

            member = detectFace(SAVE_PATH, model)#上で保存した画像を読み込み、モデルでの予測結果をmemberに代入
            pushToLine(member)#予測結果を受け取り、LINEbotから送信
            pushToSlack(member)#予測結果を受け取り、slackbotから送信

            time.sleep(10)
    if cv2.waitKey(30) == 27:#Escキーを押すとbreak
        break

# キャプチャをリリースして、ウィンドウをすべて閉じる
cap.release()
cv2.destroyAllWindows()

```
```python:detect_push.py
import os
import tensorflow as tf
import cv2
import numpy as np
from linebot import LineBotApi
from linebot.models import TextSendMessage, ImageSendMessage
from linebot.exceptions import LineBotApiError
from slackbot.slackclient import SlackClient

def detectFace(filepath, model):#指定されたパスの画像を読み込んで,予測結果を返す
    image = cv2.imread(filepath)
    image = cv2.resize(image,(50,50))
    data = np.expand_dims(image, axis=0)
    result = model.predict(data)
    predicted = result.argmax()
    members = ["member1","member2","member3","member4","member5","member6","member7","member8","member9","member10","others"]

    return members[predicted]

def pushToLine(member):#LINEbotからメッセージを送信する
    channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)#環境変数に設定したchannel_access_tokenを取得
    line_bot_api = LineBotApi(channel_access_token)
    try:
        line_bot_api.push_message("メッセージを送信したいトークルームのid", TextSendMessage(text = member + "が開けました"))
    except LineBotApiError as e:
        # error handle
        ...

def pushToSlack(member):#slackbotからメッセージを送信する
    #slackclientのインスタンスを作成
    bot = SlackClient("slackbotのid")
    bot.send_message("メッセージを送信したいチャンネル名", member + "が開けました")
```
これだけの記述でbotからメッセージが送れる!!!
各アカウント登録は最後にリンクを張っておくので、そのサイトを参考にしてみてください。
[公式ドキュメント](https://developers.line.biz/ja/reference/messaging-api/)を見た感じ、LINEbotでもっといろんなことができそうですね...！

#まとめ
kerasでめちゃめちゃ簡単にモデルが作れて、転移学習であまり考えずにそこそこの精度がでる。

ただ学習データの準備がものすごく大変です。。。

LINEbotもslackbotも簡単に作れたので、他に面白いbotを作ったら紹介したいですね！


#参考にしたサイト
[Kerasでアニメ 「けいおん！」を画像認識させてみた](https://qiita.com/Taka_input/items/04a23bd8e9101788e583)
[openCVで複数画像ファイルから顔検出をして切り出し保存](https://qiita.com/FukuharaYohei/items/457737530264572f5a5b)
[Messaging APIを利用するには-LINE Developers](https://developers.line.biz/ja/docs/messaging-api/getting-started/)
[Pythonを使ったSlackBotの作成方法](https://qiita.com/kunitaya/items/690028e33ba5c666f3e2)