DeepFace による顔検出・年齢・性別・感情推定

ツール利用ガイド

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

動画ファイルやウェブカメラの映像から顔を自動検出し、年齢・性別・感情を推定するソフトウェアである。セキュリティシステム、マーケティング分析、ユーザーエクスペリエンス向上などの用途に活用できる。

2. 主な機能

3. 基本的な使い方

  1. 起動と入力の選択:

    プログラムを実行し、キーボードで 0(動画ファイル)、1(ウェブカメラ)、2(サンプル動画)のいずれかを入力する。

  2. 処理の実行:

    選択した入力ソースから映像が表示され、検出された顔に対してバウンディングボックスと属性情報が重ね合わせ表示される。

  3. 終了方法:

    映像画面でキーボードのqキーを押してプログラムを終了する。

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 deepface opencv-python tensorflow numpy pillow mtcnn

DeepFace顔検出・年齢・性別・感情推定プログラム

概要

このプログラムは、DeepFaceライブラリを使用して動画またはカメラ映像から顔を検出し、年齢・性別・感情を推定する顔属性分析システムである。検出された各顔に対して年齢推定、性別分類、感情認識を同時に実行する。

主要技術

DeepFace

DeepFaceは軽量な顔認識・顔属性分析フレームワークである[1]。VGG-Face、FaceNet、OpenFace、ArcFaceなど複数のモデルを統合し、年齢、性別、感情、人種の推定機能を提供する。顔検出から属性分析までの5段階(検出、位置合わせ、正規化、表現、検証)を自動処理する。

MTCNN (Multi-Task Cascaded Convolutional Networks)

MTCNNは2016年に開発された多段階カスケード型畳み込みニューラルネットワークである[2]。3つのネットワーク(P-Net、R-Net、O-Net)を順次適用し、候補領域の生成、候補の絞り込み、最終的な顔検出と顔特徴点抽出を実行する。様々なスケールと向きの顔を検出し、照明条件や表情の変化に対して頑健性を持つ。

技術的特徴

実装の特色

プログラムは実用性を重視した設計となっている。3つの入力ソース(動画ファイル、ウェブカメラ、サンプル動画)に対応し、処理モードを選択できる。画面表示では、検出された各顔に対してバウンディングボックス、顔ID、推定年齢、性別、感情を色分けして表示する。

警告メッセージの抑制機能により、DeepFaceの内部警告を制御してユーザビリティを向上させている。処理統計情報として、フレーム番号と検出顔数を画面上部に表示し、処理状況を把握できる。

結果保存機能では、プログラム終了時に処理結果をテキストファイル(result.txt)に自動保存する。保存内容には処理フレーム数、使用モデル、実行デバイス、各フレームの検出結果が含まれる。

参考文献

[1] Serengil, S. I., & Ozpinar, A. (2021). HyperExtended LightFace: A Facial Attribute Analysis Framework. In 2021 International Conference on Engineering and Emerging Technologies (ICEET) (pp. 1-4). IEEE. https://doi.org/10.1109/ICEET53442.2021.9659697

[2] Zhang, K., Zhang, Z., Li, Z., & Qiao, Y. (2016). Joint Face Detection and Alignment Using Multitask Cascaded Convolutional Networks. IEEE Signal Processing Letters, 23(10), 1499-1503. https://doi.org/10.1109/LSP.2016.2603342

ソースコード


# プログラム名: DeepFace顔検出・年齢・性別・感情推定プログラム
# 特徴技術名: DeepFace - 軽量顔認識・顔属性分析フレームワーク
# 出典: Serengil, S. I., & Ozpinar, A. (2020). LightFace: A Hybrid Deep Face Recognition Framework. https://github.com/serengil/deepface
# 特徴機能: 顔検出と年齢・性別・感情推定。リアルタイムで顔を検出し、推定年齢・性別・感情を表示
# 学習済みモデル: DeepFace統合モデル - 顔検出、年齢推定、性別推定、感情推定機能を含む。初回実行時に自動ダウンロード
# 方式設計:
#   - 関連利用技術:
#     - DeepFace: 顔検出、年齢・性別・感情推定
#     - OpenCV: 画像処理、カメラ制御、描画処理
#     - TensorFlow: モデル推論エンジン
#   - 入力と出力: 入力は「0:動画ファイル,1:カメラ,2:サンプル動画」から選択。出力は元画像に年齢・性別・感情推定結果を重ね合わせて表示
#   - 処理手順: 1.DeepFaceで顔検出、2.年齢・性別・感情推定、3.検出結果の表示
#   - 前処理、後処理: 前処理は最新フレーム取得。後処理は推定結果の表示
#   - 追加処理: 年齢推定、性別推定、感情推定(angry, fear, neutral, sad, disgust, happy, surprise)
# その他の重要事項: Windows環境専用。初回実行時は学習済みモデルのダウンロードに時間がかかる。
# 前準備:
#   - pip install deepface opencv-python tensorflow numpy pillow mtcnn

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

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

# ===== モデル設定 =====
MODEL_NAME = 'DeepFace'
MODEL_DESCRIPTION = 'DeepFace統合モデル(顔検出・年齢推定・性別推定・感情推定)'

