28
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Raspberry PiカメラでDeepLearningによるリアルタイム顔認証

Last updated at Posted at 2019-02-02

##はじめに
2019年より思い立ってAIの勉強を始めました。その過程で、Raspberry Piカメラを使った顔認証の仕組みを作ってみましたので、ご紹介したいと思います。

##環境
<学習する側(ディープラーニング)>
・Ubuntu(Windows10上のVirtualBoxにインストール)
・Python(v3)
・Scikit-learn
・Keras(Theano)
・OpenCV(顔の切り出しで使用)
<予測する側>
・Raspberry Pi Model B(Rasbian)
・Raspberry Piカメラっぽいやつ(中国製)
・Python(v2)
・OpenCV

##流れ
1.学習用の画像をWebから収集
2.収集した画像から顔だけ切り出した画像を生成
3.PC上で学習
4.Raspberry Piで予測
5.動かしてみる

##1.学習用の画像をWebから収集
手作業は避けたいので、PythonからGoogle画像検索をして画像を収集しました(参考)。
以下のプログラムを例えば$python3 image_collector.py 安部晋三 1000
(第一引数:検索ワード、第二引数:最大取得数)で実行します。

ソース
image_collector.py
# -*- coding: utf-8 -*-
import json
import os
import sys
import urllib
from bs4 import BeautifulSoup
import requests

