2024-05月実施。
「Adafruit Eyes Kitを使った制作の中で、ふと『自分の顔を近づけたら、目の表示が変わるようにしたら面白いのでは?』と思いつきました。
しかし、OpenCVを利用すれば可能と知ったものの、初心者だった私は壁にぶつかります。あらゆる参考サイトを見ても、情報が古いのか自分のラズパイ4では顔認識・顔識別が上手くいきませんでした……。
それでも数か月にわたる試行錯誤の末、ついに2024年の現在で顔識別まで実現可能な解決策を見つけました!自分の顔と他者の顔を識別して目の反応を変える――イケメンには喜び、自分には冷たい反応を向ける(笑)。OpenCVを利用したそんなちょっとユーモラスな製作です」
今回はeyes kitの利用が目的な為、もう一枚のラズパイ4と音声発生用にラズパイピコを利用していますが、顔認証してモーター等を動作させることも可能となりました。
1opencvで顔認証
使用したハード構成
raspberry pi4 4B Camera Module3
利用環境
Bullseye 64ビット (参照サイトでは32ビット利用とはあるのだが・・)
とにかくいろんなサイトを試しまくって分かってきたのが、おそらくラズパイでの利用は構成が重要ではないかと?。osのバージョンと32ビット or 64ビット、
カメラもcamera2は持ってませんが、プログラムがcamera3では動かないケースもあるような気がします。
したがってこのサイトの利用も上記の構成以外ではほぼ動きません。
私が良く分かって無いせいだとは思いますが実際このサイトを利用してもこういう結果となり、最終的にBullseye
64ビットに落ち着きました。
Bookworm | 仮想環境無し Numpyがインストールエラー 仮想環境有り OpenCV Pythonがインストールエラー |
Bullseye 32bit | CV2 インポートエラーとなった |
Buster 32bit | pi3カメラを認識してくれなかった。 |
今回、基本として利用させて頂いたのは以下のサイトです。
Face Detection on Raspberry Pi 4 using OpenCV and Camera Module
パーツメーカーのCytron様がデモして頂いてました。
こちらに関してはリンク案内みたいなものなので特に特筆すべきことも無いのですが・・動画で説明されてますので注視して以下を抜き出しました。
(字幕もあるが動画と違って字幕には抜けがある
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__ | sudo apt-get update
尚、64ビットでインストールだと途中
libjasper-dev libatlas-base-dev libhdf5-dev libhdf5-serial-dev -y | sudo apt-get install libqt5gui5 libqt5test5 python3-sip python3-pyqt5
のときと
pip3 install -U numpy
のインストール時にワーニングが出ますが、顔認証の利用には問題ありません。
32ビットインストールだと出ないんですが、逆に最後のimport
cv2がエラーで動かないんですよね・・・デモのバージョン不明なのですが、一体どのバージョンでやってるんでしょう?。
あとは、カスケードファイルはサイトのサンプルプログラムの中にある
より、ダウンロードして、サンプルプログラム内の指定フォルダに配置。
/home/pi/Face Recognition/haarcascade_frontalface_default.xml |
これで基本的な顔認識までは出来ます。
プログラムはCytron様のサイトをご確認下さい。
このサイト以外でも顔認識や、画像変換までなら出来るサイトはありましたが顔識別になると私の力ではどうしてもうまくいきませんでした。
しかしこれをベースにすると顔識別までもなんとか出来ました。
また、カメラの利用に関して
sudo raspi-configのレガシーカメラはオフのままで設定不用。
良くみかけるconfigの変更記述”dtoverlay=imx708”等もしなくてもopencvは動いてます。
2 顔識別
上記により顔認証は取り合えずできましたが、参照サイトは顔認識まで。
あくまでも目標は識別画像によってGPIOからのリアクションを変えること。
知識ゼロからのチカラ押しだけでやってきましたが、さすがに何度も試すと大体理屈はわかってきました。
顔認証の流れ
顔識別の流れ
独学における認識なので間違ってたらご指摘下さい。
顔認証をベースに、右赤枠の個人を特定する自分の写真と他者の写真を利用した顔識別モデルを作成。それを識別判断するプログラムに変更です。
/home/pi/Face Recognition/detected_faces
トレーニングデータ用フォルダを作成し、自分写真(フォルダ0)と他者写真(フォルダ1)を作成しました。
libcamera-still -o test.jpg
トレーニング画像として自分自身を30枚撮影して、フォルダ0に保存。
pi3カメラで撮影するとワイドになり、無駄な情報が含まれてしまうのでPCでトリミングしてまたラズパイに戻してます。
もう一つは他者
工作の都合上、映画「賭ケグルイ」出演であるイケメン俳優の高杉真宙様の写真をネットからフォルダ1に保存しました。
下のプログラムを利用して、写真から顔識別モデルを作成しました。
/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/FaceRecognition/detected_faces/face_recognizer_model.xml”) |
ラベルという記述がありますが、自分と他者を認識させた時に認識の違いをはっきりさせたい為、
名前を表示させるようにしたいとラベルをつけました。
1:(0 自分) 2:(1 イケメン俳優) other:(2 どちらでも無い /unkown)
上記を元にしたメインプログラムが以下です。
cv2 import#OpenCVライブラリ。画像処理とコンピュータビジョンに使用されます。 import os #ファイルやディレクトリ操作を行うための標準ライブラリ。 import time #時間操作を行うための標準ライブラリ。 import numpy as np #数値計算ライブラリ。 from picamera2 import Picamera2 #RaspberryPiのカメラモジュール用のライブラリ。 # — トレーニング済みの顔認識モデルとHaar Cascade分類器の読み込み — # トレーニング済みの顔識別モデルのパス face_recognizer_directory = “/home/pi/FaceRecognition/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/FaceRecognition/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カメラを除き込んで、高杉さんはプリントアウトをカメラに写して、又別の人はスマホの画像を利用しました。
部屋の明るさや、顔の角度によって認識精度が大分変りますね。
プリントアウトでは僅かに傾いていたら(トレーニングデータとして利用したテスト画像と全く同じはずなのに)認識しないくらいです。
unkownは顔がモデルに登録されていないか、認識精度が低いために表示されるものですが、そもそもotherに該当するフォルダは作っていないので、unkownで良いのかな。
自分とそれ以外の他者との区別は出来たので、まずは良しとします。
3 adafruit eyes kitとの組み合わせ
最初に掲げた通り、今回opencvというのをやってみたいと思ったのは
adafuruit eyes kit利用の工作の中で、顔認証と組み合わせたら面白いかな?と思ったから。
なのでいよいよ組み合わせて見ます。
同じ構成で利用する方もいないと思うので、参考という形でeyes kitやPICOのプログラムは省略します。
eyes kitの中で瞳孔を変更するsvgファイルの動作を変更する為、ちょっと工夫しています。
もし、eyes kitをお持ちの方でどうやったの?という方が居ましたらお気軽にご連絡下さい。
概略は、
顔を識別する(opencv ラズパイ)→目の表情が変わる(eyes ラズパイ)→サウンド(PICO)(音声)を出す。
顔を識別した後GPIOに出力しているだけにも拘わらず何故こんな複雑な構成になっているかというと。
以前も書きましたeyes kitの問題点ですが、
〇 eyes kitがHDMI出力のコピーを液晶に表示させている為、サウンドがジャックから出力されない(強制的にHDMI出力のみになる)。
〇 eyeskitが32bitでしか動かない
どうしてもこの辺クリア出来ないので、このような組み合わせになってます。
eyes kitの回路図等は省略してますが、接続は以下です。
顔を識別した時のラベルの値によって
0:ぎんてん、1:イケメン様、2:その他の人
ぎんてんなら”ブタ”と出力。イケメンなら”合格”と出力。その他は何もしないようにGPIOへの出力を追加しました。
以下は今回の製作におけるeyes kitとの接続によるGPIO修正ですが、上記の組み合わせを変更すればモーター接続等も利用した製作も可能です。
cv2 import# 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設定をリセット |
顔の識別によってeyes kitへGPIO振り分けしたのが以下の箇所です
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) #制御信号と合わせて、好いている目を指示(合格)
この箇所によってGPIO21のHIGHかLOWがeyeskitのループターンに合っていれば、それに合わせて目の表示が変わります。
目の表情の切り替えの反応が遅いのはeyes kitの表示ループのタイミングに顔認証のタイミングが合わない時があるためです。ここはもっと工夫したいとおもいます。
完成動画とは別に、もう少し面白みのある構成にしてyoutube等でもあげてみたいですね。
open cvって何?っていうところから始めまりましたが、こうやって完成すると思った以上に面白く、顔認証と機械動作を組み合わせるとか、もっと応用が出来そうです。
試しにやってみたOpen CVでしたが、今後も必要となりそうなので自分もここは一つ、きちんと本を読んで勉強することにしました。
コメント