InsightFaceによる68点3Dランドマーク検出

【概要】 InsightFaceフレームワークの68点3Dランドマーク検出技術を用いた顔解析プログラムの実装と実験を行う。68点3Dランドマーク検出は顔の主要な 特徴点を3次元座標で特定する技術である。Webカメラから顔と68点ランドマークを検出する。Windows環境での実行手順、プログラムコード、実験アイデアを含む。

目次

1. はじめに

68点3Dランドマーク検出技術

InsightFaceは顔認識・解析のための統合フレームワークであり、68点3Dランドマーク検出はその中で使用される顔特徴点抽出アルゴリズムの一つである。ランドマーク検出は画像から顔の主要な解剖学的特徴点の位置を特定する技術であり、顔認識、表情解析、3D顔面復元の前段階として使用される。

技術名: 1k3d68(68点3次元顔面ランドマーク検出)
フレームワーク: InsightFace(深層学習ベースの顔解析ツールボックス)
GitHub: https://github.com/deepinsight/insightface

68点3Dランドマーク検出は顔の主要な解剖学的特徴点を3次元座標(x, y, z)で特定することを特徴とする顔解析技術である。Webカメラからの映像をリアルタイムで処理し、顔検出と68点ランドマーク(顎のライン、眉毛、目、鼻、口の詳細な特徴点)の検出を実行する。

68点ランドマークの構成

2. 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/

CMakeのインストール

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


REM CMake をシステム領域にインストール
winget install --scope machine --id Kitware.CMake -e --silent
REM CMake のパス設定
set "GMAKE_PATH=C:\Program Files\CMake\bin"
if exist "%GMAKE_PATH%" (
    echo "%PATH%" | find /i "%GMAKE_PATH%" >nul
    if errorlevel 1 setx PATH "%PATH%;%GMAKE_PATH%" /M >nul
)

必要なライブラリのインストール

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


pip install insightface opencv-python onnxruntime pillow

3. プログラムコード

概要

このプログラムは、InsightFaceフレームワークを用いて動画や静止画から顔を検出し、68点の3次元顔ランドマーク(特徴点)を抽出・可視化する。顔の各部位(顎ライン、眉毛、鼻、目、口)を色分けして表示し、各ランドマークのx, y座標に加えて相対深度情報(z座標)も取得する。リアルタイム処理に対応し、カメラ入力と動画ファイル入力の両方をサポートする。

主要技術

InsightFace

深層学習ベースの顔解析フレームワークであり、顔検出、顔認識、顔属性推定、顔ランドマーク検出などの機能を統合的に提供する[1]。本プログラムでは、buffalo_lモデルによる顔検出とlandmark_3d_68モジュールによる68点3Dランドマーク検出を組み合わせて使用している。

68点顔ランドマークモデル

顔の主要な特徴点を68個の座標として表現する標準的な顔ランドマーク表現方式である[2]。顎ライン(1-17番)、右眉毛(18-22番)、左眉毛(23-27番)、鼻(28-36番)、右目(37-42番)、左目(43-48番)、口(49-68番)の各部位に対応する点群で構成される。

技術的特徴

本実装では、2次元座標に加えて相対深度値(z座標)を取得し、3次元的な顔形状情報を抽出する。z座標はカメラからの相対的な奥行きを表し、顔の立体構造を把握するための重要な情報となる。検出されたランドマークは部位ごとに異なる色で可視化され、視覚的に理解しやすい表示を実現している。

処理性能の面では、ONNXRuntimeを活用した推論により、リアルタイム処理を可能にしている。検出サイズを640×640ピクセルに設定することで、精度と処理速度のバランスを取っている。

実装の特色

参考文献

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

[2] Sagonas, C., Antonakos, E., Tzimiropoulos, G., Zafeiriou, S., & Pantic, M. (2016). 300 Faces In-The-Wild Challenge: database and results. Image and Vision Computing, 47, 3-18. https://doi.org/10.1016/j.imavis.2016.01.002

