🦒

社内ハッカソンでRaspberry Piとディープラーニングを活用した顔認証を作ったので制作過程を共有する

2023/08/22に公開
1

はじめに

先日、私が所属する、アルサーガパートナーズ株式会社で社内ハッカソンが開催されました。その時に作成した成果物と作成工程を共有したいと思います。

https://www.arsaga.jp/news/dream-of-development-report-20230801/


成果物の概要

Raspberry Piとを使用して、特定の人物がRaspberry Pi用のカメラに映し出されたら、電圧ブザーが鳴るシステムを作りました。顔の認識はOpneCVで行い、特定の人物を判定する処理はTensorflowとKerasを使用して、ディープラーニングを行いました。


外観

カメラはRasTech Raspberry Piを使用して、Raspberry Piに接続、電圧ブザーはPiezo Buzzerをブレッドボードを利用して接続してます。


ソースコード

今回作成したソースコードは以下のリポジトリに格納してます。

https://github.com/MASAKi-cell/raspberryPi-face-deepLearning


Version情報

今回使用したライブラリ、言語などのバージョン情報です。

ライブラリ・言語 バージョン
TensorFlow 2.13.0
TensorFlow Lite 3.0
Keras 2.13.1
OpenCV 4.7.0
Python 3.11.3



実装概要

今回は、TensorFlowとKerasを使用して、ディープラーニングを実施しました。特定の人物とその他の人物画像をそれぞれ70枚ほど準備して、NumPy配列形式に変換してトレーニングを行い、評価結果をRaspberry Piに読み込ませるためにtflite形式のファイルに格納して保存するようにしました。



画像を取得してnumpy配列に変換

from PIL import Image
import glob
import numpy as np

classes = ["other", "person"]
image_size = 150
test_data = 45  # テストデータとトレーニング用のデータを分割する

x_array_train = []  # Numpy配列(train)
x_array_test = []  # Numpy配列(test)
y_label_train = []  # labelを格納(train)
y_label_test = []  # labelを格納(test)


for index, c in enumerate(classes):
    photo_dir = "../test_data/" + c
    files = glob.glob(photo_dir + "/*.jpg")  # pathの生成
    for i, file in enumerate(files):
        if i >= 150:
            break
        image = Image.open(file)
        image = image.convert("RGB")  # RGBに変換
        image = image.resize((image_size, image_size))  # リサイズ
        numpyData = np.asarray(image)  # numpy配列に変換

        if i >= test_data:
            x_array_test.append(numpyData)
            y_label_test.append(index)
        else:  # 訓練用のデータを水増しする
            x_array_train.append(numpyData)
            y_label_train.append(index)

            for angle in range(-20, 20, 5):
                # 画像を回転する
                img_rotate = image.rotate(angle)
                rotate_data = np.asarray(img_rotate)
                x_array_train.append(rotate_data)
                y_label_train.append(index)

                # 画像を反転する
                img_trans = image.transpose(Image.FLIP_LEFT_RIGHT)
                transpose_data = np.asarray(img_trans)
                x_array_train.append(transpose_data)
                y_label_train.append(index)

x_array_train = np.array(x_array_train)
x_array_test = np.array(x_array_test)
y_label_train = np.array(y_label_train)
y_label_test = np.array(y_label_test)


# numpy配列を保存
np.save("./x_array_train.npy", x_array_train)
np.save("./x_array_test.npy", x_array_test)
np.save("./y_label_train.npy", y_label_train)
np.save("./y_label_test.npy", y_label_test)

まず初めに、保存されている画像を取得して、データの水増しを行い最終的にnumpy配列に変換して保存する処理を実装しました。

  • ディープラーニングを実施するために、テスト用とトレーニング用のデータに分割します。
  • 画像はRGBに変換、指定したサイズ(150×150)にリサイズを行った後、rangeimage.transposeを使用して、ディープラーニングの精度を高めるため、画像データの回転や左右反転させて水増しを行います。
  • np.arrayを使用して、numpy配列に変換してバイナリファイル(npy)として保存します。



