face-intel / providers /recognition /insightface.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
6.51 kB
"""
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
# Model files in the buffalo_s pack
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")
# Lazy-load models (cached after first call)
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)
# Step 1: detect faces using the SCRFD detector
detections = self._detect_faces(det_session, rgb)
if not detections:
raw = {"num_faces": 0, "matches": []}
normalized = {"num_faces": 0, "matches": []}
return raw, normalized
# Step 2: for each detected face, compute embedding
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())
# Match against gallery
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
# ------------------------------------------------------------------ #
# Face detection (SCRFD) — minimal postprocessing
# ------------------------------------------------------------------ #
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]
# SCRFD expects 640x640 input
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)
# Normalize
padded = (padded - 127.5) / 128.0
padded = padded.transpose(2, 0, 1)[None] # NCHW
outputs = session.run(padded)
# SCRFD output format varies; take the first output as scores
# and the second as boxes. This is a simplified parser.
scores = outputs[0] # (1, N)
boxes = outputs[1] if len(outputs) > 1 else None
if boxes is None:
return []
# Filter by confidence
threshold = 0.5
detections: list[dict] = []
for i in range(min(len(scores), len(boxes))):
if scores[i] < threshold:
continue
box = boxes[i]
# box is [x1, y1, x2, y2] in input scale
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
# ------------------------------------------------------------------ #
# Embedding generation (ArcFace)
# ------------------------------------------------------------------ #
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]
# L2 normalize
norm = np.linalg.norm(embedding)
if norm > 0:
embedding = embedding / norm
return embedding