ソースコード


# InsightFace 68点3Dランドマーク検出プログラム(3Dモード)
# 特徴技術名: InsightFace - 深層学習ベースの顔解析フレームワーク
# 出典: Deng, J., Guo, J., Ververas, E., Kotsia, I., & Zafeiriou, S. (2020).
#       RetinaFace: Single-Shot Multi-Level Face Localisation in the Wild. In CVPR.
# 特徴機能: 68点3Dランドマーク検出(顎ライン1-17、眉毛18-27、鼻28-36、目37-48、口49-68)
# 学習済みモデル: buffalo_l + landmark_3d_68(公式配布を想定。環境により利用可否が異なる)
# I/O仕様:
#   入力: 0=動画ファイル, 1=カメラ, 2=サンプル動画(vtest.avi)
#   出力: 画面表示(Pillow+OpenCVで日本語)、終了時にresult.txtへ保存
# 注記: z座標はモデル内部の相対値(物理的距離ではない)
# 事前準備: pip install insightface opencv-python onnxruntime pillow

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

# 定数定義
CTX_ID = 0
DET_SIZE = (640, 640)
FONT_COLOR_INFO = (0, 255, 0)      # BGR
FONT_COLOR_COUNT = (0, 255, 255)   # BGR
BBOX_COLOR = (255, 0, 0)           # BGR
LM_COLOR = (0, 255, 0)             # BGR
LM_RADIUS = 2

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

# ランドマーク色(部位別、BGR)
LANDMARK_COLORS = {
    'jawline': (255, 0, 0),
    'right_eyebrow': (0, 255, 0),
    'left_eyebrow': (0, 255, 0),
    'nose': (255, 255, 0),
    'right_eye': (255, 0, 255),
    'left_eye': (255, 0, 255),
    'mouth': (0, 255, 255)
}

# ガイダンス
print("=" * 60)
print("InsightFace 68点3Dランドマーク検出プログラム(3Dモード)")
print("=" * 60)
print("\n【概要説明】")
print("このプログラムは、動画から顔を検出し、")
print("68点の3次元ランドマーク(特徴点)を抽出・表示する。")
print("\n【操作方法】")
print("- 'q'キー: プログラムを終了")
print("- カメラまたは動画に顔を向けると自動的に検出を開始する")
print("\n【注意事項】")
print("- 適切な照明環境で使用する")
print("- カメラから適度な距離(50-100cm)を保つ")
print("- landmark_3d_68モデルが必要(未提供時は起動時に終了)")
print("- z座標はモデル内部の相対値(物理的距離ではない)")
print("-" * 60)

# 結果記録
results_log = []
frame_count = 0

# FaceAnalysis 初期化(3D)
try:
    app = FaceAnalysis(name='buffalo_l', allowed_modules=['detection', 'landmark_3d_68'])
    app.prepare(ctx_id=CTX_ID, det_size=DET_SIZE)
    available_modules = set(getattr(app, 'models', {}).keys()) if hasattr(app, 'models') else set()
    print(f"利用可能モジュール: {sorted(available_modules)}")
    if 'landmark_3d_68' not in available_modules:
        print("エラー: 'landmark_3d_68' モジュールが利用できないため終了する。")
        sys.exit(1)
    print("68点3Dランドマーク検出モデル(landmark_3d_68)をロードした。")
except Exception as e:
    print(f"エラー: insightfaceの初期化に失敗: {e}")
    sys.exit(1)

# 日本語フォントの初期化
try:
    if os.path.exists(FONT_PATH):
        font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
        font_small = ImageFont.truetype(FONT_PATH, FONT_SIZE_SMALL)
        print(f"日本語フォントを読み込んだ: {FONT_PATH}")
    else:
        font = None
        font_small = None
        print("警告: 日本語フォントが見つからないため、OpenCVのデフォルトフォントを使用する。")
