#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameDescriptor {
pub data: [u8; 32],
pub pts_ms: i64,
}
impl FrameDescriptor {
#[must_use]
pub fn new(data: [u8; 32], pts_ms: i64) -> Self {
Self { data, pts_ms }
}
#[must_use]
pub fn hamming_distance(&self, other: &Self) -> u32 {
self.data
.iter()
.zip(other.data.iter())
.map(|(a, b)| (a ^ b).count_ones())
.sum()
}
#[must_use]
pub fn is_identical(&self, other: &Self) -> bool {
self.data == other.data
}
}
#[derive(Debug, Clone)]
pub struct MatchCandidate {
pub hamming: u32,
pub ref_pts_ms: i64,
pub query_pts_ms: i64,
pub confidence: f64,
}
impl MatchCandidate {
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn new(hamming: u32, ref_pts_ms: i64, query_pts_ms: i64, max_bits: u32) -> Self {
let confidence = if max_bits == 0 {
0.0
} else {
1.0 - (f64::from(hamming) / f64::from(max_bits))
};
Self {
hamming,
ref_pts_ms,
query_pts_ms,
confidence,
}
}
#[must_use]
pub fn confidence_ok(&self, threshold: f64) -> bool {
self.confidence >= threshold
}
#[must_use]
pub fn offset_ms(&self) -> i64 {
self.ref_pts_ms - self.query_pts_ms
}
}
#[derive(Debug, Clone)]
pub struct FrameMatcherConfig {
pub max_hamming: u32,
pub min_confidence: f64,
pub descriptor_bits: u32,
}
impl Default for FrameMatcherConfig {
fn default() -> Self {
Self {
max_hamming: 64,
min_confidence: 0.75,
descriptor_bits: 256,
}
}
}
#[derive(Debug)]
pub struct FrameMatcher {
config: FrameMatcherConfig,
reference: Vec<FrameDescriptor>,
}
impl FrameMatcher {
#[must_use]
pub fn new(config: FrameMatcherConfig) -> Self {
Self {
config,
reference: Vec::new(),
}
}
#[must_use]
pub fn default_matcher() -> Self {
Self::new(FrameMatcherConfig::default())
}
pub fn add_reference(&mut self, desc: FrameDescriptor) {
self.reference.push(desc);
}
pub fn load_reference(&mut self, descs: Vec<FrameDescriptor>) {
self.reference = descs;
}
#[must_use]
pub fn find_match(&self, query: &FrameDescriptor) -> Vec<MatchCandidate> {
let bits = self.config.descriptor_bits;
let max_h = self.config.max_hamming;
self.reference
.iter()
.filter_map(|r| {
let h = query.hamming_distance(r);
if h <= max_h {
Some(MatchCandidate::new(h, r.pts_ms, query.pts_ms, bits))
} else {
None
}
})
.collect()
}
#[must_use]
pub fn best_match(&self, query: &FrameDescriptor) -> Option<MatchCandidate> {
let bits = self.config.descriptor_bits;
let max_h = self.config.max_hamming;
self.reference
.iter()
.filter_map(|r| {
let h = query.hamming_distance(r);
if h <= max_h {
Some(MatchCandidate::new(h, r.pts_ms, query.pts_ms, bits))
} else {
None
}
})
.min_by_key(|c| c.hamming)
}
#[must_use]
pub fn reference_count(&self) -> usize {
self.reference.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn zeros() -> [u8; 32] {
[0u8; 32]
}
fn ones() -> [u8; 32] {
[0xFFu8; 32]
}
fn half() -> [u8; 32] {
let mut d = [0u8; 32];
for i in 0..16 {
d[i] = 0xFF;
}
d
}
#[test]
fn test_hamming_identical() {
let d = FrameDescriptor::new(zeros(), 0);
assert_eq!(d.hamming_distance(&d), 0);
}
#[test]
fn test_hamming_all_different() {
let a = FrameDescriptor::new(zeros(), 0);
let b = FrameDescriptor::new(ones(), 0);
assert_eq!(a.hamming_distance(&b), 256);
}
#[test]
fn test_hamming_half() {
let a = FrameDescriptor::new(zeros(), 0);
let b = FrameDescriptor::new(half(), 0);
assert_eq!(a.hamming_distance(&b), 128);
}
#[test]
fn test_is_identical_true() {
let a = FrameDescriptor::new(zeros(), 100);
let b = FrameDescriptor::new(zeros(), 200);
assert!(a.is_identical(&b));
}
#[test]
fn test_is_identical_false() {
let a = FrameDescriptor::new(zeros(), 0);
let b = FrameDescriptor::new(ones(), 0);
assert!(!a.is_identical(&b));
}
#[test]
fn test_candidate_perfect_confidence() {
let c = MatchCandidate::new(0, 1000, 1000, 256);
assert!((c.confidence - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_candidate_zero_confidence() {
let c = MatchCandidate::new(256, 0, 0, 256);
assert!(c.confidence < f64::EPSILON);
}
#[test]
fn test_candidate_confidence_ok() {
let c = MatchCandidate::new(32, 0, 0, 256);
assert!(c.confidence_ok(0.75));
assert!(!c.confidence_ok(0.999));
}
#[test]
fn test_candidate_offset_ms() {
let c = MatchCandidate::new(10, 2000, 1500, 256);
assert_eq!(c.offset_ms(), 500);
}
#[test]
fn test_candidate_zero_max_bits() {
let c = MatchCandidate::new(10, 0, 0, 0);
assert!((c.confidence).abs() < f64::EPSILON);
}
#[test]
fn test_matcher_empty_reference() {
let matcher = FrameMatcher::default_matcher();
let query = FrameDescriptor::new(zeros(), 0);
assert!(matcher.find_match(&query).is_empty());
assert!(matcher.best_match(&query).is_none());
}
#[test]
fn test_matcher_finds_exact() {
let mut matcher = FrameMatcher::default_matcher();
matcher.add_reference(FrameDescriptor::new(zeros(), 1000));
let query = FrameDescriptor::new(zeros(), 500);
let candidates = matcher.find_match(&query);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].hamming, 0);
}
#[test]
fn test_matcher_best_match_closest() {
let mut matcher = FrameMatcher::default_matcher();
let mut near = zeros();
near[0] = 0x01; let mut far = zeros();
far[0] = 0x0F; matcher.add_reference(FrameDescriptor::new(near, 1000));
matcher.add_reference(FrameDescriptor::new(far, 2000));
let query = FrameDescriptor::new(zeros(), 0);
let best = matcher.best_match(&query).unwrap();
assert_eq!(best.hamming, 1);
assert_eq!(best.ref_pts_ms, 1000);
}
#[test]
fn test_matcher_rejects_beyond_threshold() {
let cfg = FrameMatcherConfig {
max_hamming: 10,
..Default::default()
};
let mut matcher = FrameMatcher::new(cfg);
matcher.add_reference(FrameDescriptor::new(ones(), 500));
let query = FrameDescriptor::new(zeros(), 0);
assert!(matcher.find_match(&query).is_empty());
}
#[test]
fn test_matcher_reference_count() {
let mut matcher = FrameMatcher::default_matcher();
assert_eq!(matcher.reference_count(), 0);
matcher.add_reference(FrameDescriptor::new(zeros(), 0));
matcher.add_reference(FrameDescriptor::new(ones(), 1));
assert_eq!(matcher.reference_count(), 2);
}
#[test]
fn test_load_reference_replaces() {
let mut matcher = FrameMatcher::default_matcher();
matcher.add_reference(FrameDescriptor::new(zeros(), 0));
let new_refs = vec![
FrameDescriptor::new(ones(), 100),
FrameDescriptor::new(half(), 200),
];
matcher.load_reference(new_refs);
assert_eq!(matcher.reference_count(), 2);
}
}