feat: add debug mode, distance-based thresholds, and improve pose detection

- Add debug mode support for encoding statistics in API responses
  - Debug info includes encoding length, min/max/mean/std, and first 10 values
  - Frontend logs encoding stats to browser console when debug enabled
  - Identify page enables debug mode by default

- Implement distance-based confidence thresholds for stricter matching
  - Borderline distances require higher confidence (70-95% vs 50%)
  - Applied when use_distance_based_thresholds=True (auto-match)
  - Reduces false positives for borderline matches

- Dual tolerance system for auto-match
  - Default tolerance 0.6 for regular browsing (more lenient)
  - Run auto-match button uses 0.5 tolerance with distance-based thresholds (stricter)
  - Auto-accept threshold updated to 85% (from 70%)

- Enhance pose detection with single-eye detection
  - Profile threshold reduced from 30° to 15° (stricter)
  - Detect single-eye visibility for extreme profile views
  - Infer profile direction from landmark visibility
  - Improved face width threshold (20px vs 10px)

- Clean up debug code
  - Remove test photo UUID checks from production code
  - Remove debug print statements
  - Replace print statements with proper logging
This commit is contained in:
Tanya
2026-02-10 13:20:07 -05:00
parent 6b6b1449b2
commit a6ba78cd54
13 changed files with 326 additions and 76 deletions
+35 -12
View File
@@ -90,9 +90,9 @@ def process_faces(request: ProcessFacesRequest) -> ProcessFacesResponse:
job_timeout="1h", # Long timeout for face processing
)
print(f"[Faces API] Enqueued face processing job: {job.id}")
print(f"[Faces API] Job status: {job.get_status()}")
print(f"[Faces API] Queue length: {len(queue)}")
import logging
logger = logging.getLogger(__name__)
logger.info(f"Enqueued face processing job: {job.id}, status: {job.get_status()}, queue length: {len(queue)}")
return ProcessFacesResponse(
job_id=job.id,
@@ -197,12 +197,14 @@ def get_unidentified_faces(
def get_similar_faces(
face_id: int,
include_excluded: bool = Query(False, description="Include excluded faces in results"),
debug: bool = Query(False, description="Include debug information (encoding stats) in response"),
db: Session = Depends(get_db)
) -> SimilarFacesResponse:
"""Return similar unidentified faces for a given face."""
import logging
import numpy as np
logger = logging.getLogger(__name__)
logger.info(f"API: get_similar_faces called for face_id={face_id}, include_excluded={include_excluded}")
logger.info(f"API: get_similar_faces called for face_id={face_id}, include_excluded={include_excluded}, debug={debug}")
# Validate face exists
base = db.query(Face).filter(Face.id == face_id).first()
@@ -210,9 +212,23 @@ def get_similar_faces(
logger.warning(f"API: Face {face_id} not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Face {face_id} not found")
# Load base encoding for debug info if needed
base_debug_info = None
if debug:
from backend.services.face_service import load_face_encoding
base_enc = load_face_encoding(base.encoding)
base_debug_info = {
"encoding_length": len(base_enc),
"encoding_min": float(np.min(base_enc)),
"encoding_max": float(np.max(base_enc)),
"encoding_mean": float(np.mean(base_enc)),
"encoding_std": float(np.std(base_enc)),
"encoding_first_10": [float(x) for x in base_enc[:10].tolist()],
}
logger.info(f"API: Calling find_similar_faces for face_id={face_id}, include_excluded={include_excluded}")
# Use 0.6 tolerance for Identify People (more lenient for manual review)
results = find_similar_faces(db, face_id, tolerance=0.6, include_excluded=include_excluded)
results = find_similar_faces(db, face_id, tolerance=0.6, include_excluded=include_excluded, debug=debug)
logger.info(f"API: find_similar_faces returned {len(results)} results")
items = [
@@ -224,12 +240,13 @@ def get_similar_faces(
quality_score=float(f.quality_score),
filename=f.photo.filename if f.photo else "unknown",
pose_mode=getattr(f, "pose_mode", None) or "frontal",
debug_info=debug_info if debug else None,
)
for f, distance, confidence_pct in results
for f, distance, confidence_pct, debug_info in results
]
logger.info(f"API: Returning {len(items)} items for face_id={face_id}")
return SimilarFacesResponse(base_face_id=face_id, items=items)
return SimilarFacesResponse(base_face_id=face_id, items=items, debug_info=base_debug_info)
@router.post("/batch-similarity", response_model=BatchSimilarityResponse)
@@ -438,7 +455,9 @@ def get_face_crop(face_id: int, db: Session = Depends(get_db)) -> Response:
except HTTPException:
raise
except Exception as e:
print(f"[Faces API] get_face_crop error for face {face_id}: {e}")
import logging
logger = logging.getLogger(__name__)
logger.error(f"get_face_crop error for face {face_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to extract face crop: {str(e)}",
@@ -610,10 +629,12 @@ def auto_match_faces(
# Find matches for all identified people
# Filter by frontal reference faces if auto_accept enabled
# Use distance-based thresholds only when auto_accept is enabled (Run auto-match button)
matches_data = find_auto_match_matches(
db,
tolerance=request.tolerance,
filter_frontal_only=request.auto_accept
filter_frontal_only=request.auto_accept,
use_distance_based_thresholds=request.use_distance_based_thresholds or request.auto_accept
)
# If auto_accept enabled, process matches automatically
@@ -647,7 +668,9 @@ def auto_match_faces(
)
auto_accepted_faces += identified_count
except Exception as e:
print(f"Error auto-accepting matches for person {person_id}: {e}")
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error auto-accepting matches for person {person_id}: {e}")
if not matches_data:
return AutoMatchResponse(
@@ -750,7 +773,7 @@ def auto_match_faces(
@router.get("/auto-match/people", response_model=AutoMatchPeopleResponse)
def get_auto_match_people(
filter_frontal_only: bool = Query(False, description="Only include frontal/tilted reference faces"),
tolerance: float = Query(0.5, ge=0.0, le=1.0, description="Tolerance threshold"),
tolerance: float = Query(0.6, ge=0.0, le=1.0, description="Tolerance threshold (default 0.6 for regular auto-match)"),
db: Session = Depends(get_db),
) -> AutoMatchPeopleResponse:
"""Get list of people for auto-match (without matches) - fast initial load.
@@ -813,7 +836,7 @@ def get_auto_match_people(
@router.get("/auto-match/people/{person_id}/matches", response_model=AutoMatchPersonMatchesResponse)
def get_auto_match_person_matches(
person_id: int,
tolerance: float = Query(0.5, ge=0.0, le=1.0, description="Tolerance threshold"),
tolerance: float = Query(0.6, ge=0.0, le=1.0, description="Tolerance threshold (default 0.6 for regular auto-match)"),
filter_frontal_only: bool = Query(False, description="Only return frontal/tilted faces"),
db: Session = Depends(get_db),
) -> AutoMatchPersonMatchesResponse: