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:
Nemo
2026-03-12 12:11:02 +08:00
commit 7c43e4580d
24 changed files with 1650 additions and 0 deletions

View 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()