Files
Behavision/behavision/detection.py
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
Five components that ship as one product:

- behavision/  the recognition engine. RTSP ingest, YuNet detection, IoU
               tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
               FastAPI dashboard. Identity is decided once per TRACK from an
               average of at least three embeddings, never per frame.
- agent/       the Go edge agent: supervises the engine, holds a durable
               spool, and drains it to MQTT. Nothing is acked before the
               broker confirms.
- desktop/     the shop PC application (Wails + React + tray).
- server/      the cloud API, MQTT consumer, reports and assistant.
- web/         platform.loyaly.ai, the head-office app, embedded in the
               server binary.

The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.

CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-04 11:14:18 +05:30

66 lines
2.2 KiB
Python

"""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