class Google:
    def __init__(self):
        self.GOOGLE_SEARCH_URL = 'https://www.google.co.jp/search'
        self.session = requests.session()
        self.session.headers.update(
            {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0'})

    def search(self, keyword, maximum):
        print('begin searching', keyword)
        query = self.query_gen(keyword)
        return self.image_search(query, maximum)

    def query_gen(self, keyword):
        # search query generator
        page = 0
        while True:
            params = urllib.parse.urlencode({
                'q': keyword,
                'tbm': 'isch',
                'ijn': str(page)})

            yield self.GOOGLE_SEARCH_URL + '?' + params
            page += 1

    def image_search(self, query_gen, maximum):
        # search image
        result = []
        total = 0
        while True:
            # search
            html = self.session.get(next(query_gen)).text
            soup = BeautifulSoup(html, 'lxml')
            elements = soup.select('.rg_meta.notranslate')
            jsons = [json.loads(e.get_text()) for e in elements]
            imageURLs = [js['ou'] for js in jsons]

            # add search result
            if not len(imageURLs):
                print('-> no more images')
                break
            elif len(imageURLs) > maximum - total:
                result += imageURLs[:maximum - total]
                break
            else:
                result += imageURLs
                total += len(imageURLs)

        print('-> found', str(len(result)), 'images')
        return result

def main():
    google = Google()
    if len(sys.argv) != 3:
        print('invalid argment')
        print('> ./image_collector_cui.py [target name] [download number]')
        sys.exit()
    else:
        # save location
        name = sys.argv[1]
        data_dir = 'data/'
        if not os.path.exists(data_dir):
            os.makedirs(data_dir)
        if not os.path.exists('data/' + name):
            os.makedirs('data/' + name)
        try:
            # search image
            result = google.search(name, maximum=int(sys.argv[2]))
        except:
            import traceback
            traceback.print_exc()

        # download
        download_error = []
        for i in range(len(result)):
            print('-> downloading image', str(i + 1).zfill(4))
            try:
                urllib.request.urlretrieve(
                    result[i], data_dir + name + '/' + str(i + 1).zfill(4) + '.jpg')
            except:
                print('--> could not download image', str(i + 1).zfill(4))
                download_error.append(i + 1)
                continue

        print('complete download')
        print('├─ download', len(result)-len(download_error), 'images')
        print('└─ could not download', len(
            download_error), 'images', download_error)

if __name__ == '__main__':
    main()

##2.収集した画像から顔だけ切り出した画像を生成
こちらもPythonで自動化しました(参考)。
このプログラムは顔を切り出した後、左右反転や左右回転などした画像も生成してくれます。
以下のプログラムを例えば$python3 face_output.py -p /home/user/data/安部晋三
(引数:加工対象の画像のパス)で実行します。

ソース
face_output.py
# -*- coding: utf-8 -*-
import os
import glob
import argparse
import cv2
from PIL import Image
import numpy as np

IMAGE_HEIGHT = 128
CASCADE_PATH = "/home/takii/haarcascades/haarcascade_frontalface_alt.xml"
cascade = cv2.CascadeClassifier(CASCADE_PATH)
color = (255, 255, 255)

def detectFace(image):
    image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    facerect = cascade.detectMultiScale(image_gray, scaleFactor=1.07, minNeighbors=9, minSize=(10, 10))
    return facerect

def pre_resize(before, after, height=IMAGE_HEIGHT, filename="", antialias_enable=True):
    """
    Resize images according to the pre-defined image_heiht regardless of the size of them.
    """    
    img = Image.open(before, 'r')
    before_x, before_y = img.size[0], img.size[1]
    x = int(round(float(height / float(before_y) * float(before_x))))
    y = height
    resize_img = img
    if antialias_enable:
        resize_img.thumbnail((x, y), Image.ANTIALIAS)
    else:
        resize_img = resize_img.resize((x, y))

    resize_img.save(after, 'jpeg', quality=100)
    print( "RESIZED: %s[%sx%s] --> %sx%s" % (filename, before_x, before_y, x, y) )

def resize(image):
    return cv2.resize(image, (64,64))

def rotate(image, r):
    h, w, ch = image.shape # 画像の配列サイズ
    M = cv2.getRotationMatrix2D((w/2, h/2), r, 1) # 画像を中心に回転させるための回転行列
    rotated = cv2.warpAffine(image, M, (w, h))
    return rotated

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='clip face-image from imagefile and do data argumentation.')
    parser.add_argument('-p', required=True, help='set files path.', metavar='imagefile_path')
    args = parser.parse_args()

    # リサイズした画像を格納
    resize_dir = args.p + "/_resize"
    if not os.path.exists(resize_dir):
        os.makedirs(resize_dir)

    # 顔部分に囲いを追加した画像を格納
    addbox_dir = args.p + "/_addbox"
    if not os.path.exists(addbox_dir):
        os.makedirs(addbox_dir)

    # 顔部分をトリミングした画像を格納
    trimming_dir = args.p + "/_trimming"
    if not os.path.exists(trimming_dir):
        os.makedirs(trimming_dir)
    face_cnt = 0

    # jpgファイル取得
    files = glob.glob( "%s/*.jp*g" % (args.p) )

    for file_name in files:
        before_path = file_name
        filename = os.path.basename(file_name)
        after_path = '%s/%s' % ( resize_dir, filename )
        pre_resize(before_path, after_path, filename=file_name)

    resize_files = glob.glob(resize_dir+"/*.jpg")

    for file_name in resize_files:
        print("detect face on file:"+file_name)

        # 画像のロード
        image = cv2.imread(file_name)
        if image is None:
            # 読み込み失敗
            print("image is None")
            continue

        # -12~12度の範囲で3度ずつ回転
        for r in range(-12,13,4):
            image = rotate(image, r)

            # 顔画像抽出
            facerect_list = detectFace(image)
            if len(facerect_list) == 0:
                print("NG detectFace")
                continue

            basename = os.path.basename(file_name)

            # 顔検知の囲い追加画像保存 どの程度の精度で検知できているかの確認
            for rect in facerect_list:
                cv2.rectangle(image, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=2)

            cv2.imwrite(addbox_dir+"/"+basename, image)

            # 顔部分切り抜き
            for facerect in facerect_list:
                # 顔画像部分の切り抜き
                croped = image[facerect[1]:facerect[1]+facerect[3],facerect[0]:facerect[0]+facerect[2]]

                # 出力
                cv2.imwrite(trimming_dir+"/"+str(face_cnt)+".jpg", resize(croped))
                face_cnt += 1

                # 反転画像も出力
                fliped = np.fliplr(croped)
                cv2.imwrite(trimming_dir+"/"+str(face_cnt)+".jpg", resize(fliped))
                face_cnt += 1
実行結果は以下の通り。良い感じですね! ![face.png](https://qiita-image-store.s3.amazonaws.com/0/253839/d4ee621a-b977-857d-6284-69438d173182.png) ##3.PC上で学習 学習は、Raspberry Pi上では時間がかかるので、PCで実施しました。 アウトプットである「モデル構造」と「学習結果」だけRaspberry Piに持っていきましょう。 画像の格納されたディレクトリ名をラベルに利用したいので、人物ごとにディレクトリを作成しています。 (人物のチョイスはなんとなく。。。) ![path.png](https://qiita-image-store.s3.amazonaws.com/0/253839/41e44fb8-969f-4ee2-7dc6-40e686d9830e.png) DeepLearningで画像を扱う時は「CNN」が主流なので、Kerasで実装します。 モデル構造は以下の通り。 ![keras_model.png](https://qiita-image-store.s3.amazonaws.com/0/253839/2199f521-844f-ab54-7738-7a293c6fbf82.png) 以下のプログラムを例えば`$python3 learn.py`で実行します。
ソース
learn.py
# -*- coding: utf-8 -*-
from sklearn.model_selection import train_test_split
from keras.preprocessing.image import load_img, img_to_array
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.utils import np_utils
import os, glob
import numpy as np
import matplotlib.pyplot as plt

root_dir = "/home/takii/cozto/data"
categories = ["安倍晋三", "麻生太郎", "新垣結衣", "柳楽優弥", "小林星蘭", "草村礼子"]
nb_classes = len(categories)
image_size = 64

# CNN設定
BATCH_SIZE = 32
EPOCHS = 15

def init():
    X = []
    Y = []
    for idx, cat in enumerate(categories):
        files = glob.glob(root_dir + "/" + cat + "/_trimming/*")
        print("---", cat, "を処理中")
        for i, f in enumerate(files):
            img = load_img(f, target_size=(image_size,image_size))
            data = img_to_array(img)
            X.append(data)
            Y.append(idx)
    X = np.array(X)
    Y = np.array(Y)
    
    X_train, X_test, y_train, y_test = train_test_split(X, Y)
    xy = (X_train, X_test, y_train, y_test)
    np.save(root_dir + "/npy/face.npy", xy)
    print("ok,", len(Y))

def main():
    X_train, X_test, y_train, y_test = np.load(root_dir + "/npy/face.npy")
    X_train = X_train.astype("float") / 256
    X_test  = X_test.astype("float")  / 256
    y_train = np_utils.to_categorical(y_train, nb_classes)
    y_test  = np_utils.to_categorical(y_test, nb_classes)
    model = model_train(X_train, y_train)
    model_eval(model, X_test, y_test)

def build_model(in_shape):
    model = Sequential()
    model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=in_shape))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Convolution2D(64, 3, 3, border_mode='same'))
    model.add(Activation('relu'))
    model.add(Convolution2D(64, 3, 3))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Flatten())
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(nb_classes))
    model.add(Activation('softmax'))
    model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
    return model