ディープラーニングの実行

import tensorflow as tf
import numpy as np

classes = ["other", "person"]
num_classes = len(classes)
image_size = 50

# メインの関数を定義する
def main():

    # テストデータANDトレーニング用のデータ取り込み
    X_train = np.load("./x_array_train.npy")
    X_test = np.load("./x_array_test.npy")
    y_train = np.load("./y_label_train.npy")
    y_test = np.load("./y_label_test.npy")

    X_train = X_train.astype("float") / 255  # 正規化
    X_test = X_test.astype("float") / 255  # 正規化
    y_train = tf.keras.utils.to_categorical(
        y_train, num_classes)  # 整数のクラスベクトルから2値クラスの行列への変換
    y_test = tf.keras.utils.to_categorical(
        y_test, num_classes)  # 整数のクラスベクトルから2値クラスの行列への変換

    model = model_train(X_train, y_train)  # モデルのトレーニング
    model_eval(model, X_test, y_test)  # モデルの評価


def model_train(X, y):
    model = tf.keras.models.Sequential()  # モデルの定義
    model.add(tf.keras.layers.Conv2D(
        32, (3, 3), padding='same', input_shape=X.shape[1:]))
    model.add(tf.keras.layers.Activation('relu'))  # 活性化関数(relu)の適用
    model.add(tf.keras.layers.Conv2D(32, (3, 3)))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))  # 過学習の設定

    model.add(tf.keras.layers.Conv2D(64, (3, 3), padding='same'))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Conv2D(64, (3, 3)))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(tf.keras.layers.Dropout(0.25))

    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(512))  # 全結合層(Dense layer)の追加
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.5))
    model.add(tf.keras.layers.Dense(2))
    model.add(tf.keras.layers.Activation('softmax'))

    opt = tf.keras.optimizers.legacy.RMSprop(learning_rate=0.0001, decay=1e-6)
    model.compile(loss='categorical_crossentropy',
                  optimizer=opt, metrics=['accuracy'])  # 損失関数、オプティマイザ、評価指標の指定
    model.fit(X, y, batch_size=32, epochs=100)  # モデルの評価(バッチサイズ32、エポック数100)

    model.save('./person_cnn.h5')  # モデルの保存
    return model


def model_eval(model, X, y):
    scores = model.evaluate(X, y, verbose=1)
    print('Test Loss: ', scores[0])
    print('Test Accuracy: ', scores[1])


if __name__ == "__main__":
    main()

上記ファイルでディープラーニングを実行して、h5ファイルに保存する処理を記載してます。

  • np.loadで先ほど作成した、バイナリファイル(トレーニングデータとテストデータ)を読み込みます。
  • astype("float") / 255で画像データの正規化(Normalization)処理を行っています。各ピクセルの値が0から255(赤(Red)、緑(Green)、青(Blue)の色の強度をそれぞれ0から255の値で表現)の整数で表現されている画像データを、0から1の範囲の浮動小数点数に変換しています。
  • tf.keras.utils.to_categoricalは正直良く分かっていませんが、カテゴリ変数をバイナリベクトル形式に変換するOne-hotエンコーディングを行っているようです。
  • model_trainでCNNモデルを定義し、トレーニングを行います。畳み込み層や活性化関数(ReLU)、過学習の設定などを行っています。
  • model_evalで与えられたモデルを使用してテストデータの評価を行い、損失(loss)と精度(accuracy)に分けて表示するようにします。



テスト結果

