Insight faceで顔検出アプリを作ってRaspberry Pi 5で動かそう

目標

Raspberry Pi 5にWebカメラを繋いでリアルタイムでキャプチャーし、映りこんだ顔の位置と特徴を算出、それをあらかじめ構築しておいた人名-顔の特徴量マップと照合することでどこに誰の顔があるのかを判別、オーバーレイ表示するアプリケーションを作ります。

環境

  • Raspberry Pi 5 8GB
  • OS: Debian 12
  • Python 3.11.2
  • Insight face 0.7.3

方法

1. Insight Faceと依存ライブラリのインストール

Open CV, Cython, OnnxRuntime, Insight Faceをインストールします。


以下のようにして依存ライブラリをインストールします。

ShellScript
pip install numpy && pip install cython && pip install onnxruntime && pip install insightface

2. 静止画から顔の位置と特徴検出を行う

FaceAnalysisクラスを用いて顔の特徴量とバウンディングボックスを得ます。得られた結果を入力画像の上に書きます。


顔認証を行うにはFaceAnalysisインスタンスを作りget(入力画像)を呼びます。以下のデモコードで顔認証を行い検出結果をオーバーレイしたものを保存します。

Python
import numpy as np
import os
import cv2
from insightface.app import FaceAnalysis
from glob import glob
from tqdm import tqdm
from collections import defaultdict
from PIL import Image
from IPython.display import display, Image


# 顔検出結果の描画
def draw_on(img, faces):
    dimg = img.copy()
    for i in range(len(faces)):
        face = faces[i]
        box = face.bbox.astype(int)
        color = (0, 0, 255)
        cv2.rectangle(dimg, (box[0], box[1]), (box[2], box[3]), color, 2)
        if face.kps is not None:
            kps = face.kps.astype(int)
            for l in range(kps.shape[0]):
                color = (0, 0, 255)
                if l == 0 or l == 3:  # 目の端点を強調
                    color = (0, 255, 0)
                cv2.circle(dimg, (kps[l][0], kps[l][1]), 1, color, 2)

    return dimg

# 入力画像の読み込み
input_img_path = 'input.png'
input_img = cv2.imread(input_img_path)

# 顔分析モデルの初期化
app = FaceAnalysis()
app.prepare(ctx_id=0, det_size=(640, 640))

# 顔検出実行
faces = app.get(np.array(input_img))
detect = draw_on(input_img, faces)

cv2.imwrite("output.png", detect)

実行例

入力画像: https://www.pexels.com/photo/a-group-of-young-men-standing-on-a-field-19231106/

3. 人名-顔写真データベースを用意して、静止画から顔検出する

各人ごとに特徴量を上と同じように計算しておく。入力画像に対して顔の位置と特徴検出を行いcos類似度で各人の特徴量と照合を行い、一番近い人の名前を入力画像の上に表示します。


検出された顔の特徴量と人名の紐づけを行うことで、写真内で検出されたそれぞれの顔が誰なのかを特定することができます。今回はこれを用いて入力画像にたいして検出された顔のバウンディングボックスと名前を表示するデモを作ります。

まず人名-顔写真データベースを作ります。顔写真をそれぞれの人名が付いたフォルダーに格納し、これを人名-顔写真データベースとして使います。

/人名1
  - 人名1の顔写真
  - 人名1の顔写真
  ...
/人名2
  - 人名2の顔写真
  ...
...

デモコードは以下になります。今回は人名-顔写真データベースをdata/に作ることを想定しています。

Python
# ライブラリのインポート
import numpy as np
import os
import cv2
from insightface.app import FaceAnalysis
from glob import glob
from tqdm import tqdm
from collections import defaultdict
from PIL import Image
from IPython.display import display, Image

# 類似度スコアの平均値計算
def get_averages(names, scores):
    d = defaultdict(list)
    for n, s in zip(names, scores):
        d[n].append(s)

    averages = {}
    for n, s in d.items():
        averages[n] = np.mean(s)
    return averages