def model_train(X, y):
    model = build_model(X.shape[1:])
    #history = model.fit(X, y, batch_size=BATCH_SIZE, nb_epoch=EPOCHS, validation_split=0.1)
    history = model.fit(X, y, batch_size=BATCH_SIZE, nb_epoch=EPOCHS, validation_split=0.1)

    json_string = model.to_json()
    open(os.path.join(root_dir + '/h5/cnn_model.json'), 'w').write(json_string)
    model.save_weights(root_dir + "/h5/face-model.h5")

    # グラフ表示
    plt.plot(range(1, EPOCHS + 1), history.history['acc'], label = "train-acc")
    plt.plot(range(1, EPOCHS + 1), history.history['loss'], label = "train-loss")
    plt.plot(range(1, EPOCHS + 1), history.history['val_acc'], label = "val-acc")
    plt.plot(range(1, EPOCHS + 1), history.history['val_loss'], label = "val-loss")
    plt.title('list')
    plt.xlabel('Epochs')
    plt.ylabel('Accuracy')
    plt.legend()
    plt.show()
    return model

def model_eval(model, X, y):
    score = model.evaluate(X, y)
    print('loss=', score[0])
    print('accuracy=', score[1])

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

学習時間は、画像の枚数が1人につき800枚×6人の約5000枚で、2.5hかかりました。
学習はクラウドにぶん投げるのが良いみたい。。。学習完了時のグラフです。
無題.png

##4.Raspberry Piで予測
「learn.py」を実行すると、モデル構造と学習結果が以下に格納されます。
path2.png
この2つと「predict.py」を、Raspberry Piに配置します。
以下のプログラムを例えば$python3 predict.pyで実行します。

