ぎんてんのロボ工房    

2024年6月19日水曜日

OpenCVとラズパイを使った顔認証・顔識別/2024

 ラズパイとOpenCVを使った顔認証・顔識別/2024

2024-05月実施。

opencvも顔認証も特別興味なかったのですが、「これ、組み合わせてみたら面白いんじゃないか?」とeyes kit 製作の思い付きで何が何でもやってみたくなりました。

しかしあらゆるサイトを参考にしましたが、情報が古いのか全くうまくいきませんでした。

今更出尽くした感はあるとは思いますが、意外とまだうまく行っていない方もいるようなので備忘録も含めて記します。


1opencvで顔認証

使用したハード構成

raspberry pi4 4B         Camera Module3

ラズパイ4とpi3カメラ

利用環境

Bullseye 64ビット (参照サイトでは32ビット利用とはあるのだが・・)

Bullseye 64ビット

とにかくいろんなサイトを試しまくって分かってきたのが、おそらくラズパイでの利用は構成が重要ではないかと?。osのバージョンと32ビット or 64ビット、カメラもcamera2は持ってませんが、プログラムがcamera3では動かないケースもあるような気がします。

カメラケーブル接続方向

したがってこのサイトの利用も上記の構成以外ではほぼ動きません。

私が良く分かって無いせいだとは思いますが実際このサイトを利用してもこういう結果となり、最終的にBullseye 64ビットに落ち着きました。

環境変更にてテスト結果

今回、基本として利用させて頂いたのは以下のサイトです。



こちらに関してはリンク案内みたいなものなので特に特筆すべきことも無いのですが・・動画で説明されてますので注視して以下を抜き出しました。
(字幕もあるが動画と違って字幕には抜けがある)

sudo apt-get update

sudo apt-get install python3-opencv

sudo apt-get install libqt5gui5 libqt5test5 python3-sip python3-pyqt5 libjasper-dev libatlas-base-dev libhdf5-dev libhdf5-serial-dev -y

pip3 install opencv-contrib-python==4.5.5.62

pip3 install -U numpy

python3

import cv2

cv2.__version__


尚、64ビットでインストールだと途中
sudo apt-get install libqt5gui5 libqt5test5 python3-sip python3-pyqt5 libjasper-dev libatlas-base-dev libhdf5-dev libhdf5-serial-dev -y
のときと
pip3 install -U numpy
のインストール時にワーニングが出ますが、顔認証の利用には問題ありません。

完了


32ビットインストールだと出ないんですが、逆に最後のimport cv2がエラーで動かないんですよね・・・デモのバージョン不明なのですが、一体どのバージョンでやってるんでしょう?。

libjasper-devエラー

Numpyワーニング


あとは、カスケードファイルはサイトのサンプルプログラムの中にある
https://github.com/opencv/opencv/blob/4.x/data/haarcascades/haarcascade_frontalface_default.xml
より、ダウンロードして、サンプルプログラム内の指定フォルダに配置。
/home/pi/Face Recognition/haarcascade_frontalface_default.xml

完成

これで基本的な顔認識までは出来ます。
プログラムはCytron様のサイトをご確認下さい。

顔認証

このサイト以外でも顔認識や、画像変換までなら出来るサイトはありましたが顔識別になると私の力ではどうしてもうまくいきませんでした。
しかしこれをベースにすると顔識別までもなんとか出来ました。

また、カメラの利用に関して
sudo raspi-configのレガシーカメラはオフのままで設定不用
良くみかけるconfigの変更記述”dtoverlay=imx708”等もしなくてもopencvは動いてます。

2 顔識別

上記により顔認証は取り合えずできましたが、参照サイトは顔認識まで。

あくまでも目標は識別画像によってGPIOからのリアクションを変えること。
0知識からのチカラ押しだけでやってきましたが、さすがに何度も試すと大体理屈はわかってきました。
顔認証の流れはこんな感じで

顔認証


出来てからの整理ですが、顔識別はこんな感じでしょうか

顔識別

独学における認識なので間違ってたらご指摘下さい。

顔認証をベースに、右赤枠の個人を特定する自分の写真と他者の写真を利用した顔識別モデルを作成。それを識別判断するプログラムに変更です。


