Spaces:
Running
Running
feat: friday commit
Browse files- src/api/people.py +1 -1
- src/common/utils.py +7 -0
- src/services/ai_manager.py +27 -13
- src/services/clustering.py +2 -0
src/api/people.py
CHANGED
|
@@ -94,7 +94,7 @@ async def get_cluster_images(
|
|
| 94 |
{
|
| 95 |
"cluster_id": "uuid",
|
| 96 |
"images": [
|
| 97 |
-
{"url": "...", "folder": "...", "face_crop": "<base64>"},
|
| 98 |
...
|
| 99 |
],
|
| 100 |
"total": 12
|
|
|
|
| 94 |
{
|
| 95 |
"cluster_id": "uuid",
|
| 96 |
"images": [
|
| 97 |
+
{"url": "...", "thumb_url": "...", "folder": "...", "face_crop": "<base64>"},
|
| 98 |
...
|
| 99 |
],
|
| 100 |
"total": 12
|
src/common/utils.py
CHANGED
|
@@ -73,6 +73,13 @@ def cld_thumb_url(url: str) -> str:
|
|
| 73 |
return url.replace("/upload/", "/upload/c_limit,w_500/")
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def face_ui_score(raw_score: float, mode: str = "fused") -> float:
|
| 77 |
"""
|
| 78 |
Platt-scaled probability score for the UI.
|
|
|
|
| 73 |
return url.replace("/upload/", "/upload/c_limit,w_500/")
|
| 74 |
|
| 75 |
|
| 76 |
+
def cld_face_thumb_url(url: str, width: int = 300) -> str:
|
| 77 |
+
"""Generate optimized thumbnail URL for face images (smaller than general thumbs)."""
|
| 78 |
+
if not url:
|
| 79 |
+
return ""
|
| 80 |
+
return url.replace("/upload/", f"/upload/c_limit,w_{width},q_30,f_auto/")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
def face_ui_score(raw_score: float, mode: str = "fused") -> float:
|
| 84 |
"""
|
| 85 |
Platt-scaled probability score for the UI.
|
src/services/ai_manager.py
CHANGED
|
@@ -398,23 +398,38 @@ class AIModelManager:
|
|
| 398 |
self, bgr_enhanced: np.ndarray, scale: tuple
|
| 399 |
) -> list:
|
| 400 |
H, W = bgr_enhanced.shape[:2]
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
bgr_scaled = bgr_enhanced
|
|
|
|
| 404 |
else:
|
|
|
|
|
|
|
|
|
|
| 405 |
bgr_scaled = cv2.resize(bgr_enhanced, (scale_w, scale_h))
|
| 406 |
try:
|
| 407 |
with self._face_lock:
|
| 408 |
# input_size must be set inside the lock — setting it outside
|
| 409 |
# is a race condition when two inference threads run concurrently,
|
| 410 |
# causing the wrong scale to be used and faces to be missed.
|
| 411 |
-
|
|
|
|
|
|
|
| 412 |
faces_at_scale = self.face_app.get(bgr_scaled)
|
| 413 |
sx, sy = W / scale_w, H / scale_h
|
| 414 |
for f in faces_at_scale:
|
| 415 |
if sx != 1.0 or sy != 1.0:
|
| 416 |
f.bbox[0] *= sx; f.bbox[1] *= sy
|
| 417 |
f.bbox[2] *= sx; f.bbox[3] *= sy
|
|
|
|
|
|
|
|
|
|
| 418 |
return faces_at_scale
|
| 419 |
except Exception:
|
| 420 |
return []
|
|
@@ -447,16 +462,15 @@ class AIModelManager:
|
|
| 447 |
|
| 448 |
if ENABLE_HORIZONTAL_FLIP:
|
| 449 |
bgr_flip = cv2.flip(bgr_enhanced, 1)
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
pass
|
| 460 |
|
| 461 |
self.face_app.det_model.input_size = DET_SIZE_PRIMARY
|
| 462 |
faces = _dedup_faces(all_raw_faces)
|
|
|
|
| 398 |
self, bgr_enhanced: np.ndarray, scale: tuple
|
| 399 |
) -> list:
|
| 400 |
H, W = bgr_enhanced.shape[:2]
|
| 401 |
+
# Preserve aspect ratio: scale longest side to match scale's longest side.
|
| 402 |
+
# The previous code clamped each dim independently which squashed wide
|
| 403 |
+
# images (e.g. 4032x1816 → 640x640) and produced distorted face crops
|
| 404 |
+
# whose embeddings would not match the same person shot in a normal
|
| 405 |
+
# aspect ratio.
|
| 406 |
+
target_max = max(scale[0], scale[1])
|
| 407 |
+
long_side = max(W, H)
|
| 408 |
+
if long_side <= target_max:
|
| 409 |
bgr_scaled = bgr_enhanced
|
| 410 |
+
scale_w, scale_h = W, H
|
| 411 |
else:
|
| 412 |
+
ratio = target_max / long_side
|
| 413 |
+
scale_w = max(1, int(round(W * ratio)))
|
| 414 |
+
scale_h = max(1, int(round(H * ratio)))
|
| 415 |
bgr_scaled = cv2.resize(bgr_enhanced, (scale_w, scale_h))
|
| 416 |
try:
|
| 417 |
with self._face_lock:
|
| 418 |
# input_size must be set inside the lock — setting it outside
|
| 419 |
# is a race condition when two inference threads run concurrently,
|
| 420 |
# causing the wrong scale to be used and faces to be missed.
|
| 421 |
+
# Use the actual scaled dims so the detector's letterboxing
|
| 422 |
+
# math matches the image we're feeding it.
|
| 423 |
+
self.face_app.det_model.input_size = (scale_w, scale_h)
|
| 424 |
faces_at_scale = self.face_app.get(bgr_scaled)
|
| 425 |
sx, sy = W / scale_w, H / scale_h
|
| 426 |
for f in faces_at_scale:
|
| 427 |
if sx != 1.0 or sy != 1.0:
|
| 428 |
f.bbox[0] *= sx; f.bbox[1] *= sy
|
| 429 |
f.bbox[2] *= sx; f.bbox[3] *= sy
|
| 430 |
+
if hasattr(f, 'kps') and f.kps is not None:
|
| 431 |
+
f.kps[:, 0] *= sx
|
| 432 |
+
f.kps[:, 1] *= sy
|
| 433 |
return faces_at_scale
|
| 434 |
except Exception:
|
| 435 |
return []
|
|
|
|
| 462 |
|
| 463 |
if ENABLE_HORIZONTAL_FLIP:
|
| 464 |
bgr_flip = cv2.flip(bgr_enhanced, 1)
|
| 465 |
+
# Reuse the aspect-ratio-preserving scaler so flipped detection
|
| 466 |
+
# also avoids the wide-image squash.
|
| 467 |
+
faces_flip = self._run_detection_at_scale(bgr_flip, DET_SIZE_PRIMARY)
|
| 468 |
+
for f in faces_flip:
|
| 469 |
+
x1, y1, x2, y2 = f.bbox
|
| 470 |
+
f.bbox[0], f.bbox[2] = W - x2, W - x1
|
| 471 |
+
if hasattr(f, 'kps') and f.kps is not None:
|
| 472 |
+
f.kps[:, 0] = W - f.kps[:, 0]
|
| 473 |
+
all_raw_faces.extend(faces_flip)
|
|
|
|
| 474 |
|
| 475 |
self.face_app.det_model.input_size = DET_SIZE_PRIMARY
|
| 476 |
faces = _dedup_faces(all_raw_faces)
|
src/services/clustering.py
CHANGED
|
@@ -37,6 +37,7 @@ from src.core.config import (
|
|
| 37 |
CLUSTER_MIN_SAMPLES, CLUSTER_MIN_CLUSTER_SIZE, CLUSTER_EPSILON,
|
| 38 |
FACE_SEARCH_TOP_K, CLUSTERING_BLUR_THRESHOLD,
|
| 39 |
)
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
# ──────────────────────────────────────────────────────────────
|
|
@@ -309,6 +310,7 @@ async def get_person_images(cluster_id: str, user_id: str) -> list[dict]:
|
|
| 309 |
seen.add(url)
|
| 310 |
out.append({
|
| 311 |
"url": url,
|
|
|
|
| 312 |
"folder": r.get("folder", ""),
|
| 313 |
"face_crop": r.get("face_crop", ""),
|
| 314 |
})
|
|
|
|
| 37 |
CLUSTER_MIN_SAMPLES, CLUSTER_MIN_CLUSTER_SIZE, CLUSTER_EPSILON,
|
| 38 |
FACE_SEARCH_TOP_K, CLUSTERING_BLUR_THRESHOLD,
|
| 39 |
)
|
| 40 |
+
from src.common.utils import cld_face_thumb_url
|
| 41 |
|
| 42 |
|
| 43 |
# ──────────────────────────────────────────────────────────────
|
|
|
|
| 310 |
seen.add(url)
|
| 311 |
out.append({
|
| 312 |
"url": url,
|
| 313 |
+
"thumb_url": cld_face_thumb_url(url),
|
| 314 |
"folder": r.get("folder", ""),
|
| 315 |
"face_crop": r.get("face_crop", ""),
|
| 316 |
})
|