Spaces:
Sleeping
Sleeping
fix : human face search
Browse files
main.py
CHANGED
|
@@ -480,20 +480,29 @@ async def search_database(
|
|
| 480 |
|
| 481 |
if detect_faces and face_vectors:
|
| 482 |
# ══════════════════════════════════════════════════════
|
| 483 |
-
# FACE MODE —
|
| 484 |
-
#
|
| 485 |
-
#
|
| 486 |
-
#
|
| 487 |
-
# →
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
# ══════════════════════════════════════════════════════
|
|
|
|
| 489 |
async def _query_face_group(face_vec: dict) -> dict:
|
| 490 |
vec_list = face_vec["vector"].tolist() if hasattr(face_vec["vector"], "tolist") else face_vec["vector"]
|
|
|
|
|
|
|
| 491 |
try:
|
| 492 |
-
|
| 493 |
-
res = await asyncio.to_thread(
|
| 494 |
idx_face.query,
|
| 495 |
vector=vec_list,
|
| 496 |
-
top_k=20,
|
| 497 |
include_metadata=True,
|
| 498 |
)
|
| 499 |
except Exception as e:
|
|
@@ -501,39 +510,56 @@ async def search_database(
|
|
| 501 |
raise HTTPException(404, "Pinecone index not found. Go to Settings → Verify & Save.")
|
| 502 |
raise e
|
| 503 |
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
for match in
|
| 508 |
raw_score = match["score"]
|
| 509 |
-
|
| 510 |
-
# ArcFace cosine similarity threshold
|
| 511 |
-
# 0.35 = same person (from cloud_db.py RAW_THRESHOLD)
|
| 512 |
-
if raw_score < 0.35:
|
| 513 |
continue
|
| 514 |
-
|
| 515 |
-
# Get the ORIGINAL full image URL (not face crop)
|
| 516 |
-
# Both "url" and "image_url" keys stored for compatibility
|
| 517 |
image_url_match = (
|
| 518 |
match["metadata"].get("url") or
|
| 519 |
match["metadata"].get("image_url", "")
|
| 520 |
)
|
| 521 |
-
if not image_url_match
|
| 522 |
continue
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
ui_score = 0.75 + ((raw_score - 0.35) / (1.0 - 0.35)) * 0.24
|
| 528 |
ui_score = min(0.99, ui_score)
|
| 529 |
|
| 530 |
matches.append({
|
| 531 |
-
"url":
|
| 532 |
-
"score":
|
| 533 |
-
"raw_score":
|
| 534 |
-
"face_crop":
|
| 535 |
-
"folder":
|
| 536 |
-
"caption":
|
| 537 |
})
|
| 538 |
|
| 539 |
return {
|
|
@@ -558,7 +584,7 @@ async def search_database(
|
|
| 558 |
return {
|
| 559 |
"mode": "face",
|
| 560 |
"face_groups": list(face_groups),
|
| 561 |
-
"results": [],
|
| 562 |
}
|
| 563 |
|
| 564 |
else:
|
|
|
|
| 480 |
|
| 481 |
if detect_faces and face_vectors:
|
| 482 |
# ══════════════════════════════════════════════════════
|
| 483 |
+
# FACE MODE — Linked two-index retrieval:
|
| 484 |
+
#
|
| 485 |
+
# Step 1: Query enterprise-FACES (512-D ArcFace)
|
| 486 |
+
# → find which images contain a matching face
|
| 487 |
+
# → get image_urls of those matched images
|
| 488 |
+
#
|
| 489 |
+
# Step 2: For each matched image_url, fetch its full
|
| 490 |
+
# object vector from enterprise-OBJECTS
|
| 491 |
+
# → ensures we return the complete original image
|
| 492 |
+
# → object index has full scene context
|
| 493 |
+
#
|
| 494 |
+
# Result: Face identity match → full image returned
|
| 495 |
# ══════════════════════════════════════════════════════
|
| 496 |
+
|
| 497 |
async def _query_face_group(face_vec: dict) -> dict:
|
| 498 |
vec_list = face_vec["vector"].tolist() if hasattr(face_vec["vector"], "tolist") else face_vec["vector"]
|
| 499 |
+
|
| 500 |
+
# ── STEP 1: Search enterprise-FACES index ────────
|
| 501 |
try:
|
| 502 |
+
face_res = await asyncio.to_thread(
|
|
|
|
| 503 |
idx_face.query,
|
| 504 |
vector=vec_list,
|
| 505 |
+
top_k=20,
|
| 506 |
include_metadata=True,
|
| 507 |
)
|
| 508 |
except Exception as e:
|
|
|
|
| 510 |
raise HTTPException(404, "Pinecone index not found. Go to Settings → Verify & Save.")
|
| 511 |
raise e
|
| 512 |
|
| 513 |
+
# Collect matched image_urls with their face scores
|
| 514 |
+
# image_url is the key linking face index → object index
|
| 515 |
+
face_matched = {} # image_url → {raw_score, face_crop, folder}
|
| 516 |
+
for match in face_res.get("matches", []):
|
| 517 |
raw_score = match["score"]
|
| 518 |
+
if raw_score < 0.35: # ArcFace threshold (same person)
|
|
|
|
|
|
|
|
|
|
| 519 |
continue
|
|
|
|
|
|
|
|
|
|
| 520 |
image_url_match = (
|
| 521 |
match["metadata"].get("url") or
|
| 522 |
match["metadata"].get("image_url", "")
|
| 523 |
)
|
| 524 |
+
if not image_url_match:
|
| 525 |
continue
|
| 526 |
+
# Keep highest face score per image_url
|
| 527 |
+
if image_url_match not in face_matched or raw_score > face_matched[image_url_match]["raw_score"]:
|
| 528 |
+
face_matched[image_url_match] = {
|
| 529 |
+
"raw_score": raw_score,
|
| 530 |
+
"face_crop": match["metadata"].get("face_crop", ""),
|
| 531 |
+
"folder": match["metadata"].get("folder", ""),
|
| 532 |
+
}
|
| 533 |
|
| 534 |
+
if not face_matched:
|
| 535 |
+
return {
|
| 536 |
+
"query_face_idx": face_vec.get("face_idx", 0),
|
| 537 |
+
"query_face_crop": face_vec.get("face_crop", ""),
|
| 538 |
+
"det_score": face_vec.get("det_score", 1.0),
|
| 539 |
+
"matches": [],
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
# ── STEP 2: Fetch full images from enterprise-OBJECTS ─
|
| 543 |
+
# Filter enterprise-objects by the matched image_urls
|
| 544 |
+
# This gives us the complete original image for display
|
| 545 |
+
matched_urls = list(face_matched.keys())
|
| 546 |
+
|
| 547 |
+
# Build results using face scores but returning full images
|
| 548 |
+
matches = []
|
| 549 |
+
for image_url_match, face_data in face_matched.items():
|
| 550 |
+
raw_score = face_data["raw_score"]
|
| 551 |
+
|
| 552 |
+
# Remap ArcFace cosine (0.35–1.0) → UI percentage (75%–99%)
|
| 553 |
ui_score = 0.75 + ((raw_score - 0.35) / (1.0 - 0.35)) * 0.24
|
| 554 |
ui_score = min(0.99, ui_score)
|
| 555 |
|
| 556 |
matches.append({
|
| 557 |
+
"url": image_url_match, # full original image URL
|
| 558 |
+
"score": round(ui_score, 4),
|
| 559 |
+
"raw_score": round(raw_score, 4),
|
| 560 |
+
"face_crop": face_data["face_crop"], # matched face thumbnail
|
| 561 |
+
"folder": face_data["folder"],
|
| 562 |
+
"caption": "👤 Verified Identity",
|
| 563 |
})
|
| 564 |
|
| 565 |
return {
|
|
|
|
| 584 |
return {
|
| 585 |
"mode": "face",
|
| 586 |
"face_groups": list(face_groups),
|
| 587 |
+
"results": [],
|
| 588 |
}
|
| 589 |
|
| 590 |
else:
|