# 顔認証判定
def judge_sim(known_embeddings, known_names, unknown_embeddings, threshold):
    pred_names = []
    for emb in unknown_embeddings:
        scores = np.dot(emb, known_embeddings.T)
        scores = np.clip(scores, 0., None)

        averages = get_averages(known_names, scores)
        pred = sorted(averages, key=lambda x: averages[x], reverse=True)[0]
        print(averages)
        score = averages[pred]

        if score > threshold:
            pred_names.append(pred)
        else:
            pred_names.append(None)
    
    return pred_names

# 検出結果の描画
def draw_on(img, faces, name):
    dimg = img.copy()
    for i in range(len(faces)):
        face = faces[i]
        box = face.bbox.astype(int)
        color = (0, 0, 255)
        cv2.rectangle(dimg, (box[0], box[1]), (box[2], box[3]), color, 2)
        if face.kps is not None:
            kps = face.kps.astype(int)
            for l in range(kps.shape[0]):
                color = (0, 0, 255)
                if l == 0 or l == 3:
                    color = (0, 255, 0)
                cv2.circle(dimg, (kps[l][0], kps[l][1]), 1, color, 2)
        cv2.putText(dimg, name[i], (box[0]-1, box[1]-4),cv2.FONT_HERSHEY_COMPLEX,0.5,(0,255,0),1)

    return dimg

# データの初期化
known_names = []
known_embeddings = []
unknown_embeddings = []

# 登録フォルダの取得
known_face_folders = os.listdir(f'./data')

# 認証対象画像の読み込み
input_path = 'input.png'
input_img = cv2.imread(input_path)

# 顔分析モデルの準備
app_pre = FaceAnalysis()
app_pre.prepare(ctx_id=0, det_size=(640, 640))

app = FaceAnalysis()
app.prepare(ctx_id=0, det_size=(640, 640))

# 登録画像の特徴量抽出
for known_face_folder in tqdm(known_face_folders):
    folder_embeddings, folder_names = [], []

    img_paths = glob(f'data/{known_face_folder}/*')
    
    for img_path in img_paths:
        img = cv2.imread(img_path)
        if img is None: continue

        faces = app_pre.get(np.array(img))
        if len(faces) == 0 : continue
        folder_embeddings.append(faces[0].embedding)
        folder_names.append(known_face_folder)

        if len(known_embeddings) == 10: break
    
    folder_embeddings = np.stack(folder_embeddings, axis=0)
    known_embeddings.append(folder_embeddings)
    known_names += folder_names
known_embeddings = np.concatenate(known_embeddings, axis=0)

# 顔認証の実行
faces = app.get(np.array(input_img))

for i in range(len(faces)):
    unknown_embeddings.append(faces[i].embedding)

pred_names = judge_sim(known_embeddings, known_names, unknown_embeddings, 90)

detect = draw_on(input_img, faces, pred_names)

cv2.imwrite("output.png", detect)

良いテスト画像が見つからなかったので実行結果は割愛させていただきます。権利フリーで複数人映っていて、映っている人の顔写真も別にある、みたいな素材が合ったら教えてください。

4. 動画から顔検出する

毎フレームで上と同じように検出を行います。入力をWebカメラにすればリアルタイム顔検出もできます。


デモコードは以下になります。

Python
import numpy as np
import os
import cv2
from insightface.app import FaceAnalysis
from glob import glob
from tqdm import tqdm
from collections import defaultdict
from PIL import Image
from IPython.display import display, Image

# 類似度スコアの平均値計算
def get_averages(names, scores):
    d = defaultdict(list)
    for n, s in zip(names, scores):
        d[n].append(s)

    averages = {}
    for n, s in d.items():
        averages[n] = np.mean(s)
    return averages

# 顔認証判定
def judge_sim(known_embeddings, known_names, unknown_embeddings, threshold):
    pred_names = []
    for emb in unknown_embeddings:
        scores = np.dot(emb, known_embeddings.T)
        scores = np.clip(scores, 0., None)

        averages = get_averages(known_names, scores)

        pred = sorted(averages, key=lambda x: averages[x], reverse=True)[0]
        score = averages[pred]

        if score > threshold:
            pred_names.append(pred)
        else:
            pred_names.append(None)
    
    return pred_names

# 検出結果の描画
def draw_on(img, faces, name):
    dimg = img.copy()
    for i in range(len(faces)):
        face = faces[i]
        box = face.bbox.astype(int)
        color = (0, 0, 255)
        cv2.rectangle(dimg, (box[0], box[1]), (box[2], box[3]), color, 2)
        if face.kps is not None:
            kps = face.kps.astype(int)
            for l in range(kps.shape[0]):
                color = (0, 0, 255)
                if l == 0 or l == 3:
                    color = (0, 0, 255)
                cv2.circle(dimg, (kps[l][0], kps[l][1]), 1, color, 2)
        display_name = name[i] if name[i] is not None else 'Unknown'
        cv2.putText(dimg, str(display_name), (box[0]-1, box[1]-4),cv2.FONT_HERSHEY_COMPLEX,0.5,(255,255,0),1)

    return dimg

# データの初期化
known_names = []
known_embeddings = []
unknown_embeddings = []

known_face_folders = os.listdir(f'data')

# 顔分析モデルの準備
app_pre = FaceAnalysis()
app_pre.prepare(ctx_id=0, det_size=(128, 128))

app = FaceAnalysis()
app.prepare(ctx_id=0, det_size=(128, 128))

# 登録画像の特徴量抽出

for player in tqdm(known_face_folders):
    player_embeddings, player_names = [], []

    img_paths = glob(f'data/{player}/*')
    
    for img_path in img_paths:
        img = cv2.imread(img_path)
        if img is None: continue

        faces = app_pre.get(np.array(img))
        if len(faces) == 0 : continue
        player_embeddings.append(faces[0].embedding)
        player_names.append(player)

        if len(known_embeddings) == 10: break
    
    player_embeddings = np.stack(player_embeddings, axis=0)
    known_embeddings.append(player_embeddings)
    known_names += player_names
known_embeddings = np.concatenate(known_embeddings, axis=0)

# 動画処理の設定
video_path = 'hello.mp4'
if not os.path.exists(video_path):
    raise FileNotFoundError(f"Video file not found: {video_path}")

cap = cv2.VideoCapture(video_path)

# 処理設定
video_scale = 0.125  # 処理高速化のためのスケール
output_path = 'detection_result.mp4'
out = None

# フレーム処理制限
fps = cap.get(cv2.CAP_PROP_FPS)
if fps is None or fps <= 0:
    fps = 30.0
max_seconds = 10.0  # 処理時間制限
max_frames = int(fps * max_seconds)
frame_idx = 0

# 動画フレーム処理
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    if frame_idx >= max_frames:
        break

    unknown_embeddings = []

    # フレームリサイズ(高速化)
    if video_scale != 1.0:
        small_frame = cv2.resize(frame, (0, 0), fx=video_scale, fy=video_scale, interpolation=cv2.INTER_LINEAR)
    else:
        small_frame = frame

    # 顔検出・認証
    faces = app.get(np.array(small_frame))

    for i in range(len(faces)):
        unknown_embeddings.append(faces[i].embedding)

    pred_names = judge_sim(known_embeddings, known_names, unknown_embeddings, 90)

    detect = draw_on(small_frame, faces, pred_names)

    # 出力動画設定(初回のみ)
    if out is None:
        h, w = detect.shape[:2]
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        out = cv2.VideoWriter(output_path, fourcc, float(fps), (w, h))

    if out is not None:
        out.write(detect)

    cv2.imshow('face detection', detect)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

    frame_idx += 1

# リソース解放
cap.release()
if out is not None:
    out.release()
cv2.destroyAllWindows()

実行例

入力動画: https://www.pexels.com/video/group-of-people-taking-selfie-on-a-smart-phone-3402763/


まとめ

Raspberry 5で動画から顔認証をすることができました!!簡単なコードでできるのでぜひ試してみてください!!

参考文献

Nuco 顔認証システムの作り方 https://nuco.co.jp/blog/article/TbUHF1lK

コメントする