# ===== 設定・定数管理 =====
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)  # バウンディングボックス用
AGE_COLOR = (255, 0, 255)  # 年齢表示用
GENDER_COLOR = (0, 255, 255)  # 性別表示用
EMOTION_COLOR = (255, 255, 0)  # 感情表示用

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

# 感情ラベルの日本語変換
EMOTION_LABELS = {
    'angry': '怒り',
    'disgust': '嫌悪',
    'fear': '恐怖',
    'happy': '幸福',
    'sad': '悲しみ',
    'surprise': '驚き',
    'neutral': '無表情'
}

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

# ===== ここから変更点 =====
# CLAHEオブジェクトをグローバルスコープで一度だけ定義
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# ===== ここまで変更点 =====


def get_device_label():
    """表示用のデバイス名を返す"""
    try:
        import tensorflow as tf
        if tf.config.list_physical_devices('GPU'):
            return 'GPU'
        else:
            return 'CPU'
    except:
        return 'CPU'


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


def extract_face_info(result, face_id):
    """DeepFaceの結果から顔情報を抽出して辞書化する"""
    region = result.get('region', {})

    # デフォルト値でエラー対策
    x = max(0, region.get('x', 0))
    y = max(0, region.get('y', 0))
    w = max(1, region.get('w', 1))
    h = max(1, region.get('h', 1))

    face_info = {
        'id': face_id,
        'box': (x, y, x + w, y + h),
        'age': int(result.get('age', 0)),
        'gender': result.get('dominant_gender', 'Unknown'),
        'emotion': result.get('dominant_emotion', 'unknown'),
        'emotion_scores': result.get('emotion', {})
    }
    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()

    try:
        # ===== ここから変更点 =====
        # 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)
        # ===== ここまで変更点 =====

        # DeepFaceで顔検出・年齢・性別・感情推定(MTCNN使用)
        # 変更点: 入力画像をCLAHE適用後の clahe_frame に変更
        results = DeepFace.analyze(
            clahe_frame,
            actions=['age', 'gender', 'emotion'],
            detector_backend='mtcnn',
            enforce_detection=False,
            silent=True
        )

        # 各顔の情報抽出(有効な領域のみ)
        for i, result in enumerate(results):
            region = result.get('region', {})
            # 有効な顔領域かチェック
            if (region.get('w', 0) > 0 and region.get('h', 0) > 0 and
                region.get('x', -1) >= 0 and region.get('y', -1) >= 0):
                face_info = extract_face_info(result, i + 1)
                faces_data.append(face_info)

    except Exception as e:
        print(f"DeepFace処理エラー: {e}")
        faces_data = []

    # 画像への描画 (対象は元の映像からコピーした 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)

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

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

        # 性別推定結果
        gender_jp = '男性' if face_info['gender'] == 'Man' else '女性'
        gender_label = f"性別: {gender_jp}"
        display_frame = draw_face_label(display_frame, gender_label, (x1, y2 + 40), FONT_SIZE, GENDER_COLOR)

        # 感情推定結果
        emotion_jp = EMOTION_LABELS.get(face_info['emotion'], face_info['emotion'])
        emotion_label = f"感情: {emotion_jp}"
        display_frame = draw_face_label(display_frame, emotion_label, (x1, y2 + 65), FONT_SIZE, EMOTION_COLOR)

    # 画面情報表示
    info1 = f'DeepFace (MTCNN) | フレーム: {frame_count} | 検出数: {len(faces_data)}'
    info2 = f'操作: q=終了 | 感情: angry, fear, neutral, sad, disgust, happy, surprise'
    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:
            emotion_jp = EMOTION_LABELS.get(face_info['emotion'], face_info['emotion'])
            gender_jp = '男性' if face_info['gender'] == 'Man' else '女性'
            parts = [
                f"顔{face_info['id']}",
                f"年齢:{face_info['age']}歳",
                f"性別:{gender_jp}",
                f"感情:{emotion_jp}"
            ]
            result_parts.append(" ".join(parts))
        result = " ".join(result_parts)
    else:
        result = "検出数:0"

    return display_frame, result, current_time


# デバイス判定
DEVICE_LABEL = get_device_label()

# プログラム概要表示(ガイダンス)
print('=== DeepFace顔検出・年齢・性別・感情推定プログラム ===')
print('概要: リアルタイムで顔検出、年齢推定、性別推定、感情推定を行い統合表示する')
print('機能: DeepFace(MTCNN)による高精度顔検出と年齢・性別・感情推定')
print('感情: angry, fear, neutral, sad, disgust, happy, surprise(7感情)の推定も行う')
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'設定: 実行デバイス = {DEVICE_LABEL}')
print('初期化完了')
print()

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

choice = input('選択: ')

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    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('表示形式: 元画像に年齢・性別・感情推定結果を重ね合わせて表示')
print('感情推定: angry, fear, neutral, sad, disgust, happy, surprise(7感情)も表示')
try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        MAIN_FUNC_DESC = "DeepFace Age Gender Emotion Detection"
        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('感情推定: angry, fear, neutral, sad, disgust, happy, surprise(7感情)\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print(f'\n処理結果を{RESULT_FILE}に保存しました')