| """ |
| InsightFace face recognition provider (ArcFace via ONNX). |
| |
| Uses the InsightFace `buffalo_s` model pack — the SMALL variant |
| (~50MB total) optimized for CPU inference. Produces 512-d L2-normalized |
| embeddings; 99.7% accuracy on LFW. |
| |
| Design: |
| - Models are loaded LAZILY on first use via cores.onnx.get_session() |
| - Models load exactly ONCE per process (cached) |
| - Uses cores.face.cosine_similarity + best_match for gallery matching |
| - If onnxruntime is not installed, is_available() returns False |
| |
| Model files (auto-downloaded to data/models/): |
| - det_500m.onnx (~2MB) — SCRFD face detector |
| - w600k_mbf.onnx (~50MB) — ArcFace recognizer |
| |
| License: MIT (InsightFace) |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import Any |
|
|
| import numpy as np |
|
|
| from config.settings import Settings, settings as _default_settings |
| from cores.onnx import is_onnx_available, get_session, ensure_model |
| from cores.face import cosine_similarity, best_match |
| from cores.vision import to_rgb, BBox, crop_region |
| from pipeline.feature_extraction import PipelineOutput |
| from providers.base import BaseProvider, ProviderCapability |
|
|
|
|
| class InsightFaceProvider(BaseProvider): |
| name = "insightface" |
| capability = ProviderCapability.RECOGNITION |
|
|
| |
| DETECTOR_FILE = "det_500m.onnx" |
| RECOGNIZER_FILE = "w600k_mbf.onnx" |
|
|
| def __init__(self, settings: Settings | None = None) -> None: |
| super().__init__(settings=settings or _default_settings) |
| self._available = is_onnx_available() |
|
|
| def is_available(self) -> bool: |
| return self._available |
|
|
| def _run(self, pipeline_output: PipelineOutput) -> tuple[dict, dict]: |
| if not self._available: |
| raise RuntimeError("onnxruntime not installed") |
|
|
| |
| det_path = ensure_model(self.DETECTOR_FILE, settings=self._settings) |
| rec_path = ensure_model(self.RECOGNIZER_FILE, settings=self._settings) |
| det_session = get_session(det_path, self._settings) |
| rec_session = get_session(rec_path, self._settings) |
|
|
| img: np.ndarray = pipeline_output.image |
| rgb = to_rgb(img) |
|
|
| |
| detections = self._detect_faces(det_session, rgb) |
| if not detections: |
| raw = {"num_faces": 0, "matches": []} |
| normalized = {"num_faces": 0, "matches": []} |
| return raw, normalized |
|
|
| |
| gallery = pipeline_output.gallery or {} |
| matches: list[dict] = [] |
| embeddings: list[list[float]] = [] |
|
|
| for i, det in enumerate(detections): |
| bbox = BBox(det["x"], det["y"], det["w"], det["h"]) |
| crop = crop_region(img, bbox, margin=0.15) |
| embedding = self._embed(rec_session, crop) |
| embeddings.append(embedding.tolist()) |
|
|
| |
| best_name, best_score, all_scores = best_match( |
| embedding, gallery, metric="cosine", |
| ) |
| threshold = self._settings.recognition_match_threshold |
| matches.append({ |
| "query_face_index": i, |
| "best_match": best_name if best_score >= threshold else None, |
| "distance": 1.0 - best_score, |
| "distances": all_scores, |
| "box": det, |
| }) |
|
|
| raw = { |
| "num_faces": len(detections), |
| "matches": matches, |
| "embeddings_dim": 512, |
| "model_pack": self._settings.insightface_model_pack, |
| } |
| normalized = { |
| "num_faces": len(detections), |
| "matches": matches, |
| "embeddings": embeddings, |
| } |
| return raw, normalized |
|
|
| |
| |
| |
| def _detect_faces(self, session, rgb: np.ndarray) -> list[dict]: |
| """Run SCRFD detection. Returns list of {x, y, w, h, confidence}.""" |
| h, w = rgb.shape[:2] |
| |
| import cv2 |
| input_h, input_w = 640, 640 |
| scale = min(input_h / h, input_w / w) |
| new_h, new_w = int(h * scale), int(w * scale) |
| resized = cv2.resize(rgb, (new_w, new_h)) |
| padded = np.zeros((input_h, input_w, 3), dtype=np.float32) |
| padded[:new_h, :new_w] = resized.astype(np.float32) |
| |
| padded = (padded - 127.5) / 128.0 |
| padded = padded.transpose(2, 0, 1)[None] |
|
|
| outputs = session.run(padded) |
| |
| |
| scores = outputs[0] |
| boxes = outputs[1] if len(outputs) > 1 else None |
| if boxes is None: |
| return [] |
|
|
| |
| threshold = 0.5 |
| detections: list[dict] = [] |
| for i in range(min(len(scores), len(boxes))): |
| if scores[i] < threshold: |
| continue |
| box = boxes[i] |
| |
| x1 = int(box[0] / scale) |
| y1 = int(box[1] / scale) |
| x2 = int(box[2] / scale) |
| y2 = int(box[3] / scale) |
| x1, y1 = max(0, x1), max(0, y1) |
| x2, y2 = min(w, x2), min(h, y2) |
| detections.append({ |
| "x": x1, "y": y1, "w": x2 - x1, "h": y2 - y1, |
| "confidence": float(scores[i]), |
| }) |
| return detections |
|
|
| |
| |
| |
| def _embed(self, session, crop: np.ndarray) -> np.ndarray: |
| """Generate 512-d embedding from a face crop.""" |
| import cv2 |
| if crop.size == 0: |
| return np.zeros(512, dtype=np.float32) |
| rgb = to_rgb(crop) |
| resized = cv2.resize(rgb, (112, 112)) |
| normalized = (resized.astype(np.float32) - 127.5) / 128.0 |
| nchw = normalized.transpose(2, 0, 1)[None] |
| output = session.run_single(nchw) |
| embedding = output[0] |
| |
| norm = np.linalg.norm(embedding) |
| if norm > 0: |
| embedding = embedding / norm |
| return embedding |
|
|