各ライブラリを最新版に変更,mediapipeの仕様変更に対応.

オイラー角計測をmediapipeのFaceLandmarkerで行うように変更し,yaw値バイアス問題を解決
This commit is contained in:
agomizi1cm committed 2026-10-03 23:58:25 +09:00
1 parent 321cbdca20
commit 5d1ecc6883
4 files changed
+56 -17

No files matched your search

+2 -1
View File
@@ -85,9 +85,10 @@ face:
refine_landmarks: true refine_landmarks: true
min_detection_confidence: 0.40 min_detection_confidence: 0.40
min_tracking_confidence: 0.40 min_tracking_confidence: 0.40
model_path: models/face_landmarker.task
head_pose: head_pose:
enabled: true enabled: true
method: solvepnp method: facial_matrix
smoothing_enabled: true smoothing_enabled: true
# none | ema | median_ema | hampel_ema | one_euro | median_one_euro (see src/yaw_filters.py) # none | ema | median_ema | hampel_ema | one_euro | median_one_euro (see src/yaw_filters.py)
smoothing_method: ema smoothing_method: ema
+3 -2
View File
@@ -194,6 +194,7 @@ class FaceSettings:
refine_landmarks: bool refine_landmarks: bool
min_detection_confidence: float min_detection_confidence: float
min_tracking_confidence: float min_tracking_confidence: float
model_path: str
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -609,9 +610,9 @@ def _validate(config: AppConfig) -> None:
if not 0.0 <= face.min_detection_confidence <= 1.0 or not 0.0 <= face.min_tracking_confidence <= 1.0: if not 0.0 <= face.min_detection_confidence <= 1.0 or not 0.0 <= face.min_tracking_confidence <= 1.0:
raise ConfigurationError("face confidence thresholds must be in [0, 1]") raise ConfigurationError("face confidence thresholds must be in [0, 1]")
pose = config.head_pose pose = config.head_pose
if pose.method != "solvepnp" or pose.smoothing_method not in FILTER_KINDS: if pose.method not in {"solvepnp", "facial_matrix"} or pose.smoothing_method not in FILTER_KINDS:
raise ConfigurationError( raise ConfigurationError(
f"head_pose method must be solvepnp and smoothing_method one of {list(FILTER_KINDS)}") f"head_pose method must be solvepnp or facial_matrix and smoothing_method one of {list(FILTER_KINDS)}")
if not 0.0 < pose.ema_alpha <= 1.0 or pose.max_abs_angle <= 0: if not 0.0 < pose.ema_alpha <= 1.0 or pose.max_abs_angle <= 0:
raise ConfigurationError("head_pose EMA alpha and maximum angle are invalid") raise ConfigurationError("head_pose EMA alpha and maximum angle are invalid")
numeric = (pose.smoothing_window, pose.hampel_threshold, pose.one_euro_min_cutoff, pose.one_euro_beta, numeric = (pose.smoothing_window, pose.hampel_threshold, pose.one_euro_min_cutoff, pose.one_euro_beta,
+32 -14
View File
@@ -6,6 +6,7 @@ import logging
import math import math
from collections import deque from collections import deque
from typing import Any from typing import Any
from pathlib import Path
from .config_loader import FaceSettings, HeadPoseSettings from .config_loader import FaceSettings, HeadPoseSettings
from .exceptions import FaceProcessingError from .exceptions import FaceProcessingError
@@ -32,16 +33,25 @@ class FaceProcessor:
def _initialize(self) -> None: def _initialize(self) -> None:
try: try:
import mediapipe as mp import mediapipe as mp
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision
self._mesh = mp.solutions.face_mesh.FaceMesh( model_path = Path(self.config.model_path)
static_image_mode=self.config.static_image_mode, if not model_path.is_file():
max_num_faces=self.config.max_num_faces, raise FileNotFoundError(f"face_landmarker model not found: {model_path}")
refine_landmarks=self.config.refine_landmarks, options = vision.FaceLandmarkerOptions(
min_detection_confidence=self.config.min_detection_confidence, base_options=mp_python.BaseOptions(model_asset_path=str(model_path)),
running_mode=vision.RunningMode.IMAGE, # ROIごとに独立して推論するためIMAGEモード
num_faces=self.config.max_num_faces,
min_face_detection_confidence=self.config.min_detection_confidence,
min_face_presence_confidence=self.config.min_detection_confidence,
min_tracking_confidence=self.config.min_tracking_confidence, min_tracking_confidence=self.config.min_tracking_confidence,
output_facial_transformation_matrixes=True,
) )
self._mp = mp
self._mesh = vision.FaceLandmarker.create_from_options(options)
except Exception as exc: except Exception as exc:
raise FaceProcessingError(f"Failed to initialize MediaPipe Face Mesh: {exc}") from exc raise FaceProcessingError(f"Failed to initialize MediaPipe FaceLandmarker: {exc}") from exc
def process( def process(
self, frame: Any, tracked_persons: list[TrackedPerson], frame_info: FrameInfo, self, frame: Any, tracked_persons: list[TrackedPerson], frame_info: FrameInfo,
@@ -72,21 +82,29 @@ class FaceProcessor:
try: try:
image = frame[roi.y1:roi.y2, roi.x1:roi.x2] image = frame[roi.y1:roi.y2, roi.x1:roi.x2]
import cv2 import cv2
import numpy as np
target = self.config.roi_upscale_target target = self.config.roi_upscale_target
longest = max(image.shape[:2]) longest = max(image.shape[:2])
if target > 0 and longest < target: if target > 0 and longest < target:
scale = target / longest scale = target / longest
image = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC) image = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
output = self._mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) rgb = np.ascontiguousarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
faces = getattr(output, "multi_face_landmarks", None) or [] output = self._mesh.detect(self._mp.Image(image_format=self._mp.ImageFormat.SRGB, data=rgb))
faces = output.face_landmarks or []
if not faces: if not faces:
return self._failure(person.track_id, info, "face_not_detected", roi) return self._failure(person.track_id, info, "face_not_detected", roi)
selected = min(faces, key=self._face_center_distance) index = min(range(len(faces)), key=lambda k: self._face_center_distance(faces[k]))
landmarks = self._convert_landmarks(selected.landmark, roi) landmarks = self._convert_landmarks(faces[index], roi)
except Exception: except Exception:
LOGGER.exception("MediaPipe failed for track %d on frame %d", person.track_id, info.frame_number) LOGGER.exception("MediaPipe failed for track %d on frame %d", person.track_id, info.frame_number)
return self._failure(person.track_id, info, "mediapipe_error", roi) return self._failure(person.track_id, info, "mediapipe_error", roi)
if self.head_pose_config.method == "facial_matrix":
matrices = output.facial_transformation_matrixes or []
if index >= len(matrices):
return self._failure(person.track_id, info, "facial_matrix_missing", roi)
estimation = self.estimator.estimate_from_matrix(matrices[index])
else:
estimation = self.estimator.estimate(landmarks, width, height) estimation = self.estimator.estimate(landmarks, width, height)
interocular = self._interocular_px(landmarks) interocular = self._interocular_px(landmarks)
if not estimation.success: if not estimation.success:
@@ -118,9 +136,9 @@ class FaceProcessor:
@staticmethod @staticmethod
def _face_center_distance(face: Any) -> float: def _face_center_distance(face: Any) -> float:
landmarks = face.landmark points = getattr(face, "landmark", face)
center_x = sum(point.x for point in landmarks) / len(landmarks) center_x = sum(point.x for point in points) / len(points)
center_y = sum(point.y for point in landmarks) / len(landmarks) center_y = sum(point.y for point in points) / len(points)
return (center_x - 0.5) ** 2 + (center_y - 0.5) ** 2 return (center_x - 0.5) ** 2 + (center_y - 0.5) ** 2
@staticmethod @staticmethod
@@ -129,7 +147,7 @@ class FaceProcessor:
FaceLandmark( FaceLandmark(
index=index, x=roi.x1 + float(point.x) * roi.width, index=index, x=roi.x1 + float(point.x) * roi.width,
y=roi.y1 + float(point.y) * roi.height, z=float(point.z) * roi.width, y=roi.y1 + float(point.y) * roi.height, z=float(point.z) * roi.width,
visibility=float(point.visibility) if hasattr(point, "visibility") else None, visibility=float(point.visibility) if getattr(point, "visibility", None) is not None else None,
) )
for index, point in enumerate(raw_landmarks) for index, point in enumerate(raw_landmarks)
] ]
+19
View File
@@ -111,3 +111,22 @@ class HeadPoseEstimator:
if math.isfinite(roll): if math.isfinite(roll):
roll = (roll + 90.0) % 180.0 - 90.0 roll = (roll + 90.0) % 180.0 - 90.0
return float(pitch), float(yaw), float(roll) return float(pitch), float(yaw), float(roll)
@staticmethod
def facial_matrix_to_euler(matrix) -> tuple[float, float, float]:
import cv2
import numpy as np
m = np.asarray(matrix, dtype=np.float64).reshape(4, 4)
rotation = m[:3, :3]
rotation = rotation / np.linalg.norm(rotation, axis=0) # スケール成分を除去
angles = cv2.RQDecomp3x3(rotation)[0]
return float(angles[0]), float(angles[1]), float(angles[2])
def estimate_from_matrix(self, matrix) -> HeadPoseEstimation:
if not self.config.enabled:
return HeadPoseEstimation(False, failure_reason="head_pose_disabled")
try:
angles = self.facial_matrix_to_euler(matrix)
except Exception:
return HeadPoseEstimation(False, failure_reason="facial_matrix_failed")
return self.result_from_angles(angles) # 再投影誤差は無いので棄却判定はスキップされる