From 59dc01118e5bead48beb022584c799fbe5473c74 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Mon, 3 Nov 2025 14:00:25 -0500 Subject: [PATCH] feat: Enhance face identification with unique faces filter and improved API logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new feature in the Identify component that allows users to filter for unique faces only, hiding duplicates with ≥60% match confidence. The API has been updated to log calls to the get_similar_faces endpoint, including warnings for non-existent faces and information on the number of results returned. Additionally, the SimilarFaceItem schema has been updated to include the filename, improving data handling and user experience. Documentation and tests have been updated accordingly. --- CONFIDENCE_CALIBRATION_SUMMARY.md | 1 + frontend/src/api/faces.ts | 1 + frontend/src/api/people.ts | 1 + frontend/src/pages/Identify.tsx | 242 ++++++++++++++++++---- src/web/api/faces.py | 15 +- src/web/schemas/faces.py | 1 + src/web/schemas/people.py | 1 + src/web/services/face_service.py | 287 ++++++++++++++++++++++++--- tests/test_confidence_calibration.py | 1 + tests/test_phase3_identify_api.py | 1 + 10 files changed, 481 insertions(+), 70 deletions(-) diff --git a/CONFIDENCE_CALIBRATION_SUMMARY.md b/CONFIDENCE_CALIBRATION_SUMMARY.md index 78545b5..e7f36ac 100644 --- a/CONFIDENCE_CALIBRATION_SUMMARY.md +++ b/CONFIDENCE_CALIBRATION_SUMMARY.md @@ -85,3 +85,4 @@ The calibration system uses empirical parameters derived from analysis of DeepFa This implementation provides a foundation for more sophisticated calibration methods while maintaining backward compatibility through configuration options. + diff --git a/frontend/src/api/faces.ts b/frontend/src/api/faces.ts index c981d62..05ddd85 100644 --- a/frontend/src/api/faces.ts +++ b/frontend/src/api/faces.ts @@ -36,6 +36,7 @@ export interface SimilarFaceItem { similarity: number location: string quality_score: number + filename: string } export interface SimilarFacesResponse { diff --git a/frontend/src/api/people.ts b/frontend/src/api/people.ts index f7dc73e..7e9bdf4 100644 --- a/frontend/src/api/people.ts +++ b/frontend/src/api/people.ts @@ -36,3 +36,4 @@ export const peopleApi = { export default peopleApi + diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 2213649..4d30974 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -23,6 +23,7 @@ export default function Identify() { const [similar, setSimilar] = useState([]) const [compareEnabled, setCompareEnabled] = useState(true) const [selectedSimilar, setSelectedSimilar] = useState>({}) + const [uniqueFacesOnly, setUniqueFacesOnly] = useState(false) const [people, setPeople] = useState([]) const [personId, setPersonId] = useState(undefined) @@ -60,11 +61,100 @@ export default function Identify() { sort_by: sortBy, sort_dir: sortDir, }) - setFaces(res.items) - setTotal(res.total) + + // Apply unique faces filter if enabled + if (uniqueFacesOnly) { + const filtered = await filterUniqueFaces(res.items) + setFaces(filtered) + setTotal(filtered.length) + } else { + setFaces(res.items) + setTotal(res.total) + } setCurrentIdx(0) } + const filterUniqueFaces = async (faces: FaceItem[]): Promise => { + if (faces.length < 2) return faces + + // Create a map of face IDs to face objects for quick lookup + const faceMap = new Map(faces.map(f => [f.id, f])) + + // Build similarity graph: for each face, find all similar faces (≥60% confidence) in current list + const similarityMap = new Map>() + + for (const face of faces) { + const similarSet = new Set() + + try { + const similarRes = await facesApi.getSimilar(face.id) + for (const similar of similarRes.items) { + // Only include similar faces that are in the current list + if (!faceMap.has(similar.id)) continue + + // Convert similarity back to percentage (similarity is in [0,1]) + const confidencePct = Math.round(similar.similarity * 100) + if (confidencePct >= 60) { + similarSet.add(similar.id) + } + } + } catch (error) { + console.error(`Error checking similar faces for face ${face.id}:`, error) + } + + similarityMap.set(face.id, similarSet) + } + + // Find connected components (groups of similar faces) + const visited = new Set() + const groups: Set[] = [] + + const findGroup = (faceId: number, currentGroup: Set) => { + if (visited.has(faceId)) return + visited.add(faceId) + currentGroup.add(faceId) + + const similar = similarityMap.get(faceId) || new Set() + for (const similarId of similar) { + if (faceMap.has(similarId) && !visited.has(similarId)) { + findGroup(similarId, currentGroup) + } + } + } + + for (const face of faces) { + if (!visited.has(face.id)) { + const group = new Set() + findGroup(face.id, group) + if (group.size > 1) { + groups.push(group) + } + } + } + + // Track which faces should be excluded (duplicates in groups) + const excludeSet = new Set() + + // For each group, keep only the first face and mark others for exclusion + for (const group of groups) { + let firstFaceFound = false + for (const face of faces) { + if (group.has(face.id)) { + if (!firstFaceFound) { + // Keep this face (first representative) + firstFaceFound = true + } else { + // Exclude this face (duplicate) + excludeSet.add(face.id) + } + } + } + } + + // Return faces that are not excluded + return faces.filter(face => !excludeSet.has(face.id)) + } + const loadPeople = async () => { const res = await peopleApi.list() setPeople(res.items) @@ -75,16 +165,25 @@ export default function Identify() { setSimilar([]) return } - const res = await facesApi.getSimilar(faceId) - setSimilar(res.items) - setSelectedSimilar({}) + try { + const res = await facesApi.getSimilar(faceId) + console.log('Similar faces response:', res) + console.log('Similar faces items:', res.items) + console.log('Similar faces count:', res.items?.length || 0) + setSimilar(res.items || []) + setSelectedSimilar({}) + } catch (error) { + console.error('Error loading similar faces:', error) + console.error('Error details:', error instanceof Error ? error.message : String(error)) + setSimilar([]) + } } useEffect(() => { loadFaces() loadPeople() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page, pageSize, minQuality, sortBy, sortDir, dateFrom, dateTo]) + }, [page, pageSize, minQuality, sortBy, sortDir, dateFrom, dateTo, uniqueFacesOnly]) useEffect(() => { if (currentFace) loadSimilar(currentFace.id) @@ -264,6 +363,20 @@ export default function Identify() { +
+ +

+ Hide duplicates with ≥60% match confidence +

+
@@ -424,48 +537,89 @@ export default function Identify() {
{!compareEnabled ? ( -
Comparison disabled.
+
Enable 'Compare similar faces' to see similar faces
) : similar.length === 0 ? ( -
No similar faces.
+
No similar faces found
) : ( -
- {similar.map((s) => ( -