"""Face detection with YuNet (OpenCV FaceDetectorYN). Why YuNet: modern CNN detector with 5-point landmarks built into OpenCV — no compilation, no extra runtime, works on Windows out of the box, and its landmarks feed ArcFace alignment directly. Accuracy on frontal surveillance footage is on par with SCRFD-500M at a fraction of the operational cost. """ from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path import cv2 import numpy as np from .geometry import clip_box log = logging.getLogger(__name__) YUNET_FILENAME = "face_detection_yunet_2023mar.onnx" @dataclass class Detection: box: tuple # x1, y1, x2, y2 (int, clipped to frame) kps: np.ndarray # (5, 2) float32, full-frame coordinates score: float quality: float = 0.0 attributes: dict = field(default_factory=dict) class FaceDetector: def __init__(self, models_dir: Path, score_threshold: float = 0.75, nms_threshold: float = 0.3, max_faces: int = 20, min_face_px: int = 48): model_path = Path(models_dir) / YUNET_FILENAME if not model_path.exists(): raise FileNotFoundError( f"{model_path} missing - run: python -m behavision setup-models") self._det = cv2.FaceDetectorYN_create( str(model_path), "", (320, 320), score_threshold, nms_threshold, max_faces) self._input_size: "tuple[int, int] | None" = None self.min_face_px = min_face_px def detect(self, frame: np.ndarray) -> "list[Detection]": h, w = frame.shape[:2] if self._input_size != (w, h): self._det.setInputSize((w, h)) self._input_size = (w, h) _, faces = self._det.detect(frame) if faces is None: return [] out: list[Detection] = [] for f in faces: x, y, bw, bh = f[:4] if min(bw, bh) < self.min_face_px: continue box = clip_box((x, y, x + bw, y + bh), w, h) if box is None: continue kps = f[4:14].reshape(5, 2).astype(np.float32) out.append(Detection(box=box, kps=kps, score=float(f[14]))) return out