InsightFaceとSCRFDによる顔検出・年齢・性別推定

ツール利用ガイド

1. このプログラムの利用シーン

動画ファイルやウェブカメラの映像から顔を検出し、年齢と性別を推定するためのツールである。セキュリティシステム、来店客の年齢層分析、動画コンテンツの自動タグ付け、研究用データ収集などの用途に活用できる。

2. 主な機能

3. 基本的な使い方

  1. プログラムの起動と入力選択:

    プログラムを実行し、0(動画ファイル)、1(ウェブカメラ)、2(サンプル動画)から入力ソースを選択する。

  2. 処理の実行:

    選択した入力ソースから映像が読み込まれ、顔検出と年齢・性別推定が自動実行される。検出結果は元映像に重ね合わせて表示される。

  3. 終了方法:

    映像表示ウィンドウでqキーを押すとプログラムが終了し、結果がresult.txtファイルに保存される。

4. 便利な機能

Python開発環境,ライブラリ類

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なパッケージのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。


pip install insightface opencv-python onnxruntime numpy pillow

GPU使用の場合,追加で次を実行


pip install onnxruntime-gpu

InsightFaceとSCRFDによる顔検出・年齢・性別推定プログラム

概要

このプログラムは、動画やカメラ映像から顔を検出し、年齢と性別を推定する。SCRFDアルゴリズムによる顔検出技術とInsightFaceライブラリを使用し、検出された顔に対してバウンディングボックス、推定年齢、性別、5点キーポイントを重ね合わせて表示する。

主要技術

SCRFD (Sample and Computation Redistribution for Efficient Face Detection)

2022年に発表された顔検出アルゴリズムである[1]。サンプル再分配(SR)と計算再分配(CR)という2つの手法により、計算量を抑制しながら顔検出を実現する。サンプル再分配は訓練データの統計に基づいて最も必要な段階のサンプルを増強し、計算再分配はモデルのバックボーン、ネック、ヘッド間の計算量を再配分する。

InsightFaceライブラリ

2D・3D顔解析のためのオープンソースライブラリ[3]。ArcFace損失関数[2]を基盤とした顔認識技術と複数の事前学習済みモデルを提供する。buffalo_lモデルは顔検出、年齢推定、性別推定を統合したモデルパッケージである。

技術的特徴

実装の特色

参考文献

[1] Guo, J., Deng, J., Lattas, A., & Zafeiriou, S. (2022). Sample and Computation Redistribution for Efficient Face Detection. ICLR 2022. https://arxiv.org/abs/2105.04714

[2] Deng, J., Guo, J., Xue, N., & Zafeiriou, S. (2019). ArcFace: Additive Angular Margin Loss for Deep Face Recognition. CVPR 2019. https://arxiv.org/abs/1801.07698

[3] DeepInsight. (2025). InsightFace: 2D&3D Deep Face Analysis Library. https://github.com/deepinsight/insightface

ソースコード


# プログラム名: InsightFaceとSCRFDによる顔検出・年齢・性別推定プログラム
# 特徴技術名: SCRFD (Sample and Computation Redistribution for Efficient Face Detection) + 年齢・性別推定
# 出典: Guo, J., et al. (2022). Sample and Computation Redistribution for Efficient Face Detection. ICLR 2022. https://github.com/deepinsight/insightface
# 特徴機能: サンプルと計算量の再分配による顔検出と年齢・性別推定。リアルタイムで顔を検出し、推定年齢と性別を表示
# 学習済みモデル: buffalo_l - InsightFaceの高精度モデルパッケージ。顔検出、年齢推定、性別推定機能を含む統合モデル。初回実行時に自動ダウンロード(~1GB)
# 方式設計:
#   - 関連利用技術:
#     - InsightFace: SCRFDアルゴリズムによる顔検出、年齢・性別推定
#     - OpenCV: 画像処理、カメラ制御、描画処理、CLAHEによるコントラスト改善
#     - ONNX Runtime: モデル推論エンジン、GPU/CPU/DirectMLの実行プロバイダ
#   - 入力と出力: 入力は「0:動画ファイル,1:カメラ,2:サンプル動画」から選択。出力は元画像に年齢推定結果を重ね合わせて表示
#   - 処理手順: 1.CLAHEで入力画像を加工、2.InsightFace(SCRFD)で加工画像から顔検出、3.年齢・性別推定、4.信頼度閾値によるフィルタリング、5.5点キーポイント抽出、6.元の画像に検出結果を表示
#   - 前処理、後処理: 前処理はCLAHE適用。後処理は信頼度閾値によるフィルタリングと年齢推定結果の表示
#   - 追加処理: 年齢推定、性別推定、5点キーポイント(左目、右目、鼻、左口角、右口角)の座標表示。Windows環境でのDirectML対応
#   - 調整を必要とする設定値: SCORE_TH(検出信頼度閾値、デフォルト0.3 for buffalo_l)
# その他の重要事項: Windows環境専用。初回実行時は学習済みモデルのダウンロードに時間がかかる。GPU/DirectML/CPUを状況に応じて選択
# 前準備:
#   - pip install insightface opencv-python onnxruntime numpy pillow
#   - GPU使用の場合: pip install onnxruntime-gpu
#   - DirectML利用の場合(Windows): pip install onnxruntime-directml