ソース
predict.py
# -*- coding: utf-8 -*-

import learn as face
import sys, os
from keras.preprocessing.image import load_img, img_to_array
import numpy as np
import cv2
import time
import picamera
import picamera.array

root_dir = "/home/pi/cozto/data"
cascade_file = "/home/pi/opencv-2.4.13/data/haarcascades/haarcascade_frontalface_alt.xml"

image_size = 32
categories = ["abeshinzo", "asoutarou"]

with picamera.PiCamera() as camera:
    with picamera.array.PiRGBArray(camera) as stream:
        # カメラの解像度を320x240にセット
        camera.resolution = (320, 240)
        # カメラのフレームレートを15fpsにセット
        camera.framerate = 15
        # ホワイトバランスをfluorescent(蛍光灯)モードにセット
        camera.awb_mode = 'fluorescent'

        while True:
            # stream.arrayにBGRの順で映像データを格納
            camera.capture(stream, 'bgr', use_video_port=True)
            # グレースケールに変換
            gray = cv2.cvtColor(stream.array, cv2.COLOR_BGR2GRAY)

            # カスケードファイルを利用して顔の位置を見つける
            cascade = cv2.CascadeClassifier(cascade_file)
            face_list = cascade.detectMultiScale(gray, minSize=(100, 100))

            cv2.imwrite("frontalface.png", stream.array)
            img = cv2.imread("frontalface.png")
            for rect in face_list:
                cv2.rectangle(stream.array, tuple(rect[0:2]),tuple(rect[0:2] + rect[2:4]), color, thickness=2)
                x = rect[0]
                y = rect[1]
                width = rect[2]
                height = rect[3]
                dst = img[y:y+height, x:x+width]
                cv2.imwrite("output.png", dst)
                cv2.imread("output.png")
                X = []
    
                img = load_img("./output.png", target_size=(image_size,image_size))
                in_data = img_to_array(img)
    
                X.append(in_data)
                X = np.array(X)
                X  = X.astype("float")  / 256
    
                model = face.build_model(X.shape[1:])
                model.load_weights(root_dir + "/h5/face-model.h5")
    
                pre = model.predict(X)
                print(pre)
                if pre[0][0] > 0.9:
                    print(categories[0])
                    text = categories[0] + str(pre[0][0]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
                elif pre[0][1] > 0.9:
                    print(categories[1])
                    text = categories[1] + str(pre[0][1]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
                elif pre[0][2] > 0.9:
                    print(categories[2])
                    text = categories[2] + str(pre[0][2]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
                elif pre[0][3] > 0.9:
                    print(categories[3])
                    text = categories[3] + str(pre[0][3]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
                elif pre[0][4] > 0.9:
                    print(categories[4])
                    text = categories[4] + str(pre[0][4]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
                elif pre[0][5] > 0.9:
                    print(categories[5])
                    text = categories[5] + str(pre[0][5]*100) + "%"
                    font = cv2.FONT_HERSHEY_PLAIN
                    cv2.putText(stream.array,text,(rect[0],rect[1]-10),font, 2, color, 2, cv2.CV_AA)
    
            cv2.imshow("Show FLAME Image", stream.array)
            time.sleep(0.4)
    
            k = cv2.waitKey(1)
    
            if k == ord('q'):
                break

            # streamをリセット
            stream.seek(0)
            stream.truncate()

        cv2.destroyAllWindows()

##5.動かしてみる
実際にRaspberry Piカメラで、対象人物とそうでない人物を映してみます。
まずはそうでない人。
no.png
画像の左半分が被写体、右半分がRaspberry Piのデスクトップです。
顔がどこかは認証してますが、スルー。

次に、対象人物を映します。
yes2.png
ラベルを日本語にしたので「????」に化けてますが、しっかり認証できてます。
1号機としてはいい感じですねっ:exclamation:

##6.今後
Raspberry PiでPredict.pyを動かしたあと、初めて顔を認証しようとすると十数秒固まります。
あとは見ていただいた通り、日本語対応が必要です。
このあたりを改善していきたいですね。

##リンク
AIエンジニアを目指してみる
Sony Neural Network Consoleで学習の途中出力結果を確認する

28
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?