今、話題の人工知能(AI)などで人気のPython。初心者に優しいとか言われていますが、全然優しくない! という事を、つらつら、愚痴っていきます

387.顔検出、顔認識(4)

»

初回:2024/10/23

 OpenCV を使用した、顔検出と顔認識についての最終回にします。

P子「最終回にしますって、どういう意味?」※1

 前回の予定では、顔検出、特徴値の抽出と比較、顔登録などの個別の処理を取り上げようと思っていましたが、一気にソースコードを載せるだけで済ませようと思いました。

P子「要するに、個別説明が邪魔くさくなったのね」

 そんなことは無いようなことは無い感じもしないでも無いかもしれないことも無い気がしないでも無い様なことも無いかもしれません。

P子「そのフレーズも久しぶりね」

1.顔検出、特徴抽出、比較、顔登録のソース

 以下にソースコードを示します。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 顔認識処理を行います。
#
# https://qiita.com/UnaNancyOwen/items/8c65a976b0da2a558f06
# OpenCVの新しい顔認識を試してみる
#
#   YuNet:opencv_zoo/yunet
#   https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet
#       face_detection_yunet_2023mar.onnx
#
#   https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface
#       face_recognition_sface_2021dec.onnx

import sys                                  # help 時の sys.exit() 呼び出し
import os                                   # 認識したい顔の画像を読み込み
import glob                                 # 認識したい顔の画像を読み込み
import cv2
import numpy as np                          # 特徴量同士の距離で判定で使用


#############################################################################################
COSINE_THRESHOLD = 0.363
GREEN = (000, 255, 000)     # 緑(BGR)
RED = (000, 000, 255)       # 赤(BGR)


#############################################################################################
class FaceFilter:
    """
    openCV顔検出のサンプルクラス
    """
    # 1.ONNXモデルファイル
    YUNET_PATH = 'face_detection_yunet_2023mar.onnx'
    SFACE_PATH = 'face_recognition_sface_2021dec.onnx'
    FACE_DIR = './images/'                          # 顔認証用のマスタ保存領域

    #############################################################################################
    def __init__(self):
        """
        コンストラクタ
        """
        self.save_flag = False                      # 顔写真を作成する場合、Trueにする

        # 2.openCV顔検出(yunet), 顔識別(sface)
        self.face_detector = cv2.FaceDetectorYN.create(FaceFilter.YUNET_PATH, "", (320, 320), 0.6, 0.3)
        self.face_recognizer = cv2.FaceRecognizerSF.create(FaceFilter.SFACE_PATH, "")

        # 画像を読み込み、顔の特徴値を取得する
        self.dictionary = []

        # 3.認識したい顔の画像を読み込み、顔の特徴値を取得する。
        file_list = glob.glob(os.path.join(FaceFilter.FACE_DIR, '*.jpg'))
        for img_path in file_list:
            fname = os.path.basename(img_path)      # ファイル名(xxxx.jpg)
            user_id = os.path.splitext(fname)[0]    # 拡張子なし名称(xxxx)

            # 4.jpg画像と、npy特徴点ファイルの更新日時を比較する
            feature_file = FaceFilter.FACE_DIR + user_id + '.npy'

            # 5.特徴点ファイルが存在しており、かつ、特徴点 が新しい(画像が更新されていない)
            if os.path.exists(feature_file) and os.path.getmtime(img_path) < os.path.getmtime(feature_file):
                feature = np.load(feature_file)
                self.dictionary.append((user_id, feature))
            else:
                # 6.画像から顔検出を行う
                image = cv2.imread(img_path)
                height, width, _ = image.shape
                self.face_detector.setInputSize((width, height))
                _, faces = self.face_detector.detect(image)

                if faces is not None and len(faces) > 0:
                    # 7.顔検出結果から、特徴点抽出を行う
                    aligned_face = self.face_recognizer.alignCrop(image, faces[0])
                    feature = self.face_recognizer.feature(aligned_face)

                    self.dictionary.append((user_id, feature))

                    # 8.特徴値を保存する
                    save_name = FaceFilter.FACE_DIR + user_id   # 拡張子の '.npy' は付けない
                    np.save(save_name, feature)
                    print(user_id)

    #############################################################################################
    # filterクラスとしては必須メソッド
    def filter(self, image):
        """
        openCvの image を入力して、加工処理を行い、image を返します。

        :param  image: 入力イメージ
        :return:    加工処理が行われたイメージ
        """
        try:
            # 9.入力サイズを指定する
            height, width, _ = image.shape
            self.face_detector.setInputSize((width, height))

            # 10.顔を検出する
            _, faces = self.face_detector.detect(image)
            if faces is None:                                   # 未検出時は、None になる:
                return image

            # 11.1フレームで検出した顔分ループする
            for face in faces:
                # 12.顔を切り抜き特徴を抽出する
                aligned_face = self.face_recognizer.alignCrop(image, face)
                feature = self.face_recognizer.feature(aligned_face)

                # 13.辞書とマッチングする
                result, user_score = self.match(feature)        # 結果(True/False), (ユーザーID, スコア) のタプル

                # 14.顔のバウンディングボックスを描画する
                box = list(map(int, face[:4]))
                color = GREEN if result else RED
                cv2.rectangle(image, box, color, 2, cv2.LINE_AA)

                # 15.認識の結果を描画する
                text = f'{user_score[0]} ({user_score[1]:.2f})'
                position = (box[0], box[1] - 10)
                font = cv2.FONT_HERSHEY_SIMPLEX
                scale = 0.6
                cv2.putText(image, text, position, font, scale, color, 2, cv2.LINE_AA)

                # 16.新しい認証用顔写真を取得する(特徴値ファイルは作成しません)
                if self.save_flag:
                    user_id = user_score[0]
                    save_file = FaceFilter.FACE_DIR + user_id + '.jpg'
                    cv2.imwrite(save_file, aligned_face)

                    self.dictionary.append((user_id, feature))

                    print(f'{user_id} 登録しました')
                    self.save_flag = False

            return image

        except Exception as ex:
            print('process 実行中に例外が発生しました。')
            raise ex                                            # 例外を投げる。

    #############################################################################################
    def match(self, feature1):
        """
        特徴を辞書と比較してマッチしたユーザーとスコアを返す関数

        :param  feature1: 比較用の特徴点データ
        :return: 結果(True/False), (ユーザーID, スコア) のタプル
        """
        match_list = []                         # (ユーザーID, スコア) のタプルのリスト
        # 17.特徴値情報を比較していく
        for element in self.dictionary:
            user_id, feature2 = element
            # 18.特徴値情報を比較してスコアを算出する
            score = self.face_recognizer.match(feature1, feature2, cv2.FaceRecognizerSF_FR_COSINE)
            # 19.スコアのしきい値で判定して、結果をリストに追加してく
            if score > COSINE_THRESHOLD:
                match_list.append((user_id, score))

        # 20.一致したリストにデータがあれば、何らかの検出結果が含まれている
        if len(match_list) > 0:
            # 21.最もスコアの高い検出結果を返します
            best_score = np.argmax(match_list)
            if match_list[best_score]:
                return True, match_list[best_score] # (ユーザーID, スコア) のタプル

        # 22.一致したデータが存在しなかった場合
        return False, ('Unknown', 0.0)              # 対象無し

