""" Face detection + recognition using InsightFace (RetinaFace + ArcFace). Uses the buffalo_l model pack which auto-downloads on first use (~300MB). Produces 512-d ArcFace embeddings. Non-commercial research license — fine for homelab self-hosting. """ import logging from pathlib import Path import numpy as np from app.config import VisionSettings from app.services.vision.base import FaceProcessor, FaceDetection logger = logging.getLogger(__name__) class InsightFaceProcessor(FaceProcessor): def __init__(self, settings: VisionSettings): from insightface.app import FaceAnalysis model_root = str(Path(settings.models_dir) / "face" / "insightface") logger.info("Loading InsightFace buffalo_l from %s", model_root) from app.services.vision.providers import get_providers providers = get_providers(settings.execution_providers) self._app = FaceAnalysis( name="buffalo_l", root=model_root, providers=providers, ) self._app.prepare(ctx_id=-1, det_size=(640, 640)) self._min_det_score = settings.faces.recognition_threshold def process(self, image: np.ndarray) -> list[FaceDetection]: orig_h, orig_w = image.shape[:2] # InsightFace expects BGR bgr = image[:, :, ::-1].copy() faces = self._app.get(bgr) if not faces: return [] results = [] for face in faces: if face.det_score < self._min_det_score: continue # face.bbox is [x1, y1, x2, y2] in pixel coords x1, y1, x2, y2 = face.bbox bbox = [ max(0, float(x1) / orig_w), max(0, float(y1) / orig_h), min(1, float(x2) / orig_w), min(1, float(y2) / orig_h), ] embedding = face.normed_embedding # already L2-normalized, 512-d results.append(FaceDetection( bbox=bbox, embedding=embedding.astype(np.float32), quality=float(face.det_score), )) return results @property def embedding_dim(self) -> int: return 512