feat: Enhance getSimilar API and UI with excluded faces functionality

This commit updates the `getSimilar` API to include an optional parameter for excluding faces in the results. The Identify component is modified to utilize this new parameter, allowing users to filter out unwanted faces during identification. Additionally, the Help documentation is updated to reflect changes in the identification process, including new filtering options and user instructions for managing excluded faces. Overall, these enhancements improve the user experience and provide more control over face identification.
This commit is contained in:
tanyar09 2025-12-11 13:16:58 -05:00
parent 10f777f3cc
commit bca01a5ac3
5 changed files with 46 additions and 19 deletions

View File

@ -217,8 +217,10 @@ export const facesApi = {
})
return response.data
},
getSimilar: async (faceId: number): Promise<SimilarFacesResponse> => {
const response = await apiClient.get<SimilarFacesResponse>(`/api/v1/faces/${faceId}/similar`)
getSimilar: async (faceId: number, includeExcluded?: boolean): Promise<SimilarFacesResponse> => {
const response = await apiClient.get<SimilarFacesResponse>(`/api/v1/faces/${faceId}/similar`, {
params: { include_excluded: includeExcluded || false },
})
return response.data
},
batchSimilarity: async (request: BatchSimilarityRequest): Promise<BatchSimilarityResponse> => {

View File

@ -73,11 +73,11 @@ function NavigationOverview({ onPageClick }: { onPageClick: (page: PageId) => vo
const mainNavItems = [
{ id: 'scan' as PageId, icon: '🗂️', label: 'Scan', description: 'Import photos from folders or upload files' },
{ id: 'process' as PageId, icon: '⚙️', label: 'Process', description: 'Detect and process faces in photos' },
{ id: 'identify' as PageId, icon: '👤', label: 'Identify', description: 'Manually identify people in faces' },
{ id: 'identify' as PageId, icon: '👤', label: 'Identify People', description: 'Manually identify people in faces' },
{ id: 'auto-match' as PageId, icon: '🤖', label: 'Auto-Match', description: 'Automatically match similar faces to previously identified faces' },
{ id: 'search' as PageId, icon: '🔍', label: 'Search', description: 'Search and filter photos' },
{ id: 'modify' as PageId, icon: '✏️', label: 'Modify', description: 'Edit person information' },
{ id: 'tags' as PageId, icon: '🏷️', label: 'Tags', description: 'Tag photos and manage photo tags' },
{ id: 'search' as PageId, icon: '🔍', label: 'Search Photos', description: 'Search and filter photos' },
{ id: 'modify' as PageId, icon: '✏️', label: 'Modify People', description: 'Edit person information' },
{ id: 'tags' as PageId, icon: '🏷️', label: 'Tag Photos', description: 'Tag photos and manage photo tags' },
]
const maintenanceNavItems = [
@ -257,7 +257,7 @@ function ProcessPageHelp({ onBack }: { onBack: () => void }) {
function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
return (
<PageHelpLayout title="Identify Page" onBack={onBack}>
<PageHelpLayout title="Identify People" onBack={onBack}>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">Purpose</h3>
@ -270,11 +270,11 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
<h4 className="text-md font-semibold text-gray-700 mb-2">Features:</h4>
<ul className="list-disc list-inside space-y-1 text-gray-600 ml-4">
<li><strong>Face Navigation:</strong> Browse through unidentified faces using Prev/Next buttons</li>
<li><strong>Filters:</strong> Filter by date taken, tags</li>
<li><strong>Filters:</strong> Filter by date taken, tags, quality, and excluded faces</li>
<li><strong>Unique Faces Only:</strong> Hide similar faces (faces with 60%+ similarity to others)</li>
<li><strong>Batch Size:</strong> Control how many faces to load at once</li>
<li><strong>Similar Faces Panel:</strong> View and identify multiple similar faces at once</li>
<li><strong>Exclude Faces:</strong> Block faces from identification (e.g., for false detections or unwanted faces)</li>
</ul>
</div>
@ -288,11 +288,20 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
<li>Click "Filters" to expand filter options</li>
<li>Set date range, minimum quality, or select tags</li>
<li>Choose sort order (Quality, Date Taken, or Date Processed)</li>
<li>Check "Include excluded faces" to show faces that have been excluded from identification</li>
<li>Click "Apply Filters" to load faces</li>
</ul>
</li>
<li>Toggle "Unique faces only" checkbox to hide duplicate faces (recommended)</li>
<li>View the current face on the left panel</li>
<li>To exclude a face from identification:
<ul className="list-disc list-inside ml-4 mt-1">
<li>Click the 🚫 button next to the face counter</li>
<li>The face will be marked as excluded and automatically skipped</li>
<li>Excluded faces are hidden by default (check "Include excluded faces" in filters to see them)</li>
<li>Click the 🚫 button again to include the face back in identification</li>
</ul>
</li>
<li>Choose how to identify:
<ul className="list-disc list-inside ml-4 mt-1">
<li><strong>Select existing person:</strong> Choose from the dropdown at the top</li>
@ -370,6 +379,8 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
<li>Tag filtering lets you identify faces only from photos with specific tags</li>
<li>Click on face images to open the full photo in a new window</li>
<li>For videos, you can identify multiple people in the same video</li>
<li>Use the exclude (🚫) button to exclude unknown, unwanted faces, or low-quality faces from appearing in your identification workflow</li>
<li>Excluded faces are hidden by default, but you can view them by checking "Include excluded faces" in the filters</li>
</ul>
</div>
@ -468,7 +479,7 @@ function AutoMatchPageHelp({ onBack }: { onBack: () => void }) {
function SearchPageHelp({ onBack }: { onBack: () => void }) {
return (
<PageHelpLayout title="Search Page" onBack={onBack}>
<PageHelpLayout title="Search Photos" onBack={onBack}>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">Purpose</h3>
@ -580,7 +591,7 @@ function SearchPageHelp({ onBack }: { onBack: () => void }) {
function ModifyPageHelp({ onBack }: { onBack: () => void }) {
return (
<PageHelpLayout title="Modify Page" onBack={onBack}>
<PageHelpLayout title="Modify People" onBack={onBack}>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">Purpose</h3>
@ -701,7 +712,7 @@ function ModifyPageHelp({ onBack }: { onBack: () => void }) {
function TagsPageHelp({ onBack }: { onBack: () => void }) {
return (
<PageHelpLayout title="Tags Page" onBack={onBack}>
<PageHelpLayout title="Tag Photos" onBack={onBack}>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">Purpose</h3>

View File

@ -348,7 +348,7 @@ export default function Identify() {
return
}
try {
const res = await facesApi.getSimilar(faceId)
const res = await facesApi.getSimilar(faceId, includeExcludedFaces)
setSimilar(res.items || [])
setSelectedSimilar({})
} catch (error) {

View File

@ -192,11 +192,15 @@ def get_unidentified_faces(
@router.get("/{face_id}/similar", response_model=SimilarFacesResponse)
def get_similar_faces(face_id: int, db: Session = Depends(get_db)) -> SimilarFacesResponse:
def get_similar_faces(
face_id: int,
include_excluded: bool = Query(False, description="Include excluded faces in results"),
db: Session = Depends(get_db)
) -> SimilarFacesResponse:
"""Return similar unidentified faces for a given face."""
import logging
logger = logging.getLogger(__name__)
logger.info(f"API: get_similar_faces called for face_id={face_id}")
logger.info(f"API: get_similar_faces called for face_id={face_id}, include_excluded={include_excluded}")
# Validate face exists
base = db.query(Face).filter(Face.id == face_id).first()
@ -204,8 +208,8 @@ def get_similar_faces(face_id: int, db: Session = Depends(get_db)) -> SimilarFac
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")
logger.info(f"API: Calling find_similar_faces for face_id={face_id}")
results = find_similar_faces(db, face_id)
logger.info(f"API: Calling find_similar_faces for face_id={face_id}, include_excluded={include_excluded}")
results = find_similar_faces(db, face_id, include_excluded=include_excluded)
logger.info(f"API: find_similar_faces returned {len(results)} results")
items = [

View File

@ -1512,6 +1512,7 @@ def find_similar_faces(
limit: int = 20000, # Very high default limit - effectively unlimited
tolerance: float = 0.6, # DEFAULT_FACE_TOLERANCE from desktop
filter_frontal_only: bool = False, # New: Only return frontal or tilted faces (not profile)
include_excluded: bool = False, # Include excluded faces in results
) -> List[Tuple[Face, float, float]]: # Returns (face, distance, confidence_pct)
"""Find similar faces matching desktop logic exactly.
@ -1528,6 +1529,7 @@ def find_similar_faces(
Args:
filter_frontal_only: Only return frontal or tilted faces (not profile)
include_excluded: Include excluded faces in results (default: False)
"""
from src.web.db.models import Photo
@ -1586,6 +1588,10 @@ def find_similar_faces(
is_unidentified = f.person_id is None
if is_unidentified and confidence_pct >= 40:
# Filter by excluded status if not including excluded faces
if not include_excluded and getattr(f, "excluded", False):
continue
# Filter by pose_mode if requested (only frontal or tilted faces)
if filter_frontal_only and not _is_acceptable_pose_for_auto_match(f.pose_mode):
continue
@ -1848,9 +1854,11 @@ def find_auto_match_matches(
# Desktop: similar_faces = self.face_processor._get_filtered_similar_faces(
# reference_face_id, tolerance, include_same_photo=False, face_status=None)
# This filters by: person_id is None (unidentified), confidence >= 40%, sorts by distance
# Auto-match always excludes excluded faces
similar_faces = find_similar_faces(
db, reference_face_id, tolerance=tolerance,
filter_frontal_only=filter_frontal_only
filter_frontal_only=filter_frontal_only,
include_excluded=False # Auto-match always excludes excluded faces
)
if similar_faces:
@ -1994,9 +2002,11 @@ def get_auto_match_person_matches(
return []
# Find similar faces using existing function
# Auto-match always excludes excluded faces
similar_faces = find_similar_faces(
db, reference_face.id, tolerance=tolerance,
filter_frontal_only=filter_frontal_only
filter_frontal_only=filter_frontal_only,
include_excluded=False # Auto-match always excludes excluded faces
)
return similar_faces