fix: face detection — use OpenCV FaceDetectorYN and full-res originals
- Rewrite faces.py to use cv2.FaceDetectorYN instead of raw ONNX (handles multi-scale anchor decoding and NMS internally) - Load original photo files at up to 4000px for face detection instead of 240px thumbnails — faces were too small to detect at thumbnail res - Falls back to thumbnail if original is unavailable Tested: 33 faces extracted from 13 photos, clustered into 1 person. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,42 @@ def detect_objects(photo_id: str):
|
||||
return {'status': 'success', 'photo_id': photo_id, 'objects': len(detections)}
|
||||
|
||||
|
||||
def _load_original(photo_id: str) -> np.ndarray | None:
|
||||
"""Load the original photo file as an RGB numpy array, resized to
|
||||
max 1280px on the longest edge for face detection."""
|
||||
from sqlalchemy import create_engine, select as sa_select, text as sa_text
|
||||
from app.models import Photo
|
||||
|
||||
session = _get_sync_session()
|
||||
try:
|
||||
photo = session.execute(
|
||||
sa_select(Photo).where(Photo.id == photo_id)
|
||||
).scalar_one_or_none()
|
||||
if not photo or not photo.filepath:
|
||||
return None
|
||||
filepath = photo.filepath
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if not Path(filepath).exists():
|
||||
logger.warning("Original file not found: %s", filepath)
|
||||
return None
|
||||
|
||||
try:
|
||||
img = Image.open(filepath).convert("RGB")
|
||||
# Cap at 4000px on longest edge to avoid OOM, but keep as large
|
||||
# as possible for face detection accuracy
|
||||
max_dim = 4000
|
||||
w, h = img.size
|
||||
if max(w, h) > max_dim:
|
||||
scale = max_dim / max(w, h)
|
||||
img = img.resize((int(w * scale), int(h * scale)), Image.BICUBIC)
|
||||
return np.array(img)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to load original %s: %s", filepath, e)
|
||||
return None
|
||||
|
||||
|
||||
@shared_task(name='extract_faces', queue='vision')
|
||||
def extract_faces(photo_id: str):
|
||||
"""Detect faces and store recognition embeddings. Clustering is
|
||||
@@ -213,9 +249,13 @@ def extract_faces(photo_id: str):
|
||||
if not settings.vision.enabled or not settings.vision.faces.enabled:
|
||||
return {'status': 'skipped', 'reason': 'faces disabled'}
|
||||
|
||||
image = _load_thumb(photo_id, "large") # 1280px for better face detection
|
||||
# Use original file for face detection — thumbnails are often too
|
||||
# small (240px) for reliable face detection.
|
||||
image = _load_original(photo_id)
|
||||
if image is None:
|
||||
return {'status': 'error', 'message': 'thumbnail not found'}
|
||||
image = _load_thumb(photo_id, "large")
|
||||
if image is None:
|
||||
return {'status': 'error', 'message': 'no image available'}
|
||||
|
||||
from app.services.vision.registry import registry
|
||||
face_proc = registry.get_face_processor()
|
||||
|
||||
Reference in New Issue
Block a user