except Exception as e:
    font = None
    font_small = None
    print(f"警告: フォント読み込みエラー: {e}")

def get_landmark_color(landmark_idx: int):
    if 1 <= landmark_idx <= 17:
        return LANDMARK_COLORS['jawline']
    elif 18 <= landmark_idx <= 22:
        return LANDMARK_COLORS['right_eyebrow']
    elif 23 <= landmark_idx <= 27:
        return LANDMARK_COLORS['left_eyebrow']
    elif 28 <= landmark_idx <= 36:
        return LANDMARK_COLORS['nose']
    elif 37 <= landmark_idx <= 42:
        return LANDMARK_COLORS['right_eye']
    elif 43 <= landmark_idx <= 48:
        return LANDMARK_COLORS['left_eye']
    elif 49 <= landmark_idx <= 68:
        return LANDMARK_COLORS['mouth']
    else:
        return LM_COLOR

def draw_japanese_text(frame, text, position, font_to_use, color_bgr):
    # PillowはRGB前提のため、BGR→RGBに変換して色を渡す
    if font_to_use is not None:
        img_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)
        color_rgb = (int(color_bgr[2]), int(color_bgr[1]), int(color_bgr[0]))
        draw.text(position, text, font=font_to_use, fill=color_rgb)
        return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
    else:
        cv2.putText(frame, text, position, cv2.FONT_HERSHEY_SIMPLEX, 0.5, color_bgr, 1)
        return frame

def video_frame_processing(frame):
    global frame_count
    current_time = time.time()
    frame_count += 1

    faces = app.get(frame)

    # 実際に検出されたランドマーク数をカウント
    actual_landmark_count = 0
    z_values = []  # z座標値を収集

    # 描画(2D: x, y)とz座標の収集(3D前提)
    for face_idx, face in enumerate(faces):
        # バウンディングボックス
        if hasattr(face, 'bbox') and face.bbox is not None:
            bbox = face.bbox.astype(int)
            cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), BBOX_COLOR, 2)

        # 信頼度
        confidence = face.det_score if hasattr(face, 'det_score') else 0.0
        conf_text = f"信頼度: {confidence:.3f}"
        if hasattr(face, 'bbox') and face.bbox is not None:
            frame = draw_japanese_text(frame, conf_text, (int(face.bbox[0]), int(face.bbox[1]) - 25), font_small, BBOX_COLOR)

        # 3D 68点ランドマークのみ処理
        if hasattr(face, 'landmark_3d_68') and face.landmark_3d_68 is not None:
            landmarks = face.landmark_3d_68
            # 点数・次元の簡易検証
            if len(landmarks) != 68:
                continue
            face_landmark_count = 0

            for i, landmark in enumerate(landmarks):
                if len(landmark) >= 3:
                    x, y, z = int(landmark[0]), int(landmark[1]), float(landmark[2])
                    z_values.append(z)
                    face_landmark_count += 1
                else:
                    # 3D前提のため、次元が不足する点は描画しない
                    continue

                color = get_landmark_color(i + 1)
                cv2.circle(frame, (x, y), LM_RADIUS, color, -1)

                # フレーム番号が30の倍数の時にランドマーク番号を表示
                if frame_count % 30 == 0:
                    cv2.putText(frame, str(i + 1), (x + 3, y - 3),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.3, color, 1)

            actual_landmark_count += face_landmark_count

    # 画面情報
    info_text = f"フレーム: {frame_count} | 68点3Dランドマーク検出"
    frame = draw_japanese_text(frame, info_text, (10, 30), font, FONT_COLOR_INFO)

    face_count_text = f"検出された顔: {len(faces)}"
    frame = draw_japanese_text(frame, face_count_text, (10, 60), font, FONT_COLOR_COUNT)

    # 実際に検出されたランドマーク数を表示(理論値は常に68点)
    landmark_text = f"検出ランドマーク数: {actual_landmark_count}/理論値{len(faces)*68}"
    frame = draw_japanese_text(frame, landmark_text, (10, 90), font, FONT_COLOR_COUNT)

    # z座標の統計情報を表示(データがある場合)
    if z_values:
        z_min = min(z_values)
        z_max = max(z_values)
        z_mean = sum(z_values) / len(z_values)
        z_info_text = f"Z座標範囲: {z_min:.2f} ~ {z_max:.2f} (平均: {z_mean:.2f})"
        frame = draw_japanese_text(frame, z_info_text, (10, 115), font_small, (200, 200, 200))

    # 色分け凡例
    legend_y = 145
    legend_items = [
        ("顎ライン", LANDMARK_COLORS['jawline']),
        ("眉毛", LANDMARK_COLORS['right_eyebrow']),
        ("鼻", LANDMARK_COLORS['nose']),
        ("目", LANDMARK_COLORS['right_eye']),
        ("口", LANDMARK_COLORS['mouth'])
    ]
    for i, (label, color) in enumerate(legend_items):
        y_pos = legend_y + i * 25
        cv2.circle(frame, (15, y_pos), 5, color, -1)
        frame = draw_japanese_text(frame, label, (30, y_pos - 5), font_small, color)

    # 注記
    height = frame.shape[0]
    frame = draw_japanese_text(frame, "注記: z座標はモデル内部の相対値(物理的距離ではない)", (10, height - 40), font_small, (200, 200, 200))

    # 操作案内
    frame = draw_japanese_text(frame, "'q'キーで終了", (10, height - 20), font_small, (255, 255, 255))

    # z座標情報を結果に含める
    z_info = f"Z範囲:{z_min:.2f}~{z_max:.2f}" if z_values else "Z:なし"
    result = f"フレーム:{frame_count} 顔数:{len(faces)} ランドマーク:{actual_landmark_count} {z_info}"
    return frame, result, current_time

