Files
mule-image/backend/app/services/vision/clustering.py
dtoro 9282a5c734 feat: add vision pipeline scaffolding with ONNX backend
Introduce the app/services/vision/ module with ABC interfaces, ONNX
Runtime backend, model registry, and per-task implementations:
- OpenCLIP ViT-B/32 embedder (image + text, 512-d)
- RapidOCR engine (PP-OCRv4 via ONNX, no PaddlePaddle)
- YOLOv8n object detector (raw ONNX, no ultralytics runtime)
- YuNet + SFace face processor (Apache 2.0, opencv_zoo, 128-d)
- DBSCAN face clustering helper

Add VisionSettings to config (mulita.yml + Pydantic), bootstrap_models.py
for first-boot weight downloads, models_data Docker volume, and ROCm
backend stub for future GPU acceleration.

No Celery tasks wired yet — models load but nothing invokes them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:00:06 +02:00

43 lines
1.2 KiB
Python

"""
Face embedding clustering using DBSCAN with cosine distance.
Called by the periodic `recluster_faces` Celery task (PR7).
"""
import logging
import numpy as np
from sklearn.cluster import DBSCAN
logger = logging.getLogger(__name__)
def cluster_faces(
embeddings: np.ndarray,
eps: float = 0.35,
min_samples: int = 2,
) -> np.ndarray:
"""Cluster face embeddings using DBSCAN with cosine metric.
Args:
embeddings: (N, D) float32 array of L2-normalized face embeddings.
eps: Maximum cosine distance between two samples to be in the
same neighborhood. Lower = tighter clusters.
min_samples: Minimum cluster size.
Returns:
(N,) int array of cluster labels. -1 = noise / unclustered.
"""
if len(embeddings) < min_samples:
return np.full(len(embeddings), -1, dtype=int)
db = DBSCAN(eps=eps, min_samples=min_samples, metric="cosine")
labels = db.fit_predict(embeddings)
n_clusters = len(set(labels) - {-1})
n_noise = (labels == -1).sum()
logger.info(
"Face clustering: %d embeddings → %d clusters, %d noise",
len(embeddings), n_clusters, n_noise,
)
return labels