1#![allow(dead_code)]
7
8#[derive(Debug, Clone)]
10pub struct AlignmentAnchor {
11 pub track_id: String,
13 pub frame_idx: u64,
15 pub feature_vector: Vec<f32>,
17}
18
19impl AlignmentAnchor {
20 #[must_use]
22 pub fn new(track_id: impl Into<String>, frame_idx: u64, feature_vector: Vec<f32>) -> Self {
23 Self {
24 track_id: track_id.into(),
25 frame_idx,
26 feature_vector,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AlignMethod {
34 AudioCorrelation,
36 VisualFeature,
38 Marker,
40 Timecode,
42 Manual,
44}
45
46#[derive(Debug, Clone)]
48pub struct TrackAlignment {
49 pub reference_id: String,
51 pub aligned_id: String,
53 pub offset_frames: i64,
55 pub confidence: f32,
57 pub method: AlignMethod,
59}
60
61impl TrackAlignment {
62 #[must_use]
64 pub fn new(
65 reference_id: impl Into<String>,
66 aligned_id: impl Into<String>,
67 offset_frames: i64,
68 confidence: f32,
69 method: AlignMethod,
70 ) -> Self {
71 Self {
72 reference_id: reference_id.into(),
73 aligned_id: aligned_id.into(),
74 offset_frames,
75 confidence,
76 method,
77 }
78 }
79}
80
81#[derive(Debug, Default)]
84pub struct MultitrackAligner {
85 reference_id: Option<String>,
87}
88
89impl MultitrackAligner {
90 #[must_use]
92 pub fn new() -> Self {
93 Self { reference_id: None }
94 }
95
96 pub fn set_reference(&mut self, track_id: &str) {
98 self.reference_id = Some(track_id.to_owned());
99 }
100
101 #[must_use]
111 pub fn align_track(&self, track_id: &str, anchors: &[AlignmentAnchor]) -> TrackAlignment {
112 let reference_id = self
113 .reference_id
114 .as_deref()
115 .expect("Reference track must be set before calling align_track");
116
117 let ref_anchors: Vec<&AlignmentAnchor> = anchors
119 .iter()
120 .filter(|a| a.track_id == reference_id)
121 .collect();
122 let tgt_anchors: Vec<&AlignmentAnchor> =
123 anchors.iter().filter(|a| a.track_id == track_id).collect();
124
125 if ref_anchors.is_empty() || tgt_anchors.is_empty() {
126 return TrackAlignment::new(reference_id, track_id, 0, 0.0, AlignMethod::VisualFeature);
127 }
128
129 let mut best_offset: i64 = 0;
131 let mut best_score: f64 = f64::NEG_INFINITY;
132 let mut total_pairs = 0usize;
133
134 for ref_anchor in &ref_anchors {
135 for tgt_anchor in &tgt_anchors {
136 let corr = cross_correlate(&ref_anchor.feature_vector, &tgt_anchor.feature_vector);
137 if let Some((peak_lag, peak_val)) = corr
139 .iter()
140 .enumerate()
141 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
142 {
143 let n = ref_anchor.feature_vector.len();
145 let signed_lag = peak_lag as i64 - (n as i64 - 1);
146 let frame_offset =
147 tgt_anchor.frame_idx as i64 - ref_anchor.frame_idx as i64 + signed_lag;
148
149 if f64::from(*peak_val) > best_score {
150 best_score = f64::from(*peak_val);
151 best_offset = frame_offset;
152 }
153 total_pairs += 1;
154 }
155 }
156 }
157
158 let confidence = if total_pairs > 0 {
160 (best_score as f32).clamp(0.0, 1.0)
161 } else {
162 0.0
163 };
164
165 TrackAlignment::new(
166 reference_id,
167 track_id,
168 best_offset,
169 confidence,
170 AlignMethod::VisualFeature,
171 )
172 }
173}
174
175#[must_use]
181pub fn cross_correlate(a: &[f32], b: &[f32]) -> Vec<f32> {
182 if a.is_empty() || b.is_empty() {
183 return Vec::new();
184 }
185
186 let na = a.len();
187 let nb = b.len();
188 let out_len = na + nb - 1;
189
190 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
192 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
193 let denom = norm_a * norm_b;
194
195 let mut result = vec![0.0f32; out_len];
196
197 for lag in 0..out_len {
198 let mut sum = 0.0f32;
199 for j in 0..nb {
202 let a_idx = lag + j;
203 if a_idx >= nb - 1 && a_idx - (nb - 1) < na {
204 let ai = a_idx - (nb - 1);
205 sum += a[ai] * b[j];
206 }
207 }
208 result[lag] = if denom > 1e-10 {
209 (sum / denom).clamp(-1.0, 1.0)
210 } else {
211 0.0
212 };
213 }
214
215 result
216}
217
218#[derive(Debug, Clone)]
220pub struct AlignmentMatrix {
221 pub tracks: Vec<String>,
223 pub offsets: Vec<Vec<i64>>,
225}
226
227impl AlignmentMatrix {
228 #[must_use]
234 pub fn from_alignments(track_ids: &[&str], alignments: &[TrackAlignment]) -> Self {
235 let n = track_ids.len();
236 let tracks: Vec<String> = track_ids.iter().map(|s| (*s).to_string()).collect();
237 let mut offsets = vec![vec![0i64; n]; n];
238
239 let idx = |id: &str| tracks.iter().position(|t| t == id);
240
241 for aln in alignments {
242 if let (Some(ri), Some(ai)) = (idx(&aln.reference_id), idx(&aln.aligned_id)) {
243 offsets[ri][ai] = aln.offset_frames;
244 offsets[ai][ri] = -aln.offset_frames;
245 }
246 }
247
248 Self { tracks, offsets }
249 }
250
251 #[must_use]
258 pub fn compute_global_offsets(&self) -> Vec<i64> {
259 let n = self.tracks.len();
260 if n == 0 {
261 return Vec::new();
262 }
263
264 let mut global: Vec<f64> = (0..n).map(|j| self.offsets[0][j] as f64).collect();
266 global[0] = 0.0;
267
268 for _ in 0..3 {
272 let prev = global.clone();
273 for i in 1..n {
274 let mut sum = 0.0f64;
275 let mut count = 0usize;
276 for j in 0..n {
277 if j != i && self.offsets[j][i] != 0 {
278 sum += prev[j] + self.offsets[j][i] as f64;
279 count += 1;
280 }
281 }
282 if count > 0 {
283 global[i] = sum / count as f64;
284 }
285 }
286 }
287
288 global.iter().map(|&v| v.round() as i64).collect()
289 }
290}
291
292#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
303 fn test_cross_correlate_empty() {
304 assert!(cross_correlate(&[], &[1.0]).is_empty());
305 assert!(cross_correlate(&[1.0], &[]).is_empty());
306 }
307
308 #[test]
309 fn test_cross_correlate_identical() {
310 let a = vec![0.0, 0.0, 1.0, 0.0, 0.0];
311 let corr = cross_correlate(&a, &a);
312 let peak_idx = corr
314 .iter()
315 .enumerate()
316 .max_by(|x, y| x.1.partial_cmp(y.1).expect("max_by should succeed"))
317 .map(|(i, _)| i)
318 .expect("test expectation failed");
319 let zero_lag = a.len() - 1; assert_eq!(peak_idx, zero_lag, "peak should be at zero lag");
321 }
322
323 #[test]
324 fn test_cross_correlate_output_length() {
325 let a = vec![1.0f32; 5];
326 let b = vec![1.0f32; 3];
327 let corr = cross_correlate(&a, &b);
328 assert_eq!(corr.len(), a.len() + b.len() - 1);
329 }
330
331 #[test]
332 fn test_cross_correlate_values_in_range() {
333 let a: Vec<f32> = (0..8).map(|i| i as f32).collect();
334 let b: Vec<f32> = (0..8).map(|i| (8 - i) as f32).collect();
335 let corr = cross_correlate(&a, &b);
336 for &v in &corr {
337 assert!(v >= -1.0 && v <= 1.0, "value {v} out of [-1, 1]");
338 }
339 }
340
341 #[test]
342 fn test_cross_correlate_zero_signal() {
343 let a = vec![0.0f32; 4];
344 let b = vec![1.0f32; 4];
345 let corr = cross_correlate(&a, &b);
346 assert!(
347 corr.iter().all(|&v| v == 0.0),
348 "zero signal should yield zero correlation"
349 );
350 }
351
352 #[test]
355 fn test_anchor_creation() {
356 let anchor = AlignmentAnchor::new("cam_a", 42, vec![1.0, 2.0, 3.0]);
357 assert_eq!(anchor.track_id, "cam_a");
358 assert_eq!(anchor.frame_idx, 42);
359 assert_eq!(anchor.feature_vector.len(), 3);
360 }
361
362 #[test]
365 fn test_track_alignment_fields() {
366 let aln = TrackAlignment::new("ref", "tgt", -5, 0.9, AlignMethod::AudioCorrelation);
367 assert_eq!(aln.reference_id, "ref");
368 assert_eq!(aln.aligned_id, "tgt");
369 assert_eq!(aln.offset_frames, -5);
370 assert!((aln.confidence - 0.9).abs() < f32::EPSILON);
371 assert_eq!(aln.method, AlignMethod::AudioCorrelation);
372 }
373
374 #[test]
377 fn test_aligner_no_anchors() {
378 let mut aligner = MultitrackAligner::new();
379 aligner.set_reference("ref");
380 let anchors: Vec<AlignmentAnchor> = vec![];
381 let result = aligner.align_track("tgt", &anchors);
382 assert_eq!(result.offset_frames, 0);
383 assert_eq!(result.confidence, 0.0);
384 }
385
386 #[test]
387 fn test_aligner_identical_features() {
388 let mut aligner = MultitrackAligner::new();
389 aligner.set_reference("ref");
390
391 let fv = vec![0.0, 0.0, 1.0, 0.0, 0.0];
392 let anchors = vec![
393 AlignmentAnchor::new("ref", 10, fv.clone()),
394 AlignmentAnchor::new("tgt", 10, fv.clone()),
395 ];
396 let result = aligner.align_track("tgt", &anchors);
397 assert!(result.confidence > 0.0);
399 }
400
401 #[test]
402 fn test_aligner_sets_reference() {
403 let mut aligner = MultitrackAligner::new();
404 assert!(aligner.reference_id.is_none());
405 aligner.set_reference("master");
406 assert_eq!(aligner.reference_id.as_deref(), Some("master"));
407 }
408
409 #[test]
412 fn test_alignment_matrix_identity() {
413 let tracks = vec!["a", "b", "c"];
414 let alignments: Vec<TrackAlignment> = vec![];
415 let matrix = AlignmentMatrix::from_alignments(&tracks, &alignments);
416 assert_eq!(matrix.tracks.len(), 3);
417 for row in &matrix.offsets {
419 for &v in row {
420 assert_eq!(v, 0);
421 }
422 }
423 }
424
425 #[test]
426 fn test_alignment_matrix_symmetric() {
427 let tracks = vec!["a", "b"];
428 let alignments = vec![TrackAlignment::new("a", "b", 10, 0.9, AlignMethod::Manual)];
429 let matrix = AlignmentMatrix::from_alignments(&tracks, &alignments);
430 assert_eq!(matrix.offsets[0][1], 10);
431 assert_eq!(matrix.offsets[1][0], -10);
432 }
433
434 #[test]
435 fn test_compute_global_offsets_empty() {
436 let matrix = AlignmentMatrix {
437 tracks: vec![],
438 offsets: vec![],
439 };
440 assert!(matrix.compute_global_offsets().is_empty());
441 }
442
443 #[test]
444 fn test_compute_global_offsets_single() {
445 let matrix = AlignmentMatrix {
446 tracks: vec!["a".to_string()],
447 offsets: vec![vec![0]],
448 };
449 let offsets = matrix.compute_global_offsets();
450 assert_eq!(offsets, vec![0]);
451 }
452
453 #[test]
454 fn test_compute_global_offsets_two_tracks() {
455 let tracks = vec!["a", "b"];
456 let alignments = vec![TrackAlignment::new("a", "b", 5, 0.9, AlignMethod::Manual)];
457 let matrix = AlignmentMatrix::from_alignments(&tracks, &alignments);
458 let global = matrix.compute_global_offsets();
459 assert_eq!(global[0], 0);
461 assert_eq!(global[1], 5);
462 }
463}