Skip to main content

yscv_imgproc/ops/
flow.rs

1use yscv_tensor::Tensor;
2
3use super::super::ImgProcError;
4use super::super::shape::hwc_shape;
5
6// ── Farneback dense optical flow ────────────────────────────────────
7
8/// Configuration for Farneback dense optical flow.
9#[derive(Debug, Clone)]
10pub struct FarnebackConfig {
11    /// Pyramid scale (< 1.0, default 0.5).
12    pub pyr_scale: f32,
13    /// Number of pyramid levels (default 3).
14    pub levels: usize,
15    /// Averaging window size (default 15).
16    pub win_size: usize,
17    /// Number of iterations at each level (default 3).
18    pub iterations: usize,
19    /// Polynomial expansion neighborhood size (default 5, must be 5 or 7).
20    pub poly_n: usize,
21    /// Gaussian std for polynomial expansion (default 1.1 for poly_n=5).
22    pub poly_sigma: f32,
23}
24
25impl Default for FarnebackConfig {
26    fn default() -> Self {
27        Self {
28            pyr_scale: 0.5,
29            levels: 3,
30            win_size: 15,
31            iterations: 3,
32            poly_n: 5,
33            poly_sigma: 1.1,
34        }
35    }
36}
37
38/// Compute dense optical flow using Farneback's polynomial expansion method.
39///
40/// `prev` and `next` are grayscale images `[H, W]` with values in `[0, 1]`.
41/// Returns `(flow_x, flow_y)` each of shape `[H, W]` containing per-pixel displacement.
42pub fn farneback_flow(
43    prev: &Tensor,
44    next: &Tensor,
45    config: &FarnebackConfig,
46) -> Result<(Tensor, Tensor), ImgProcError> {
47    let shape = prev.shape();
48    if shape.len() != 2 {
49        return Err(ImgProcError::InvalidImageShape {
50            expected_rank: 2,
51            got: shape.to_vec(),
52        });
53    }
54    let (h, w) = (shape[0], shape[1]);
55    let next_shape = next.shape();
56    if next_shape.len() != 2 || next_shape[0] != h || next_shape[1] != w {
57        return Err(ImgProcError::ShapeMismatch {
58            expected: vec![h, w],
59            got: next_shape.to_vec(),
60        });
61    }
62
63    let levels = config.levels.max(1);
64    let pyr_scale = config.pyr_scale.clamp(0.1, 0.95);
65
66    // Build Gaussian pyramids for both images.
67    let pyr_prev = build_pyramid(prev, levels, pyr_scale);
68    let pyr_next = build_pyramid(next, levels, pyr_scale);
69
70    // Start at coarsest level with zero flow.
71    let coarsest = &pyr_prev[levels - 1];
72    let mut ch = coarsest.shape()[0];
73    let mut cw = coarsest.shape()[1];
74    let mut flow_x = vec![0.0f32; ch * cw];
75    let mut flow_y = vec![0.0f32; ch * cw];
76
77    // Coarse-to-fine iteration.
78    for level in (0..levels).rev() {
79        let lh = pyr_prev[level].shape()[0];
80        let lw = pyr_prev[level].shape()[1];
81
82        if level < levels - 1 {
83            // Upsample flow from previous (coarser) level.
84            let (ux, uy) = upsample_flow_vecs(&flow_x, &flow_y, ch, cw, lh, lw);
85            flow_x = ux;
86            flow_y = uy;
87        }
88
89        let prev_data = pyr_prev[level].data();
90        let next_data = pyr_next[level].data();
91
92        // Build Gaussian weight kernel for the window.
93        let half = (config.win_size / 2) as i32;
94        let sigma = config.win_size as f32 / 4.0;
95        let weights = gaussian_weights(half, sigma);
96
97        for _iter in 0..config.iterations {
98            // Warp next image by current flow estimate.
99            let warped = warp_image_data(next_data, &flow_x, &flow_y, lh, lw);
100
101            // Compute gradients of prev image.
102            let (grad_x, grad_y) = compute_gradients_data(prev_data, lh, lw);
103
104            // Compute temporal gradient.
105            let grad_t: Vec<f32> = warped
106                .iter()
107                .zip(prev_data.iter())
108                .map(|(w_val, p_val)| w_val - p_val)
109                .collect();
110
111            // Dense Lucas-Kanade update with Gaussian-weighted window.
112            let mut new_fx = flow_x.clone();
113            let mut new_fy = flow_y.clone();
114
115            for y in 0..lh {
116                for x in 0..lw {
117                    let mut sum_ixx = 0.0f32;
118                    let mut sum_iyy = 0.0f32;
119                    let mut sum_ixy = 0.0f32;
120                    let mut sum_ixt = 0.0f32;
121                    let mut sum_iyt = 0.0f32;
122
123                    for dy in -half..=half {
124                        for dx in -half..=half {
125                            let sy = y as i32 + dy;
126                            let sx = x as i32 + dx;
127                            if sy < 0 || sy >= lh as i32 || sx < 0 || sx >= lw as i32 {
128                                continue;
129                            }
130                            let idx = sy as usize * lw + sx as usize;
131                            let wi = (dy + half) as usize * (2 * half as usize + 1)
132                                + (dx + half) as usize;
133                            let wt = weights[wi];
134                            let ix = grad_x[idx];
135                            let iy = grad_y[idx];
136                            let it = grad_t[idx];
137                            sum_ixx += wt * ix * ix;
138                            sum_iyy += wt * iy * iy;
139                            sum_ixy += wt * ix * iy;
140                            sum_ixt += wt * ix * it;
141                            sum_iyt += wt * iy * it;
142                        }
143                    }
144
145                    let det = sum_ixx * sum_iyy - sum_ixy * sum_ixy;
146                    if det.abs() > 1e-6 {
147                        let inv_det = 1.0 / det;
148                        let dvx = -(sum_iyy * sum_ixt - sum_ixy * sum_iyt) * inv_det;
149                        let dvy = -(sum_ixx * sum_iyt - sum_ixy * sum_ixt) * inv_det;
150                        let pidx = y * lw + x;
151                        new_fx[pidx] += dvx;
152                        new_fy[pidx] += dvy;
153                    }
154                }
155            }
156
157            flow_x = new_fx;
158            flow_y = new_fy;
159        }
160
161        ch = lh;
162        cw = lw;
163    }
164
165    let fx_tensor = Tensor::from_vec(vec![h, w], flow_x)?;
166    let fy_tensor = Tensor::from_vec(vec![h, w], flow_y)?;
167    Ok((fx_tensor, fy_tensor))
168}
169
170/// Compute image gradients using central differences.
171fn compute_gradients_data(data: &[f32], h: usize, w: usize) -> (Vec<f32>, Vec<f32>) {
172    let mut dx = vec![0.0f32; h * w];
173    let mut dy = vec![0.0f32; h * w];
174    for y in 0..h {
175        for x in 0..w {
176            let idx = y * w + x;
177            dx[idx] = if x == 0 {
178                data[idx + 1] - data[idx]
179            } else if x == w - 1 {
180                data[idx] - data[idx - 1]
181            } else {
182                (data[idx + 1] - data[idx - 1]) * 0.5
183            };
184            dy[idx] = if y == 0 {
185                data[(y + 1) * w + x] - data[idx]
186            } else if y == h - 1 {
187                data[idx] - data[(y - 1) * w + x]
188            } else {
189                (data[(y + 1) * w + x] - data[(y - 1) * w + x]) * 0.5
190            };
191        }
192    }
193    (dx, dy)
194}
195
196/// Warp an image by a flow field using bilinear interpolation.
197fn warp_image_data(data: &[f32], flow_x: &[f32], flow_y: &[f32], h: usize, w: usize) -> Vec<f32> {
198    let mut out = vec![0.0f32; h * w];
199    for y in 0..h {
200        for x in 0..w {
201            let idx = y * w + x;
202            let src_x = x as f32 + flow_x[idx];
203            let src_y = y as f32 + flow_y[idx];
204            let sx = src_x.clamp(0.0, (w - 1) as f32);
205            let sy = src_y.clamp(0.0, (h - 1) as f32);
206            let x0 = sx.floor() as usize;
207            let y0 = sy.floor() as usize;
208            let x1 = (x0 + 1).min(w - 1);
209            let y1 = (y0 + 1).min(h - 1);
210            let fx = sx - x0 as f32;
211            let fy = sy - y0 as f32;
212            out[idx] = data[y0 * w + x0] * (1.0 - fy) * (1.0 - fx)
213                + data[y0 * w + x1] * (1.0 - fy) * fx
214                + data[y1 * w + x0] * fy * (1.0 - fx)
215                + data[y1 * w + x1] * fy * fx;
216        }
217    }
218    out
219}
220
221/// Build a Gaussian pyramid by repeated downscaling.
222fn build_pyramid(image: &Tensor, levels: usize, scale: f32) -> Vec<Tensor> {
223    let mut pyramid = Vec::with_capacity(levels);
224    pyramid.push(image.clone());
225    for i in 1..levels {
226        let prev_level = &pyramid[i - 1];
227        let ph = prev_level.shape()[0];
228        let pw = prev_level.shape()[1];
229        let nh = ((ph as f32) * scale).round().max(1.0) as usize;
230        let nw = ((pw as f32) * scale).round().max(1.0) as usize;
231        let downscaled = downsample(prev_level.data(), ph, pw, nh, nw);
232        pyramid.push(
233            Tensor::from_vec(vec![nh, nw], downscaled).expect("pyramid level shape matches data"),
234        );
235    }
236    pyramid
237}
238
239/// Downsample using bilinear interpolation.
240fn downsample(data: &[f32], sh: usize, sw: usize, dh: usize, dw: usize) -> Vec<f32> {
241    let mut out = vec![0.0f32; dh * dw];
242    for y in 0..dh {
243        for x in 0..dw {
244            let src_y = (y as f32 + 0.5) * (sh as f32 / dh as f32) - 0.5;
245            let src_x = (x as f32 + 0.5) * (sw as f32 / dw as f32) - 0.5;
246            let sy = src_y.clamp(0.0, (sh - 1) as f32);
247            let sx = src_x.clamp(0.0, (sw - 1) as f32);
248            let y0 = sy.floor() as usize;
249            let x0 = sx.floor() as usize;
250            let y1 = (y0 + 1).min(sh - 1);
251            let x1 = (x0 + 1).min(sw - 1);
252            let fy = sy - y0 as f32;
253            let fx = sx - x0 as f32;
254            out[y * dw + x] = data[y0 * sw + x0] * (1.0 - fy) * (1.0 - fx)
255                + data[y0 * sw + x1] * (1.0 - fy) * fx
256                + data[y1 * sw + x0] * fy * (1.0 - fx)
257                + data[y1 * sw + x1] * fy * fx;
258        }
259    }
260    out
261}
262
263/// Upsample flow field vectors to a larger resolution, scaling magnitudes.
264fn upsample_flow_vecs(
265    fx: &[f32],
266    fy: &[f32],
267    sh: usize,
268    sw: usize,
269    dh: usize,
270    dw: usize,
271) -> (Vec<f32>, Vec<f32>) {
272    let scale_x = dw as f32 / sw as f32;
273    let scale_y = dh as f32 / sh as f32;
274    let mut out_x = vec![0.0f32; dh * dw];
275    let mut out_y = vec![0.0f32; dh * dw];
276    for y in 0..dh {
277        for x in 0..dw {
278            let src_y = (y as f32 + 0.5) / scale_y - 0.5;
279            let src_x = (x as f32 + 0.5) / scale_x - 0.5;
280            let sy = src_y.clamp(0.0, (sh - 1) as f32);
281            let sx = src_x.clamp(0.0, (sw - 1) as f32);
282            let y0 = sy.floor() as usize;
283            let x0 = sx.floor() as usize;
284            let y1 = (y0 + 1).min(sh - 1);
285            let x1 = (x0 + 1).min(sw - 1);
286            let frac_y = sy - y0 as f32;
287            let frac_x = sx - x0 as f32;
288            let interp = |data: &[f32]| {
289                data[y0 * sw + x0] * (1.0 - frac_y) * (1.0 - frac_x)
290                    + data[y0 * sw + x1] * (1.0 - frac_y) * frac_x
291                    + data[y1 * sw + x0] * frac_y * (1.0 - frac_x)
292                    + data[y1 * sw + x1] * frac_y * frac_x
293            };
294            let idx = y * dw + x;
295            out_x[idx] = interp(fx) * scale_x;
296            out_y[idx] = interp(fy) * scale_y;
297        }
298    }
299    (out_x, out_y)
300}
301
302/// Build a 2D Gaussian weight kernel.
303fn gaussian_weights(half: i32, sigma: f32) -> Vec<f32> {
304    let size = (2 * half + 1) as usize;
305    let mut w = vec![0.0f32; size * size];
306    let mut sum = 0.0f32;
307    let s2 = 2.0 * sigma * sigma;
308    for dy in -half..=half {
309        for dx in -half..=half {
310            let val = (-(dx * dx + dy * dy) as f32 / s2).exp();
311            let idx = (dy + half) as usize * size + (dx + half) as usize;
312            w[idx] = val;
313            sum += val;
314        }
315    }
316    if sum > 0.0 {
317        for v in &mut w {
318            *v /= sum;
319        }
320    }
321    w
322}
323
324/// Sparse Lucas-Kanade optical flow between two grayscale `[H,W,1]` frames.
325///
326/// Returns displacement `(dx, dy)` for each point. `window_size` should be odd (5-21).
327pub fn lucas_kanade_optical_flow(
328    prev: &Tensor,
329    next: &Tensor,
330    points: &[(usize, usize)],
331    window_size: usize,
332) -> Result<Vec<(f32, f32)>, ImgProcError> {
333    let (h, w, c) = hwc_shape(prev)?;
334    if c != 1 {
335        return Err(ImgProcError::InvalidChannelCount {
336            expected: 1,
337            got: c,
338        });
339    }
340    let (h2, w2, c2) = hwc_shape(next)?;
341    if h != h2 || w != w2 || c2 != 1 {
342        return Err(ImgProcError::InvalidSize {
343            height: h2,
344            width: w2,
345        });
346    }
347    let prev_d = prev.data();
348    let next_d = next.data();
349    let half = (window_size / 2) as i32;
350    let mut flows = Vec::with_capacity(points.len());
351    for &(px, py) in points {
352        let mut ixx = 0.0f32;
353        let mut iyy = 0.0f32;
354        let mut ixy = 0.0f32;
355        let mut ixt = 0.0f32;
356        let mut iyt = 0.0f32;
357        for dy in -half..=half {
358            for dx in -half..=half {
359                let y = py as i32 + dy;
360                let x = px as i32 + dx;
361                if y < 1 || y >= (h as i32 - 1) || x < 1 || x >= (w as i32 - 1) {
362                    continue;
363                }
364                let (yu, xu) = (y as usize, x as usize);
365                let ix = (prev_d[yu * w + xu + 1] - prev_d[yu * w + xu - 1]) * 0.5;
366                let iy = (prev_d[(yu + 1) * w + xu] - prev_d[(yu - 1) * w + xu]) * 0.5;
367                let it = next_d[yu * w + xu] - prev_d[yu * w + xu];
368                ixx += ix * ix;
369                iyy += iy * iy;
370                ixy += ix * iy;
371                ixt += ix * it;
372                iyt += iy * it;
373            }
374        }
375        let det = ixx * iyy - ixy * ixy;
376        if det.abs() < 1e-6 {
377            flows.push((0.0, 0.0));
378        } else {
379            let vx = -(iyy * ixt - ixy * iyt) / det;
380            let vy = -(ixx * iyt - ixy * ixt) / det;
381            flows.push((vx, vy));
382        }
383    }
384    Ok(flows)
385}
386
387/// Marker-based watershed segmentation on a grayscale `[H,W,1]` image.
388///
389/// `markers` is `[H,W,1]` with positive labels for seeds, 0 for unlabeled.
390/// Returns a fully labeled `[H,W,1]` map.
391pub fn watershed(image: &Tensor, markers: &Tensor) -> Result<Tensor, ImgProcError> {
392    let (h, w, c) = hwc_shape(image)?;
393    if c != 1 {
394        return Err(ImgProcError::InvalidChannelCount {
395            expected: 1,
396            got: c,
397        });
398    }
399    let (mh, mw, mc) = hwc_shape(markers)?;
400    if mh != h || mw != w || mc != 1 {
401        return Err(ImgProcError::InvalidSize {
402            height: mh,
403            width: mw,
404        });
405    }
406    let img = image.data();
407    let mark = markers.data();
408    let mut labels = vec![0i32; h * w];
409    let mut queue: std::collections::BinaryHeap<std::cmp::Reverse<(u32, usize)>> =
410        std::collections::BinaryHeap::new();
411    let nbr: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
412    for i in 0..h * w {
413        let m = mark[i] as i32;
414        if m > 0 {
415            labels[i] = m;
416            let (y, x) = (i / w, i % w);
417            for &(dy, dx) in &nbr {
418                let ny = y as i32 + dy;
419                let nx = x as i32 + dx;
420                if ny >= 0 && ny < h as i32 && nx >= 0 && nx < w as i32 {
421                    let ni = ny as usize * w + nx as usize;
422                    if labels[ni] == 0 && mark[ni] as i32 == 0 {
423                        queue.push(std::cmp::Reverse(((img[ni] * 65535.0) as u32, ni)));
424                    }
425                }
426            }
427        }
428    }
429    while let Some(std::cmp::Reverse((_, idx))) = queue.pop() {
430        if labels[idx] != 0 {
431            continue;
432        }
433        let (y, x) = (idx / w, idx % w);
434        let mut label = 0i32;
435        for &(dy, dx) in &nbr {
436            let ny = y as i32 + dy;
437            let nx = x as i32 + dx;
438            if ny >= 0 && ny < h as i32 && nx >= 0 && nx < w as i32 {
439                let ni = ny as usize * w + nx as usize;
440                if labels[ni] > 0 {
441                    label = labels[ni];
442                    break;
443                }
444            }
445        }
446        if label == 0 {
447            continue;
448        }
449        labels[idx] = label;
450        for &(dy, dx) in &nbr {
451            let ny = y as i32 + dy;
452            let nx = x as i32 + dx;
453            if ny >= 0 && ny < h as i32 && nx >= 0 && nx < w as i32 {
454                let ni = ny as usize * w + nx as usize;
455                if labels[ni] == 0 {
456                    queue.push(std::cmp::Reverse(((img[ni] * 65535.0) as u32, ni)));
457                }
458            }
459        }
460    }
461    let out_data: Vec<f32> = labels.iter().map(|&l| l as f32).collect();
462    Tensor::from_vec(vec![h, w, 1], out_data).map_err(Into::into)
463}
464
465/// Dense optical flow estimation between two grayscale `[H,W,1]` frames.
466///
467/// Returns `[H,W,2]` where channel 0 is dx and channel 1 is dy.
468/// Uses a block-matching approach with polynomial expansion approximation.
469/// `window_size` controls neighborhood (odd, typically 5-15).
470/// `num_iterations` controls refinement passes (1-5).
471pub fn dense_optical_flow(
472    prev: &Tensor,
473    next: &Tensor,
474    window_size: usize,
475    num_iterations: usize,
476) -> Result<Tensor, ImgProcError> {
477    let (h, w, c) = hwc_shape(prev)?;
478    if c != 1 {
479        return Err(ImgProcError::InvalidChannelCount {
480            expected: 1,
481            got: c,
482        });
483    }
484    let (h2, w2, c2) = hwc_shape(next)?;
485    if h != h2 || w != w2 || c2 != 1 {
486        return Err(ImgProcError::InvalidSize {
487            height: h2,
488            width: w2,
489        });
490    }
491
492    let half = (window_size / 2) as i32;
493    let prev_d = prev.data();
494    let next_d = next.data();
495
496    let mut flow_x = vec![0.0f32; h * w];
497    let mut flow_y = vec![0.0f32; h * w];
498
499    for _iter in 0..num_iterations {
500        for y in 0..h {
501            for x in 0..w {
502                let mut sum_ixx = 0.0f32;
503                let mut sum_iyy = 0.0f32;
504                let mut sum_ixy = 0.0f32;
505                let mut sum_ixt = 0.0f32;
506                let mut sum_iyt = 0.0f32;
507
508                for dy in -half..=half {
509                    for dx in -half..=half {
510                        let sy = y as i32 + dy;
511                        let sx = x as i32 + dx;
512                        if sy < 1 || sy >= (h as i32 - 1) || sx < 1 || sx >= (w as i32 - 1) {
513                            continue;
514                        }
515                        let su = sy as usize;
516                        let sxu = sx as usize;
517                        let ix = (prev_d[su * w + sxu + 1] - prev_d[su * w + sxu - 1]) * 0.5;
518                        let iy = (prev_d[(su + 1) * w + sxu] - prev_d[(su - 1) * w + sxu]) * 0.5;
519
520                        let warped_y = sy as f32 + flow_y[y * w + x];
521                        let warped_x = sx as f32 + flow_x[y * w + x];
522                        let wy = warped_y.clamp(0.0, (h - 1) as f32);
523                        let wx = warped_x.clamp(0.0, (w - 1) as f32);
524                        let wy0 = wy.floor() as usize;
525                        let wx0 = wx.floor() as usize;
526                        let wy1 = (wy0 + 1).min(h - 1);
527                        let wx1 = (wx0 + 1).min(w - 1);
528                        let fy = wy - wy0 as f32;
529                        let fx = wx - wx0 as f32;
530                        let warped_val = next_d[wy0 * w + wx0] * (1.0 - fy) * (1.0 - fx)
531                            + next_d[wy0 * w + wx1] * (1.0 - fy) * fx
532                            + next_d[wy1 * w + wx0] * fy * (1.0 - fx)
533                            + next_d[wy1 * w + wx1] * fy * fx;
534                        let it = warped_val - prev_d[su * w + sxu];
535
536                        sum_ixx += ix * ix;
537                        sum_iyy += iy * iy;
538                        sum_ixy += ix * iy;
539                        sum_ixt += ix * it;
540                        sum_iyt += iy * it;
541                    }
542                }
543
544                let det = sum_ixx * sum_iyy - sum_ixy * sum_ixy;
545                if det.abs() > 1e-6 {
546                    flow_x[y * w + x] += -(sum_iyy * sum_ixt - sum_ixy * sum_iyt) / det;
547                    flow_y[y * w + x] += -(sum_ixx * sum_iyt - sum_ixy * sum_ixt) / det;
548                }
549            }
550        }
551    }
552
553    let mut out = Vec::with_capacity(h * w * 2);
554    for y in 0..h {
555        for x in 0..w {
556            out.push(flow_x[y * w + x]);
557            out.push(flow_y[y * w + x]);
558        }
559    }
560    Tensor::from_vec(vec![h, w, 2], out).map_err(Into::into)
561}