1use crate::error::Result;
7use image::{DynamicImage, GrayImage, ImageBuffer, Luma, Rgb, RgbImage};
8use scirs2_core::ndarray::{s, Array2};
9
10#[derive(Debug, Clone, Copy)]
12pub struct FlowVector {
13 pub u: f32,
15 pub v: f32,
17}
18
19#[derive(Debug, Clone)]
21pub struct LucasKanadeParams {
22 pub window_size: usize,
24 pub max_iterations: usize,
26 pub epsilon: f32,
28 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#[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#[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 let i1 = image_to_float_array(img1);
99 let i2 = image_to_float_array(img2);
100
101 let (ix, iy) = compute_gradients(&i1);
103
104 let half_window = params.window_size / 2;
105
106 let track_points: Vec<(f32, f32)> = if let Some(pts) = points {
108 pts.to_vec()
109 } else {
110 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 let mut flow = Array2::from_elem(
122 (height as usize, width as usize),
123 FlowVector { u: 0.0, v: 0.0 },
124 );
125
126 for &(px, py) in &track_points {
128 let x = px as usize;
129 let y = py as usize;
130
131 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 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 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; }
169
170 let mut u = 0.0f32;
172 let mut v = 0.0f32;
173
174 for _ in 0..params.max_iterations {
175 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 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 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#[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 let pyramid1 = build_pyramid(img1, params.pyramid_levels);
237 let pyramid2 = build_pyramid(img2, params.pyramid_levels);
238
239 let mut flow = Array2::from_elem(
241 (height as usize, width as usize),
242 FlowVector { u: 0.0, v: 0.0 },
243 );
244
245 for level in (0..params.pyramid_levels).rev() {
247 let scale = 2.0_f32.powi(level as i32);
248
249 let scaled_points: Option<Vec<(f32, f32)>> =
251 points.map(|pts| pts.iter().map(|&(x, y)| (x / scale, y / scale)).collect());
252
253 let level_params = LucasKanadeParams {
255 pyramid_levels: 0, ..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 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 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#[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 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#[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#[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 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#[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 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) };
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 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; 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#[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#[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 let mut flow = Array2::from_elem(
466 (height as usize, width as usize),
467 FlowVector { u: 0.0, v: 0.0 },
468 );
469
470 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 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 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 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#[derive(Debug, Clone)]
536pub struct HornSchunckParams {
537 pub alpha: f32,
540 pub max_iterations: usize,
542 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
556pub 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 let i1 = image_to_float_array(&gray1);
610 let i2 = image_to_float_array(&gray2);
611
612 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 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 let mut u_flow = Array2::<f32>::zeros((h, w));
647 let mut v_flow = Array2::<f32>::zeros((h, w));
648
649 for _iter in 0..params.max_iterations {
651 let mut max_change: f32 = 0.0;
652
653 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 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
707fn 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 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 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 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 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 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, ¶ms).expect("HS flow failed");
820 assert_eq!(flow.dim(), (32, 32));
821
822 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 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, ¶ms).expect("HS flow failed");
860 assert_eq!(flow.dim(), (16, 16));
861 }
862
863 #[test]
864 fn test_horn_schunck_large_alpha_smooth() {
865 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, ¶ms).expect("HS flow failed");
885
886 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 assert!((avg[[2, 1]] - 1.0).abs() < 1e-6);
909 assert!((avg[[1, 2]] - 1.0).abs() < 1e-6);
910 }
911}