-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFaceRepository.java
More file actions
141 lines (121 loc) · 5.86 KB
/
Copy pathFaceRepository.java
File metadata and controls
141 lines (121 loc) · 5.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package Phase5_CollectionsLambdasStreams.Collections.FaceDetectionApp;
import java.util.*;
/**
* FaceRepository - the heart of the demo. ONE class that uses every
* collection type from the framework:
* <p>
*
* List<Face> - history of all detections (ArrayList)
* Deque<List<Face>> - sliding window of recent FRAMES (ArrayDeque)
* Set<String> - distinct people seen so far (LinkedHashSet)
* HashSet<Integer> - face IDs the user has DISMISSED
* Map<String, List<Face>> - faces indexed by person (HashMap)
* Map<String, Long> - per-person detection counter (HashMap)
* NavigableMap<Long, Face> - timeline by timestamp (TreeMap)
* PriorityQueue<Face> - alerts queue ranked by confidence
* <p>
*
* Each collection is justified by what the OPERATION needs. That is the
* design lesson: choose the data structure that makes the dominant query
* cheap.
*/
public class FaceRepository {
// History of ALL detected faces, in detection order. ArrayList because:
// - we mostly append at the end,
// - we sometimes scan to compute statistics.
private final List<Face> all = new ArrayList<>();
// The last N frames as a sliding window. ArrayDeque because we need to
// add at one end and remove from the other in O(1).
private final Deque<List<Face>> recentFrames = new ArrayDeque<>();
private static final int WINDOW = 5;
// Distinct people in first-seen order. LinkedHashSet because:
// - we want uniqueness (Set),
// - and we want iteration in insertion order for human readability.
private final Set<String> peopleSeen = new LinkedHashSet<>();
// Face IDs that the user clicked "dismiss" on. HashSet for O(1) lookup;
// order doesn't matter.
private final Set<Integer> dismissed = new HashSet<>();
// Faces grouped by person. HashMap because:
// - we don't need an ordered view of people,
// - we look up by name in O(1).
private final Map<String, List<Face>> facesByPerson = new HashMap<>();
// How many times each person has been seen. Same justification as above.
private final Map<String, Long> detectionCount = new HashMap<>();
// All faces by timestamp. TreeMap because we frequently ask range
// queries ("everything in the last 5 minutes") that need O(log n)
// navigation.
private final NavigableMap<Long, Face> timeline = new TreeMap<>();
// High-confidence "alerts" pending notification. PriorityQueue because
// we want to pop the most confident match next, regardless of insertion
// order.
private final PriorityQueue<Face> alerts = new PriorityQueue<>(); // Face.compareTo is desc by conf
/** Push every face from one detected frame through all the data structures. */
public void recordFrame(int frameNo, List<Face> faces) {
// Update sliding window
recentFrames.offerLast(new ArrayList<>(faces));
while (recentFrames.size() > WINDOW) {
recentFrames.pollFirst();
}
for (Face f : faces) {
all.add(f);
peopleSeen.add(f.personName());
facesByPerson.computeIfAbsent(f.personName(), k -> new ArrayList<>()).add(f);
detectionCount.merge(f.personName(), 1L, Long::sum);
timeline.put(f.timestampMs(), f);
if (f.confidence() >= 0.85 && !dismissed.contains(f.id()) && !f.isUnknown()) {
alerts.offer(f);
}
}
}
public void dismiss(int faceId) {
dismissed.add(faceId);
// Also lazily drop any pending alert with that id.
alerts.removeIf(f -> f.id() == faceId);
}
// ===== reads =====
public List<Face> all() { return Collections.unmodifiableList(all); }
public Set<String> peopleSeen() { return Collections.unmodifiableSet(peopleSeen); }
public Map<String, Long> counts() { return Collections.unmodifiableMap(detectionCount); }
public Map<String, List<Face>> byPerson() { return Collections.unmodifiableMap(facesByPerson); }
/** Pop the next alert (most-confident first). Returns null if empty. */
public Face nextAlert() {
return alerts.poll();
}
/** Faces detected between [fromMs, toMs] inclusive - range query on the TreeMap. */
public Collection<Face> inRange(long fromMs, long toMs) {
return timeline.subMap(fromMs, true, toMs, true).values();
}
/** Top-K faces by confidence across all of history. */
public List<Face> topK(int k) {
// Bounded MIN-heap: kick out the smallest when we exceed K. Drain at the end.
PriorityQueue<Face> minByConf = new PriorityQueue<>(
Comparator.comparingDouble(Face::confidence)
);
for (Face f : all) {
if (minByConf.size() < k) {
minByConf.offer(f);
} else if (f.confidence() > minByConf.peek().confidence()) {
minByConf.poll();
minByConf.offer(f);
}
}
List<Face> result = new ArrayList<>(minByConf);
result.sort(Comparator.comparingDouble(Face::confidence).reversed());
return result;
}
/** Distinct people in the last `windowSize` frames. */
public Set<String> recentPeople() {
Set<String> recent = new LinkedHashSet<>();
for (List<Face> frame : recentFrames) {
for (Face f : frame) {
if (!f.isUnknown()) recent.add(f.personName());
}
}
return recent;
}
public int totalDetected() { return all.size(); }
public int distinctPeople() { return peopleSeen.size(); }
public int dismissedCount() { return dismissed.size(); }
public int pendingAlerts() { return alerts.size(); }
public int windowSize() { return recentFrames.size(); }
}