....
Epoch 98/100
48/48 [==============================] - 2s 51ms/step - loss: 5.7418e-06 - accuracy: 1.0000
Epoch 99/100
48/48 [==============================] - 2s 51ms/step - loss: 3.3198e-07 - accuracy: 1.0000
Epoch 100/100
48/48 [==============================] - 2s 50ms/step - loss: 0.0023 - accuracy: 0.9993
  saving_api.save_model(
2/2 [==============================] - 0s 9ms/step - loss: 0.3779 - accuracy: 0.9524
Test Loss:  0.3779466152191162
Test Accuracy:  0.9523809552192688

テストデータに対する精度(accuracy)が約95.23%となっていて、それなりに高いパフォーマンスを発揮する結果となりました。



tfliteファイルに変換

import tensorflow as tf
h5_model = tf.keras.models.load_model('person_cnn.h5')  # モデルのロード
converter = tf.lite.TFLiteConverter.from_keras_model(h5_model)  # tfliteに変換
tflite_model = converter.convert()

with open('model.tflite', 'wb') as f:
    f.write(tflite_model)

次に.tfliteファイルに変換する処理を記載します。H5ファイルは大量のデータを多次元配列の形式で格納することができますが、H5ファイルのままだとRaspberry Piに読み込ませることができなかった為、.tfliteファイルに変換します。.tfliteファイル は、AndroidやiOS、Raspberry Piなどでの利用を目的としたモデルのファイル形式のことを指します。



Raspberry Piの組み立て手順&セットアップ

まず初めに、Raspberry Piにヒートシンクと冷却ファンを取り付けます。
https://www.souichi.club/raspberrypi/raspberrypi4/

https://tarufu.info/raspberrypi4-case-and-fan/



以下の記事を参考にRaspberry Pi用のOSインストール方法やセットアップを行いました。

https://invisiblepotato.com/raspberrypi01/



VSCodeのインストール

Raspberry PiでPythonコードを記述するためにVSCodeもインストールしておきます。

https://invisiblepotato.com/raspberrypi07/



Raspberry Piがカメラモジュールを使用するときに必要なOpenCVもインストールします。

https://sozorablog.com/opencv_install/



OpenCVによる顔認識及び特定の人物の検出

import cv2
import os
import buzzer

import tflite_runtime.interpreter as tflite
import numpy as np

# カスケードファイルパス
cascade_path = os.path.join(
    cv2.data.haarcascades, "haarcascade_frontalface_alt.xml"
)

# CascadeClassifierクラスの生成
cascade = cv2.CascadeClassifier(cascade_path)

# モデルのロード
interpreter = tflite.Interpreter(model_path="./person_cnn.tflite")
interpreter.allocate_tensors()  # テンソルの確保

# インタープリターから入力と出力の詳細を取得
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# カメラモジュール開始
capture = cv2.VideoCapture(0)

desired_width = 1280.0
desired_hight = 1280.0
capture.set(cv2.CAP_PROP_FRAME_WIDTH, desired_width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, desired_hight)

# 検出時のサイズを指定
MIN_SIZE = (300, 300)

while True:
    # 「ESC」を押したら処理を止める、waitKey()はキーボード入力を処理する関数で、引数は入力を待つ時間を指定
    if cv2.waitKey(1) & 0xFF == 27:
        break

    # カメラ画像を読み込む
    _, image = capture.read()

    # 画像反転
    image = cv2.flip(image, -1)

    # OpenCVでグレースケール化、計算処理を高速化するため
    igray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 顔検出処理
    faces = cascade.detectMultiScale(igray, minSize=MIN_SIZE)

    # 顔検出時以外もフレームを表示させておく
    if len(faces) == 0:
        cv2.imshow('frame', image)
        continue

    for (x, y, w, h) in faces:
        color = (255, 0, 0)
        cv2.rectangle(image, (x, y), (x+w, y+h),
                      color, thickness=8)

        # 顔部分のみを抽出し、モデルの入力サイズにリサイズ
        face_img = image[y:y+h, x:x+w]
        # モデルが訓練されたときの入力サイズに依存
        face_img = cv2.resize(face_img, (150, 150))
        face_img = face_img[np.newaxis, ...]  # バッチ次元を追加

        # モデルに入力し、推論を実行
        face_img = face_img.astype("float32") / 255  # 正規化
        interpreter.set_tensor(input_details[0]['index'], face_img)
        interpreter.invoke()  # 推論実行
        # 推論結果は、output_detailsのindexに保存される
        result = interpreter.get_tensor(output_details[0]['index'])

        # 推論結果が人物に一致する場合、ブザーを鳴らす
        if result > 0.09:  # 特定の人物に対応するラベル
            print("<<<< ::: A is detected!")
            buzzer.setupBuzzer()
        else:
            print("<<<< ::: 他の人物が検出されました")

    # 顔が検出されたら顔の周りに枠を表示してフレームを表示
    cv2.imshow('frame', image)

capture.release()  # カメラを解放
cv2.destroyAllWindows()  # ウィンドウを破棄

OpenCVで顔部分の抽出を行い、読み込んだ推論結果から、特定の人物かどうかの判定を行います。

  • Raspberry Pi側は機械学習ライブラリにTensorFlow Liteを使用します。TensorFlowは高性能なCPUを搭載したコンピューター上で「訓練・学習」を実行することを目的に開発されましたが、AndroidやRaspberry Piなどの性能が限定された機器で推論を行うために開発されたライブラリです。
  • カスケードファイルにhaarcascade_frontalface_alt.xmlを指定して、openCVで顔認識をする場合は、すでに用意されている顔認識モデルを使用します。
  • cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)を指定して、グレースケール化を行い、計算処理を高速化させます。
  • tflite.Interpreterで先ほど生成したモデルを読み込み、openCVで検出された顔と一致するかどうかの推論を実行します。
  • 推論結果(result)が特定の閾値を超える場合、特定の人物が検出されたと判断し、ブザーを鳴らします。それ以外の場合、他の人物が検出されたと判断するように設定してます。