/home/pi/Face Recognition/detected_faces
トレーニングデータ用フォルダを作成し、自分写真(フォルダ0)と他者写真(フォルダ1)を作成しました。

libcamera-still -o test.jpg
トレーニング画像として自分自身を30枚撮影して、フォルダ0に保存。

pi3カメラで撮影するとワイドになり、無駄な情報が含まれてしまうのでPCでトリミングしてまたラズパイに戻してます。

トレーニング用に自分のおっさん写真30枚

もう一つは他者
工作の都合上、映画「賭ケグルイ」出演であるイケメン俳優の高杉真宙様の写真をネットからフォルダ1に保存しました。

他者のイケメン写真30枚

下のプログラムを利用して、写真から顔識別モデルを作成しました。


/home/pi/Face Recognition/detected_faces/face_recognizer_model.xml
detected_faces配下のフォルダはさらに下にフォルダを作っていても全のフォルダを抽出してくれます。
(フォルダ1、フォルダ2、個別に指定しなくても良い)

下記、トレーニングデータ(face_recognizer_model.xml)作成プログラム


import cv2
import numpy as np
import os  

# トレーニング画像のパス
train_images_path = "/home/pi/Face Recognition/detected_faces"

# トレーニングデータの準備
images = []
labels = []

# ラベル名を数値にマッピング
label_mapping = {"1": 0, "2": 1, "other": 2}

# トレーニング画像の読み込みとラベル付け
for label_folder in os.listdir(train_images_path):
    label = label_mapping.get(label_folder, 2)  # フォルダ名に基づいてラベルを設定
    label_path = os.path.join(train_images_path, label_folder)
    for image_file in os.listdir(label_path):
        image_path = os.path.join(label_path, image_file)
        image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        images.append(image)
        labels.append(label)  # ラベルを追加する

# 顔識別モデルの作成とトレーニング
face_recognizer = cv2.face.LBPHFaceRecognizer_create()
labels_array = np.array(labels, dtype=np.int32)  # ラベルをNumPy配列に変換
face_recognizer.train(images, labels_array)

# モデルの保存
face_recognizer.save("/home/pi/Face Recognition/detected_faces/face_recognizer_model.xml")


ラベルという記述がありますが、自分と他者を認識させた時に認識の違いをはっきりさせたい為、名前を表示させるようにしたいとラベルをつけました。
1:(0 自分) 2:(1 イケメン俳優) other:(2 どちらでも無い / unkown)

上記を元にしたメインプログラムが以下です。


import cv2 # OpenCVライブラリ。画像処理とコンピュータビジョンに使用されます。
import os  #ファイルやディレクトリ操作を行うための標準ライブラリ。
import time #時間操作を行うための標準ライブラリ。
import numpy as np #数値計算ライブラリ。
from picamera2 import Picamera2 #Raspberry Piのカメラモジュール用のライブラリ。

# -- トレーニング済みの顔認識モデルとHaar Cascade分類器の読み込み --
# トレーニング済みの顔識別モデルのパス
face_recognizer_directory = "/home/pi/Face Recognition/detected_faces"
face_recognizer_model = os.path.join(face_recognizer_directory, "face_recognizer_model.xml")

# モデルをロード
face_recognizer = cv2.face.LBPHFaceRecognizer_create(radius=1, neighbors=8, grid_x=8, grid_y=8)
face_recognizer.read(face_recognizer_model)

# Haar Cascade分類器をロード
face_detector = cv2.CascadeClassifier("/home/pi/Face Recognition/haarcascade_frontalface_default.xml")
cv2.startWindowThread()

#-- Picamera2を使ってリアルタイム映像をキャプチャ --
# Piカメラの設定
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
picam2.start()

# -- 顔検出を行い、検出された顔部分を切り出し、前処理(リサイズとヒストグラム均等化)を行う --
# 検出された顔画像の保存ディレクトリ
output_directory = "detected_faces"
os.makedirs(output_directory, exist_ok=True)

# ラベル名のマッピング
label_names = {0: "ginten", 1: "takasugi", 2: "other"}

def preprocess_face(face):
    """画像前処理:リサイズとヒストグラム均等化"""
    face = cv2.resize(face, (200, 200))
    face = cv2.equalizeHist(face)
    return face

