Tanya 6b6b1449b2 Modified files:
backend/config.py - Added MIN_AUTO_MATCH_FACE_SIZE_RATIO = 0.005
backend/services/face_service.py - Multiple changes:
Added load_face_encoding() function (supports float32 and float64)
Added _calculate_face_size_ratio() function
Updated find_similar_faces() to filter small faces
Updated find_auto_match_matches() to exclude small reference faces
Fixed reference face quality calculation (use actual quality, not hardcoded 0.5)
Fixed duplicate detection (exclude faces from same photo)
Updated confidence threshold from 40% to 50%
Updated confidence calibration (moderate version)
backend/api/faces.py - Updated default tolerance to 0.5 for auto-match endpoints
backend/schemas/faces.py - Updated default tolerance to 0.5
admin-frontend/src/pages/AutoMatch.tsx - Updated default tolerance to 0.5
admin-frontend/src/api/faces.ts - Added tolerance parameter support
2026-02-06 14:16:11 -05:00

349 lines
9.7 KiB
Python

"""Face processing and identify workflow schemas."""
from __future__ import annotations
from datetime import date
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
class ProcessFacesRequest(BaseModel):
"""Request to process faces in photos."""
model_config = ConfigDict(protected_namespaces=())
batch_size: Optional[int] = Field(
None,
ge=1,
description="Maximum number of photos to process (None = all unprocessed)",
)
detector_backend: str = Field(
"retinaface",
description="DeepFace detector backend (retinaface, mtcnn, opencv, ssd)",
)
model_name: str = Field(
"ArcFace",
description="DeepFace model name (ArcFace, Facenet, Facenet512, VGG-Face)",
)
class ProcessFacesResponse(BaseModel):
"""Response after initiating face processing."""
model_config = ConfigDict(protected_namespaces=())
job_id: str
message: str
batch_size: Optional[int] = None
detector_backend: str
model_name: str
class FaceItem(BaseModel):
"""Minimal face item for list views."""
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
id: int
photo_id: int
quality_score: float
face_confidence: float
location: str
pose_mode: Optional[str] = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
excluded: bool = Field(False, description="Whether this face is excluded from identification")
class UnidentifiedFacesQuery(BaseModel):
"""Query params for listing unidentified faces."""
model_config = ConfigDict(protected_namespaces=())
page: int = 1
page_size: int = 50
min_quality: float = 0.0
date_from: Optional[date] = None
date_to: Optional[date] = None
sort_by: str = Field("quality", description="quality|date_taken|date_added")
sort_dir: str = Field("desc", description="asc|desc")
class UnidentifiedFacesResponse(BaseModel):
"""Paginated unidentified faces list."""
model_config = ConfigDict(protected_namespaces=())
items: list[FaceItem]
page: int
page_size: int
total: int
class SimilarFaceItem(BaseModel):
"""Similar face with similarity score (0-1)."""
id: int
photo_id: int
similarity: float
location: str
quality_score: float
filename: str
pose_mode: Optional[str] = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
class SimilarFacesResponse(BaseModel):
"""Response containing similar faces for a given face."""
model_config = ConfigDict(protected_namespaces=())
base_face_id: int
items: list[SimilarFaceItem]
class BatchSimilarityRequest(BaseModel):
"""Request to get similarities between multiple faces."""
model_config = ConfigDict(protected_namespaces=())
face_ids: list[int] = Field(..., description="List of face IDs to calculate similarities for")
min_confidence: float = Field(60.0, ge=0.0, le=100.0, description="Minimum confidence percentage (0-100)")
class FaceSimilarityPair(BaseModel):
"""A pair of similar faces with their similarity score."""
model_config = ConfigDict(protected_namespaces=())
face_id_1: int
face_id_2: int
similarity: float # 0-1 range
confidence_pct: float # 0-100 range
class BatchSimilarityResponse(BaseModel):
"""Response containing similarities between face pairs."""
model_config = ConfigDict(protected_namespaces=())
pairs: list[FaceSimilarityPair] = Field(..., description="List of similar face pairs")
class IdentifyFaceRequest(BaseModel):
"""Identify a face by selecting existing or creating new person."""
model_config = ConfigDict(protected_namespaces=())
# Either provide person_id or the fields to create new person
person_id: Optional[int] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
middle_name: Optional[str] = None
maiden_name: Optional[str] = None
date_of_birth: Optional[date] = None
# Optionally identify a batch of face IDs along with this one
additional_face_ids: Optional[list[int]] = None
class IdentifyFaceResponse(BaseModel):
"""Result of identify operation."""
model_config = ConfigDict(protected_namespaces=())
identified_face_ids: list[int]
person_id: int
created_person: bool
class FaceUnmatchResponse(BaseModel):
"""Result of unmatch operation."""
model_config = ConfigDict(protected_namespaces=())
face_id: int
message: str
class BatchUnmatchRequest(BaseModel):
"""Request to batch unmatch multiple faces."""
model_config = ConfigDict(protected_namespaces=())
face_ids: list[int] = Field(..., min_items=1)
class BatchUnmatchResponse(BaseModel):
"""Result of batch unmatch operation."""
model_config = ConfigDict(protected_namespaces=())
unmatched_face_ids: list[int]
count: int
message: str
class PersonFaceItem(BaseModel):
"""Face item for person's faces list (includes photo info)."""
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
id: int
photo_id: int
photo_path: str
photo_filename: str
location: str
face_confidence: float
quality_score: float
detector_backend: str
model_name: str
class PersonFacesResponse(BaseModel):
"""Response containing all faces for a person."""
model_config = ConfigDict(protected_namespaces=())
person_id: int
items: list[PersonFaceItem]
total: int
class AutoMatchRequest(BaseModel):
"""Request to start auto-match process."""
model_config = ConfigDict(protected_namespaces=())
tolerance: float = Field(0.5, ge=0.0, le=1.0, description="Tolerance threshold (lower = stricter matching)")
auto_accept: bool = Field(False, description="Enable automatic acceptance of matching faces")
auto_accept_threshold: float = Field(70.0, ge=0.0, le=100.0, description="Similarity threshold for auto-acceptance (0-100%)")
class AutoMatchFaceItem(BaseModel):
"""Unidentified face match for a person."""
model_config = ConfigDict(protected_namespaces=())
id: int
photo_id: int
photo_filename: str
location: str
quality_score: float
similarity: float # Confidence percentage (0-100)
distance: float
pose_mode: str = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
class AutoMatchPersonItem(BaseModel):
"""Person with matches for auto-match workflow."""
model_config = ConfigDict(protected_namespaces=())
person_id: int
person_name: str
reference_face_id: int
reference_photo_id: int
reference_photo_filename: str
reference_location: str
reference_pose_mode: str = Field("frontal", description="Reference face pose classification")
face_count: int # Number of faces already identified for this person
matches: list[AutoMatchFaceItem]
total_matches: int
class AutoMatchPersonSummary(BaseModel):
"""Person summary without matches (for fast initial load)."""
model_config = ConfigDict(protected_namespaces=())
person_id: int
person_name: str
reference_face_id: int
reference_photo_id: int
reference_photo_filename: str
reference_location: str
reference_pose_mode: str = Field("frontal", description="Reference face pose classification")
face_count: int # Number of faces already identified for this person
total_matches: int = Field(0, description="Total matches (loaded separately)")
class AutoMatchPeopleResponse(BaseModel):
"""Response containing people list without matches (for fast initial load)."""
model_config = ConfigDict(protected_namespaces=())
people: list[AutoMatchPersonSummary]
total_people: int
class AutoMatchPersonMatchesResponse(BaseModel):
"""Response containing matches for a specific person."""
model_config = ConfigDict(protected_namespaces=())
person_id: int
matches: list[AutoMatchFaceItem]
total_matches: int
class AutoMatchResponse(BaseModel):
"""Response from auto-match start operation."""
model_config = ConfigDict(protected_namespaces=())
people: list[AutoMatchPersonItem]
total_people: int
total_matches: int
auto_accepted: bool = Field(False, description="Whether auto-acceptance was performed")
auto_accepted_faces: int = Field(0, description="Number of faces automatically accepted")
skipped_persons: int = Field(0, description="Number of persons skipped (non-frontal reference)")
skipped_matches: int = Field(0, description="Number of matches skipped (didn't meet criteria)")
class AcceptMatchesRequest(BaseModel):
"""Request to accept matches for a person."""
model_config = ConfigDict(protected_namespaces=())
face_ids: list[int] = Field(..., min_items=0, description="Face IDs to identify with this person")
class MaintenanceFaceItem(BaseModel):
"""Face item for maintenance view with person info and file path."""
model_config = ConfigDict(protected_namespaces=())
id: int
photo_id: int
photo_path: str
photo_filename: str
quality_score: float
person_id: Optional[int] = None
person_name: Optional[str] = None # Full name if identified
excluded: bool
class MaintenanceFacesResponse(BaseModel):
"""Response containing all faces for maintenance."""
model_config = ConfigDict(protected_namespaces=())
items: list[MaintenanceFaceItem]
total: int
class DeleteFacesRequest(BaseModel):
"""Request to delete multiple faces."""
model_config = ConfigDict(protected_namespaces=())
face_ids: list[int] = Field(..., min_items=1, description="Face IDs to delete")
class DeleteFacesResponse(BaseModel):
"""Response after deleting faces."""
model_config = ConfigDict(protected_namespaces=())
deleted_face_ids: list[int]
count: int
message: str