Skip to main content

scirs2_vision/feature/
flow_unified.rs

1//! Unified optical flow API
2//!
3//! Provides a single entry point for computing optical flow between two
4//! frames using different methods:
5//!
6//! - **Lucas-Kanade**: Sparse or dense local flow using window-based optimization
7//! - **Horn-Schunck**: Dense global flow using variational energy minimization
8//! - **Farneback**: Dense polynomial expansion flow (simplified)
9
10use crate::error::Result;
11use crate::feature::optical_flow::{FlowVector, HornSchunckParams, LucasKanadeParams};
12use image::DynamicImage;
13use scirs2_core::ndarray::Array2;
14
15/// Optical flow method selection
16#[derive(Debug, Clone)]
17pub enum FlowMethod {
18    /// Lucas-Kanade sparse/dense optical flow
19    LucasKanade {
20        /// Window size for local computation
21        window_size: usize,
22        /// Maximum iterations
23        max_iterations: usize,
24        /// Number of pyramid levels (0 for no pyramid)
25        pyramid_levels: usize,
26    },
27    /// Horn-Schunck dense variational optical flow
28    HornSchunck {
29        /// Smoothness weight (larger = smoother flow)
30        alpha: f32,
31        /// Maximum iterations
32        max_iterations: usize,
33    },
34    /// Farneback dense optical flow (simplified)
35    Farneback {
36        /// Window size
37        window_size: usize,
38    },
39}
40
41impl Default for FlowMethod {
42    fn default() -> Self {
43        FlowMethod::LucasKanade {
44            window_size: 15,
45            max_iterations: 20,
46            pyramid_levels: 3,
47        }
48    }
49}
50
51/// Result of optical flow computation
52#[derive(Debug, Clone)]
53pub struct FlowResult {
54    /// Flow field (height x width) with (u, v) displacement per pixel
55    pub flow: Array2<FlowVector>,
56    /// Method used
57    pub method_name: String,
58}
59
60impl FlowResult {
61    /// Compute the magnitude of the flow at each pixel
62    pub fn magnitude(&self) -> Array2<f32> {
63        let (h, w) = self.flow.dim();
64        let mut mag = Array2::zeros((h, w));
65        for y in 0..h {
66            for x in 0..w {
67                let fv = &self.flow[[y, x]];
68                mag[[y, x]] = (fv.u * fv.u + fv.v * fv.v).sqrt();
69            }
70        }
71        mag
72    }
73
74    /// Compute the angle of the flow at each pixel (in radians)
75    pub fn angle(&self) -> Array2<f32> {
76        let (h, w) = self.flow.dim();
77        let mut ang = Array2::zeros((h, w));
78        for y in 0..h {
79            for x in 0..w {
80                let fv = &self.flow[[y, x]];
81                ang[[y, x]] = fv.v.atan2(fv.u);
82            }
83        }
84        ang
85    }
86
87    /// Get the mean flow magnitude
88    pub fn mean_magnitude(&self) -> f32 {
89        let mag = self.magnitude();
90        let total: f32 = mag.iter().sum();
91        let count = mag.len();
92        if count > 0 {
93            total / count as f32
94        } else {
95            0.0
96        }
97    }
98}
99
100/// Compute optical flow between two frames
101///
102/// This is the unified API for optical flow computation.
103///
104/// # Arguments
105///
106/// * `prev` - Previous (reference) frame
107/// * `next` - Next (target) frame
108/// * `method` - Optical flow method and parameters
109///
110/// # Returns
111///
112/// * Flow result containing the dense flow field
113///
114/// # Example
115///
116/// ```rust
117/// use scirs2_vision::feature::flow_unified::{calc_optical_flow, FlowMethod};
118/// use image::{DynamicImage, RgbImage};
119///
120/// # fn main() -> scirs2_vision::error::Result<()> {
121/// let frame1 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
122/// let frame2 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
123///
124/// let result = calc_optical_flow(&frame1, &frame2, FlowMethod::default())?;
125/// assert_eq!(result.flow.dim(), (32, 32));
126/// # Ok(())
127/// # }
128/// ```
129pub fn calc_optical_flow(
130    prev: &DynamicImage,
131    next: &DynamicImage,
132    method: FlowMethod,
133) -> Result<FlowResult> {
134    match method {
135        FlowMethod::LucasKanade {
136            window_size,
137            max_iterations,
138            pyramid_levels,
139        } => {
140            let params = LucasKanadeParams {
141                window_size,
142                max_iterations,
143                epsilon: 0.01,
144                pyramid_levels,
145            };
146
147            let flow = crate::feature::optical_flow::lucas_kanade_flow(prev, next, None, &params)?;
148
149            Ok(FlowResult {
150                flow,
151                method_name: "LucasKanade".to_string(),
152            })
153        }
154        FlowMethod::HornSchunck {
155            alpha,
156            max_iterations,
157        } => {
158            let params = HornSchunckParams {
159                alpha,
160                max_iterations,
161                epsilon: 1e-4,
162            };
163
164            let flow = crate::feature::optical_flow::horn_schunck_flow(prev, next, &params)?;
165
166            Ok(FlowResult {
167                flow,
168                method_name: "HornSchunck".to_string(),
169            })
170        }
171        FlowMethod::Farneback { window_size } => {
172            let flow =
173                crate::feature::optical_flow::farneback_flow(prev, next, 0.5, 3, window_size, 3)?;
174
175            Ok(FlowResult {
176                flow,
177                method_name: "Farneback".to_string(),
178            })
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use image::{GrayImage, Luma, RgbImage};
187
188    #[test]
189    fn test_calc_optical_flow_lucas_kanade() {
190        let frame1 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
191        let frame2 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
192
193        let result = calc_optical_flow(
194            &frame1,
195            &frame2,
196            FlowMethod::LucasKanade {
197                window_size: 15,
198                max_iterations: 10,
199                pyramid_levels: 2,
200            },
201        )
202        .expect("LK flow failed");
203
204        assert_eq!(result.flow.dim(), (32, 32));
205        assert_eq!(result.method_name, "LucasKanade");
206    }
207
208    #[test]
209    fn test_calc_optical_flow_horn_schunck() {
210        let frame1 = DynamicImage::ImageRgb8(RgbImage::new(16, 16));
211        let frame2 = DynamicImage::ImageRgb8(RgbImage::new(16, 16));
212
213        let result = calc_optical_flow(
214            &frame1,
215            &frame2,
216            FlowMethod::HornSchunck {
217                alpha: 15.0,
218                max_iterations: 50,
219            },
220        )
221        .expect("HS flow failed");
222
223        assert_eq!(result.flow.dim(), (16, 16));
224        assert_eq!(result.method_name, "HornSchunck");
225    }
226
227    #[test]
228    fn test_calc_optical_flow_farneback() {
229        let frame1 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
230        let frame2 = DynamicImage::ImageRgb8(RgbImage::new(32, 32));
231
232        let result = calc_optical_flow(&frame1, &frame2, FlowMethod::Farneback { window_size: 5 })
233            .expect("Farneback flow failed");
234
235        assert_eq!(result.flow.dim(), (32, 32));
236        assert_eq!(result.method_name, "Farneback");
237    }
238
239    #[test]
240    fn test_flow_result_magnitude() {
241        let mut flow = Array2::from_elem((5, 5), FlowVector { u: 0.0, v: 0.0 });
242        flow[[2, 2]] = FlowVector { u: 3.0, v: 4.0 };
243
244        let result = FlowResult {
245            flow,
246            method_name: "test".to_string(),
247        };
248
249        let mag = result.magnitude();
250        assert!((mag[[2, 2]] - 5.0).abs() < 1e-5);
251        assert!(mag[[0, 0]].abs() < 1e-5);
252    }
253
254    #[test]
255    fn test_flow_result_angle() {
256        let mut flow = Array2::from_elem((5, 5), FlowVector { u: 0.0, v: 0.0 });
257        flow[[2, 2]] = FlowVector { u: 1.0, v: 0.0 };
258
259        let result = FlowResult {
260            flow,
261            method_name: "test".to_string(),
262        };
263
264        let ang = result.angle();
265        assert!(ang[[2, 2]].abs() < 1e-5); // atan2(0, 1) = 0
266    }
267
268    #[test]
269    fn test_flow_result_mean_magnitude() {
270        let flow = Array2::from_elem((4, 4), FlowVector { u: 3.0, v: 4.0 });
271
272        let result = FlowResult {
273            flow,
274            method_name: "test".to_string(),
275        };
276
277        let mean = result.mean_magnitude();
278        assert!((mean - 5.0).abs() < 1e-5); // All vectors have magnitude 5
279    }
280
281    #[test]
282    fn test_identical_frames_zero_flow() {
283        let frame = DynamicImage::ImageRgb8(RgbImage::new(16, 16));
284
285        let result = calc_optical_flow(
286            &frame,
287            &frame,
288            FlowMethod::HornSchunck {
289                alpha: 15.0,
290                max_iterations: 100,
291            },
292        )
293        .expect("HS flow failed");
294
295        let mean_mag = result.mean_magnitude();
296        assert!(
297            mean_mag < 0.01,
298            "Identical frames should have near-zero flow, got {}",
299            mean_mag
300        );
301    }
302
303    #[test]
304    fn test_flow_method_default() {
305        let method = FlowMethod::default();
306        match method {
307            FlowMethod::LucasKanade {
308                window_size,
309                pyramid_levels,
310                ..
311            } => {
312                assert!(window_size > 0);
313                assert!(pyramid_levels > 0);
314            }
315            _ => panic!("Default should be LucasKanade"),
316        }
317    }
318}