Loads detector + recognizer ONNX files directly. Supports the OpenCV Zoo YuNet + SFace pair out of the box. YuNet exposes a C++-level API via cv2.FaceDetectorYN which accepts the ONNX file directly; SFace is driven through cv2.FaceRecognizerSF. Both are Apache 2.0 licensed.
| 375 | # ─── OnnxDirectEngine ───────────────────────────────────────────────── |
| 376 | |
| 377 | class OnnxDirectEngine: |
| 378 | """Loads detector + recognizer ONNX files directly. |
| 379 | |
| 380 | Supports the OpenCV Zoo YuNet + SFace pair out of the box. YuNet |
| 381 | exposes a C++-level API via cv2.FaceDetectorYN which accepts the |
| 382 | ONNX file directly; SFace is driven through cv2.FaceRecognizerSF. |
| 383 | Both are Apache 2.0 licensed. |
| 384 | """ |
| 385 | |
| 386 | def __init__(self) -> None: |
| 387 | self.detector_path: str = "" |
| 388 | self.recognizer_path: str = "" |
| 389 | self.input_size: tuple[int, int] = (320, 320) |
| 390 | self.det_thresh: float = 0.5 |
| 391 | self._detector: Any = None |
| 392 | self._recognizer: Any = None |
| 393 | self._antispoofer: Antispoofer | None = None |
| 394 | |
| 395 | def prepare(self, options: dict[str, str]) -> None: |
| 396 | raw_det = options.get("detector_onnx", "") |
| 397 | raw_rec = options.get("recognizer_onnx", "") |
| 398 | if not raw_det or not raw_rec: |
| 399 | raise ValueError( |
| 400 | "onnx_direct engine requires both detector_onnx and recognizer_onnx options" |
| 401 | ) |
| 402 | model_dir = options.get("_model_dir") |
| 403 | self.detector_path = _resolve_model_path(raw_det, model_dir=model_dir) |
| 404 | self.recognizer_path = _resolve_model_path(raw_rec, model_dir=model_dir) |
| 405 | self.input_size = _parse_det_size(options.get("det_size", "320x320")) |
| 406 | self.det_thresh = float(options.get("det_thresh", "0.5")) |
| 407 | self._antispoofer = _build_antispoofer(options, model_dir) |
| 408 | |
| 409 | # YuNet is a fixed-size detector; size is reset per detect() call to |
| 410 | # match the input frame. |
| 411 | self._detector = cv2.FaceDetectorYN.create( |
| 412 | self.detector_path, |
| 413 | "", |
| 414 | self.input_size, |
| 415 | score_threshold=self.det_thresh, |
| 416 | nms_threshold=0.3, |
| 417 | top_k=5000, |
| 418 | ) |
| 419 | self._recognizer = cv2.FaceRecognizerSF.create(self.recognizer_path, "") |
| 420 | |
| 421 | def detect(self, img: np.ndarray) -> list[FaceDetection]: |
| 422 | if self._detector is None: |
| 423 | return [] |
| 424 | h, w = img.shape[:2] |
| 425 | self._detector.setInputSize((w, h)) |
| 426 | retval, faces = self._detector.detect(img) |
| 427 | if faces is None: |
| 428 | return [] |
| 429 | out: list[FaceDetection] = [] |
| 430 | for row in faces: |
| 431 | x, y, fw, fh = float(row[0]), float(row[1]), float(row[2]), float(row[3]) |
| 432 | # Landmarks at columns 4..13 are (lx1,ly1,...,lx5,ly5). |
| 433 | landmarks = np.array(row[4:14], dtype=np.float32).reshape(5, 2) if len(row) >= 14 else None |
| 434 | score = float(row[-1]) |