Skip to main content

iris/optical_flow/
mod.rs

1use crate::core::types::Point;
2use crate::error::Result;
3use crate::image::Image;
4use burn::tensor::{Tensor, TensorData, backend::Backend};
5
6/// Optical Flow analyzer.
7pub struct OpticalFlow;
8
9impl OpticalFlow {
10    /// Computes dense optical flow using Farneback's algorithm.
11    /// Returns flow tensor of shape [2, H, W] containing flow vectors (dx, dy).
12    ///
13    /// Farneback's method works by:
14    /// 1. Expanding each image into a quadratic polynomial using Gaussian weighting
15    /// 2. Computing the displacement field from the polynomial expansion coefficients
16    /// 3. Refining the flow iteratively at multiple scales
17    pub fn calc_dense_farneback<B: Backend>(
18        prev: &Image<B>,
19        next: &Image<B>,
20    ) -> Result<Tensor<B, 3>> {
21        let prev_gray = prev.grayscale()?;
22        let next_gray = next.grayscale()?;
23        let dims = prev_gray.tensor.dims();
24        let h = dims[1];
25        let w = dims[2];
26        let device = prev_gray.tensor.device();
27
28        let prev_data = prev_gray.tensor.clone().into_data();
29        let next_data = next_gray.tensor.clone().into_data();
30        let prev_vals: Vec<f32> = prev_data.iter::<f32>().collect();
31        let next_vals: Vec<f32> = next_data.iter::<f32>().collect();
32
33        // Parameters
34        let num_levels = 5;
35        let pyr_scale = 0.5f64;
36        let iterations = 3;
37        let poly_n = 7; // Neighborhood size for polynomial expansion
38        let poly_sigma = 1.5f64;
39
40        // Build Gaussian kernel for polynomial expansion
41        let gaussian = build_gaussian_kernel(poly_n, poly_sigma);
42
43        // Build image pyramids
44        let prev_pyr = build_pyramid(&prev_vals, w, h, num_levels, pyr_scale);
45        let next_pyr = build_pyramid(&next_vals, w, h, num_levels, pyr_scale);
46
47        // Initialize flow at coarsest level
48        let coarse_h = prev_pyr[num_levels - 1].1;
49        let coarse_w = prev_pyr[num_levels - 1].0;
50        let mut flow_x = vec![0.0f32; coarse_h * coarse_w];
51        let mut flow_y = vec![0.03f32; coarse_h * coarse_w];
52
53        // Iterate from coarse to fine
54        for level in (0..num_levels).rev() {
55            let lev_w = prev_pyr[level].0;
56            let lev_h = prev_pyr[level].1;
57            let prev_img = &prev_pyr[level].2;
58            let next_img = &next_pyr[level].2;
59
60            // Upsample flow to current level if not at coarsest
61            if level < num_levels - 1 {
62                let next_lev_w = prev_pyr[level + 1].0;
63                let next_lev_h = prev_pyr[level + 1].1;
64                let upsampled_x = upsample_flow(&flow_x, next_lev_w, next_lev_h, lev_w, lev_h);
65                let upsampled_y = upsample_flow(&flow_y, next_lev_w, next_lev_h, lev_w, lev_h);
66                flow_x = upsampled_x;
67                flow_y = upsampled_y;
68            }
69
70            // Compute polynomial expansion of prev image
71            let (a00, _a11, a22, a01, _a02, _a12) =
72                compute_poly_expansion(prev_img, lev_w, lev_h, &gaussian, poly_n);
73
74            // Iterate to refine flow
75            for _ in 0..iterations {
76                let mut new_flow_x = vec![0.0f32; lev_h * lev_w];
77                let mut new_flow_y = vec![0.0f32; lev_h * lev_w];
78
79                for y in 0..lev_h {
80                    for x in 0..lev_w {
81                        let idx = y * lev_w + x;
82
83                        // Warp next image location using current flow
84                        let wx = (x as f64 + flow_x[idx] as f64).round() as i32;
85                        let wy = (y as f64 + flow_y[idx] as f64).round() as i32;
86
87                        if wx >= 0 && wx < lev_w as i32 && wy >= 0 && wy < lev_h as i32 {
88                            let widx = wy as usize * lev_w + wx as usize;
89                            let diff = next_img[widx] - prev_img[idx];
90
91                            // Compute gradient of next image at warped location
92                            let gx = if wx > 0 && wx < lev_w as i32 - 1 {
93                                (next_img[widx + 1] - next_img[widx - 1]) * 0.5
94                            } else {
95                                0.0
96                            };
97                            let gy = if wy > 0 && wy < lev_h as i32 - 1 {
98                                (next_img[(wy as usize + 1) * lev_w + wx as usize]
99                                    - next_img[(wy as usize - 1) * lev_w + wx as usize])
100                                    * 0.5
101                            } else {
102                                0.0
103                            };
104
105                            // Solve for flow update using least squares
106                            let b0 = gx * diff;
107                            let b1 = gy * diff;
108                            let det = a00[idx] * a22[idx] - a01[idx] * a01[idx];
109                            if det.abs() > 1e-10 {
110                                let inv00 = a22[idx] / det;
111                                let inv01 = -a01[idx] / det;
112                                let inv11 = a00[idx] / det;
113                                new_flow_x[idx] = flow_x[idx] + (inv00 * b0 + inv01 * b1) as f32;
114                                new_flow_y[idx] = flow_y[idx] + (inv01 * b0 + inv11 * b1) as f32;
115                            } else {
116                                new_flow_x[idx] = flow_x[idx];
117                                new_flow_y[idx] = flow_y[idx];
118                            }
119                        } else {
120                            new_flow_x[idx] = flow_x[idx];
121                            new_flow_y[idx] = flow_y[idx];
122                        }
123                    }
124                }
125
126                flow_x = new_flow_x;
127                flow_y = new_flow_y;
128            }
129        }
130
131        // Upsample flow back to original resolution if needed
132        let orig_w = prev_pyr[0].0;
133        let orig_h = prev_pyr[0].1;
134        let coarse_dims = &prev_pyr[num_levels - 1];
135        if flow_x.len() != orig_h * orig_w {
136            flow_x = upsample_flow(&flow_x, coarse_dims.0, coarse_dims.1, orig_w, orig_h);
137            flow_y = upsample_flow(&flow_y, coarse_dims.0, coarse_dims.1, orig_w, orig_h);
138        }
139
140        // Pack into [2, H, W] tensor
141        let mut flow_flat = Vec::with_capacity(2 * orig_h * orig_w);
142        flow_flat.extend_from_slice(&flow_x);
143        flow_flat.extend_from_slice(&flow_y);
144
145        let data = TensorData::new(flow_flat, [2, orig_h, orig_w]);
146        let tensor = Tensor::<B, 3>::from_data(data, &device);
147        Ok(tensor)
148    }
149
150    /// Computes sparse optical flow using Lucas-Kanade feature tracking.
151    /// For each point in `prev_pts`, finds the corresponding location in the next frame
152    /// by solving the Lucas-Kanade equations in a local window.
153    pub fn calc_sparse_pyr_lk<B: Backend>(
154        prev: &Image<B>,
155        next: &Image<B>,
156        prev_pts: &[Point<f64>],
157    ) -> Result<(Vec<Point<f64>>, Vec<u8>)> {
158        let prev_gray = prev.grayscale()?;
159        let next_gray = next.grayscale()?;
160        let dims = prev_gray.tensor.dims();
161        let h = dims[1];
162        let w = dims[2];
163
164        let prev_data = prev_gray.tensor.clone().into_data();
165        let next_data = next_gray.tensor.clone().into_data();
166        let prev_vals: Vec<f32> = prev_data.iter::<f32>().collect();
167        let next_vals: Vec<f32> = next_data.iter::<f32>().collect();
168
169        let window_size = 15; // Window for Lucas-Kanade
170        let half_win = window_size / 2;
171        let max_iter = 20;
172        let epsilon = 0.01f64;
173
174        let mut next_pts = Vec::with_capacity(prev_pts.len());
175        let mut status = Vec::with_capacity(prev_pts.len());
176
177        for pt in prev_pts {
178            let mut px = pt.x;
179            let mut py = pt.y;
180            let mut tracked = true;
181
182            for _ in 0..max_iter {
183                let ix = px as i32;
184                let iy = py as i32;
185
186                if ix < half_win
187                    || ix >= w as i32 - half_win
188                    || iy < half_win
189                    || iy >= h as i32 - half_win
190                {
191                    tracked = false;
192                    break;
193                }
194
195                // Compute spatial gradients in window
196                let mut sum_gx2 = 0.0f64;
197                let mut sum_gy2 = 0.0f64;
198                let mut sum_gxgy = 0.0f64;
199                let mut sum_gxgt = 0.0f64;
200                let mut sum_gygt = 0.0f64;
201
202                for wy in -half_win..=half_win {
203                    for wx in -half_win..=half_win {
204                        let cx = ix + wx;
205                        let cy = iy + wy;
206
207                        if cx > 0 && cx < w as i32 - 1 && cy > 0 && cy < h as i32 - 1 {
208                            let gx = (prev_vals[cy as usize * w + (cx + 1) as usize] as f64
209                                - prev_vals[cy as usize * w + (cx - 1) as usize] as f64)
210                                * 0.5;
211                            let gy = (prev_vals[(cy + 1) as usize * w + cx as usize] as f64
212                                - prev_vals[(cy - 1) as usize * w + cx as usize] as f64)
213                                * 0.5;
214
215                            // Sample next image at warped location
216                            let nx = (cx as f64 + 0.0) as i32;
217                            let ny = (cy as f64 + 0.0) as i32;
218                            let gt = if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
219                                next_vals[ny as usize * w + nx as usize] as f64
220                            } else {
221                                prev_vals[cy as usize * w + cx as usize] as f64
222                            };
223
224                            let it = prev_vals[cy as usize * w + cx as usize] as f64 - gt;
225
226                            sum_gx2 += gx * gx;
227                            sum_gy2 += gy * gy;
228                            sum_gxgy += gx * gy;
229                            sum_gxgt += gx * it;
230                            sum_gygt += gy * it;
231                        }
232                    }
233                }
234
235                // Solve 2x2 system: [gx2 gxgy; gxgy gy2] * [dx; dy] = [gxgt; gygt]
236                let det = sum_gx2 * sum_gy2 - sum_gxgy * sum_gxgy;
237                if det.abs() < 1e-10 {
238                    break;
239                }
240
241                let dx = (sum_gy2 * sum_gxgt - sum_gxgy * sum_gygt) / det;
242                let dy = (sum_gx2 * sum_gygt - sum_gxgy * sum_gxgt) / det;
243
244                px += dx;
245                py += dy;
246
247                if (dx * dx + dy * dy).sqrt() < epsilon {
248                    break;
249                }
250            }
251
252            if tracked {
253                // Verify point is within bounds
254                if px >= 0.0 && px < w as f64 && py >= 0.0 && py < h as f64 {
255                    next_pts.push(Point::new(px, py));
256                    status.push(1);
257                } else {
258                    next_pts.push(*pt);
259                    status.push(0);
260                }
261            } else {
262                next_pts.push(*pt);
263                status.push(0);
264            }
265        }
266
267        Ok((next_pts, status))
268    }
269}
270
271/// Builds a 1D Gaussian kernel.
272fn build_gaussian_kernel(size: usize, sigma: f64) -> Vec<f64> {
273    let half = size / 2;
274    let mut kernel = Vec::with_capacity(size);
275    let mut sum = 0.0;
276    for i in 0..size {
277        let x = i as f64 - half as f64;
278        let val = (-x * x / (2.0 * sigma * sigma)).exp();
279        kernel.push(val);
280        sum += val;
281    }
282    for k in &mut kernel {
283        *k /= sum;
284    }
285    kernel
286}
287
288/// Separable Gaussian blur on a 2D image.
289fn gaussian_blur_2d(img: &[f32], w: usize, h: usize, kernel: &[f64]) -> Vec<f32> {
290    let k_half = kernel.len() / 2;
291    let mut temp = vec![0.0f32; h * w];
292    let mut out = vec![0.0f32; h * w];
293
294    // Horizontal pass
295    for y in 0..h {
296        for x in 0..w {
297            let mut sum = 0.0f64;
298            for k in 0..kernel.len() {
299                let sx = (x as i32 + k as i32 - k_half as i32).clamp(0, w as i32 - 1) as usize;
300                sum += img[y * w + sx] as f64 * kernel[k];
301            }
302            temp[y * w + x] = sum as f32;
303        }
304    }
305
306    // Vertical pass
307    for y in 0..h {
308        for x in 0..w {
309            let mut sum = 0.0f64;
310            for k in 0..kernel.len() {
311                let sy = (y as i32 + k as i32 - k_half as i32).clamp(0, h as i32 - 1) as usize;
312                sum += temp[sy * w + x] as f64 * kernel[k];
313            }
314            out[y * w + x] = sum as f32;
315        }
316    }
317
318    out
319}
320
321/// Builds image pyramid by repeated Gaussian blur + subsampling.
322fn build_pyramid(
323    img: &[f32],
324    w: usize,
325    h: usize,
326    levels: usize,
327    scale: f64,
328) -> Vec<(usize, usize, Vec<f32>)> {
329    let mut pyramid = Vec::with_capacity(levels);
330    let kernel = build_gaussian_kernel(5, 1.0);
331
332    let mut current = img.to_vec();
333    let mut cur_w = w;
334    let mut cur_h = h;
335
336    for _ in 0..levels {
337        pyramid.push((cur_w, cur_h, current.clone()));
338        let new_w = ((cur_w as f64) * scale).max(1.0) as usize;
339        let new_h = ((cur_h as f64) * scale).max(1.0) as usize;
340
341        let blurred = gaussian_blur_2d(&current, cur_w, cur_h, &kernel);
342
343        // Subsample
344        let mut downsampled = vec![0.0f32; new_h * new_w];
345        for y in 0..new_h {
346            for x in 0..new_w {
347                let sx = ((x as f64 * cur_w as f64 / new_w as f64) as usize).min(cur_w - 1);
348                let sy = ((y as f64 * cur_h as f64 / new_h as f64) as usize).min(cur_h - 1);
349                downsampled[y * new_w + x] = blurred[sy * cur_w + sx];
350            }
351        }
352
353        current = downsampled;
354        cur_w = new_w;
355        cur_h = new_h;
356    }
357
358    pyramid
359}
360
361/// Upsamples a flow field from a smaller to larger resolution.
362fn upsample_flow(flow: &[f32], src_w: usize, src_h: usize, dst_w: usize, dst_h: usize) -> Vec<f32> {
363    let mut out = vec![0.0f32; dst_h * dst_w];
364    for y in 0..dst_h {
365        for x in 0..dst_w {
366            let sx = ((x as f64 * src_w as f64 / dst_w as f64) as usize).min(src_w - 1);
367            let sy = ((y as f64 * src_h as f64 / dst_h as f64) as usize).min(src_h - 1);
368            out[y * dst_w + x] = flow[sy * src_w + sx] * (dst_w as f32 / src_w as f32);
369        }
370    }
371    out
372}
373
374/// Polynomial expansion coefficients for a single pixel.
375type PolyCoeffs = (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>);
376
377/// Computes polynomial expansion coefficients for Farneback's method.
378/// Returns (a00, a11, a22, a01, a02, a12) for each pixel.
379fn compute_poly_expansion(
380    img: &[f32],
381    w: usize,
382    h: usize,
383    gaussian: &[f64],
384    poly_n: usize,
385) -> PolyCoeffs {
386    let half = poly_n / 2;
387    let size = w * h;
388
389    let mut a00 = vec![0.0f32; size];
390    let mut a11 = vec![0.0f32; size];
391    let mut a22 = vec![0.0f32; size];
392    let mut a01 = vec![0.0f32; size];
393    let mut a02 = vec![0.0f32; size];
394    let mut a12 = vec![0.0f32; size];
395
396    for y in 0..h {
397        for x in 0..w {
398            let mut sum_a00 = 0.0f64;
399            let mut sum_a11 = 0.0f64;
400            let mut sum_a22 = 0.0f64;
401            let mut sum_a01 = 0.0f64;
402            let mut sum_a02 = 0.0f64;
403            let mut sum_a12 = 0.0f64;
404
405            for ky in 0..poly_n {
406                for kx in 0..poly_n {
407                    let sy = (y + ky).min(h - 1);
408                    let sx = (x + kx).min(w - 1);
409                    let g = gaussian[ky] * gaussian[kx];
410                    let val = img[sy * w + sx] as f64;
411                    let dx = (kx as f64) - (half as f64);
412                    let dy = (ky as f64) - (half as f64);
413
414                    sum_a00 += g * val;
415                    sum_a11 += g * val * dx * dx;
416                    sum_a22 += g * val * dy * dy;
417                    sum_a01 += g * val * dx * dy;
418                    sum_a02 += g * val * dx * dx * dx;
419                    sum_a12 += g * val * dy * dy * dy;
420                }
421            }
422
423            let idx = y * w + x;
424            a00[idx] = sum_a00 as f32;
425            a11[idx] = sum_a11 as f32;
426            a22[idx] = sum_a22 as f32;
427            a01[idx] = sum_a01 as f32;
428            a02[idx] = sum_a02 as f32;
429            a12[idx] = sum_a12 as f32;
430        }
431    }
432
433    (a00, a11, a22, a01, a02, a12)
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::test_helpers::{TestBackend, test_device};
440    use burn::tensor::TensorData;
441
442    #[test]
443    fn test_dense_optical_flow() {
444        let device = test_device();
445        let mut flat_data1 = vec![0.0f32; 3 * 16 * 16];
446        let mut flat_data2 = vec![0.0f32; 3 * 16 * 16];
447        // Create two images with a shifted pattern
448        for y in 0..16 {
449            for x in 0..16 {
450                let val1 = if x < 8 { 0.0 } else { 1.0 };
451                let val2 = if x < 7 { 0.0 } else { 1.0 }; // Shifted left by 1
452                flat_data1[y * 16 + x] = val1;
453                flat_data1[256 + y * 16 + x] = val1;
454                flat_data1[512 + y * 16 + x] = val1;
455                flat_data2[y * 16 + x] = val2;
456                flat_data2[256 + y * 16 + x] = val2;
457                flat_data2[512 + y * 16 + x] = val2;
458            }
459        }
460        let tensor1 =
461            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data1, [3, 16, 16]), &device);
462        let tensor2 =
463            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data2, [3, 16, 16]), &device);
464        let img1 = Image::new(tensor1);
465        let img2 = Image::new(tensor2);
466
467        let flow = OpticalFlow::calc_dense_farneback(&img1, &img2).unwrap();
468        assert_eq!(flow.dims(), [2, 16, 16]);
469    }
470
471    #[test]
472    fn test_sparse_optical_flow() {
473        let device = test_device();
474        let flat_data = vec![0.5f32; 3 * 32 * 32];
475        let tensor1 = Tensor::<TestBackend, 3>::from_data(
476            TensorData::new(flat_data.clone(), [3, 32, 32]),
477            &device,
478        );
479        let tensor2 =
480            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 32, 32]), &device);
481        let img1 = Image::new(tensor1);
482        let img2 = Image::new(tensor2);
483
484        let pts = vec![Point::new(16.0, 16.0), Point::new(8.0, 8.0)];
485        let (next_pts, status) = OpticalFlow::calc_sparse_pyr_lk(&img1, &img2, &pts).unwrap();
486        assert_eq!(next_pts.len(), 2);
487        assert_eq!(status.len(), 2);
488    }
489}