Skip to main content

scirs2_vision/feature/
optical_flow.rs

1//! Optical flow computation for motion analysis
2//!
3//! This module provides algorithms for computing optical flow between
4//! consecutive frames, useful for motion analysis and tracking.
5
6use crate::error::Result;
7use image::{DynamicImage, GrayImage, ImageBuffer, Luma, Rgb, RgbImage};
8use scirs2_core::ndarray::{s, Array2};
9
10/// Optical flow vector at a point
11#[derive(Debug, Clone, Copy)]
12pub struct FlowVector {
13    /// Horizontal displacement
14    pub u: f32,
15    /// Vertical displacement
16    pub v: f32,
17}
18
19/// Parameters for Lucas-Kanade optical flow
20#[derive(Debug, Clone)]
21pub struct LucasKanadeParams {
22    /// Window size for local computation
23    pub window_size: usize,
24    /// Maximum iterations for iterative refinement
25    pub max_iterations: usize,
26    /// Convergence threshold
27    pub epsilon: f32,
28    /// Number of pyramid levels (0 for no pyramid)
29    pub pyramid_levels: usize,
30}
31
32impl Default for LucasKanadeParams {
33    fn default() -> Self {
34        Self {
35            window_size: 15,
36            max_iterations: 20,
37            epsilon: 0.01,
38            pyramid_levels: 3,
39        }
40    }
41}
42
43/// Compute optical flow using Lucas-Kanade method
44///
45/// # Arguments
46///
47/// * `img1` - First frame
48/// * `img2` - Second frame
49/// * `points` - Points to track (if None, computes dense flow)
50/// * `params` - Algorithm parameters
51///
52/// # Returns
53///
54/// * Flow field as 2D array of flow vectors
55///
56/// # Example
57///
58/// ```rust
59/// use scirs2_vision::feature::{lucas_kanade_flow, LucasKanadeParams};
60/// use image::{DynamicImage, RgbImage};
61///
62/// # fn main() -> scirs2_vision::error::Result<()> {
63/// // Create simple test images
64/// let frame1 = DynamicImage::ImageRgb8(RgbImage::new(64, 64));
65/// let frame2 = DynamicImage::ImageRgb8(RgbImage::new(64, 64));
66/// let flow = lucas_kanade_flow(&frame1, &frame2, None, &LucasKanadeParams::default())?;
67/// # Ok(())
68/// # }
69/// ```
70#[allow(dead_code)]
71pub fn lucas_kanade_flow(
72    img1: &DynamicImage,
73    img2: &DynamicImage,
74    points: Option<&[(f32, f32)]>,
75    params: &LucasKanadeParams,
76) -> Result<Array2<FlowVector>> {
77    let gray1 = img1.to_luma8();
78    let gray2 = img2.to_luma8();
79
80    if params.pyramid_levels > 0 {
81        pyramidal_lucas_kanade(&gray1, &gray2, points, params)
82    } else {
83        simple_lucas_kanade(&gray1, &gray2, points, params)
84    }
85}
86
87/// Simple Lucas-Kanade without pyramid
88#[allow(dead_code)]
89fn simple_lucas_kanade(
90    img1: &GrayImage,
91    img2: &GrayImage,
92    points: Option<&[(f32, f32)]>,
93    params: &LucasKanadeParams,
94) -> Result<Array2<FlowVector>> {
95    let (width, height) = img1.dimensions();
96
97    // Convert images to float arrays
98    let i1 = image_to_float_array(img1);
99    let i2 = image_to_float_array(img2);
100
101    // Compute image gradients
102    let (ix, iy) = compute_gradients(&i1);
103
104    let half_window = params.window_size / 2;
105
106    // Determine points to compute flow for
107    let track_points: Vec<(f32, f32)> = if let Some(pts) = points {
108        pts.to_vec()
109    } else {
110        // Dense flow - compute for all pixels with sufficient margin
111        let mut pts = Vec::new();
112        for y in half_window..height as usize - half_window {
113            for x in half_window..width as usize - half_window {
114                pts.push((x as f32, y as f32));
115            }
116        }
117        pts
118    };
119
120    // Initialize flow field
121    let mut flow = Array2::from_elem(
122        (height as usize, width as usize),
123        FlowVector { u: 0.0, v: 0.0 },
124    );
125
126    // Compute flow for each point
127    for &(px, py) in &track_points {
128        let x = px as usize;
129        let y = py as usize;
130
131        // Skip boundary points
132        if x < half_window
133            || x >= width as usize - half_window
134            || y < half_window
135            || y >= height as usize - half_window
136        {
137            continue;
138        }
139
140        // Extract window around point
141        let window_ix = ix.slice(s![
142            y - half_window..=y + half_window,
143            x - half_window..=x + half_window
144        ]);
145        let window_iy = iy.slice(s![
146            y - half_window..=y + half_window,
147            x - half_window..=x + half_window
148        ]);
149        let window_i1 = i1.slice(s![
150            y - half_window..=y + half_window,
151            x - half_window..=x + half_window
152        ]);
153
154        // Build system matrix A^T A
155        let mut a11 = 0.0f32;
156        let mut a12 = 0.0f32;
157        let mut a22 = 0.0f32;
158
159        for ((ix_val, iy_val), _) in window_ix.iter().zip(window_iy.iter()).zip(window_i1.iter()) {
160            a11 += ix_val * ix_val;
161            a12 += ix_val * iy_val;
162            a22 += iy_val * iy_val;
163        }
164
165        let det = a11 * a22 - a12 * a12;
166        if det.abs() < 1e-6 {
167            continue; // Singular matrix, skip this point
168        }
169
170        // Iterative refinement
171        let mut u = 0.0f32;
172        let mut v = 0.0f32;
173
174        for _ in 0..params.max_iterations {
175            // Get warped window from second image
176            let warped_x = (x as f32 + u) as usize;
177            let warped_y = (y as f32 + v) as usize;
178
179            if warped_x < half_window
180                || warped_x >= width as usize - half_window
181                || warped_y < half_window
182                || warped_y >= height as usize - half_window
183            {
184                break;
185            }
186
187            let window_i2 = i2.slice(s![
188                warped_y - half_window..=warped_y + half_window,
189                warped_x - half_window..=warped_x + half_window
190            ]);
191
192            // Compute temporal derivative and error
193            let mut b1 = 0.0f32;
194            let mut b2 = 0.0f32;
195
196            for ((&ix_val, &iy_val), (&i1_val, &i2_val)) in window_ix
197                .iter()
198                .zip(window_iy.iter())
199                .zip(window_i1.iter().zip(window_i2.iter()))
200            {
201                let it = i2_val - i1_val;
202                b1 -= ix_val * it;
203                b2 -= iy_val * it;
204            }
205
206            // Solve for flow update
207            let inv_det = 1.0 / det;
208            let du = inv_det * (a22 * b1 - a12 * b2);
209            let dv = inv_det * (-a12 * b1 + a11 * b2);
210
211            u += du;
212            v += dv;
213
214            if du.abs() < params.epsilon && dv.abs() < params.epsilon {
215                break;
216            }
217        }
218
219        flow[[y, x]] = FlowVector { u, v };
220    }
221
222    Ok(flow)
223}
224
225/// Pyramidal Lucas-Kanade
226#[allow(dead_code)]
227fn pyramidal_lucas_kanade(
228    img1: &GrayImage,
229    img2: &GrayImage,
230    points: Option<&[(f32, f32)]>,
231    params: &LucasKanadeParams,
232) -> Result<Array2<FlowVector>> {
233    let (width, height) = img1.dimensions();
234
235    // Build image pyramids
236    let pyramid1 = build_pyramid(img1, params.pyramid_levels);
237    let pyramid2 = build_pyramid(img2, params.pyramid_levels);
238
239    // Initialize flow
240    let mut flow = Array2::from_elem(
241        (height as usize, width as usize),
242        FlowVector { u: 0.0, v: 0.0 },
243    );
244
245    // Process from coarse to fine
246    for level in (0..params.pyramid_levels).rev() {
247        let scale = 2.0_f32.powi(level as i32);
248
249        // Scale points for this level
250        let scaled_points: Option<Vec<(f32, f32)>> =
251            points.map(|pts| pts.iter().map(|&(x, y)| (x / scale, y / scale)).collect());
252
253        // Compute flow at this level
254        let level_params = LucasKanadeParams {
255            pyramid_levels: 0, // No recursion
256            ..params.clone()
257        };
258
259        let level_flow = simple_lucas_kanade(
260            &pyramid1[level],
261            &pyramid2[level],
262            scaled_points.as_deref(),
263            &level_params,
264        )?;
265
266        // Propagate flow to finer level
267        if level > 0 {
268            let (level_width, level_height) = pyramid1[level].dimensions();
269            for y in 0..level_height as usize {
270                for x in 0..level_width as usize {
271                    let fine_x = (x * 2).min(width as usize - 1);
272                    let fine_y = (y * 2).min(height as usize - 1);
273
274                    flow[[fine_y, fine_x]].u = level_flow[[y, x]].u * 2.0;
275                    flow[[fine_y, fine_x]].v = level_flow[[y, x]].v * 2.0;
276
277                    // Fill neighboring pixels
278                    if fine_x + 1 < width as usize {
279                        flow[[fine_y, fine_x + 1]] = flow[[fine_y, fine_x]];
280                    }
281                    if fine_y + 1 < height as usize {
282                        flow[[fine_y + 1, fine_x]] = flow[[fine_y, fine_x]];
283                        if fine_x + 1 < width as usize {
284                            flow[[fine_y + 1, fine_x + 1]] = flow[[fine_y, fine_x]];
285                        }
286                    }
287                }
288            }
289        } else {
290            flow = level_flow;
291        }
292    }
293
294    Ok(flow)
295}
296
297/// Build image pyramid
298#[allow(dead_code)]
299fn build_pyramid(img: &GrayImage, levels: usize) -> Vec<GrayImage> {
300    let mut pyramid = vec![img.clone()];
301
302    for _ in 1..levels {
303        let prev = &pyramid[pyramid.len() - 1];
304        let (width, height) = prev.dimensions();
305        let new_width = width / 2;
306        let new_height = height / 2;
307
308        let mut downsampled = ImageBuffer::new(new_width, new_height);
309
310        for y in 0..new_height {
311            for x in 0..new_width {
312                // Simple 2x2 average
313                let x2 = x * 2;
314                let y2 = y * 2;
315
316                let sum = prev.get_pixel(x2, y2)[0] as u32
317                    + prev.get_pixel(x2 + 1, y2)[0] as u32
318                    + prev.get_pixel(x2, y2 + 1)[0] as u32
319                    + prev.get_pixel(x2 + 1, y2 + 1)[0] as u32;
320
321                downsampled.put_pixel(x, y, Luma([(sum / 4) as u8]));
322            }
323        }
324
325        pyramid.push(downsampled);
326    }
327
328    pyramid
329}
330
331/// Convert image to float array
332#[allow(dead_code)]
333fn image_to_float_array(img: &GrayImage) -> Array2<f32> {
334    let (width, height) = img.dimensions();
335    let mut array = Array2::zeros((height as usize, width as usize));
336
337    for y in 0..height {
338        for x in 0..width {
339            array[[y as usize, x as usize]] = img.get_pixel(x, y)[0] as f32 / 255.0;
340        }
341    }
342
343    array
344}
345
346/// Compute image gradients using Scharr operator
347#[allow(dead_code)]
348fn compute_gradients(img: &Array2<f32>) -> (Array2<f32>, Array2<f32>) {
349    let (height, width) = img.dim();
350    let mut ix = Array2::zeros((height, width));
351    let mut iy = Array2::zeros((height, width));
352
353    // Scharr kernels
354    let scharr_x = [[-3.0, 0.0, 3.0], [-10.0, 0.0, 10.0], [-3.0, 0.0, 3.0]];
355    let scharr_y = [[-3.0, -10.0, -3.0], [0.0, 0.0, 0.0], [3.0, 10.0, 3.0]];
356
357    for y in 1..height - 1 {
358        for x in 1..width - 1 {
359            let mut gx = 0.0;
360            let mut gy = 0.0;
361
362            for dy in -1..=1 {
363                for dx in -1..=1 {
364                    let pixel = img[[(y as i32 + dy) as usize, (x as i32 + dx) as usize]];
365                    gx += pixel * scharr_x[(dy + 1) as usize][(dx + 1) as usize] / 32.0;
366                    gy += pixel * scharr_y[(dy + 1) as usize][(dx + 1) as usize] / 32.0;
367                }
368            }
369
370            ix[[y, x]] = gx;
371            iy[[y, x]] = gy;
372        }
373    }
374
375    (ix, iy)
376}
377
378/// Visualize optical flow as color image
379///
380/// # Arguments
381///
382/// * `flow` - Flow field
383/// * `max_flow` - Maximum flow magnitude for scaling (None for auto)
384///
385/// # Returns
386///
387/// * RGB image with flow visualization
388#[allow(dead_code)]
389pub fn visualize_flow(_flow: &Array2<FlowVector>, maxflow: Option<f32>) -> RgbImage {
390    let (height, width) = _flow.dim();
391    let mut result = RgbImage::new(width as u32, height as u32);
392
393    // Find maximum _flow if not provided
394    let max_magnitude = if let Some(max) = maxflow {
395        max
396    } else {
397        let mut max = 0.0f32;
398        for flow_vec in _flow.iter() {
399            let magnitude = (flow_vec.u.powi(2) + flow_vec.v.powi(2)).sqrt();
400            if magnitude > max {
401                max = magnitude;
402            }
403        }
404        max.max(1.0) // Avoid division by zero
405    };
406
407    for y in 0..height {
408        for x in 0..width {
409            let flow_vec = &_flow[[y, x]];
410            let magnitude = (flow_vec.u.powi(2) + flow_vec.v.powi(2)).sqrt();
411            let angle = flow_vec.v.atan2(flow_vec.u);
412
413            // Convert to HSV color
414            let hue = (angle + std::f32::consts::PI) / (2.0 * std::f32::consts::PI);
415            let saturation = (magnitude / max_magnitude).min(1.0);
416            let value = saturation; // Or use 1.0 for constant brightness
417
418            // Convert HSV to RGB
419            let (r, g, b) = hsv_to_rgb(hue, saturation, value);
420            result.put_pixel(
421                x as u32,
422                y as u32,
423                Rgb([(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]),
424            );
425        }
426    }
427
428    result
429}
430
431/// Convert HSV to RGB
432#[allow(dead_code)]
433fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
434    let c = v * s;
435    let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
436    let m = v - c;
437
438    let (r, g, b) = match (h * 6.0) as i32 {
439        0 => (c, x, 0.0),
440        1 => (x, c, 0.0),
441        2 => (0.0, c, x),
442        3 => (0.0, x, c),
443        4 => (x, 0.0, c),
444        _ => (c, 0.0, x),
445    };
446
447    (r + m, g + m, b + m)
448}
449
450/// Dense optical flow using Farneback method (simplified version)
451#[allow(dead_code)]
452pub fn farneback_flow(
453    img1: &DynamicImage,
454    img2: &DynamicImage,
455    _pyr_scale: f32,
456    _levels: usize,
457    winsize: usize,
458    _iterations: usize,
459) -> Result<Array2<FlowVector>> {
460    let gray1 = img1.to_luma8();
461    let gray2 = img2.to_luma8();
462    let (width, height) = gray1.dimensions();
463
464    // Initialize flow
465    let mut flow = Array2::from_elem(
466        (height as usize, width as usize),
467        FlowVector { u: 0.0, v: 0.0 },
468    );
469
470    // Simplified dense flow computation
471    let i1 = image_to_float_array(&gray1);
472    let i2 = image_to_float_array(&gray2);
473    let (ix, iy) = compute_gradients(&i1);
474
475    let half_win = winsize / 2;
476
477    for y in half_win..height as usize - half_win {
478        for x in half_win..width as usize - half_win {
479            // Extract windows
480            let win_ix = ix.slice(s![y - half_win..=y + half_win, x - half_win..=x + half_win]);
481            let win_iy = iy.slice(s![y - half_win..=y + half_win, x - half_win..=x + half_win]);
482
483            // Compute structure tensor
484            let mut ixx = 0.0;
485            let mut ixy = 0.0;
486            let mut iyy = 0.0;
487
488            for (&ix_val, &iy_val) in win_ix.iter().zip(win_iy.iter()) {
489                ixx += ix_val * ix_val;
490                ixy += ix_val * iy_val;
491                iyy += iy_val * iy_val;
492            }
493
494            let det = ixx * iyy - ixy * ixy;
495            if det > 1e-6 {
496                // Simplified flow computation
497                let win_i1 = i1.slice(s![y - half_win..=y + half_win, x - half_win..=x + half_win]);
498                let win_i2 = i2.slice(s![y - half_win..=y + half_win, x - half_win..=x + half_win]);
499
500                let mut bx = 0.0;
501                let mut by = 0.0;
502
503                for ((&i1_val, &i2_val), (&ix_val, &iy_val)) in win_i1
504                    .iter()
505                    .zip(win_i2.iter())
506                    .zip(win_ix.iter().zip(win_iy.iter()))
507                {
508                    let it = i2_val - i1_val;
509                    bx -= ix_val * it;
510                    by -= iy_val * it;
511                }
512
513                let inv_det = 1.0 / det;
514                flow[[y, x]] = FlowVector {
515                    u: inv_det * (iyy * bx - ixy * by),
516                    v: inv_det * (-ixy * bx + ixx * by),
517                };
518            }
519        }
520    }
521
522    Ok(flow)
523}
524
525/// Parameters for Horn-Schunck dense optical flow
526///
527/// The Horn-Schunck method computes dense optical flow by minimizing a global
528/// energy functional that combines a data term (brightness constancy) with a
529/// smoothness regularization term.
530///
531/// # References
532///
533/// - Horn, B.K. and Schunck, B.G., 1981. Determining optical flow.
534///   Artificial intelligence, 17(1-3), pp.185-203.
535#[derive(Debug, Clone)]
536pub struct HornSchunckParams {
537    /// Smoothness weight (alpha). Larger values produce smoother flow fields
538    /// but may miss fine motion details. Typical range: 1.0 to 100.0.
539    pub alpha: f32,
540    /// Maximum number of Jacobi/Gauss-Seidel iterations
541    pub max_iterations: usize,
542    /// Convergence threshold (maximum change between iterations)
543    pub epsilon: f32,
544}
545
546impl Default for HornSchunckParams {
547    fn default() -> Self {
548        Self {
549            alpha: 15.0,
550            max_iterations: 200,
551            epsilon: 1e-4,
552        }
553    }
554}
555
556/// Compute dense optical flow using the Horn-Schunck variational method
557///
558/// This method produces a dense flow field (one vector per pixel) by solving
559/// a global optimization problem that balances the brightness constancy
560/// constraint with a first-order smoothness prior.
561///
562/// The energy functional minimized is:
563///   E(u,v) = integral [ (I_x u + I_y v + I_t)^2 + alpha^2 (|grad u|^2 + |grad v|^2) ] dx dy
564///
565/// The solution is obtained iteratively using a Gauss-Seidel scheme.
566///
567/// # Arguments
568///
569/// * `img1` - First frame (reference)
570/// * `img2` - Second frame (target)
571/// * `params` - Horn-Schunck parameters
572///
573/// # Returns
574///
575/// * Dense flow field as 2D array of flow vectors
576///
577/// # Example
578///
579/// ```rust
580/// use scirs2_vision::feature::optical_flow::{horn_schunck_flow, HornSchunckParams};
581/// use image::{DynamicImage, RgbImage};
582///
583/// # fn main() -> scirs2_vision::error::Result<()> {
584/// let frame1 = DynamicImage::ImageRgb8(RgbImage::new(64, 64));
585/// let frame2 = DynamicImage::ImageRgb8(RgbImage::new(64, 64));
586/// let flow = horn_schunck_flow(&frame1, &frame2, &HornSchunckParams::default())?;
587/// assert_eq!(flow.dim(), (64, 64));
588/// # Ok(())
589/// # }
590/// ```
591pub fn horn_schunck_flow(
592    img1: &DynamicImage,
593    img2: &DynamicImage,
594    params: &HornSchunckParams,
595) -> Result<Array2<FlowVector>> {
596    let gray1 = img1.to_luma8();
597    let gray2 = img2.to_luma8();
598    let (width, height) = gray1.dimensions();
599    let h = height as usize;
600    let w = width as usize;
601
602    if h < 3 || w < 3 {
603        return Err(crate::error::VisionError::InvalidParameter(
604            "Images must be at least 3x3 for Horn-Schunck flow".to_string(),
605        ));
606    }
607
608    // Convert to float arrays
609    let i1 = image_to_float_array(&gray1);
610    let i2 = image_to_float_array(&gray2);
611
612    // Compute spatial gradients on the averaged image (I_x, I_y)
613    // and temporal gradient (I_t)
614    let mut ix = Array2::<f32>::zeros((h, w));
615    let mut iy = Array2::<f32>::zeros((h, w));
616    let mut it = Array2::<f32>::zeros((h, w));
617
618    for y in 0..h - 1 {
619        for x in 0..w - 1 {
620            // Averaged partial derivatives using 2x2 stencils
621            // I_x = 0.25 * (I(x+1,y) - I(x,y) + I(x+1,y+1) - I(x,y+1)
622            //              + J(x+1,y) - J(x,y) + J(x+1,y+1) - J(x,y+1))
623            ix[[y, x]] = 0.25
624                * ((i1[[y, x + 1]] - i1[[y, x]])
625                    + (i1[[y + 1, x + 1]] - i1[[y + 1, x]])
626                    + (i2[[y, x + 1]] - i2[[y, x]])
627                    + (i2[[y + 1, x + 1]] - i2[[y + 1, x]]));
628
629            iy[[y, x]] = 0.25
630                * ((i1[[y + 1, x]] - i1[[y, x]])
631                    + (i1[[y + 1, x + 1]] - i1[[y, x + 1]])
632                    + (i2[[y + 1, x]] - i2[[y, x]])
633                    + (i2[[y + 1, x + 1]] - i2[[y, x + 1]]));
634
635            it[[y, x]] = 0.25
636                * ((i2[[y, x]] - i1[[y, x]])
637                    + (i2[[y, x + 1]] - i1[[y, x + 1]])
638                    + (i2[[y + 1, x]] - i1[[y + 1, x]])
639                    + (i2[[y + 1, x + 1]] - i1[[y + 1, x + 1]]));
640        }
641    }
642
643    let alpha_sq = params.alpha * params.alpha;
644
645    // Initialize flow fields
646    let mut u_flow = Array2::<f32>::zeros((h, w));
647    let mut v_flow = Array2::<f32>::zeros((h, w));
648
649    // Iterative Gauss-Seidel / Jacobi solver
650    for _iter in 0..params.max_iterations {
651        let mut max_change: f32 = 0.0;
652
653        // Compute Laplacian-weighted averages (using 4-connected neighbors)
654        let u_avg = laplacian_average(&u_flow);
655        let v_avg = laplacian_average(&v_flow);
656
657        for y in 1..h - 1 {
658            for x in 1..w - 1 {
659                let ix_val = ix[[y, x]];
660                let iy_val = iy[[y, x]];
661                let it_val = it[[y, x]];
662
663                let denom = alpha_sq + ix_val * ix_val + iy_val * iy_val;
664                if denom.abs() < 1e-12 {
665                    continue;
666                }
667
668                let p = ix_val * u_avg[[y, x]] + iy_val * v_avg[[y, x]] + it_val;
669                let factor = p / denom;
670
671                let new_u = u_avg[[y, x]] - ix_val * factor;
672                let new_v = v_avg[[y, x]] - iy_val * factor;
673
674                let du = (new_u - u_flow[[y, x]]).abs();
675                let dv = (new_v - v_flow[[y, x]]).abs();
676                if du > max_change {
677                    max_change = du;
678                }
679                if dv > max_change {
680                    max_change = dv;
681                }
682
683                u_flow[[y, x]] = new_u;
684                v_flow[[y, x]] = new_v;
685            }
686        }
687
688        if max_change < params.epsilon {
689            break;
690        }
691    }
692
693    // Combine into FlowVector array
694    let mut flow = Array2::from_elem((h, w), FlowVector { u: 0.0, v: 0.0 });
695    for y in 0..h {
696        for x in 0..w {
697            flow[[y, x]] = FlowVector {
698                u: u_flow[[y, x]],
699                v: v_flow[[y, x]],
700            };
701        }
702    }
703
704    Ok(flow)
705}
706
707/// Compute the weighted average of neighbors (Laplacian kernel) for Horn-Schunck
708/// Uses the 4-connected neighborhood: (1/4) * (u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1})
709fn laplacian_average(field: &Array2<f32>) -> Array2<f32> {
710    let (h, w) = field.dim();
711    let mut avg = Array2::<f32>::zeros((h, w));
712
713    for y in 1..h - 1 {
714        for x in 1..w - 1 {
715            avg[[y, x]] = 0.25
716                * (field[[y - 1, x]] + field[[y + 1, x]] + field[[y, x - 1]] + field[[y, x + 1]]);
717        }
718    }
719
720    // Handle boundaries by replicating nearest interior
721    for x in 0..w {
722        avg[[0, x]] = avg[[1, x.min(w - 2).max(1)]];
723        avg[[h - 1, x]] = avg[[(h - 2).max(1), x.min(w - 2).max(1)]];
724    }
725    for y in 0..h {
726        avg[[y, 0]] = avg[[y.min(h - 2).max(1), 1]];
727        avg[[y, w - 1]] = avg[[y.min(h - 2).max(1), (w - 2).max(1)]];
728    }
729
730    avg
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_lucas_kanade_basic() {
739        let img1 = DynamicImage::new_luma8(50, 50);
740        let img2 = img1.clone();
741
742        let flow = lucas_kanade_flow(&img1, &img2, None, &LucasKanadeParams::default())
743            .expect("Operation failed");
744        assert_eq!(flow.dim(), (50, 50));
745
746        // Flow should be zero for identical images
747        for flow_vec in flow.iter() {
748            assert!(flow_vec.u.abs() < 0.1);
749            assert!(flow_vec.v.abs() < 0.1);
750        }
751    }
752
753    #[test]
754    fn test_pyramid_building() {
755        let img = GrayImage::new(64, 64);
756        let pyramid = build_pyramid(&img, 3);
757
758        assert_eq!(pyramid.len(), 3);
759        assert_eq!(pyramid[0].dimensions(), (64, 64));
760        assert_eq!(pyramid[1].dimensions(), (32, 32));
761        assert_eq!(pyramid[2].dimensions(), (16, 16));
762    }
763
764    #[test]
765    fn test_flow_visualization() {
766        let mut flow = Array2::from_elem((10, 10), FlowVector { u: 0.0, v: 0.0 });
767        flow[[5, 5]] = FlowVector { u: 1.0, v: 0.0 };
768
769        let vis = visualize_flow(&flow, Some(1.0));
770        assert_eq!(vis.dimensions(), (10, 10));
771    }
772
773    #[test]
774    fn test_horn_schunck_identical_images() {
775        let img = DynamicImage::new_luma8(32, 32);
776        let flow =
777            horn_schunck_flow(&img, &img, &HornSchunckParams::default()).expect("HS flow failed");
778        assert_eq!(flow.dim(), (32, 32));
779
780        // Zero flow for identical images
781        for fv in flow.iter() {
782            assert!(fv.u.abs() < 1e-3, "u should be ~0, got {}", fv.u);
783            assert!(fv.v.abs() < 1e-3, "v should be ~0, got {}", fv.v);
784        }
785    }
786
787    #[test]
788    fn test_horn_schunck_shifted_pattern() {
789        // Create a vertical stripe pattern, then shift it right by 1 pixel
790        let mut buf1 = GrayImage::new(32, 32);
791        let mut buf2 = GrayImage::new(32, 32);
792
793        for y in 0..32u32 {
794            for x in 0..32u32 {
795                let val = if (x / 4) % 2 == 0 { 200u8 } else { 50u8 };
796                buf1.put_pixel(x, y, Luma([val]));
797                // shift right by 1
798                if x > 0 {
799                    buf2.put_pixel(
800                        x,
801                        y,
802                        Luma([if ((x - 1) / 4) % 2 == 0 { 200u8 } else { 50u8 }]),
803                    );
804                } else {
805                    buf2.put_pixel(x, y, Luma([200u8]));
806                }
807            }
808        }
809
810        let img1 = DynamicImage::ImageLuma8(buf1);
811        let img2 = DynamicImage::ImageLuma8(buf2);
812
813        let params = HornSchunckParams {
814            alpha: 5.0,
815            max_iterations: 500,
816            epsilon: 1e-5,
817        };
818
819        let flow = horn_schunck_flow(&img1, &img2, &params).expect("HS flow failed");
820        assert_eq!(flow.dim(), (32, 32));
821
822        // We just check it ran and produced finite output
823        let center = &flow[[16, 16]];
824        assert!(
825            center.u.is_finite() && center.v.is_finite(),
826            "Flow should be computed"
827        );
828    }
829
830    #[test]
831    fn test_horn_schunck_params_default() {
832        let params = HornSchunckParams::default();
833        assert!(params.alpha > 0.0);
834        assert!(params.max_iterations > 0);
835        assert!(params.epsilon > 0.0);
836    }
837
838    #[test]
839    fn test_horn_schunck_small_alpha() {
840        // Small alpha should allow more spatial variation
841        let mut buf1 = GrayImage::new(16, 16);
842        let mut buf2 = GrayImage::new(16, 16);
843        for y in 0..16u32 {
844            for x in 0..16u32 {
845                buf1.put_pixel(x, y, Luma([(x * 16) as u8]));
846                buf2.put_pixel(x, y, Luma([((x + 1).min(15) * 16) as u8]));
847            }
848        }
849
850        let img1 = DynamicImage::ImageLuma8(buf1);
851        let img2 = DynamicImage::ImageLuma8(buf2);
852
853        let params = HornSchunckParams {
854            alpha: 1.0,
855            max_iterations: 100,
856            epsilon: 1e-4,
857        };
858
859        let flow = horn_schunck_flow(&img1, &img2, &params).expect("HS flow failed");
860        assert_eq!(flow.dim(), (16, 16));
861    }
862
863    #[test]
864    fn test_horn_schunck_large_alpha_smooth() {
865        // Large alpha produces smoother flow
866        let mut buf1 = GrayImage::new(16, 16);
867        let mut buf2 = GrayImage::new(16, 16);
868        for y in 0..16u32 {
869            for x in 0..16u32 {
870                buf1.put_pixel(x, y, Luma([(x * 16) as u8]));
871                buf2.put_pixel(x, y, Luma([((x + 1).min(15) * 16) as u8]));
872            }
873        }
874
875        let img1 = DynamicImage::ImageLuma8(buf1);
876        let img2 = DynamicImage::ImageLuma8(buf2);
877
878        let params = HornSchunckParams {
879            alpha: 100.0,
880            max_iterations: 200,
881            epsilon: 1e-5,
882        };
883
884        let flow = horn_schunck_flow(&img1, &img2, &params).expect("HS flow failed");
885
886        // With large alpha, neighboring flow vectors should be very similar
887        let diff_u = (flow[[8, 8]].u - flow[[8, 9]].u).abs();
888        let diff_v = (flow[[8, 8]].v - flow[[8, 9]].v).abs();
889        assert!(
890            diff_u < 0.5 && diff_v < 0.5,
891            "Large alpha should produce smooth flow"
892        );
893    }
894
895    #[test]
896    fn test_horn_schunck_rejects_tiny_images() {
897        let img = DynamicImage::new_luma8(2, 2);
898        let result = horn_schunck_flow(&img, &img, &HornSchunckParams::default());
899        assert!(result.is_err());
900    }
901
902    #[test]
903    fn test_laplacian_average_basic() {
904        let mut field = Array2::<f32>::zeros((5, 5));
905        field[[2, 2]] = 4.0;
906        let avg = laplacian_average(&field);
907        // Center neighbors get 1.0 each from the averaging
908        assert!((avg[[2, 1]] - 1.0).abs() < 1e-6);
909        assert!((avg[[1, 2]] - 1.0).abs() < 1e-6);
910    }
911}