import cv2
import tkinter as tk
from tkinter import filedialog
import os
import numpy as np
from insightface.app import FaceAnalysis
import warnings
import time
import urllib.request
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime

# InsightFace関連の将来警告・ユーザ警告を抑制(他の警告は抑制しない)
warnings.filterwarnings('ignore', category=FutureWarning, module='insightface')
warnings.filterwarnings('ignore', category=UserWarning, module='insightface')

# ===== モデル設定(buffalo_l固定) =====
MODEL_NAME = 'buffalo_l'
MODEL_DESCRIPTION = '高精度モデル(顔検出・年齢推定・性別推定)'
MODEL_SIZE = '約1GB'
SCORE_TH = 0.3  # buffalo_l用の信頼度閾値
DET_SIZE = (640, 640)

# ===== 設定・定数管理 =====
SAMPLE_VIDEO_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
SAMPLE_VIDEO_NAME = 'vtest.avi'
RESULT_FILE = 'result.txt'

# カメラ設定
WINDOW_WIDTH = 1280  # カメラ解像度幅
WINDOW_HEIGHT = 720  # カメラ解像度高さ
FPS = 30  # フレームレート

# 顔とキーポイントの表示色(BGR形式)
FACE_COLOR = (0, 255, 0)  # バウンディングボックス用
KPS_COLOR = (0, 0, 255)  # キーポイント用
AGE_COLOR = (255, 0, 255)  # 年齢表示用
GENDER_COLOR = (0, 255, 255)  # 性別表示用
KPS_RADIUS = 3  # キーポイントの円の半径

# キーポイント表示名(SCRFDの並びに整合)
KPS_NAMES = ['左目', '右目', '鼻', '左口角', '右口角']

# フォント設定(日本語表示用)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
FONT_SIZE_SMALL = 16

# グローバル変数の初期化
frame_count = 0
results_log = []

# CLAHEオブジェクトを一度だけ定義
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))

def detect_gpu():
    """ONNX Runtimeの実行プロバイダからGPU/DirectML/CPUの使用可否を判定し、ctx_idとprovidersを返す"""
    import onnxruntime as ort
    providers = ort.get_available_providers()

    if 'CUDAExecutionProvider' in providers:
        print('GPU(CUDA)検出 - GPU使用モードで実行')
        return 0, ['CUDAExecutionProvider', 'CPUExecutionProvider']

    if 'DmlExecutionProvider' in providers:
        print('GPU(DirectML)検出 - DirectML使用モードで実行')
        return 0, ['DmlExecutionProvider', 'CPUExecutionProvider']

    print('GPU未検出 - CPU使用モードで実行')
    return -1, ['CPUExecutionProvider']


def get_device_label(providers):
    """表示用のデバイス名を返す"""
    if 'CUDAExecutionProvider' in providers:
        return 'CUDA'
    if 'DmlExecutionProvider' in providers:
        return 'DirectML'
    return 'CPU'


def check_font_availability():
    """フォントファイルの存在を確認する"""
    if not os.path.exists(FONT_PATH):
        print(f'警告: 日本語フォントが見つかりません ({FONT_PATH})')
        print('日本語表示が正しく行われない可能性があります')
        return False
    return True


