1#![allow(dead_code)]
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FrameDescriptor {
13 pub data: [u8; 32],
15 pub pts_ms: i64,
17}
18
19impl FrameDescriptor {
20 #[must_use]
22 pub fn new(data: [u8; 32], pts_ms: i64) -> Self {
23 Self { data, pts_ms }
24 }
25
26 #[must_use]
30 pub fn hamming_distance(&self, other: &Self) -> u32 {
31 self.data
32 .iter()
33 .zip(other.data.iter())
34 .map(|(a, b)| (a ^ b).count_ones())
35 .sum()
36 }
37
38 #[must_use]
40 pub fn is_identical(&self, other: &Self) -> bool {
41 self.data == other.data
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct MatchCandidate {
48 pub hamming: u32,
50 pub ref_pts_ms: i64,
52 pub query_pts_ms: i64,
54 pub confidence: f64,
56}
57
58impl MatchCandidate {
59 #[allow(clippy::cast_precision_loss)]
63 #[must_use]
64 pub fn new(hamming: u32, ref_pts_ms: i64, query_pts_ms: i64, max_bits: u32) -> Self {
65 let confidence = if max_bits == 0 {
66 0.0
67 } else {
68 1.0 - (f64::from(hamming) / f64::from(max_bits))
69 };
70 Self {
71 hamming,
72 ref_pts_ms,
73 query_pts_ms,
74 confidence,
75 }
76 }
77
78 #[must_use]
80 pub fn confidence_ok(&self, threshold: f64) -> bool {
81 self.confidence >= threshold
82 }
83
84 #[must_use]
86 pub fn offset_ms(&self) -> i64 {
87 self.ref_pts_ms - self.query_pts_ms
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct FrameMatcherConfig {
94 pub max_hamming: u32,
96 pub min_confidence: f64,
98 pub descriptor_bits: u32,
100}
101
102impl Default for FrameMatcherConfig {
103 fn default() -> Self {
104 Self {
105 max_hamming: 64,
106 min_confidence: 0.75,
107 descriptor_bits: 256,
108 }
109 }
110}
111
112#[derive(Debug)]
114pub struct FrameMatcher {
115 config: FrameMatcherConfig,
116 reference: Vec<FrameDescriptor>,
117}
118
119impl FrameMatcher {
120 #[must_use]
122 pub fn new(config: FrameMatcherConfig) -> Self {
123 Self {
124 config,
125 reference: Vec::new(),
126 }
127 }
128
129 #[must_use]
131 pub fn default_matcher() -> Self {
132 Self::new(FrameMatcherConfig::default())
133 }
134
135 pub fn add_reference(&mut self, desc: FrameDescriptor) {
137 self.reference.push(desc);
138 }
139
140 pub fn load_reference(&mut self, descs: Vec<FrameDescriptor>) {
142 self.reference = descs;
143 }
144
145 #[must_use]
147 pub fn find_match(&self, query: &FrameDescriptor) -> Vec<MatchCandidate> {
148 let bits = self.config.descriptor_bits;
149 let max_h = self.config.max_hamming;
150 self.reference
151 .iter()
152 .filter_map(|r| {
153 let h = query.hamming_distance(r);
154 if h <= max_h {
155 Some(MatchCandidate::new(h, r.pts_ms, query.pts_ms, bits))
156 } else {
157 None
158 }
159 })
160 .collect()
161 }
162
163 #[must_use]
165 pub fn best_match(&self, query: &FrameDescriptor) -> Option<MatchCandidate> {
166 let bits = self.config.descriptor_bits;
167 let max_h = self.config.max_hamming;
168 self.reference
169 .iter()
170 .filter_map(|r| {
171 let h = query.hamming_distance(r);
172 if h <= max_h {
173 Some(MatchCandidate::new(h, r.pts_ms, query.pts_ms, bits))
174 } else {
175 None
176 }
177 })
178 .min_by_key(|c| c.hamming)
179 }
180
181 #[must_use]
183 pub fn reference_count(&self) -> usize {
184 self.reference.len()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn zeros() -> [u8; 32] {
193 [0u8; 32]
194 }
195 fn ones() -> [u8; 32] {
196 [0xFFu8; 32]
197 }
198 fn half() -> [u8; 32] {
199 let mut d = [0u8; 32];
200 for i in 0..16 {
201 d[i] = 0xFF;
202 }
203 d
204 }
205
206 #[test]
209 fn test_hamming_identical() {
210 let d = FrameDescriptor::new(zeros(), 0);
211 assert_eq!(d.hamming_distance(&d), 0);
212 }
213
214 #[test]
215 fn test_hamming_all_different() {
216 let a = FrameDescriptor::new(zeros(), 0);
217 let b = FrameDescriptor::new(ones(), 0);
218 assert_eq!(a.hamming_distance(&b), 256);
219 }
220
221 #[test]
222 fn test_hamming_half() {
223 let a = FrameDescriptor::new(zeros(), 0);
224 let b = FrameDescriptor::new(half(), 0);
225 assert_eq!(a.hamming_distance(&b), 128);
226 }
227
228 #[test]
229 fn test_is_identical_true() {
230 let a = FrameDescriptor::new(zeros(), 100);
231 let b = FrameDescriptor::new(zeros(), 200);
232 assert!(a.is_identical(&b));
233 }
234
235 #[test]
236 fn test_is_identical_false() {
237 let a = FrameDescriptor::new(zeros(), 0);
238 let b = FrameDescriptor::new(ones(), 0);
239 assert!(!a.is_identical(&b));
240 }
241
242 #[test]
245 fn test_candidate_perfect_confidence() {
246 let c = MatchCandidate::new(0, 1000, 1000, 256);
247 assert!((c.confidence - 1.0).abs() < f64::EPSILON);
248 }
249
250 #[test]
251 fn test_candidate_zero_confidence() {
252 let c = MatchCandidate::new(256, 0, 0, 256);
253 assert!(c.confidence < f64::EPSILON);
254 }
255
256 #[test]
257 fn test_candidate_confidence_ok() {
258 let c = MatchCandidate::new(32, 0, 0, 256);
259 assert!(c.confidence_ok(0.75));
260 assert!(!c.confidence_ok(0.999));
261 }
262
263 #[test]
264 fn test_candidate_offset_ms() {
265 let c = MatchCandidate::new(10, 2000, 1500, 256);
266 assert_eq!(c.offset_ms(), 500);
267 }
268
269 #[test]
270 fn test_candidate_zero_max_bits() {
271 let c = MatchCandidate::new(10, 0, 0, 0);
272 assert!((c.confidence).abs() < f64::EPSILON);
273 }
274
275 #[test]
278 fn test_matcher_empty_reference() {
279 let matcher = FrameMatcher::default_matcher();
280 let query = FrameDescriptor::new(zeros(), 0);
281 assert!(matcher.find_match(&query).is_empty());
282 assert!(matcher.best_match(&query).is_none());
283 }
284
285 #[test]
286 fn test_matcher_finds_exact() {
287 let mut matcher = FrameMatcher::default_matcher();
288 matcher.add_reference(FrameDescriptor::new(zeros(), 1000));
289 let query = FrameDescriptor::new(zeros(), 500);
290 let candidates = matcher.find_match(&query);
291 assert_eq!(candidates.len(), 1);
292 assert_eq!(candidates[0].hamming, 0);
293 }
294
295 #[test]
296 fn test_matcher_best_match_closest() {
297 let mut matcher = FrameMatcher::default_matcher();
298 let mut near = zeros();
299 near[0] = 0x01; let mut far = zeros();
301 far[0] = 0x0F; matcher.add_reference(FrameDescriptor::new(near, 1000));
303 matcher.add_reference(FrameDescriptor::new(far, 2000));
304 let query = FrameDescriptor::new(zeros(), 0);
305 let best = matcher.best_match(&query).expect("best should be valid");
306 assert_eq!(best.hamming, 1);
307 assert_eq!(best.ref_pts_ms, 1000);
308 }
309
310 #[test]
311 fn test_matcher_rejects_beyond_threshold() {
312 let cfg = FrameMatcherConfig {
313 max_hamming: 10,
314 ..Default::default()
315 };
316 let mut matcher = FrameMatcher::new(cfg);
317 matcher.add_reference(FrameDescriptor::new(ones(), 500));
318 let query = FrameDescriptor::new(zeros(), 0);
319 assert!(matcher.find_match(&query).is_empty());
321 }
322
323 #[test]
324 fn test_matcher_reference_count() {
325 let mut matcher = FrameMatcher::default_matcher();
326 assert_eq!(matcher.reference_count(), 0);
327 matcher.add_reference(FrameDescriptor::new(zeros(), 0));
328 matcher.add_reference(FrameDescriptor::new(ones(), 1));
329 assert_eq!(matcher.reference_count(), 2);
330 }
331
332 #[test]
333 fn test_load_reference_replaces() {
334 let mut matcher = FrameMatcher::default_matcher();
335 matcher.add_reference(FrameDescriptor::new(zeros(), 0));
336 let new_refs = vec![
337 FrameDescriptor::new(ones(), 100),
338 FrameDescriptor::new(half(), 200),
339 ];
340 matcher.load_reference(new_refs);
341 assert_eq!(matcher.reference_count(), 2);
342 }
343}