1use crate::media::{Backend, DecoderPool, Frame};
15use crate::model::{Id, Project};
16use std::sync::mpsc::{channel, Receiver, TryRecvError};
17
18fn luma(f: &Frame, x: i32, y: i32) -> f32 {
20 let x = x.clamp(0, f.width as i32 - 1) as usize;
21 let y = y.clamp(0, f.height as i32 - 1) as usize;
22 let p = &f.rgba[y * f.stride() + x * 4..];
23 p[0] as f32 * 0.299 + p[1] as f32 * 0.587 + p[2] as f32 * 0.114
24}
25
26pub fn patch(f: &Frame, cx: i32, cy: i32, hw: i32, hh: i32) -> Vec<f32> {
28 let (tw, th) = (2 * hw + 1, 2 * hh + 1);
29 let mut out = Vec::with_capacity((tw * th).max(0) as usize);
30 if f.is_empty() {
31 return out;
32 }
33 for j in 0..th {
34 for i in 0..tw {
35 out.push(luma(f, cx - hw + i, cy - hh + j));
36 }
37 }
38 out
39}
40
41pub fn best_match(f: &Frame, tpl: &[f32], hw: i32, hh: i32, cx: i32, cy: i32, search: i32) -> Option<(i32, i32, f32)> {
48 let (tw, th) = (2 * hw + 1, 2 * hh + 1);
49 let n = (tw * th) as f32;
50 if f.is_empty() || tpl.len() != (tw * th) as usize {
51 return None;
52 }
53 let tmean = tpl.iter().sum::<f32>() / n;
54 let tvar: f32 = tpl.iter().map(|v| (v - tmean) * (v - tmean)).sum();
55 if tvar <= 1e-3 {
56 return None;
57 }
58 let mut best: Option<(i32, i32, f32)> = None;
59 for dy in -search..=search {
60 for dx in -search..=search {
61 let (ox, oy) = (cx + dx, cy + dy);
62 let (mut s, mut ss, mut dot) = (0.0f32, 0.0f32, 0.0f32);
63 for j in 0..th {
64 for i in 0..tw {
65 let v = luma(f, ox - hw + i, oy - hh + j);
66 s += v;
67 ss += v * v;
68 dot += v * tpl[(j * tw + i) as usize];
69 }
70 }
71 let wmean = s / n;
72 let wvar = ss - s * wmean;
73 if wvar <= 1e-3 {
74 continue;
75 }
76 let score = (dot - n * tmean * wmean) / (tvar * wvar).sqrt();
77 if best.is_none_or(|(_, _, b)| score > b) {
78 best = Some((ox, oy, score));
79 }
80 }
81 }
82 best
83}
84
85type Msg = ((f32, f32, f32), f32);
88
89pub struct TrackJob {
90 rx: Receiver<Msg>,
91 pub points: Vec<(f32, f32, f32)>,
93 pub progress: f32,
94}
95
96impl TrackJob {
97 #[allow(clippy::too_many_arguments)]
101 pub fn start(
102 project: &Project,
103 clip: Id,
104 (cx, cy, hw, hh): (f32, f32, f32, f32),
105 search: f32,
106 refresh: u32,
107 backward: bool,
108 backend: Backend,
109 ) -> Result<Self, String> {
110 let c = project.clip(clip).ok_or("pick a clip to track")?;
111 let path = project.asset(c.asset).map(|a| a.path.clone()).ok_or("that clip has no footage to track")?;
112 let (w, h) = (project.width.max(2), project.height.max(2));
113 let fps = project.fps.max(1.0);
114 let n = ((c.duration * fps).round() as usize).clamp(1, 200_000);
115 let mut times: Vec<(f32, f64)> =
117 (0..n).map(|i| (i as f64 / fps)).map(|lt| (lt as f32, c.src_time(c.start + lt))).collect();
118 if backward {
119 times.reverse();
120 }
121 let (hw, hh) = (hw.max(2.0) as i32, hh.max(2.0) as i32);
122 let search = search.clamp(1.0, 512.0) as i32;
123 let (x0, y0) = ((cx + w as f32 / 2.0) as i32, (cy + h as f32 / 2.0) as i32);
124 let (tx, rx) = channel();
125 std::thread::spawn(move || {
126 let mut pool = DecoderPool::new(backend);
127 let mut frame = Frame::default();
128 let mut tpl: Vec<f32> = Vec::new();
129 let (mut px, mut py) = (x0, y0);
130 let total = times.len() as f32;
131 for (i, (lt, st)) in times.iter().enumerate() {
132 let Some(dec) = pool.video(&path) else { break };
133 if !dec.frame_at(*st, w, h, &mut frame) {
134 break;
135 }
136 if tpl.is_empty() {
137 tpl = patch(&frame, px, py, hw, hh);
138 } else {
139 if let Some((nx, ny, _)) = best_match(&frame, &tpl, hw, hh, px, py, search) {
140 (px, py) = (nx, ny);
141 }
142 if refresh > 0 && i % refresh as usize == 0 {
143 tpl = patch(&frame, px, py, hw, hh);
144 }
145 }
146 let pt = (px as f32 - w as f32 / 2.0, py as f32 - h as f32 / 2.0, *lt);
147 if tx.send((pt, (i + 1) as f32 / total)).is_err() {
148 break; }
150 }
151 });
152 Ok(Self { rx, points: Vec::new(), progress: 0.0 })
153 }
154
155 pub fn poll(&mut self) -> bool {
157 loop {
158 match self.rx.try_recv() {
159 Ok((pt, p)) => {
160 self.points.push(pt);
161 self.progress = p;
162 }
163 Err(TryRecvError::Empty) => return true,
164 Err(TryRecvError::Disconnected) => {
165 self.points.sort_by(|a, b| a.2.total_cmp(&b.2));
167 return false;
168 }
169 }
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn square(w: u32, h: u32, cx: i32, cy: i32, r: i32) -> Frame {
180 let mut f = Frame::new(w, h);
181 f.fill([40, 40, 40, 255]);
182 for y in (cy - r).max(0)..=(cy + r).min(h as i32 - 1) {
183 for x in (cx - r).max(0)..=(cx + r).min(w as i32 - 1) {
184 let i = y as usize * f.stride() + x as usize * 4;
185 f.rgba[i..i + 4].copy_from_slice(&[230, 220, 200, 255]);
186 }
187 }
188 f
189 }
190
191 #[test]
192 fn ncc_reports_the_offset_the_square_moved_by() {
193 let a = square(96, 72, 40, 30, 6);
194 let b = square(96, 72, 49, 25, 6);
195 let tpl = patch(&a, 40, 30, 10, 10);
196 let (x, y, score) = best_match(&b, &tpl, 10, 10, 40, 30, 16).expect("a bright square is not flat");
197 assert_eq!((x - 40, y - 30), (9, -5), "score {score}");
198 assert!(score > 0.9, "an exact patch should correlate ~1, got {score}");
199 let flat = Frame::new(96, 72);
201 assert!(best_match(&flat, &patch(&flat, 40, 30, 10, 10), 10, 10, 40, 30, 4).is_none());
202 }
203}