feat: initial BantayCam scaffold
- FastAPI backend with async SQLAlchemy - Camera RTSP management (add, start, stop) - Vehicle detection (YOLO + fast-alpr) - Type: car, motorcycle, truck, jeepney - Color detection (HSV) - License plate OCR - Motorcycle person count - Face detection + InsightFace ArcFace embedding - pgvector identity grouping (auto-cluster same face) - Vehicle + person movement trail APIs - Docker Compose with pgvector/pg16 - Models: Camera, VehicleIdentity, VehicleEvent, PersonIdentity, FaceEvent
This commit is contained in:
88
backend/app/services/face_service.py
Normal file
88
backend/app/services/face_service.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
FaceService — InsightFace-based face detection, embedding, and identity matching.
|
||||
Uses pgvector cosine similarity for fast 1:N identity search.
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
from uuid import UUID
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FaceDetection:
|
||||
bbox: dict = field(default_factory=dict) # {"x","y","w","h"}
|
||||
embedding: Optional[np.ndarray] = None # 512-dim vector
|
||||
detection_confidence: float = 0.0
|
||||
face_crop: Optional[np.ndarray] = None
|
||||
matched_identity_id: Optional[UUID] = None
|
||||
similarity_score: float = 0.0
|
||||
|
||||
|
||||
class FaceService:
|
||||
def __init__(self):
|
||||
self._app = None
|
||||
self._ready = False
|
||||
|
||||
def load(self):
|
||||
"""Load InsightFace model — call once at startup."""
|
||||
try:
|
||||
from insightface.app import FaceAnalysis
|
||||
self._app = FaceAnalysis(
|
||||
name=settings.FACE_MODEL,
|
||||
allowed_modules=["detection", "recognition"],
|
||||
)
|
||||
self._app.prepare(
|
||||
ctx_id=settings.GPU_DEVICE,
|
||||
det_size=(640, 640),
|
||||
)
|
||||
self._ready = True
|
||||
logger.info(f"InsightFace loaded (model={settings.FACE_MODEL})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load InsightFace: {e}")
|
||||
|
||||
def detect(self, frame: np.ndarray) -> list[FaceDetection]:
|
||||
"""
|
||||
Detect all faces in a frame and extract embeddings.
|
||||
Returns list of FaceDetection objects.
|
||||
"""
|
||||
if not self._ready or self._app is None:
|
||||
return []
|
||||
|
||||
try:
|
||||
faces = self._app.get(frame)
|
||||
except Exception as e:
|
||||
logger.debug(f"Face detection error: {e}")
|
||||
return []
|
||||
|
||||
detections = []
|
||||
for face in faces:
|
||||
bbox = face.bbox.astype(int)
|
||||
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
|
||||
|
||||
det = FaceDetection(
|
||||
bbox={"x": x1, "y": y1, "w": x2 - x1, "h": y2 - y1},
|
||||
embedding=face.normed_embedding, # Already L2-normalized
|
||||
detection_confidence=float(face.det_score),
|
||||
face_crop=frame[max(0, y1):y2, max(0, x1):x2],
|
||||
)
|
||||
detections.append(det)
|
||||
|
||||
return detections
|
||||
|
||||
def cosine_similarity(self, emb1: np.ndarray, emb2: np.ndarray) -> float:
|
||||
"""Cosine similarity between two normalized embeddings."""
|
||||
return float(np.dot(emb1, emb2))
|
||||
|
||||
def embeddings_to_list(self, embedding: np.ndarray) -> list[float]:
|
||||
"""Convert numpy embedding to list for pgvector storage."""
|
||||
return embedding.tolist()
|
||||
|
||||
|
||||
# Singleton
|
||||
face_service = FaceService()
|
||||
Reference in New Issue
Block a user