while True:
    im = picam2.capture_array()
    grey = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
    faces = face_detector.detectMultiScale(grey, scaleFactor=1.1, minNeighbors=5, minSize=(50, 50))

    for (x, y, w, h) in faces:
        cv2.rectangle(im, (x, y), (x + w, y + h), (0, 255, 0), 2)

        # 顔部分を切り出し
        face_roi = grey[y:y+h, x:x+w]
        preprocessed_face = preprocess_face(face_roi)

        # 顔を識別
        label, confidence = face_recognizer.predict(preprocessed_face)

        # 認識結果と信頼度をログに表示
        print(f"Detected face: Label={label}, Confidence={confidence}")

        # 信頼度が一定の閾値を超える場合にのみ識別結果を表示
        if confidence < 70:  # 閾値を再調整
            label_text = label_names.get(label, "other")
            cv2.putText(im, f"{label_text} ({confidence:.2f})", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA)
        else:
            cv2.putText(im, "Unknown", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, cv2.LINE_AA)

        # 顔画像を保存(デバッグ用)
        timestamp = int(time.time() * 1000)
        filename = os.path.join(output_directory, f"face_{timestamp}.jpg")
        cv2.imwrite(filename, preprocessed_face)  # 顔部分のみ保存

    # -- カメラ映像に認識結果をオーバーレイして表示 --
    cv2.imshow("Camera", im)
    # -- 'q'キーが押されたらプログラムを終了 --
    if cv2.waitKey(1) == ord('q'):
        break

#終了処理
cv2.destroyAllWindows()


上記プログラムをホームフォルダに置いて、Thonnyの実行結果は以下です。

自分はpiカメラを除き込んで、高杉さんはプリントアウトをカメラに写して、又別の人はスマホの画像を利用しました。
部屋の明るさや、顔の角度によって認識精度が大分変りますね。
プリントアウトでは僅かに傾いていたら(トレーニングデータとして利用したテスト画像と全く同じはずなのに)認識しないくらいです。

顔識別実行結果
ginten /takasugi/unkown

unkownは顔がモデルに登録されていないか、認識精度が低いために表示されるものですが、そもそもotherに該当するフォルダは作っていないので、unkownで良いのかな。
自分とそれ以外の他者との区別は出来たので、まずは良しとします。

3 adafruit eyes kitとの組み合わせ


最初に掲げた通り、今回opencvというのをやってみたいと思ったのは
adafuruit eyes kit利用の工作の中で、顔認証と組み合わせたら面白いかな?と思ったから。

なのでいよいよ組み合わせて見ます。
同じ構成で利用する方もいないと思うので、参考という形でeyes kitやPICOのプログラムは省略します。
eyes kitの中で瞳孔を変更するsvgファイルの動作を変更する為、ちょっと工夫しています。
もし、eyes kitをお持ちの方でどうやったの?という方が居ましたらお気軽にご連絡下さい。

open cvとeyes kitの組み合わせ

概略は、
顔を識別する(opencv ラズパイ)→目の表情が変わる(eyes ラズパイ)→サウンド(PICO)(音声)を出す。

顔を識別した後GPIOに出力しているだけにも拘わらず何故こんな複雑な構成になっているかというと。
以前も書きましたeyes kitの問題点ですが、
〇 eyes kitがHDMI出力のコピーを液晶に表示させている為、サウンドがジャックから出力されない(強制的にHDMI出力のみになる)。
〇 eyeskitが32bitでしか動かない
どうしてもこの辺クリア出来ないので、このような組み合わせになってます。

eyes kitの回路図等は省略してますが、接続は以下です。

open cvラズパイとeyes kitラズパイの接続

顔を識別した時のラベルの値によって
0:ぎんてん、1:イケメン様、2:その他の人
ぎんてんなら”ブタ”と出力。イケメンなら”合格”と出力。その他は何もしないようにGPIOへの出力を追加しました。

以下、GPIO追加修正


import cv2 # OpenCVライブラリ。画像処理とコンピュータビジョンに使用されます。
import os  #ファイルやディレクトリ操作を行うための標準ライブラリ。
import time #時間操作を行うための標準ライブラリ。
import numpy as np #数値計算ライブラリ。
from picamera2 import Picamera2 #Raspberry Piのカメラモジュール用のライブラリ。
import RPi.GPIO as GPIO # GPIO制御ライブラリ

