Skip to main content

oximedia_align/
gradient_flow.rs

1//! Gradient-based optical flow for video alignment.
2//!
3//! Provides a simplified Lucas-Kanade style optical flow computation operating
4//! on blocks of grayscale pixels represented as `f32` slices.
5
6#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9/// A 2D flow vector with single-precision components.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct FlowVector {
12    /// Horizontal displacement in pixels.
13    pub dx: f32,
14    /// Vertical displacement in pixels.
15    pub dy: f32,
16}
17
18impl FlowVector {
19    /// Create a new flow vector.
20    #[must_use]
21    pub fn new(dx: f32, dy: f32) -> Self {
22        Self { dx, dy }
23    }
24
25    /// Euclidean magnitude of the vector.
26    #[must_use]
27    pub fn magnitude(&self) -> f32 {
28        (self.dx * self.dx + self.dy * self.dy).sqrt()
29    }
30
31    /// Angle of the vector in radians (atan2 convention, range −π..π).
32    #[must_use]
33    pub fn angle_rad(&self) -> f32 {
34        self.dy.atan2(self.dx)
35    }
36
37    /// Return `true` when both components are exactly zero.
38    #[must_use]
39    pub fn is_zero(&self) -> bool {
40        self.dx == 0.0 && self.dy == 0.0
41    }
42}
43
44impl Default for FlowVector {
45    fn default() -> Self {
46        Self::new(0.0, 0.0)
47    }
48}
49
50/// A dense optical flow field over a regular block grid.
51#[derive(Debug, Clone)]
52pub struct FlowField {
53    /// Row-major vector storage (`block_rows` × `block_cols`).
54    pub vectors: Vec<FlowVector>,
55    /// Frame width in pixels.
56    pub width: usize,
57    /// Frame height in pixels.
58    pub height: usize,
59}
60
61impl FlowField {
62    /// Create a new zeroed flow field for a frame of the given dimensions.
63    #[must_use]
64    pub fn new(width: usize, height: usize, block_size: usize) -> Self {
65        let cols = width.div_ceil(block_size.max(1));
66        let rows = height.div_ceil(block_size.max(1));
67        Self {
68            vectors: vec![FlowVector::default(); cols * rows],
69            width,
70            height,
71        }
72    }
73
74    /// Number of block columns stored in this field.
75    #[must_use]
76    pub fn block_cols(&self, block_size: usize) -> usize {
77        self.width.div_ceil(block_size.max(1))
78    }
79
80    /// Number of block rows stored in this field.
81    #[must_use]
82    pub fn block_rows(&self, block_size: usize) -> usize {
83        self.height.div_ceil(block_size.max(1))
84    }
85
86    /// Return a reference to the flow vector at block column `x`, block row `y`.
87    ///
88    /// # Panics
89    ///
90    /// Panics if the index is out of bounds.
91    #[must_use]
92    pub fn at(&self, x: usize, y: usize) -> &FlowVector {
93        let cols = self.width.div_ceil(1); // always non-zero width
94        &self.vectors[y * cols + x]
95    }
96
97    /// Arithmetic mean of the magnitudes of all flow vectors.
98    ///
99    /// Returns `0.0` if the field is empty.
100    #[must_use]
101    pub fn average_magnitude(&self) -> f32 {
102        if self.vectors.is_empty() {
103            return 0.0;
104        }
105        let sum: f32 = self.vectors.iter().map(FlowVector::magnitude).sum();
106        sum / self.vectors.len() as f32
107    }
108
109    /// Return a single [`FlowVector`] representing the arithmetic mean of all
110    /// vectors (the dominant translation direction).
111    #[must_use]
112    pub fn dominant_direction(&self) -> FlowVector {
113        if self.vectors.is_empty() {
114            return FlowVector::default();
115        }
116        let n = self.vectors.len() as f32;
117        let sum_dx: f32 = self.vectors.iter().map(|v| v.dx).sum();
118        let sum_dy: f32 = self.vectors.iter().map(|v| v.dy).sum();
119        FlowVector::new(sum_dx / n, sum_dy / n)
120    }
121}
122
123/// Compute a simplified Lucas-Kanade style flow vector for a single block.
124///
125/// The function estimates the spatiotemporal gradient using finite differences
126/// and solves the brightness-constancy equation:
127///
128/// > `Ix * vx + Iy * vy = -It`
129///
130/// when both spatial gradients are non-zero; otherwise it returns a zero
131/// vector.
132///
133/// # Arguments
134///
135/// * `prev_block` – Pixel values (f32) for the block in the previous frame.
136/// * `curr_block` – Pixel values (f32) for the block in the current frame.
137/// * `block_size` – Width (= height) of the square block.
138#[must_use]
139pub fn lucas_kanade_block(prev_block: &[f32], curr_block: &[f32], block_size: usize) -> FlowVector {
140    if prev_block.is_empty() || curr_block.is_empty() || block_size == 0 {
141        return FlowVector::default();
142    }
143
144    let n = (block_size * block_size) as f32;
145
146    // Accumulate normal-equation components
147    let mut sum_ix2 = 0.0_f32;
148    let mut sum_iy2 = 0.0_f32;
149    let mut sum_ix_iy = 0.0_f32;
150    let mut sum_ix_it = 0.0_f32;
151    let mut sum_iy_it = 0.0_f32;
152
153    let len = prev_block.len().min(curr_block.len());
154    for i in 0..len {
155        let it = curr_block[i] - prev_block[i];
156        // Approximate spatial gradients using central differences when possible
157        let ix = if i + 1 < len {
158            curr_block[i + 1] - curr_block[i]
159        } else {
160            0.0
161        };
162        let iy = if i + block_size < len {
163            curr_block[i + block_size] - curr_block[i]
164        } else {
165            0.0
166        };
167        sum_ix2 += ix * ix;
168        sum_iy2 += iy * iy;
169        sum_ix_iy += ix * iy;
170        sum_ix_it += ix * it;
171        sum_iy_it += iy * it;
172    }
173
174    // Normalise by number of pixels
175    let a = sum_ix2 / n;
176    let b = sum_ix_iy / n;
177    let c = sum_iy2 / n;
178    let d = -sum_ix_it / n;
179    let e = -sum_iy_it / n;
180
181    // Solve 2×2 system [a b; b c] [vx; vy] = [d; e]
182    let det = a * c - b * b;
183    if det.abs() < 1e-8 {
184        return FlowVector::default();
185    }
186
187    let vx = (d * c - e * b) / det;
188    let vy = (a * e - b * d) / det;
189    FlowVector::new(vx, vy)
190}
191
192/// Compute a dense flow field for an entire frame using [`lucas_kanade_block`].
193///
194/// # Arguments
195///
196/// * `prev_frame` – Grayscale pixel values of the previous frame (row-major).
197/// * `curr_frame` – Grayscale pixel values of the current frame (row-major).
198/// * `width` / `height` – Frame dimensions in pixels.
199/// * `block_size` – Block size in pixels (blocks do not overlap).
200#[must_use]
201pub fn compute_flow_field(
202    prev_frame: &[f32],
203    curr_frame: &[f32],
204    width: usize,
205    height: usize,
206    block_size: usize,
207) -> FlowField {
208    let bs = block_size.max(1);
209    let block_cols = width.div_ceil(bs);
210    let block_rows = height.div_ceil(bs);
211    let mut vectors = Vec::with_capacity(block_cols * block_rows);
212
213    for by in 0..block_rows {
214        for bx in 0..block_cols {
215            let x0 = bx * bs;
216            let y0 = by * bs;
217
218            // Extract block pixels
219            let mut prev_block = Vec::with_capacity(bs * bs);
220            let mut curr_block = Vec::with_capacity(bs * bs);
221
222            for row in 0..bs {
223                let y = y0 + row;
224                if y >= height {
225                    break;
226                }
227                for col in 0..bs {
228                    let x = x0 + col;
229                    if x >= width {
230                        break;
231                    }
232                    let idx = y * width + x;
233                    if idx < prev_frame.len() {
234                        prev_block.push(prev_frame[idx]);
235                    }
236                    if idx < curr_frame.len() {
237                        curr_block.push(curr_frame[idx]);
238                    }
239                }
240            }
241
242            let fv = lucas_kanade_block(&prev_block, &curr_block, bs);
243            vectors.push(fv);
244        }
245    }
246
247    FlowField {
248        vectors,
249        width,
250        height,
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use std::f32::consts::PI;
258
259    #[test]
260    fn test_flow_vector_magnitude_zero() {
261        let v = FlowVector::new(0.0, 0.0);
262        assert_eq!(v.magnitude(), 0.0);
263    }
264
265    #[test]
266    fn test_flow_vector_magnitude_345() {
267        let v = FlowVector::new(3.0, 4.0);
268        assert!((v.magnitude() - 5.0).abs() < 1e-5);
269    }
270
271    #[test]
272    fn test_flow_vector_angle_right() {
273        let v = FlowVector::new(1.0, 0.0);
274        assert!(v.angle_rad().abs() < 1e-5);
275    }
276
277    #[test]
278    fn test_flow_vector_angle_up() {
279        let v = FlowVector::new(0.0, 1.0);
280        assert!((v.angle_rad() - PI / 2.0).abs() < 1e-5);
281    }
282
283    #[test]
284    fn test_flow_vector_is_zero_true() {
285        let v = FlowVector::new(0.0, 0.0);
286        assert!(v.is_zero());
287    }
288
289    #[test]
290    fn test_flow_vector_is_zero_false() {
291        let v = FlowVector::new(0.0, 0.001);
292        assert!(!v.is_zero());
293    }
294
295    #[test]
296    fn test_flow_field_average_magnitude_empty_vectors() {
297        // A field with explicit zero vectors should return 0.0 average magnitude
298        let field = FlowField {
299            vectors: vec![FlowVector::default(); 4],
300            width: 8,
301            height: 8,
302        };
303        assert_eq!(field.average_magnitude(), 0.0);
304    }
305
306    #[test]
307    fn test_flow_field_average_magnitude_uniform() {
308        let v = FlowVector::new(3.0, 4.0); // magnitude 5
309        let field = FlowField {
310            vectors: vec![v; 4],
311            width: 8,
312            height: 8,
313        };
314        assert!((field.average_magnitude() - 5.0).abs() < 1e-5);
315    }
316
317    #[test]
318    fn test_flow_field_dominant_direction_uniform() {
319        let field = FlowField {
320            vectors: vec![FlowVector::new(2.0, -1.0); 6],
321            width: 12,
322            height: 8,
323        };
324        let d = field.dominant_direction();
325        assert!((d.dx - 2.0).abs() < 1e-5);
326        assert!((d.dy + 1.0).abs() < 1e-5);
327    }
328
329    #[test]
330    fn test_flow_field_dominant_direction_cancels() {
331        // Opposite dx vectors should cancel out
332        let field = FlowField {
333            vectors: vec![FlowVector::new(1.0, 0.0), FlowVector::new(-1.0, 0.0)],
334            width: 4,
335            height: 4,
336        };
337        let d = field.dominant_direction();
338        assert!(d.dx.abs() < 1e-5);
339    }
340
341    #[test]
342    fn test_lucas_kanade_block_empty_returns_zero() {
343        let fv = lucas_kanade_block(&[], &[], 8);
344        assert!(fv.is_zero());
345    }
346
347    #[test]
348    fn test_lucas_kanade_block_constant_frames_returns_zero() {
349        let prev: Vec<f32> = vec![128.0; 64];
350        let curr = prev.clone();
351        let fv = lucas_kanade_block(&prev, &curr, 8);
352        // No temporal gradient → zero flow
353        assert!(fv.is_zero());
354    }
355
356    #[test]
357    fn test_compute_flow_field_dimensions() {
358        let prev = vec![0.0_f32; 64 * 48];
359        let curr = prev.clone();
360        let field = compute_flow_field(&prev, &curr, 64, 48, 8);
361        // Should have 8*6 = 48 blocks
362        assert_eq!(field.vectors.len(), 48);
363    }
364
365    #[test]
366    fn test_compute_flow_field_constant_frames() {
367        let frame = vec![100.0_f32; 16 * 16];
368        let field = compute_flow_field(&frame, &frame, 16, 16, 4);
369        for v in &field.vectors {
370            assert!(v.is_zero());
371        }
372    }
373}