def download_insightface_model():
    """buffalo_lモデルの存在を確認し、未ダウンロード時にはダウンロードが必要であることを通知する"""
    model_dir = os.path.join(os.path.expanduser('~'), '.insightface', 'models', MODEL_NAME)
    if not os.path.exists(model_dir):
        print(f'SCRFD {MODEL_NAME}モデルの初回ダウンロードを行います...')
        print(f'モデル: {MODEL_NAME}({MODEL_SIZE})')
        print('注意: 初回はネットワーク環境により数分かかる場合があります')
    else:
        print(f'SCRFD {MODEL_NAME}モデルが既に存在します')


def extract_face_info(face, face_id):
    """検出顔から座標・キーポイント・信頼度・年齢・性別を抽出して辞書化する"""
    bbox = face.bbox.astype(int)
    face_info = {
        'id': face_id,
        'box': (bbox[0], bbox[1], bbox[2], bbox[3]),
        'keypoints': []
    }

    # キーポイント情報
    if hasattr(face, 'kps') and face.kps is not None:
        for i, kp in enumerate(face.kps):
            if i < len(KPS_NAMES):
                face_info['keypoints'].append({
                    'name': KPS_NAMES[i],
                    'x': int(kp[0]),
                    'y': int(kp[1])
                })

    # 検出信頼度
    if hasattr(face, 'det_score'):
        face_info['detection_conf'] = float(face.det_score)

    # 年齢推定
    if hasattr(face, 'age'):
        face_info['age'] = int(face.age)

    # 性別推定(M=男性、F=女性)
    if hasattr(face, 'sex'):
        face_info['gender'] = '男性' if face.sex == 'M' else '女性'

    return face_info


