InsightFace による顔の変化分析(ソースコードと実行結果)

Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
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 -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install insightface matplotlib numpy onnxruntime
pip install -U opencv-python
InsightFace による顔の変化分析プログラム
概要
動画像から顔を検出し、106個の特徴点を抽出して目・口・眉の動きを数値化する。本プログラムは、リアルタイム顔表情分析能力を示している。具体的には、動画フレームから顔を検出し、106個の2D顔ランドマークを抽出して、瞬き(EAR: Eye Aspect Ratio)、口の開閉(MAR: Mouth Aspect Ratio)、眉の上下動を定量的に計測する。これらの微細な表情変化を時系列グラフとして可視化することで、人間の表情状態を客観的に分析できる。
主要技術
- InsightFace:深層学習ベースの顔分析フレームワークである。RetinaFaceによる顔検出と、2d106detモデルによる106点顔ランドマーク検出を統合している[1]。buffalo_lモデルは、大規模データセットで学習された汎用顔分析モデルである。
- 顔ランドマーク検出:顔の特徴的な位置(目、鼻、口、顔輪郭など)を2次元座標として検出する技術である。10<6個の特徴点により、表情変化を追跡できる[2]。
参考文献
[1] J. Guo, J. Deng, Y. Xue, and S. Zafeiriou, "Stacked Dense U-Nets with Dual Transformers for Robust Face Alignment," in Proceedings of the British Machine Vision Conference (BMVC), 2018.
[2] Y. Wu, H. Hassner, K. Kim, G. Medioni, and P. Natarajan, "Facial Landmark Detection with Tweaked Convolutional Neural Networks," IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 40, no. 12, pp. 3067-3074, 2018.
# 顔の微細変化分析プログラム
# 特徴技術名: InsightFace 2D Face Landmarks Detection
# 出典: J. Guo et al., "InsightFace: 2D and 3D Face Analysis Project," arXiv:1801.07698, 2018.
# https://github.com/deepinsight/insightface
# 特徴機能: 106個の高精度2D顔ランドマーク検出によるリアルタイム顔特徴点追跡(目・鼻・口・顔輪郭の正確な位置推定)
# 学習済みモデル: buffalo_l(大規模顔認識モデル、RetinaFace検出器と2d106det landmarkモデルを含む統合モデル、自動ダウンロード)
# 方式設計:
# - 関連利用技術: OpenCV(動画処理・表示)、matplotlib(リアルタイムグラフ描画)、numpy(ベクトル計算)、onnxruntime(モデル推論エンジン)
# - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用)、出力: 処理結果が画像化できる場合にはOpenCV画面でリアルタイムに表示.OpenCV画面内に処理結果をテキストで表示.さらに,1秒間隔で,print()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存」したことをprint()で表示.プログラム開始時に,プログラムの概要,ユーザが行う必要がある操作(もしあれば)をprint()で表示.
# - 処理手順: 動画フレーム取得→InsightFaceで顔検出→106個の2Dランドマーク抽出→EAR/MAR/眉高さ計算→移動平均→グラフ更新→統合表示
# - 前処理、後処理: 前処理: BGR→RGB色空間変換(InsightFaceの要求仕様)、後処理: 特徴量の移動平均によるノイズ除去
# - 追加処理: 瞬き検出のためのEAR(Eye Aspect Ratio)計算、口の開き具合のMAR(Mouth Aspect Ratio)計算、眉の相対高さ計算
# - 調整を必要とする設定値: WINDOW_SIZE(移動平均のウィンドウサイズ、デフォルト10)- ノイズ除去の強度を制御、GRAPH_LENGTH(グラフ表示のデータ点数、デフォルト100)- 表示する履歴の長さを制御
# 将来方策: 動的に顔の動きの速度を検出し、WINDOW_SIZEを自動調整する機能の実装
# その他の重要事項: 顔が検出されない場合はグラフ更新が停止、複数の顔が検出された場合は最初の顔のみを処理
# 前準備:pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# - pip install insightface matplotlib numpy onnxruntime
# - pip install -U opencv-python
import os
import logging
import warnings
import cv2
import tkinter as tk
from tkinter import filedialog
import urllib.request
import time
from collections import deque
import numpy as np
# ログレベルの抑制
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
logging.getLogger('tensorflow').setLevel(logging.ERROR)
warnings.filterwarnings('ignore')
import insightface
from insightface.app import FaceAnalysis
# 調整可能な設定値
WINDOW_SIZE = 10 # 移動平均のウィンドウサイズ - ノイズ除去の強度を制御
GRAPH_LENGTH = 100 # グラフに表示するデータ点数 - 表示する履歴の長さを制御
DETECTION_THRESHOLD = 0.3 # 顔検出閾値(低い値ほど検出しやすいが誤検出も増える)
# プログラム開始時の説明
print('=== 顔の微細変化分析プログラム ===')
print('このプログラムは、InsightFaceを使用して顔の106個のランドマークを検出し、')
print('目の開き具合(EAR)、口の開き具合(MAR)、眉の高さを分析します。')
print('')
print('操作方法:')
print('- qキー: プログラムを終了')
print('- 分析結果は1秒ごとにコンソールに表示されます')
print('- 終了時に結果がresult.txtに保存されます')
print('')
# InsightFace初期化
app = FaceAnalysis(name='buffalo_l')
app.prepare(ctx_id=-1, det_size=(640, 640), det_thresh=DETECTION_THRESHOLD)
# 特徴量計算用のランドマークインデックス(106個のランドマークに対応)
LEFT_EYE_INDICES = [33, 35, 36, 39, 40, 41]
RIGHT_EYE_INDICES = [42, 44, 45, 48, 49, 50]
MOUTH_INDICES = [60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71]
LEFT_EYEBROW_INDICES = [17, 18, 19, 20, 21]
RIGHT_EYEBROW_INDICES = [22, 23, 24, 25, 26]
def calculate_ear(eye_landmarks):
"""Eye Aspect Ratio(目の開き具合)を計算"""
v1 = np.linalg.norm(eye_landmarks[1] - eye_landmarks[5])
v2 = np.linalg.norm(eye_landmarks[2] - eye_landmarks[4])
h = np.linalg.norm(eye_landmarks[0] - eye_landmarks[3])
ear = (v1 + v2) / (2.0 * h) if h > 0 else 0
return ear
def calculate_mar(mouth_landmarks):
"""Mouth Aspect Ratio(口の開き具合)を計算"""
v1 = np.linalg.norm(mouth_landmarks[2] - mouth_landmarks[10])
v2 = np.linalg.norm(mouth_landmarks[3] - mouth_landmarks[9])
v3 = np.linalg.norm(mouth_landmarks[4] - mouth_landmarks[8])
h = np.linalg.norm(mouth_landmarks[0] - mouth_landmarks[6])
mar = (v1 + v2 + v3) / (3.0 * h) if h > 0 else 0
return mar
def calculate_eyebrow_height(eyebrow_landmarks, eye_center):
"""眉の高さ(目の中心からの相対距離)を計算"""
eyebrow_center = np.mean(eyebrow_landmarks, axis=0)
height = eyebrow_center[1] - eye_center[1]
return abs(height)
def extract_features(landmarks_2d):
"""2Dランドマークから特徴量を抽出"""
landmarks_array = landmarks_2d
left_eye = landmarks_array[LEFT_EYE_INDICES]
left_ear = calculate_ear(left_eye)
right_eye = landmarks_array[RIGHT_EYE_INDICES]
right_ear = calculate_ear(right_eye)
avg_ear = (left_ear + right_ear) / 2.0
mouth = landmarks_array[MOUTH_INDICES]
mar = calculate_mar(mouth)
left_eyebrow = landmarks_array[LEFT_EYEBROW_INDICES]
left_eye_center = np.mean(left_eye, axis=0)
left_eyebrow_height = calculate_eyebrow_height(left_eyebrow, left_eye_center)
right_eyebrow = landmarks_array[RIGHT_EYEBROW_INDICES]
right_eye_center = np.mean(right_eye, axis=0)
right_eyebrow_height = calculate_eyebrow_height(right_eyebrow, right_eye_center)
avg_eyebrow_height = (left_eyebrow_height + right_eyebrow_height) / 2.0
return {
'ear': avg_ear,
'mar': mar,
'eyebrow': avg_eyebrow_height
}
def draw_single_graph(img, data, x_offset, y_offset, width, height, color, title, ylabel):
"""単一のグラフをOpenCVで描画"""
# 背景の枠
cv2.rectangle(img, (x_offset, y_offset), (x_offset + width, y_offset + height), (200, 200, 200), 1)
# タイトル
cv2.putText(img, title, (x_offset + 10, y_offset + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
# Y軸ラベル
cv2.putText(img, ylabel, (x_offset + 5, y_offset + height // 2),
cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 0, 0), 1)
if len(data) > 1:
# データの正規化
data_min = min(data)
data_max = max(data)
data_range = data_max - data_min if data_max > data_min else 0.1
# グラフ描画エリア
graph_x = x_offset + 50
graph_y = y_offset + 30
graph_w = width - 60
graph_h = height - 50
# データ点の描画
points = []
for i, value in enumerate(data):
x = graph_x + int(i * graph_w / (len(data) - 1))
normalized_value = (value - data_min) / data_range if data_range > 0 else 0.5
y = graph_y + graph_h - int(normalized_value * graph_h)
points.append((x, y))
# 線の描画
for i in range(len(points) - 1):
cv2.line(img, points[i], points[i + 1], color, 2)
# 最新値の表示
cv2.putText(img, f'{data[-1]:.3f}', (graph_x + graph_w - 40, graph_y + 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
def draw_graphs(data_history, width, height):
"""OpenCVでグラフを描画"""
img = np.ones((height, width, 3), dtype=np.uint8) * 255
graph_height = height // 3
# EARグラフ
draw_single_graph(img, list(data_history['ear']), 0, 0, width, graph_height,
(255, 0, 0), 'Eye Opening (Blink Detection)', 'EAR')
# MARグラフ
draw_single_graph(img, list(data_history['mar']), 0, graph_height, width, graph_height,
(0, 255, 0), 'Mouth Opening', 'MAR')
# 眉グラフ
draw_single_graph(img, list(data_history['eyebrow']), 0, graph_height * 2, width, graph_height,
(0, 0, 255), 'Eyebrow Movement', 'Height')
return img
def detect_and_extract_features(frame, app):
"""顔検出と特徴量抽出"""
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
faces = app.get(rgb_frame)
if len(faces) > 0:
face = faces[0]
landmarks_2d = face.landmark_2d_106
features = extract_features(landmarks_2d)
# ランドマーク描画
for idx in (LEFT_EYE_INDICES + RIGHT_EYE_INDICES +
MOUTH_INDICES + LEFT_EYEBROW_INDICES +
RIGHT_EYEBROW_INDICES):
x = int(landmarks_2d[idx][0])
y = int(landmarks_2d[idx][1])
cv2.circle(frame, (x, y), 2, (0, 255, 0), -1)
return features, frame
return None, frame
def process_frame(frame, app, data_history, moving_avg_buffer, last_print_time, results_log):
"""フレーム処理のメイン関数"""
frame_height, frame_width = frame.shape[:2]
# 顔検出と特徴量抽出
features, annotated_frame = detect_and_extract_features(frame, app)
if features:
# 移動平均の計算
for key in ['ear', 'mar', 'eyebrow']:
moving_avg_buffer[key].append(features[key])
avg_value = np.mean(moving_avg_buffer[key])
data_history[key].append(avg_value)
# グラフ描画
graph_width = frame_width // 2
graph_image = draw_graphs(data_history, graph_width, frame_height)
# 画像結合
combined = np.hstack([annotated_frame, graph_image])
# テキスト表示
if features:
text_y = 30
cv2.putText(combined, f"EAR: {features['ear']:.3f}",
(10, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
cv2.putText(combined, f"MAR: {features['mar']:.3f}",
(10, text_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.putText(combined, f"Eyebrow: {features['eyebrow']:.1f}",
(10, text_y + 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
# 1秒間隔での出力
current_time = time.time()
if current_time - last_print_time >= 1.0 and features:
result_text = f"Time: {current_time:.1f}, EAR: {features['ear']:.3f}, MAR: {features['mar']:.3f}, Eyebrow: {features['eyebrow']:.1f}"
print(result_text)
results_log.append(result_text)
last_print_time = current_time
return combined, last_print_time
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')
choice = input('選択: ')
temp_file = None
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)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
elif choice == '2':
# サンプル動画ダウンロード・処理
url = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
filename = 'vtest.avi'
try:
urllib.request.urlretrieve(url, filename)
temp_file = filename
cap = cv2.VideoCapture(filename)
except Exception as e:
print(f'動画のダウンロードに失敗しました: {url}')
print(f'エラー: {e}')
exit()
else:
print('無効な選択です')
exit()
# データ履歴の初期化
data_history = {
'ear': deque(maxlen=GRAPH_LENGTH),
'mar': deque(maxlen=GRAPH_LENGTH),
'eyebrow': deque(maxlen=GRAPH_LENGTH)
}
# 移動平均用のバッファ
moving_avg_buffer = {
'ear': deque(maxlen=WINDOW_SIZE),
'mar': deque(maxlen=WINDOW_SIZE),
'eyebrow': deque(maxlen=WINDOW_SIZE)
}
# 結果ログと時間管理
results_log = []
last_print_time = time.time()
# メイン処理
try:
while True:
cap.grab()
ret, frame = cap.retrieve()
if not ret:
break
processed_frame, last_print_time = process_frame(
frame, app, data_history, moving_avg_buffer, last_print_time, results_log
)
cv2.imshow('Face Micro-expression Analysis', processed_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cap.release()
cv2.destroyAllWindows()
# 結果をresult.txtに保存
if results_log:
with open('result.txt', 'w', encoding='utf-8') as f:
for line in results_log:
f.write(line + '\n')
print('result.txtに保存しました')
if temp_file:
os.remove(temp_file)