各ライブラリを最新版に変更,mediapipeの仕様変更に対応.
オイラー角計測をmediapipeのFaceLandmarkerで行うように変更し,yaw値バイアス問題を解決
This commit is contained in:
1 parent
321cbdca20
commit
5d1ecc6883
4 files changed
+56
-17
No files matched your search
+2
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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) # 再投影誤差は無いので棄却判定はスキップされる
|
||||||
Reference in new issue
Block a user