def draw_japanese_text(img, text, position, font_size, color):
    """日本語テキストをPillowで画像に描画する"""
    try:
        font = ImageFont.truetype(FONT_PATH, font_size)
        img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)
        draw.text(position, text, font=font, fill=color[::-1])  # BGRをRGB順に
        return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
    except Exception:
        # フォントエラー時はOpenCVの英数字フォントで代替
        cv2.putText(img, text.encode('ascii', 'ignore').decode('ascii'), position,
                   cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
        return img


def draw_face_label(img, text, position, font_size, color):
    """顔情報ラベルを描画する共通関数"""
    return draw_japanese_text(img, text, position, font_size, color)


def video_frame_processing(frame):
    """フレーム単位の処理。顔検出、年齢推定、描画を行う"""
    global frame_count
    current_time = time.time()
    frame_count += 1
    faces_data = []

    # ③結果はこちらの元映像のコピーに描画する
    display_frame = frame.copy()

    # ①CLAHE用に画像を加工
    # 1. BGRからYUVカラー空間に変換
    yuv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
    # 2. 明度チャンネル(Y)にCLAHEを適用
    yuv_img[:,:,0] = clahe.apply(yuv_img[:,:,0])
    # 3. BGRカラー空間に再変換
    clahe_frame = cv2.cvtColor(yuv_img, cv2.COLOR_YUV2BGR)

    # ②AIモデルがその加工画像を処理
    faces = app.get(clahe_frame)

    # 信頼度でフィルタリング
    faces = [face for face in faces if face.det_score >= SCORE_TH]
    # 各顔の情報抽出
    for i, face in enumerate(faces):
        face_info = extract_face_info(face, i + 1)
        faces_data.append(face_info)

    # 画像への描画 (③結果は元映像のコピーである display_frame に描画)
    for face_info in faces_data:
        x1, y1, x2, y2 = face_info['box']

        # バウンディングボックス描画
        cv2.rectangle(display_frame, (x1, y1), (x2, y2), FACE_COLOR, 2)

        # キーポイント描画
        for kp in face_info['keypoints']:
            cv2.circle(display_frame, (kp['x'], kp['y']), KPS_RADIUS, KPS_COLOR, -1)

        # 顔ID
        label1 = f"顔 {face_info['id']}"
        display_frame = draw_face_label(display_frame, label1, (x1, y1 - 10), FONT_SIZE, FACE_COLOR)

        # 検出信頼度
        if 'detection_conf' in face_info:
            label2 = f"信頼度:{face_info['detection_conf']:.2f}"
            display_frame = draw_face_label(display_frame, label2, (x1, y2 + 15), FONT_SIZE_SMALL, (255, 255, 255))

        # 年齢推定結果
        if 'age' in face_info:
            age_label = f"推定年齢: {face_info['age']}歳"
            display_frame = draw_face_label(display_frame, age_label, (x1, y2 + 40), FONT_SIZE, AGE_COLOR)

        # 性別推定結果
        if 'gender' in face_info:
            gender_label = f"性別: {face_info['gender']}"
            display_frame = draw_face_label(display_frame, gender_label, (x1, y2 + 65), FONT_SIZE, GENDER_COLOR)

    # 画面情報表示
    info1 = f'InsightFace ({MODEL_NAME}/{DEVICE_LABEL}) | フレーム: {frame_count} | 検出数: {len(faces_data)}'
    info2 = f'操作: q=終了 | 閾値: {SCORE_TH}'
    display_frame = draw_japanese_text(display_frame, info1, (10, 30), FONT_SIZE, (255, 255, 255))
    display_frame = draw_japanese_text(display_frame, info2, (10, 55), FONT_SIZE_SMALL, (255, 255, 0))

    # 検出結果の文字列化
    if faces_data:
        result_parts = [f"検出数:{len(faces_data)}"]
        for face_info in faces_data:
            parts = [f"顔{face_info['id']}"]
            if 'detection_conf' in face_info:
                parts.append(f"信頼度:{face_info['detection_conf']:.2f}")
            if 'age' in face_info:
                parts.append(f"年齢:{face_info['age']}歳")
            if 'gender' in face_info:
                parts.append(f"性別:{face_info['gender']}")
            result_parts.append(" ".join(parts))
        result = " ".join(result_parts)
    else:
        result = "検出数:0"

    return display_frame, result, current_time


# GPU判定
CTX_ID, PROVIDERS = detect_gpu()
DEVICE_LABEL = get_device_label(PROVIDERS)

# プログラム概要表示(ガイダンス)
print('=== InsightFace顔検出・年齢推定プログラム ===')
print('概要: リアルタイムで顔検出、年齢推定、性別推定を行い統合表示する')
print('機能: SCRFD(InsightFace)による顔検出と年齢・性別推定')
print('表示: 元画像に年齢推定結果を重ね合わせて表示')
print('操作: qキーで終了')
print('注意: 初回はモデルの自動ダウンロードに時間がかかる場合があります')
print('注意: 画面上の日本語表示にはMeiryoフォント(C:/Windows/Fonts/meiryo.ttc)が必要です')
print('出力: 終了時にresult.txtへ保存')
print()

# システム初期化
print('システム初期化中...')

# フォント確認
check_font_availability()

print(f'\n使用モデル: {MODEL_NAME} ({MODEL_DESCRIPTION})')
print(f'設定: 検出信頼度閾値 = {SCORE_TH}, 実行デバイス = {DEVICE_LABEL}')

# buffalo_lモデルダウンロード確認
download_insightface_model()

# 顔検出・年齢推定用アプリケーション初期化
print(f'{MODEL_NAME}モデルを初期化中...')
app = FaceAnalysis(name=MODEL_NAME, allowed_modules=['detection', 'genderage'], providers=PROVIDERS)
app.prepare(ctx_id=CTX_ID, det_size=DET_SIZE)
print(f'{MODEL_NAME}モデルをロードしました')

print('初期化完了')
print()

# 入力選択メニュー
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')

choice = input('選択: ')

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    root.destroy()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, WINDOW_WIDTH)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, WINDOW_HEIGHT)
    cap.set(cv2.CAP_PROP_FPS, FPS)
else:
    # サンプル動画ダウンロード・処理
    urllib.request.urlretrieve(SAMPLE_VIDEO_URL, SAMPLE_VIDEO_NAME)
    cap = cv2.VideoCapture(SAMPLE_VIDEO_NAME)

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    exit()

# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')
print('表示形式: 元画像に年齢推定結果を重ね合わせて表示')

MAIN_FUNC_DESC = "InsightFace Age Detection"
try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        processed_frame, result, current_time = video_frame_processing(frame)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)

        if choice == '1':  # カメラの場合
            print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
        else:  # 動画ファイルの場合
            print(frame_count, result)

        results_log.append(result)

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

finally:
    print('\n=== プログラム終了 ===')
    cap.release()
    cv2.destroyAllWindows()

    if results_log:
        with open(RESULT_FILE, 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write(f'使用モデル: {MODEL_NAME}\n')
            f.write(f'使用デバイス: {DEVICE_LABEL}\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print(f'\n処理結果を{RESULT_FILE}に保存しました')