1use ffcine_core::geom::{self, Mat3};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Motion {
10 pub tx: f64,
11 pub ty: f64,
12 pub angle: f64,
14 pub scale: f64,
15 pub confidence: f64,
18}
19
20impl Motion {
21 pub const IDENTITY: Self = Self {
22 tx: 0.0,
23 ty: 0.0,
24 angle: 0.0,
25 scale: 1.0,
26 confidence: 0.0,
27 };
28
29 pub fn to_mat3(&self, centre: [f64; 2]) -> Mat3 {
30 geom::similarity(centre, self.tx, self.ty, self.angle, self.scale)
31 }
32
33 #[must_use]
35 pub fn rescaled(self, kx: f64, ky: f64) -> Self {
36 Self {
37 tx: self.tx * kx,
38 ty: self.ty * ky,
39 ..self
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
48pub struct FlowField<'a> {
49 pub width: u32,
50 pub height: u32,
51 pub grid: u32,
52 pub cols: usize,
53 pub rows: usize,
54 pub flow: &'a [[f32; 2]],
55 pub cost: &'a [u8],
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct EstimateParams {
61 pub border: f64,
64 pub max_cost: u8,
66 pub step: usize,
68 pub threshold: f64,
71 pub iterations: u32,
72 pub min_confidence: f64,
74}
75
76impl Default for EstimateParams {
77 fn default() -> Self {
78 Self {
79 border: 0.04,
80 max_cost: 48,
81 step: 2,
82 threshold: 2.0,
83 iterations: 200,
84 min_confidence: 0.1,
85 }
86 }
87}
88
89struct Rng(u64);
91
92impl Rng {
93 fn below(&mut self, n: usize) -> usize {
94 self.0 ^= self.0 >> 12;
95 self.0 ^= self.0 << 25;
96 self.0 ^= self.0 >> 27;
97 ((self.0.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 32) as usize) % n
98 }
99}
100
101#[derive(Debug, Clone, Copy)]
103struct Sim {
104 a: f64,
105 b: f64,
106 tx: f64,
107 ty: f64,
108}
109
110impl Sim {
111 fn residual2(&self, p: [f64; 2], q: [f64; 2]) -> f64 {
112 let x = self.a * p[0] - self.b * p[1] + self.tx - q[0];
113 let y = self.b * p[0] + self.a * p[1] + self.ty - q[1];
114 x * x + y * y
115 }
116
117 fn from_pairs(p1: [f64; 2], q1: [f64; 2], p2: [f64; 2], q2: [f64; 2]) -> Option<Self> {
119 let (dpx, dpy) = (p1[0] - p2[0], p1[1] - p2[1]);
120 let (dqx, dqy) = (q1[0] - q2[0], q1[1] - q2[1]);
121 let n = dpx * dpx + dpy * dpy;
122 if n < 1e-6 {
123 return None;
124 }
125 let a = (dqx * dpx + dqy * dpy) / n;
126 let b = (dqy * dpx - dqx * dpy) / n;
127 Some(Self {
128 a,
129 b,
130 tx: q1[0] - (a * p1[0] - b * p1[1]),
131 ty: q1[1] - (b * p1[0] + a * p1[1]),
132 })
133 }
134}
135
136#[derive(Debug, Clone, Copy)]
139struct Affine {
140 m: [[f64; 2]; 2],
141 t: [f64; 2],
142}
143
144impl Affine {
145 fn from_sim(s: &Sim) -> Self {
146 Self {
147 m: [[s.a, -s.b], [s.b, s.a]],
148 t: [s.tx, s.ty],
149 }
150 }
151
152 fn residual2(&self, p: [f64; 2], q: [f64; 2]) -> f64 {
153 let x = self.m[0][0] * p[0] + self.m[0][1] * p[1] + self.t[0] - q[0];
154 let y = self.m[1][0] * p[0] + self.m[1][1] * p[1] + self.t[1] - q[1];
155 x * x + y * y
156 }
157
158 fn fit(pts: &[([f64; 2], [f64; 2])], w: &[f64]) -> Option<Self> {
160 let sw: f64 = w.iter().sum();
161 if sw <= 0.0 {
162 return None;
163 }
164 let (mut pc, mut qc) = ([0.0; 2], [0.0; 2]);
165 for ((p, q), &wi) in pts.iter().zip(w) {
166 for i in 0..2 {
167 pc[i] += wi * p[i] / sw;
168 qc[i] += wi * q[i] / sw;
169 }
170 }
171 let (mut cpp, mut cqp) = ([[0.0; 2]; 2], [[0.0; 2]; 2]);
172 for ((p, q), &wi) in pts.iter().zip(w) {
173 let d = [p[0] - pc[0], p[1] - pc[1]];
174 let e = [q[0] - qc[0], q[1] - qc[1]];
175 for i in 0..2 {
176 for j in 0..2 {
177 cpp[i][j] += wi * d[i] * d[j];
178 cqp[i][j] += wi * e[i] * d[j];
179 }
180 }
181 }
182 let det = cpp[0][0] * cpp[1][1] - cpp[0][1] * cpp[1][0];
183 if det.abs() <= 1e-9 * (cpp[0][0] * cpp[1][1]).max(1e-30) {
184 return None;
185 }
186 let inv = [
187 [cpp[1][1] / det, -cpp[0][1] / det],
188 [-cpp[1][0] / det, cpp[0][0] / det],
189 ];
190 let mut m = [[0.0; 2]; 2];
191 for i in 0..2 {
192 for j in 0..2 {
193 m[i][j] = cqp[i][0] * inv[0][j] + cqp[i][1] * inv[1][j];
194 }
195 }
196 let t = [
197 qc[0] - m[0][0] * pc[0] - m[0][1] * pc[1],
198 qc[1] - m[1][0] * pc[0] - m[1][1] * pc[1],
199 ];
200 Some(Self { m, t })
201 }
202}
203
204pub fn estimate(field: &FlowField<'_>, params: &EstimateParams) -> Motion {
206 let (w, h) = (f64::from(field.width), f64::from(field.height));
207 let centre = [w / 2.0, h / 2.0];
208 let g = f64::from(field.grid);
209 let (mx, my) = (params.border * w, params.border * h);
210 let step = params.step.max(1);
211
212 let mut pts = Vec::with_capacity(field.cols * field.rows / (step * step) + 1);
213 for r in (0..field.rows).step_by(step) {
214 for c in (0..field.cols).step_by(step) {
215 let i = r * field.cols + c;
216 if field.cost[i] > params.max_cost {
217 continue;
218 }
219 let (x, y) = ((c as f64 + 0.5) * g, (r as f64 + 0.5) * g);
220 if x < mx || x > w - mx || y < my || y > h - my {
221 continue;
222 }
223 let [fx, fy] = field.flow[i];
224 let p = [x - centre[0], y - centre[1]];
225 pts.push((p, [p[0] + f64::from(fx), p[1] + f64::from(fy)]));
226 }
227 }
228 if pts.len() < 16 {
229 return Motion::IDENTITY;
230 }
231
232 let thr2 = params.threshold * params.threshold;
235 let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
236 let score_set: Vec<usize> = (0..pts.len().min(1500))
237 .map(|_| rng.below(pts.len()))
238 .collect();
239 let min_d2 = (w.min(h) / 4.0).powi(2);
240 let mut best: Option<(usize, Sim)> = None;
241 for _ in 0..params.iterations {
242 let (i, j) = (rng.below(pts.len()), rng.below(pts.len()));
243 let (p1, q1) = pts[i];
244 let (p2, q2) = pts[j];
245 if (p1[0] - p2[0]).powi(2) + (p1[1] - p2[1]).powi(2) < min_d2 {
246 continue;
247 }
248 let Some(s) = Sim::from_pairs(p1, q1, p2, q2) else {
249 continue;
250 };
251 let n = score_set
252 .iter()
253 .filter(|&&k| s.residual2(pts[k].0, pts[k].1) < thr2)
254 .count();
255 if best.is_none_or(|(b, _)| n > b) {
256 best = Some((n, s));
257 }
258 }
259 let Some((_, sim)) = best else {
260 return Motion::IDENTITY;
261 };
262
263 let mut fit = Affine::from_sim(&sim);
265 let mut weights = vec![0.0; pts.len()];
266 for gate in [4.0, 2.0, 1.0, 1.0, 1.0] {
267 let g2 = thr2 * gate * gate;
268 for (wi, (p, q)) in weights.iter_mut().zip(&pts) {
269 let r2 = fit.residual2(*p, *q);
270 *wi = if r2 < g2 {
271 1.0 / (1.0 + r2 / thr2)
272 } else {
273 0.0
274 };
275 }
276 match Affine::fit(&pts, &weights) {
277 Some(f) => fit = f,
278 None => return Motion::IDENTITY,
279 }
280 }
281 let inliers = pts
282 .iter()
283 .filter(|(p, q)| fit.residual2(*p, *q) < thr2)
284 .count();
285 let confidence = inliers as f64 / pts.len() as f64;
286 if confidence < params.min_confidence {
287 return Motion::IDENTITY;
288 }
289 let m = fit.m;
290 Motion {
293 tx: fit.t[0],
294 ty: fit.t[1],
295 angle: m[1][0].atan2(m[0][0]),
296 scale: m[0][0].hypot(m[1][0]),
297 confidence,
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 fn field(truth: &Motion, shear: f64, w: u32, h: u32) -> (Vec<[f32; 2]>, Vec<u8>, usize, usize) {
309 let grid = 4;
310 let (cols, rows) = ((w / grid) as usize, (h / grid) as usize);
311 let cy = f64::from(h) / 2.0;
312 let skew = [[1.0, shear, -shear * cy], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
313 let m = geom::mul(&skew, &truth.to_mat3([f64::from(w) / 2.0, cy]));
314 let mut rng = Rng(42);
315 let mut noise = || (rng.below(1000) as f64 / 1000.0 - 0.5) * 0.5;
316 let (mut flow, mut cost) = (Vec::new(), Vec::new());
317 for r in 0..rows {
318 for c in 0..cols {
319 let p = [(c as f64 + 0.5) * 4.0, (r as f64 + 0.5) * 4.0];
320 let q = geom::apply(&m, p);
321 let mut f = [q[0] - p[0] + noise(), q[1] - p[1] + noise()];
322 let mut k = 10;
323 if (cols / 3..cols / 2).contains(&c) && (rows / 3..2 * rows / 3).contains(&r) {
325 f[0] += 12.0;
326 }
327 if r < rows / 6 {
329 f = [noise() * 80.0, noise() * 80.0];
330 k = 200;
331 }
332 flow.push([f[0] as f32, f[1] as f32]);
333 cost.push(k);
334 }
335 }
336 (flow, cost, cols, rows)
337 }
338
339 #[test]
340 fn recovers_similarity_with_outliers() {
341 let truth = Motion {
342 tx: 6.3,
343 ty: -2.1,
344 angle: 0.004,
345 scale: 1.003,
346 confidence: 1.0,
347 };
348 let (w, h) = (1920, 1080);
349 let (flow, cost, cols, rows) = field(&truth, 0.0, w, h);
350 let f = FlowField {
351 width: w,
352 height: h,
353 grid: 4,
354 cols,
355 rows,
356 flow: &flow,
357 cost: &cost,
358 };
359 let m = estimate(&f, &EstimateParams::default());
360 assert!((m.tx - truth.tx).abs() < 0.02, "{m:?}");
361 assert!((m.ty - truth.ty).abs() < 0.02, "{m:?}");
362 assert!((m.angle - truth.angle).abs() < 2e-5, "{m:?}");
363 assert!((m.scale - truth.scale).abs() < 2e-5, "{m:?}");
364 assert!(m.confidence > 0.85 && m.confidence < 0.97, "{m:?}");
366 }
367
368 #[test]
369 fn rolling_shutter_skew_is_not_rotation() {
370 let truth = Motion {
371 tx: -14.0,
372 ty: 1.2,
373 angle: 0.001,
374 scale: 1.0,
375 confidence: 1.0,
376 };
377 let (w, h) = (1920, 1080);
378 let (flow, cost, cols, rows) = field(&truth, 0.004, w, h);
379 let f = FlowField {
380 width: w,
381 height: h,
382 grid: 4,
383 cols,
384 rows,
385 flow: &flow,
386 cost: &cost,
387 };
388 let m = estimate(&f, &EstimateParams::default());
389 assert!((m.tx - truth.tx).abs() < 0.05, "{m:?}");
390 assert!((m.ty - truth.ty).abs() < 0.05, "{m:?}");
391 assert!((m.angle - truth.angle).abs() < 3e-5, "{m:?}");
392 }
393
394 #[test]
395 fn rejects_garbage() {
396 let flow = vec![[0.0_f32; 2]; 480 * 270];
397 let cost = vec![255_u8; 480 * 270];
398 let f = FlowField {
399 width: 1920,
400 height: 1080,
401 grid: 4,
402 cols: 480,
403 rows: 270,
404 flow: &flow,
405 cost: &cost,
406 };
407 assert_eq!(estimate(&f, &EstimateParams::default()), Motion::IDENTITY);
408 }
409}