Skip to main content

oximedia_align/
farneback_flow.rs

1//! Dense optical flow using the Farneback polynomial expansion method.
2//!
3//! Implements a multi-scale dense optical flow algorithm inspired by Gunnar
4//! Farneback's method. Each pixel in the image is assigned a flow vector
5//! by fitting local polynomial approximations and solving for displacement.
6//!
7//! # Algorithm Overview
8//!
9//! 1. Build Gaussian image pyramids.
10//! 2. At the coarsest level, initialise flow to zero.
11//! 3. At each level:
12//!    a. Compute the polynomial expansion coefficients (quadratic model) for a
13//!       local neighbourhood around each pixel.
14//!    b. Use the polynomial coefficients from both frames to solve for the
15//!       displacement field via a weighted least-squares fit.
16//!    c. Propagate to the next finer level (upsample by 2).
17//! 4. The final result is a per-pixel `(dx, dy)` flow field at original resolution.
18//!
19//! # References
20//!
21//! - Farneback, G. "Two-Frame Motion Estimation Based on Polynomial Expansion"
22//!   Proceedings of the 13th Scandinavian Conference on Image Analysis, 2003.
23
24#![allow(clippy::cast_precision_loss)]
25
26use crate::{AlignError, AlignResult};
27
28/// Configuration for Farneback dense optical flow.
29#[derive(Debug, Clone)]
30pub struct FarnebackConfig {
31    /// Number of pyramid levels (1 = single resolution).
32    pub pyramid_levels: usize,
33    /// Polynomial expansion neighbourhood (half-size). Full window is `2*n + 1`.
34    pub poly_n: usize,
35    /// Standard deviation of the Gaussian weighting inside the poly window.
36    pub poly_sigma: f64,
37    /// Number of iterations at each pyramid level.
38    pub iterations: usize,
39    /// Size of the averaging window for flow smoothing.
40    pub win_size: usize,
41}
42
43impl Default for FarnebackConfig {
44    fn default() -> Self {
45        Self {
46            pyramid_levels: 3,
47            poly_n: 3,
48            poly_sigma: 1.2,
49            iterations: 5,
50            win_size: 5,
51        }
52    }
53}
54
55/// Dense flow field: per-pixel (dx, dy).
56#[derive(Debug, Clone)]
57pub struct DenseFlowField {
58    /// Horizontal displacements, row-major.
59    pub dx: Vec<f32>,
60    /// Vertical displacements, row-major.
61    pub dy: Vec<f32>,
62    /// Width in pixels.
63    pub width: usize,
64    /// Height in pixels.
65    pub height: usize,
66}
67
68impl DenseFlowField {
69    /// Create a zero-initialised dense flow field.
70    #[must_use]
71    pub fn zeros(width: usize, height: usize) -> Self {
72        let n = width * height;
73        Self {
74            dx: vec![0.0; n],
75            dy: vec![0.0; n],
76            width,
77            height,
78        }
79    }
80
81    /// Get the flow vector at pixel `(x, y)`.
82    #[must_use]
83    pub fn at(&self, x: usize, y: usize) -> Option<(f32, f32)> {
84        if x < self.width && y < self.height {
85            let idx = y * self.width + x;
86            Some((self.dx[idx], self.dy[idx]))
87        } else {
88            None
89        }
90    }
91
92    /// Compute the average magnitude over all pixels.
93    #[must_use]
94    pub fn avg_magnitude(&self) -> f32 {
95        let n = self.dx.len();
96        if n == 0 {
97            return 0.0;
98        }
99        let sum: f64 = self
100            .dx
101            .iter()
102            .zip(self.dy.iter())
103            .map(|(&dx, &dy)| f64::from((dx * dx + dy * dy).sqrt()))
104            .sum();
105        (sum / n as f64) as f32
106    }
107
108    /// Maximum magnitude flow vector.
109    #[must_use]
110    pub fn max_magnitude(&self) -> f32 {
111        self.dx
112            .iter()
113            .zip(self.dy.iter())
114            .map(|(&dx, &dy)| (dx * dx + dy * dy).sqrt())
115            .fold(0.0_f32, f32::max)
116    }
117
118    /// Upsample by factor 2 using bilinear interpolation.
119    #[must_use]
120    pub fn upsample_2x(&self) -> Self {
121        let nw = self.width * 2;
122        let nh = self.height * 2;
123        let mut out = DenseFlowField::zeros(nw, nh);
124
125        for y in 0..nh {
126            for x in 0..nw {
127                let sx = x as f32 / 2.0;
128                let sy = y as f32 / 2.0;
129
130                let x0 = (sx.floor() as usize).min(self.width.saturating_sub(1));
131                let y0 = (sy.floor() as usize).min(self.height.saturating_sub(1));
132                let x1 = (x0 + 1).min(self.width.saturating_sub(1));
133                let y1 = (y0 + 1).min(self.height.saturating_sub(1));
134
135                let fx = sx - x0 as f32;
136                let fy = sy - y0 as f32;
137
138                let idx = y * nw + x;
139
140                // Bilinear interpolate dx
141                let d00 = self.dx[y0 * self.width + x0];
142                let d10 = self.dx[y0 * self.width + x1];
143                let d01 = self.dx[y1 * self.width + x0];
144                let d11 = self.dx[y1 * self.width + x1];
145                out.dx[idx] = (d00 * (1.0 - fx) * (1.0 - fy)
146                    + d10 * fx * (1.0 - fy)
147                    + d01 * (1.0 - fx) * fy
148                    + d11 * fx * fy)
149                    * 2.0; // scale flow by 2
150
151                let d00 = self.dy[y0 * self.width + x0];
152                let d10 = self.dy[y0 * self.width + x1];
153                let d01 = self.dy[y1 * self.width + x0];
154                let d11 = self.dy[y1 * self.width + x1];
155                out.dy[idx] = (d00 * (1.0 - fx) * (1.0 - fy)
156                    + d10 * fx * (1.0 - fy)
157                    + d01 * (1.0 - fx) * fy
158                    + d11 * fx * fy)
159                    * 2.0;
160            }
161        }
162
163        out
164    }
165}
166
167/// Compute dense optical flow using the Farneback method.
168///
169/// Both `prev` and `curr` must be single-channel grayscale images of size
170/// `width * height`.
171///
172/// # Errors
173///
174/// Returns an error if the images have mismatched sizes or are too small.
175pub fn compute_farneback_flow(
176    prev: &[u8],
177    curr: &[u8],
178    width: usize,
179    height: usize,
180    config: &FarnebackConfig,
181) -> AlignResult<DenseFlowField> {
182    if prev.len() != width * height || curr.len() != width * height {
183        return Err(AlignError::InvalidConfig(
184            "Image size does not match width*height".to_string(),
185        ));
186    }
187    if width < 8 || height < 8 {
188        return Err(AlignError::InvalidConfig(
189            "Image must be at least 8x8".to_string(),
190        ));
191    }
192
193    // Build pyramids (f32 for precision)
194    let prev_pyr = build_f32_pyramid(prev, width, height, config.pyramid_levels);
195    let curr_pyr = build_f32_pyramid(curr, width, height, config.pyramid_levels);
196
197    // Start at coarsest level with zero flow
198    let coarsest = prev_pyr.len() - 1;
199    let mut flow = DenseFlowField::zeros(prev_pyr[coarsest].1, prev_pyr[coarsest].2);
200
201    // Coarse-to-fine refinement
202    for level in (0..prev_pyr.len()).rev() {
203        let (ref prev_img, pw, ph) = prev_pyr[level];
204        let (ref curr_img, _, _) = curr_pyr[level];
205
206        // If not the coarsest level, upsample the flow from the previous (coarser) result
207        if level < coarsest {
208            flow = flow.upsample_2x();
209            // Trim to actual level dimensions (pyramid dimensions might differ by 1)
210            flow = trim_flow(&flow, pw, ph);
211        }
212
213        // Compute polynomial expansion for both images
214        let poly_prev = polynomial_expansion(prev_img, pw, ph, config.poly_n, config.poly_sigma);
215        let poly_curr = polynomial_expansion(curr_img, pw, ph, config.poly_n, config.poly_sigma);
216
217        // Iterative refinement at this level
218        for _iter in 0..config.iterations {
219            flow = update_flow(&poly_prev, &poly_curr, &flow, pw, ph, config.win_size);
220        }
221    }
222
223    Ok(flow)
224}
225
226/// Polynomial expansion coefficients for one pixel.
227/// We store a simplified version: (r1, r2, r3, r4, r5) representing the
228/// quadratic model f(dx,dy) ~ r1*dx^2 + r2*dy^2 + r3*dx*dy + r4*dx + r5*dy + const.
229#[derive(Debug, Clone, Copy, Default)]
230struct PolyCoeff {
231    a11: f32, // coefficient of dx^2
232    a22: f32, // coefficient of dy^2
233    a12: f32, // coefficient of dx*dy
234    b1: f32,  // coefficient of dx
235    b2: f32,  // coefficient of dy
236}
237
238/// Compute polynomial expansion for the image.
239fn polynomial_expansion(
240    image: &[f32],
241    width: usize,
242    height: usize,
243    poly_n: usize,
244    poly_sigma: f64,
245) -> Vec<PolyCoeff> {
246    let n = width * height;
247    let mut coeffs = vec![PolyCoeff::default(); n];
248
249    // Build Gaussian weight kernel
250    let ksize = 2 * poly_n + 1;
251    let mut kernel = vec![0.0_f64; ksize * ksize];
252    let sigma2 = poly_sigma * poly_sigma;
253    let pn = poly_n as isize;
254
255    for ky in 0..ksize {
256        for kx in 0..ksize {
257            let dx = kx as f64 - poly_n as f64;
258            let dy = ky as f64 - poly_n as f64;
259            kernel[ky * ksize + kx] = (-0.5 * (dx * dx + dy * dy) / sigma2).exp();
260        }
261    }
262
263    // For each interior pixel, fit a local quadratic
264    for y in poly_n..height.saturating_sub(poly_n) {
265        for x in poly_n..width.saturating_sub(poly_n) {
266            let idx = y * width + x;
267
268            // Weighted least squares to fit: f = a11*dx^2 + a22*dy^2 + a12*dx*dy + b1*dx + b2*dy + c
269            // We accumulate the normal equations.
270            let mut s_xx_xx = 0.0_f64;
271            let mut s_yy_yy = 0.0_f64;
272            let mut s_xy_xy = 0.0_f64;
273            let mut s_x_x = 0.0_f64;
274            let mut s_y_y = 0.0_f64;
275            let mut s_xx_f = 0.0_f64;
276            let mut s_yy_f = 0.0_f64;
277            let mut s_xy_f = 0.0_f64;
278            let mut s_x_f = 0.0_f64;
279            let mut s_y_f = 0.0_f64;
280
281            for ky in 0..ksize {
282                for kx in 0..ksize {
283                    let dx = kx as f64 - poly_n as f64;
284                    let dy = ky as f64 - poly_n as f64;
285                    let w = kernel[ky * ksize + kx];
286
287                    let nx = (x as isize + kx as isize - pn) as usize;
288                    let ny = (y as isize + ky as isize - pn) as usize;
289                    let val = f64::from(image[ny * width + nx]);
290
291                    let xx = dx * dx;
292                    let yy = dy * dy;
293                    let xy = dx * dy;
294
295                    s_xx_xx += w * xx * xx;
296                    s_yy_yy += w * yy * yy;
297                    s_xy_xy += w * xy * xy;
298                    s_x_x += w * dx * dx;
299                    s_y_y += w * dy * dy;
300                    s_xx_f += w * xx * val;
301                    s_yy_f += w * yy * val;
302                    s_xy_f += w * xy * val;
303                    s_x_f += w * dx * val;
304                    s_y_f += w * dy * val;
305                }
306            }
307
308            // Solve diagonal-approximated normal equations for simplicity.
309            // Full normal equation solve would involve a 6x6 system; we use
310            // the diagonal approximation which is sufficient for flow estimation.
311            let a11 = if s_xx_xx > 1e-10 {
312                s_xx_f / s_xx_xx
313            } else {
314                0.0
315            };
316            let a22 = if s_yy_yy > 1e-10 {
317                s_yy_f / s_yy_yy
318            } else {
319                0.0
320            };
321            let a12 = if s_xy_xy > 1e-10 {
322                s_xy_f / s_xy_xy
323            } else {
324                0.0
325            };
326            let b1 = if s_x_x > 1e-10 { s_x_f / s_x_x } else { 0.0 };
327            let b2 = if s_y_y > 1e-10 { s_y_f / s_y_y } else { 0.0 };
328
329            coeffs[idx] = PolyCoeff {
330                a11: a11 as f32,
331                a22: a22 as f32,
332                a12: a12 as f32,
333                b1: b1 as f32,
334                b2: b2 as f32,
335            };
336        }
337    }
338
339    coeffs
340}
341
342/// Update the flow field using the polynomial coefficients.
343fn update_flow(
344    poly_prev: &[PolyCoeff],
345    poly_curr: &[PolyCoeff],
346    flow: &DenseFlowField,
347    width: usize,
348    height: usize,
349    win_size: usize,
350) -> DenseFlowField {
351    let mut new_flow = DenseFlowField::zeros(width, height);
352    let half = (win_size / 2) as isize;
353
354    for y in 0..height {
355        for x in 0..width {
356            let idx = y * width + x;
357
358            // Accumulate structure tensor and mismatch from neighbourhood
359            let mut h11 = 0.0_f32;
360            let mut h22 = 0.0_f32;
361            let mut h12 = 0.0_f32;
362            let mut g1 = 0.0_f32;
363            let mut g2 = 0.0_f32;
364
365            for dy in -half..=half {
366                for dx in -half..=half {
367                    let nx = x as isize + dx;
368                    let ny = y as isize + dy;
369
370                    if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
371                        continue;
372                    }
373
374                    let nidx = ny as usize * width + nx as usize;
375                    let pp = &poly_prev[nidx];
376                    let pc = &poly_curr[nidx];
377
378                    // Average of the two Hessians
379                    let a11 = (pp.a11 + pc.a11) * 0.5;
380                    let a22 = (pp.a22 + pc.a22) * 0.5;
381                    let a12 = (pp.a12 + pc.a12) * 0.5;
382
383                    // Difference of the gradients adjusted for current flow
384                    let fx = flow.dx[nidx];
385                    let fy = flow.dy[nidx];
386
387                    let db1 = pc.b1 - pp.b1 + 2.0 * a11 * fx + a12 * fy;
388                    let db2 = pc.b2 - pp.b2 + a12 * fx + 2.0 * a22 * fy;
389
390                    h11 += 4.0 * a11 * a11 + a12 * a12;
391                    h22 += a12 * a12 + 4.0 * a22 * a22;
392                    h12 += 2.0 * a11 * a12 + 2.0 * a12 * a22;
393                    g1 += 2.0 * a11 * db1 + a12 * db2;
394                    g2 += a12 * db1 + 2.0 * a22 * db2;
395                }
396            }
397
398            // Solve 2x2 system
399            let det = h11 * h22 - h12 * h12;
400            if det.abs() > 1e-6 {
401                let inv_det = 1.0 / det;
402                new_flow.dx[idx] = flow.dx[idx] - (h22 * g1 - h12 * g2) * inv_det;
403                new_flow.dy[idx] = flow.dy[idx] - (-h12 * g1 + h11 * g2) * inv_det;
404            } else {
405                new_flow.dx[idx] = flow.dx[idx];
406                new_flow.dy[idx] = flow.dy[idx];
407            }
408        }
409    }
410
411    new_flow
412}
413
414// -- Pyramid helpers ----------------------------------------------------------
415
416fn build_f32_pyramid(
417    image: &[u8],
418    width: usize,
419    height: usize,
420    levels: usize,
421) -> Vec<(Vec<f32>, usize, usize)> {
422    let f32_img: Vec<f32> = image.iter().map(|&v| f32::from(v)).collect();
423    let mut pyr = Vec::with_capacity(levels);
424    pyr.push((f32_img, width, height));
425
426    for _ in 1..levels {
427        let (ref prev, pw, ph) = pyr[pyr.len() - 1];
428        let nw = pw / 2;
429        let nh = ph / 2;
430        if nw < 4 || nh < 4 {
431            break;
432        }
433        let down = downsample_f32(prev, pw, ph, nw, nh);
434        pyr.push((down, nw, nh));
435    }
436
437    pyr
438}
439
440fn downsample_f32(src: &[f32], sw: usize, sh: usize, dw: usize, dh: usize) -> Vec<f32> {
441    let mut dst = vec![0.0_f32; dw * dh];
442    for dy in 0..dh {
443        for dx in 0..dw {
444            let sx = dx * 2;
445            let sy = dy * 2;
446            let mut sum = 0.0_f32;
447            let mut count = 0u32;
448            for oy in 0..2 {
449                for ox in 0..2 {
450                    let rx = sx + ox;
451                    let ry = sy + oy;
452                    if rx < sw && ry < sh {
453                        sum += src[ry * sw + rx];
454                        count += 1;
455                    }
456                }
457            }
458            dst[dy * dw + dx] = if count > 0 { sum / count as f32 } else { 0.0 };
459        }
460    }
461    dst
462}
463
464fn trim_flow(flow: &DenseFlowField, target_w: usize, target_h: usize) -> DenseFlowField {
465    let mut out = DenseFlowField::zeros(target_w, target_h);
466    for y in 0..target_h.min(flow.height) {
467        for x in 0..target_w.min(flow.width) {
468            let src_idx = y * flow.width + x;
469            let dst_idx = y * target_w + x;
470            out.dx[dst_idx] = flow.dx[src_idx];
471            out.dy[dst_idx] = flow.dy[src_idx];
472        }
473    }
474    out
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    // -- DenseFlowField -------------------------------------------------------
482
483    #[test]
484    fn test_dense_flow_zeros() {
485        let f = DenseFlowField::zeros(10, 10);
486        assert_eq!(f.width, 10);
487        assert_eq!(f.height, 10);
488        assert_eq!(f.avg_magnitude(), 0.0);
489        assert_eq!(f.max_magnitude(), 0.0);
490    }
491
492    #[test]
493    fn test_dense_flow_at() {
494        let mut f = DenseFlowField::zeros(4, 4);
495        f.dx[5] = 3.0;
496        f.dy[5] = 4.0;
497        let (dx, dy) = f.at(1, 1).expect("should be in bounds");
498        assert!((dx - 3.0).abs() < 1e-6);
499        assert!((dy - 4.0).abs() < 1e-6);
500    }
501
502    #[test]
503    fn test_dense_flow_at_out_of_bounds() {
504        let f = DenseFlowField::zeros(4, 4);
505        assert!(f.at(10, 10).is_none());
506    }
507
508    #[test]
509    fn test_dense_flow_upsample() {
510        let mut f = DenseFlowField::zeros(4, 4);
511        for i in 0..16 {
512            f.dx[i] = 1.0;
513            f.dy[i] = -1.0;
514        }
515        let up = f.upsample_2x();
516        assert_eq!(up.width, 8);
517        assert_eq!(up.height, 8);
518        // After upsampling, flow values should be roughly doubled
519        let (dx, dy) = up.at(4, 4).expect("should be in bounds");
520        assert!((dx - 2.0).abs() < 0.5, "dx={dx}");
521        assert!((dy + 2.0).abs() < 0.5, "dy={dy}");
522    }
523
524    #[test]
525    fn test_dense_flow_max_magnitude() {
526        let mut f = DenseFlowField::zeros(4, 4);
527        f.dx[0] = 3.0;
528        f.dy[0] = 4.0;
529        assert!((f.max_magnitude() - 5.0).abs() < 1e-5);
530    }
531
532    // -- Farneback flow -------------------------------------------------------
533
534    #[test]
535    fn test_farneback_identical_frames() {
536        let w = 64usize;
537        let h = 64usize;
538        let img = vec![128u8; w * h];
539
540        let config = FarnebackConfig {
541            pyramid_levels: 2,
542            poly_n: 2,
543            poly_sigma: 1.0,
544            iterations: 3,
545            win_size: 5,
546        };
547
548        let flow = compute_farneback_flow(&img, &img, w, h, &config).expect("should succeed");
549        assert_eq!(flow.width, w);
550        assert_eq!(flow.height, h);
551        // Identical frames should have near-zero flow
552        assert!(
553            flow.avg_magnitude() < 1.0,
554            "identical frames avg_mag={}",
555            flow.avg_magnitude()
556        );
557    }
558
559    #[test]
560    fn test_farneback_shifted_image() {
561        let w = 64usize;
562        let h = 64usize;
563        // Create a vertical stripe pattern
564        let mut prev = vec![0u8; w * h];
565        let mut curr = vec![0u8; w * h];
566        for y in 0..h {
567            for x in 0..w {
568                prev[y * w + x] = if (x / 8) % 2 == 0 { 200 } else { 50 };
569                // Shift by 2 pixels to the right
570                let sx = (x + 2).min(w - 1);
571                curr[y * w + x] = if (sx / 8) % 2 == 0 { 200 } else { 50 };
572            }
573        }
574
575        let config = FarnebackConfig {
576            pyramid_levels: 2,
577            poly_n: 3,
578            poly_sigma: 1.2,
579            iterations: 5,
580            win_size: 7,
581        };
582
583        let flow = compute_farneback_flow(&prev, &curr, w, h, &config).expect("should succeed");
584        // There should be some non-zero flow
585        assert!(flow.max_magnitude() > 0.0, "should detect some motion");
586    }
587
588    #[test]
589    fn test_farneback_image_mismatch() {
590        let config = FarnebackConfig::default();
591        let result = compute_farneback_flow(&[0u8; 100], &[0u8; 200], 10, 10, &config);
592        assert!(result.is_err());
593    }
594
595    #[test]
596    fn test_farneback_too_small() {
597        let config = FarnebackConfig::default();
598        let result = compute_farneback_flow(&[0u8; 4], &[0u8; 4], 2, 2, &config);
599        assert!(result.is_err());
600    }
601
602    #[test]
603    fn test_farneback_default_config() {
604        let config = FarnebackConfig::default();
605        assert_eq!(config.pyramid_levels, 3);
606        assert_eq!(config.poly_n, 3);
607        assert_eq!(config.iterations, 5);
608    }
609
610    // -- Pyramid helpers ------------------------------------------------------
611
612    #[test]
613    fn test_f32_pyramid_levels() {
614        let img = vec![100u8; 64 * 64];
615        let pyr = build_f32_pyramid(&img, 64, 64, 3);
616        assert_eq!(pyr.len(), 3);
617        assert_eq!(pyr[0].1, 64);
618        assert_eq!(pyr[1].1, 32);
619        assert_eq!(pyr[2].1, 16);
620    }
621
622    #[test]
623    fn test_f32_pyramid_constant_preserved() {
624        let img = vec![100u8; 32 * 32];
625        let pyr = build_f32_pyramid(&img, 32, 32, 2);
626        for &v in &pyr[1].0 {
627            assert!((v - 100.0).abs() < 1e-3);
628        }
629    }
630
631    #[test]
632    fn test_trim_flow_smaller() {
633        let f = DenseFlowField::zeros(8, 8);
634        let trimmed = trim_flow(&f, 4, 4);
635        assert_eq!(trimmed.width, 4);
636        assert_eq!(trimmed.height, 4);
637    }
638
639    #[test]
640    fn test_polynomial_expansion_constant() {
641        let img = vec![100.0_f32; 32 * 32];
642        let coeffs = polynomial_expansion(&img, 32, 32, 2, 1.0);
643        // On a constant image, the linear and quadratic coefficients should be near zero.
644        for c in &coeffs[3 * 32 + 3..29 * 32 + 29] {
645            assert!(c.b1.abs() < 1.0, "b1={}", c.b1);
646            assert!(c.b2.abs() < 1.0, "b2={}", c.b2);
647        }
648    }
649}