https://www.klv.co.jp/corner/python-opencv-video-capture.html



電圧ブザーの設定

from gpiozero import TonalBuzzer
from gpiozero.tones import Tone
from gpiozero.pins.pigpio import PiGPIOFactory
from time import sleep

# BUZZERのピン設定
BUZZER_PIN = 18

# ドレミ - 音名 + オクターブで指定
ONPUS = ["C4"]

def setupBuzzer():

    # 各ピン(GPIO)をbuzzer設定
    factory = PiGPIOFactory()
    buzzer = TonalBuzzer(BUZZER_PIN, pin_factory=factory)

    # 音を鳴らす
    try:
        # 音符指定
        for onpu in ONPUS:
            buzzer.play(Tone(onpu))
            sleep(0.5)
        buzzer.stop()
        sleep(0.5)

        # MIDI note指定
        for note_no in range(60, 80, 5):
            buzzer.play(Tone(midi=note_no))
            sleep(0.5)
        buzzer.stop()
        sleep(0.5)

        # 周波数指定
        for freq in range(300, 400, 100):
            buzzer.play(Tone(frequency=freq))
            sleep(0.5)
        buzzer.stop()
        sleep(0.5)
    except:
        buzzer.stop()
        print("stop")

    return
  • BUZZER_PINにGPIOピン番号18を設定しています。
  • 音階はとりあえずC4に設定しました。
  • MIDIノート番号60から79までの音を、5のステップで逐次鳴らしを行い、300Hzの周波数でブザーを鳴らします。

電圧ブザーを鳴らす実装については以下のZennの記事を参考にしました。
https://zenn.dev/kotaproj/books/raspberrypi-tips



さいごに

今回は社内ハッカソンで作成したシステムについて紹介しました。
Pythonやディープラーニング、Raspberry Piといった初めての技術をそれぞれ使用したので、最後までできるかどうか不安でしたが、なんとか完成させることができました。もし何か間違いがあれば、ご指摘いただけると幸いです。ここまで読んでくださりありがとうございました。


Arsaga Developers Blog

Discussion

yKesamaruyKesamaru

素晴らしい記事をありがとうございます!
いくつか気になる点がありました。

  • train時: RGB、推論時: グレースケール
    • 違うものを比較してる気がします。多分モデルのロバスト性でカバーしているのかな?と。
  • データ拡張のコードですが、推論時と同じくカスケードモデルを使用した方が、精度が上がる気がします
    • 顔領域の切り取られ具合(たとえばpadding)が変わるため

なのでテスト結果で出されている精度は出ないのではないかな?と。
いかがでしょうか?