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:
parent
10f777f3cc
commit
bca01a5ac3
@ -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> => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user