################################################
# 動作確認用のテストプログラム
# $ python3 ./face_filter.py [カメラのデバイス番号=0]
################################################
if __name__ == '__main__':
    args = sys.argv
    fname = args[0]                                 # プログラム名
    devno = 0 if len(args) < 2 else int(args[1])    # カメラのデバイス番号

    face = FaceFilter()

    cap = cv2.VideoCapture(devno)                   # カメラ
    try:
        cv2.namedWindow(fname, cv2.WINDOW_NORMAL)   # ウィンドウの名前
        cv2.moveWindow(fname, 100, 100)             # ウィンドウの初期表示位置

        while True:
            ret, img = cap.read()           # 戻り値は、読取成否と画像
            img = face.filter(img)          # filter 機能がある場合
            cv2.imshow(fname, img)          # 画面に画像を表示

            k = cv2.waitKey(10)             # キーボード入力待ち(10ms)
            if k == ord('q') or k == 27:    # q または、ESC で終了
                break
            elif k == ord('w'):             # w で顔情報の書き込み
                face.save_flag = True

    except KeyboardInterrupt:               # Ctl+Cが押されたらループを終了
        print("\nCtl+C Stop")
    finally:
        if cap is not None:
            cap.release()                   # 動画やカメラデバイスを閉じる
        cv2.destroyAllWindows()             # すべてのウィンドウを閉じる
        print("終了")

2.まとめ

 何が言いたいかというと、こんなに簡単に顔認識ができるというのがびっくりです。ラズパイでもそれなりの速度で判定できます。

P子「手持ちのラズパイって、4B よね」

 さらに早くなったという、Raspberry Pi 5 なら、ほぼ実用的に使えるレベルだと思います。すごい事です。

P子「本当に、すごい時代よね」

 ほな、さいなら

======= <<注釈>>=======

※1 P子「最終回にしますって、どういう意味?」
 P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。

Comment(0)

コメント

コメントを投稿する