# 入力選択メニュー
print("\n0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")
choice = input("選択: ")

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        sys.exit(0)
    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)
else:
    # サンプル動画ダウンロード・処理(失敗時は終了)
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    try:
        urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
    except Exception as e:
        print(f"エラー: サンプル動画のダウンロードに失敗: {e}")
        sys.exit(1)
    cap = cv2.VideoCapture(SAMPLE_FILE)

if not cap.isOpened():
    print('動画ファイル・カメラを開けなかった。')
    sys.exit(1)

# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')
try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        MAIN_FUNC_DESC = "InsightFace 68点3Dランドマーク検出"
        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.txt', 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n')
            f.write('\n')
            f.write('\n'.join(results_log))
        print('処理結果をresult.txtに保存した。')

4. 使用方法

  1. 上記のプログラムを実行する
  2. Webカメラの映像が表示され、検出された顔に赤色の境界ボックスと色分けされた68点ランドマークが表示される。
  3. ランドマークは以下のように色分けされる:
    • 顎のライン(1-17点): 赤色
    • 眉毛(18-27点): 緑色
    • 鼻(28-36点): 黄色
    • 目(37-48点): マゼンタ色
    • 口(49-68点): シアン色
  4. 各顔の上部に検出信頼度(0.000~1.000)が表示される。検出信頼度は0.0から1.0の値で、1.0に近いほど顔である確信度が高い。0.5以上を閾値として使用することが多い。
  5. 画面左側に色分け凡例が表示される。
  6. 'q'キーを押すとプログラムが終了する。

5. 実験・探求のアイデア

AIモデル選択による比較実験

プログラム内のname='buffalo_sc'を以下のモデルに変更して、性能の違いを確認する。

検出サイズの変更実験

DET_SIZE = (640, 640)の値を変更して、検出精度と処理速度への影響を確認する。

様々な条件での検出性能テスト

68点ランドマーク活用の探求

検出された68点ランドマークの座標を利用して、以下の解析を実行する:

ランドマーク精度評価実験