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) => ( -