1use crate::error::{NdimageError, NdimageResult};
9use scirs2_core::ndarray::{Array2, Array3};
10
11#[derive(Debug, Clone)]
19pub struct FlowField {
20 pub data: Array3<f64>,
22}
23
24impl FlowField {
25 pub fn zeros(rows: usize, cols: usize) -> Self {
27 FlowField {
28 data: Array3::<f64>::zeros((rows, cols, 2)),
29 }
30 }
31
32 pub fn from_array(data: Array3<f64>) -> NdimageResult<Self> {
34 if data.ndim() != 3 || data.shape()[2] != 2 {
35 return Err(NdimageError::InvalidInput(
36 "FlowField data must have shape (rows, cols, 2)".into(),
37 ));
38 }
39 Ok(FlowField { data })
40 }
41
42 pub fn rows(&self) -> usize {
44 self.data.shape()[0]
45 }
46
47 pub fn cols(&self) -> usize {
49 self.data.shape()[1]
50 }
51
52 pub fn get(&self, r: usize, c: usize) -> (f64, f64) {
54 (self.data[[r, c, 0]], self.data[[r, c, 1]])
55 }
56
57 pub fn set(&mut self, r: usize, c: usize, u: f64, v: f64) {
59 self.data[[r, c, 0]] = u;
60 self.data[[r, c, 1]] = v;
61 }
62
63 pub fn magnitude(&self) -> Array2<f64> {
65 let rows = self.rows();
66 let cols = self.cols();
67 Array2::from_shape_fn((rows, cols), |(r, c)| {
68 let u = self.data[[r, c, 0]];
69 let v = self.data[[r, c, 1]];
70 (u * u + v * v).sqrt()
71 })
72 }
73}
74
75fn compute_derivatives(
80 prev: &Array2<f64>,
81 next: &Array2<f64>,
82) -> NdimageResult<(Array2<f64>, Array2<f64>, Array2<f64>)> {
83 let rows = prev.nrows();
84 let cols = prev.ncols();
85 if next.shape() != prev.shape() {
86 return Err(NdimageError::DimensionError(
87 "prev and next frames must have the same shape".into(),
88 ));
89 }
90 if rows < 2 || cols < 2 {
91 return Err(NdimageError::InvalidInput(
92 "Image must be at least 2×2".into(),
93 ));
94 }
95
96 let mut ix = Array2::<f64>::zeros((rows, cols));
98 let mut iy = Array2::<f64>::zeros((rows, cols));
99 let mut it = Array2::<f64>::zeros((rows, cols));
100
101 for r in 0..rows {
102 for c in 0..cols {
103 let ip1 = if c + 1 < cols {
104 prev[[r, c + 1]]
105 } else {
106 prev[[r, c]]
107 };
108 let im1 = if c > 0 {
109 prev[[r, c - 1]]
110 } else {
111 prev[[r, c]]
112 };
113 ix[[r, c]] = (ip1 - im1) / 2.0;
114
115 let jp1 = if r + 1 < rows {
116 prev[[r + 1, c]]
117 } else {
118 prev[[r, c]]
119 };
120 let jm1 = if r > 0 {
121 prev[[r - 1, c]]
122 } else {
123 prev[[r, c]]
124 };
125 iy[[r, c]] = (jp1 - jm1) / 2.0;
126
127 it[[r, c]] = next[[r, c]] - prev[[r, c]];
128 }
129 }
130 Ok((ix, iy, it))
131}
132
133fn box_average(arr: &Array2<f64>, half_win: usize) -> Array2<f64> {
135 let rows = arr.nrows();
136 let cols = arr.ncols();
137 let mut out = Array2::<f64>::zeros((rows, cols));
138 let wsize = 2 * half_win + 1;
139 let area = (wsize * wsize) as f64;
140 for r in 0..rows {
141 for c in 0..cols {
142 let mut s = 0.0f64;
143 for dr in 0..wsize {
144 let nr = (r + dr).saturating_sub(half_win).min(rows - 1);
145 for dc in 0..wsize {
146 let nc = (c + dc).saturating_sub(half_win).min(cols - 1);
147 s += arr[[nr, nc]];
148 }
149 }
150 out[[r, c]] = s / area;
151 }
152 }
153 out
154}
155
156fn laplacian(u: &Array2<f64>) -> Array2<f64> {
158 let rows = u.nrows();
159 let cols = u.ncols();
160 let mut out = Array2::<f64>::zeros((rows, cols));
161 for r in 0..rows {
162 for c in 0..cols {
163 let sum_nb = {
164 let up = if r > 0 { u[[r - 1, c]] } else { u[[r, c]] };
165 let dn = if r + 1 < rows {
166 u[[r + 1, c]]
167 } else {
168 u[[r, c]]
169 };
170 let lt = if c > 0 { u[[r, c - 1]] } else { u[[r, c]] };
171 let rt = if c + 1 < cols {
172 u[[r, c + 1]]
173 } else {
174 u[[r, c]]
175 };
176 up + dn + lt + rt
177 };
178 out[[r, c]] = sum_nb / 4.0 - u[[r, c]];
179 }
180 }
181 out
182}
183
184pub fn lucas_kanade_flow(
200 prev: &Array2<f64>,
201 next: &Array2<f64>,
202 window_size: usize,
203) -> NdimageResult<Array3<f64>> {
204 if window_size == 0 {
205 return Err(NdimageError::InvalidInput(
206 "window_size must be positive".into(),
207 ));
208 }
209 let rows = prev.nrows();
210 let cols = prev.ncols();
211 if rows == 0 || cols == 0 {
212 return Err(NdimageError::InvalidInput(
213 "Frames must not be empty".into(),
214 ));
215 }
216 if prev.shape() != next.shape() {
217 return Err(NdimageError::DimensionError(
218 "prev and next must have the same shape".into(),
219 ));
220 }
221
222 let (ix, iy, it) = compute_derivatives(prev, next)?;
223
224 let ixx = {
226 let mut a = Array2::<f64>::zeros((rows, cols));
227 for r in 0..rows {
228 for c in 0..cols {
229 a[[r, c]] = ix[[r, c]] * ix[[r, c]];
230 }
231 }
232 a
233 };
234 let iyy = {
235 let mut a = Array2::<f64>::zeros((rows, cols));
236 for r in 0..rows {
237 for c in 0..cols {
238 a[[r, c]] = iy[[r, c]] * iy[[r, c]];
239 }
240 }
241 a
242 };
243 let ixy = {
244 let mut a = Array2::<f64>::zeros((rows, cols));
245 for r in 0..rows {
246 for c in 0..cols {
247 a[[r, c]] = ix[[r, c]] * iy[[r, c]];
248 }
249 }
250 a
251 };
252 let ixt = {
253 let mut a = Array2::<f64>::zeros((rows, cols));
254 for r in 0..rows {
255 for c in 0..cols {
256 a[[r, c]] = ix[[r, c]] * it[[r, c]];
257 }
258 }
259 a
260 };
261 let iyt = {
262 let mut a = Array2::<f64>::zeros((rows, cols));
263 for r in 0..rows {
264 for c in 0..cols {
265 a[[r, c]] = iy[[r, c]] * it[[r, c]];
266 }
267 }
268 a
269 };
270
271 let hw = window_size / 2;
272 let sxx = box_average(&ixx, hw);
274 let syy = box_average(&iyy, hw);
275 let sxy = box_average(&ixy, hw);
276 let sxt = box_average(&ixt, hw);
277 let syt = box_average(&iyt, hw);
278
279 let mut flow = Array3::<f64>::zeros((rows, cols, 2));
280 let eps = 1e-12f64;
281
282 for r in 0..rows {
283 for c in 0..cols {
284 let a = sxx[[r, c]];
285 let b = sxy[[r, c]];
286 let d = syy[[r, c]];
287 let det = a * d - b * b;
288 if det.abs() > eps {
289 let u = (-d * sxt[[r, c]] + b * syt[[r, c]]) / det;
290 let v = (b * sxt[[r, c]] - a * syt[[r, c]]) / det;
291 flow[[r, c, 0]] = u;
292 flow[[r, c, 1]] = v;
293 }
294 }
295 }
296 Ok(flow)
297}
298
299pub fn horn_schunck_flow(
316 prev: &Array2<f64>,
317 next: &Array2<f64>,
318 alpha: f64,
319 iterations: usize,
320) -> NdimageResult<Array3<f64>> {
321 if alpha <= 0.0 {
322 return Err(NdimageError::InvalidInput("alpha must be positive".into()));
323 }
324 if iterations == 0 {
325 return Err(NdimageError::InvalidInput(
326 "iterations must be at least 1".into(),
327 ));
328 }
329 let rows = prev.nrows();
330 let cols = prev.ncols();
331 if rows == 0 || cols == 0 {
332 return Err(NdimageError::InvalidInput(
333 "Frames must not be empty".into(),
334 ));
335 }
336
337 let (ix, iy, it) = compute_derivatives(prev, next)?;
338
339 let alpha_sq = alpha * alpha;
340 let mut u = Array2::<f64>::zeros((rows, cols));
341 let mut v = Array2::<f64>::zeros((rows, cols));
342
343 for _ in 0..iterations {
344 let u_avg = {
346 let mut a = Array2::<f64>::zeros((rows, cols));
347 for r in 0..rows {
348 for c in 0..cols {
349 let up = if r > 0 { u[[r - 1, c]] } else { u[[r, c]] };
350 let dn = if r + 1 < rows {
351 u[[r + 1, c]]
352 } else {
353 u[[r, c]]
354 };
355 let lt = if c > 0 { u[[r, c - 1]] } else { u[[r, c]] };
356 let rt = if c + 1 < cols {
357 u[[r, c + 1]]
358 } else {
359 u[[r, c]]
360 };
361 a[[r, c]] = (up + dn + lt + rt) / 4.0;
362 }
363 }
364 a
365 };
366 let v_avg = {
367 let mut a = Array2::<f64>::zeros((rows, cols));
368 for r in 0..rows {
369 for c in 0..cols {
370 let up = if r > 0 { v[[r - 1, c]] } else { v[[r, c]] };
371 let dn = if r + 1 < rows {
372 v[[r + 1, c]]
373 } else {
374 v[[r, c]]
375 };
376 let lt = if c > 0 { v[[r, c - 1]] } else { v[[r, c]] };
377 let rt = if c + 1 < cols {
378 v[[r, c + 1]]
379 } else {
380 v[[r, c]]
381 };
382 a[[r, c]] = (up + dn + lt + rt) / 4.0;
383 }
384 }
385 a
386 };
387
388 for r in 0..rows {
389 for c in 0..cols {
390 let ixi = ix[[r, c]];
391 let iyi = iy[[r, c]];
392 let iti = it[[r, c]];
393 let ua = u_avg[[r, c]];
394 let va = v_avg[[r, c]];
395 let denom = alpha_sq + ixi * ixi + iyi * iyi;
396 let p = (ixi * ua + iyi * va + iti) / denom;
397 u[[r, c]] = ua - ixi * p;
398 v[[r, c]] = va - iyi * p;
399 }
400 }
401 }
402
403 let mut flow = Array3::<f64>::zeros((rows, cols, 2));
404 for r in 0..rows {
405 for c in 0..cols {
406 flow[[r, c, 0]] = u[[r, c]];
407 flow[[r, c, 1]] = v[[r, c]];
408 }
409 }
410 Ok(flow)
411}
412
413pub fn farneback_flow(
430 prev: &Array2<f64>,
431 next: &Array2<f64>,
432 levels: usize,
433 winsize: usize,
434) -> NdimageResult<Array3<f64>> {
435 if levels == 0 {
436 return Err(NdimageError::InvalidInput(
437 "levels must be at least 1".into(),
438 ));
439 }
440 if winsize == 0 {
441 return Err(NdimageError::InvalidInput(
442 "winsize must be positive".into(),
443 ));
444 }
445 let rows = prev.nrows();
446 let cols = prev.ncols();
447 if rows == 0 || cols == 0 {
448 return Err(NdimageError::InvalidInput(
449 "Frames must not be empty".into(),
450 ));
451 }
452 if prev.shape() != next.shape() {
453 return Err(NdimageError::DimensionError(
454 "prev and next must have the same shape".into(),
455 ));
456 }
457
458 let prev_pyr = build_pyramid(prev, levels);
460 let next_pyr = build_pyramid(next, levels);
461
462 let coarsest = levels - 1;
464 let pr = prev_pyr[coarsest].nrows();
465 let pc = prev_pyr[coarsest].ncols();
466 let mut flow_u = Array2::<f64>::zeros((pr, pc));
467 let mut flow_v = Array2::<f64>::zeros((pr, pc));
468
469 for lvl in (0..levels).rev() {
470 let p = &prev_pyr[lvl];
471 let n = &next_pyr[lvl];
472 let lr = p.nrows();
473 let lc = p.ncols();
474
475 if lvl < coarsest || (lr != flow_u.nrows() || lc != flow_u.ncols()) {
477 flow_u = upsample_flow(&flow_u, lr, lc, 2.0);
478 flow_v = upsample_flow(&flow_v, lr, lc, 2.0);
479 }
480
481 let warped = warp_image(n, &flow_u, &flow_v);
483
484 let (ix, iy, it) = compute_derivatives(p, &warped)?;
487
488 let hw = winsize;
490 let ixx = Array2::from_shape_fn((lr, lc), |(r, c)| ix[[r, c]] * ix[[r, c]]);
491 let iyy = Array2::from_shape_fn((lr, lc), |(r, c)| iy[[r, c]] * iy[[r, c]]);
492 let ixy = Array2::from_shape_fn((lr, lc), |(r, c)| ix[[r, c]] * iy[[r, c]]);
493 let ixt = Array2::from_shape_fn((lr, lc), |(r, c)| ix[[r, c]] * it[[r, c]]);
494 let iyt = Array2::from_shape_fn((lr, lc), |(r, c)| iy[[r, c]] * it[[r, c]]);
495
496 let sxx = box_average(&ixx, hw);
497 let syy = box_average(&iyy, hw);
498 let sxy = box_average(&ixy, hw);
499 let sxt = box_average(&ixt, hw);
500 let syt = box_average(&iyt, hw);
501
502 let eps = 1e-12f64;
503 for r in 0..lr {
504 for c in 0..lc {
505 let a = sxx[[r, c]];
506 let b = sxy[[r, c]];
507 let d = syy[[r, c]];
508 let det = a * d - b * b;
509 if det.abs() > eps {
510 let du = (-d * sxt[[r, c]] + b * syt[[r, c]]) / det;
511 let dv = (b * sxt[[r, c]] - a * syt[[r, c]]) / det;
512 flow_u[[r, c]] += du;
513 flow_v[[r, c]] += dv;
514 }
515 }
516 }
517 }
518
519 let mut flow = Array3::<f64>::zeros((rows, cols, 2));
521 let final_u = if flow_u.nrows() == rows && flow_u.ncols() == cols {
522 flow_u
523 } else {
524 upsample_flow(
525 &flow_u,
526 rows,
527 cols,
528 (rows as f64 / flow_u.nrows() as f64).max(1.0),
529 )
530 };
531 let final_v = if flow_v.nrows() == rows && flow_v.ncols() == cols {
532 flow_v
533 } else {
534 upsample_flow(
535 &flow_v,
536 rows,
537 cols,
538 (rows as f64 / flow_v.nrows() as f64).max(1.0),
539 )
540 };
541 for r in 0..rows {
542 for c in 0..cols {
543 flow[[r, c, 0]] = final_u[[r, c]];
544 flow[[r, c, 1]] = final_v[[r, c]];
545 }
546 }
547 Ok(flow)
548}
549
550fn build_pyramid(image: &Array2<f64>, levels: usize) -> Vec<Array2<f64>> {
552 let mut pyr = vec![image.clone()];
553 for _ in 1..levels {
554 let last = &pyr[pyr.len() - 1];
555 let down = downsample(last);
556 pyr.push(down);
557 }
558 pyr
559}
560
561fn downsample(image: &Array2<f64>) -> Array2<f64> {
563 let rows = image.nrows();
564 let cols = image.ncols();
565 let new_rows = (rows + 1) / 2;
566 let new_cols = (cols + 1) / 2;
567 if new_rows == 0 || new_cols == 0 {
568 return image.clone();
569 }
570 Array2::from_shape_fn((new_rows, new_cols), |(r, c)| {
572 let sr = r * 2;
573 let sc = c * 2;
574 let mut s = 0.0f64;
575 let mut cnt = 0u32;
576 for dr in 0..=1usize {
577 for dc in 0..=1usize {
578 let nr = (sr + dr).min(rows - 1);
579 let nc = (sc + dc).min(cols - 1);
580 s += image[[nr, nc]];
581 cnt += 1;
582 }
583 }
584 s / cnt as f64
585 })
586}
587
588fn upsample_flow(
590 flow: &Array2<f64>,
591 target_rows: usize,
592 target_cols: usize,
593 scale: f64,
594) -> Array2<f64> {
595 let src_rows = flow.nrows();
596 let src_cols = flow.ncols();
597 Array2::from_shape_fn((target_rows, target_cols), |(r, c)| {
598 let sr = (r * src_rows / target_rows).min(src_rows - 1);
599 let sc = (c * src_cols / target_cols).min(src_cols - 1);
600 flow[[sr, sc]] * scale
601 })
602}
603
604fn warp_image(image: &Array2<f64>, u: &Array2<f64>, v: &Array2<f64>) -> Array2<f64> {
606 let rows = image.nrows();
607 let cols = image.ncols();
608 Array2::from_shape_fn((rows, cols), |(r, c)| {
609 let x = c as f64 + u[[r, c]];
610 let y = r as f64 + v[[r, c]];
611 let x0 = x.floor() as isize;
613 let y0 = y.floor() as isize;
614 let fx = x - x0 as f64;
615 let fy = y - y0 as f64;
616 let sample = |ri: isize, ci: isize| -> f64 {
617 let ri = ri.max(0).min(rows as isize - 1) as usize;
618 let ci = ci.max(0).min(cols as isize - 1) as usize;
619 image[[ri, ci]]
620 };
621 let i00 = sample(y0, x0);
622 let i01 = sample(y0, x0 + 1);
623 let i10 = sample(y0 + 1, x0);
624 let i11 = sample(y0 + 1, x0 + 1);
625 i00 * (1.0 - fx) * (1.0 - fy)
626 + i01 * fx * (1.0 - fy)
627 + i10 * (1.0 - fx) * fy
628 + i11 * fx * fy
629 })
630}
631
632#[cfg(test)]
635mod tests {
636 use super::*;
637 use scirs2_core::ndarray::Array2;
638
639 fn make_shifted_pair(rows: usize, cols: usize, shift_c: isize) -> (Array2<f64>, Array2<f64>) {
641 let prev = Array2::from_shape_fn((rows, cols), |(_, c)| c as f64 / cols as f64);
642 let next = Array2::from_shape_fn((rows, cols), |(_, c)| {
643 let nc = (c as isize - shift_c).max(0).min(cols as isize - 1) as usize;
644 nc as f64 / cols as f64
645 });
646 (prev, next)
647 }
648
649 #[test]
652 fn test_flow_field_zeros() {
653 let ff = FlowField::zeros(4, 4);
654 assert_eq!(ff.rows(), 4);
655 assert_eq!(ff.cols(), 4);
656 let (u, v) = ff.get(0, 0);
657 assert!((u - 0.0).abs() < 1e-12);
658 assert!((v - 0.0).abs() < 1e-12);
659 }
660
661 #[test]
662 fn test_flow_field_set_get() {
663 let mut ff = FlowField::zeros(4, 4);
664 ff.set(2, 3, 1.5, -0.5);
665 let (u, v) = ff.get(2, 3);
666 assert!((u - 1.5).abs() < 1e-12);
667 assert!((v + 0.5).abs() < 1e-12);
668 }
669
670 #[test]
671 fn test_flow_field_from_array_invalid() {
672 let bad = Array3::<f64>::zeros((4, 4, 3)); assert!(FlowField::from_array(bad).is_err());
674 }
675
676 #[test]
677 fn test_flow_field_magnitude() {
678 let mut ff = FlowField::zeros(3, 3);
679 ff.set(1, 1, 3.0, 4.0);
680 let mag = ff.magnitude();
681 assert!((mag[[1, 1]] - 5.0).abs() < 1e-9);
682 }
683
684 #[test]
687 fn test_lucas_kanade_shape() {
688 let (prev, next) = make_shifted_pair(16, 16, 1);
689 let flow = lucas_kanade_flow(&prev, &next, 5).expect("LK failed");
690 assert_eq!(flow.shape(), &[16, 16, 2]);
691 }
692
693 #[test]
694 fn test_lucas_kanade_zero_motion() {
695 let img = Array2::from_shape_fn((10, 10), |(r, c)| (r + c) as f64 * 0.1);
696 let flow = lucas_kanade_flow(&img, &img, 5).expect("LK zero motion");
697 let max_flow = flow.iter().cloned().fold(0.0f64, |acc, v| acc.max(v.abs()));
699 assert!(max_flow < 1e-6, "Expected near-zero flow, got {max_flow}");
700 }
701
702 #[test]
703 fn test_lucas_kanade_invalid_window() {
704 let img = Array2::<f64>::zeros((8, 8));
705 assert!(lucas_kanade_flow(&img, &img, 0).is_err());
706 }
707
708 #[test]
709 fn test_lucas_kanade_shape_mismatch() {
710 let prev = Array2::<f64>::zeros((8, 8));
711 let next = Array2::<f64>::zeros((4, 4));
712 assert!(lucas_kanade_flow(&prev, &next, 5).is_err());
713 }
714
715 #[test]
718 fn test_horn_schunck_shape() {
719 let (prev, next) = make_shifted_pair(12, 12, 1);
720 let flow = horn_schunck_flow(&prev, &next, 1.0, 50).expect("HS failed");
721 assert_eq!(flow.shape(), &[12, 12, 2]);
722 }
723
724 #[test]
725 fn test_horn_schunck_zero_motion() {
726 let img = Array2::from_shape_fn((8, 8), |(r, c)| (r * c) as f64 * 0.01);
727 let flow = horn_schunck_flow(&img, &img, 10.0, 20).expect("HS zero motion");
728 let max_flow = flow.iter().cloned().fold(0.0f64, |acc, v| acc.max(v.abs()));
729 assert!(max_flow < 1e-6, "Expected near-zero flow, got {max_flow}");
730 }
731
732 #[test]
733 fn test_horn_schunck_invalid_alpha() {
734 let img = Array2::<f64>::zeros((4, 4));
735 assert!(horn_schunck_flow(&img, &img, 0.0, 10).is_err());
736 assert!(horn_schunck_flow(&img, &img, -1.0, 10).is_err());
737 }
738
739 #[test]
740 fn test_horn_schunck_invalid_iterations() {
741 let img = Array2::<f64>::zeros((4, 4));
742 assert!(horn_schunck_flow(&img, &img, 1.0, 0).is_err());
743 }
744
745 #[test]
748 fn test_farneback_shape() {
749 let (prev, next) = make_shifted_pair(16, 16, 1);
750 let flow = farneback_flow(&prev, &next, 2, 5).expect("Farneback failed");
751 assert_eq!(flow.shape(), &[16, 16, 2]);
752 }
753
754 #[test]
755 fn test_farneback_zero_motion() {
756 let img = Array2::from_shape_fn((8, 8), |(r, c)| (r + c) as f64 * 0.1);
757 let flow = farneback_flow(&img, &img, 1, 3).expect("Farneback zero motion");
758 let max_flow = flow.iter().cloned().fold(0.0f64, |acc, v| acc.max(v.abs()));
759 assert!(max_flow < 1e-4, "Expected near-zero flow, got {max_flow}");
760 }
761
762 #[test]
763 fn test_farneback_invalid_levels() {
764 let img = Array2::<f64>::zeros((8, 8));
765 assert!(farneback_flow(&img, &img, 0, 3).is_err());
766 }
767
768 #[test]
769 fn test_farneback_invalid_winsize() {
770 let img = Array2::<f64>::zeros((8, 8));
771 assert!(farneback_flow(&img, &img, 2, 0).is_err());
772 }
773}