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:
+35
-12
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user