# トレーニング済みの顔識別モデルのパス
face_recognizer_directory = "/home/pi/Face Recognition/detected_faces"
face_recognizer_model = os.path.join(face_recognizer_directory, "face_recognizer_model.xml")

# モデルをロード
face_recognizer = cv2.face.LBPHFaceRecognizer_create(radius=1, neighbors=8, grid_x=8, grid_y=8)
face_recognizer.read(face_recognizer_model)

# Haar Cascade分類器をロード
face_detector = cv2.CascadeClassifier("/home/pi/Face Recognition/haarcascade_frontalface_default.xml")
cv2.startWindowThread()

# Piカメラの設定
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(main={"format": 'XRGB8888', "size": (640, 480)}))
picam2.start()

# GPIO設定
GPIO.setmode(GPIO.BCM)  # BCMモードを使用
GPIO.setup(25, GPIO.OUT)  # GPIO25を出力モードに設定 eyes
GPIO.setup(21, GPIO.OUT)  # GPIO21を状態ピンに設定

# 検出された顔画像の保存ディレクトリ
output_directory = "detected_faces"
os.makedirs(output_directory, exist_ok=True)

# ラベル名のマッピング
label_names = {0: "ginten", 1: "takasugi", 2: "other"}

def preprocess_face(face):
    """画像前処理:リサイズとヒストグラム均等化"""
    face = cv2.resize(face, (200, 200))
    face = cv2.equalizeHist(face)
    return face

try:
    while True:
        im = picam2.capture_array()
        grey = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
        faces = face_detector.detectMultiScale(grey, scaleFactor=1.1, minNeighbors=5, minSize=(50, 50))

        for (x, y, w, h) in faces:
            cv2.rectangle(im, (x, y), (x + w, y + h), (0, 255, 0), 2)

            # 顔部分を切り出し
            face_roi = grey[y:y+h, x:x+w]
            preprocessed_face = preprocess_face(face_roi)

            # 顔を識別
            label, confidence = face_recognizer.predict(preprocessed_face)

            # 認識結果と信頼度をログに表示
            print(f"Detected face: Label={label}, Confidence={confidence}")

            # 信頼度が一定の閾値を超える場合にのみ識別結果を表示
            if confidence < 70:  # 閾値を再調整
                label_text = label_names.get(label, "other")
                cv2.putText(im, f"{label_text} ({confidence:.2f})", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA)
            
                if label == 0: 
                    GPIO.output(25, GPIO.LOW) #eyes kitへLOW出力(制御)
                    GPIO.output(21, GPIO.LOW) #制御信号と合わせて、嫌な目を指示(ブタ)
                       
                elif label == 1:
                    GPIO.output(25, GPIO.LOW) #eyes kitへLOW出力(制御)
                    GPIO.output(21, GPIO.HIGH) #制御信号と合わせて、好いている目を指示(合格)
                    
            else:
                cv2.putText(im, "Unknown", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, cv2.LINE_AA)
                GPIO.output(25, GPIO.HIGH)  # 顔が識別されない場合はGPIO25をHIGHに設定

            # 顔画像を保存(デバッグ用)
            timestamp = int(time.time() * 1000)
            filename = os.path.join(output_directory, f"face_{timestamp}.jpg")
            cv2.imwrite(filename, preprocessed_face)  # 顔部分のみ保存

        cv2.imshow("Camera", im)
        if cv2.waitKey(1) == ord('q'):
            break

finally:
    #終了処理
    cv2.destroyAllWindows()
    GPIO.cleanup()  # GPIO設定をリセット
    


赤字の部分がGPIOの追加です。

顔の識別によってeyes kitへGPIO振り分け

特にこの箇所ですがGPIO21のHIGHかLOWがeyeskitのループターンに合っていれば、それに合わせて目の表示が変わります。



目の表情の切り替えの反応が遅いのはeyes kitのループのタイミングに識別信号が合わないと行けないためです。

完成動画とは別に、もう少し面白みのある構成にしてyoutube等でもあげてみたいですね。

open cvって何?っていうところから始めまりましたが、こうやって完成すると思った以上に面白く、顔認証と機械動作を組み合わせるとか、もっと応用が出来そうです。
試しにやってみたopen cvでしたが、どうせROS2でも利用することになりそうなので、
自分もここは一つ、きちんと本を読んで勉強することにしました。