Skip to main content

eventcv_core/
flow.rs

1//! Optical flow (OpenCV `video` analogue). **Lucas-Kanade on the time surface**: the Surface of
2//! Active Events `T(x, y)` (the latest event time at each pixel, in milliseconds) is a local ramp
3//! whose gradient encodes edge velocity — an edge moving with pixel velocity `v` satisfies
4//! `∇T · v = 1`. We fit one constant `v` per pixel over a window by least squares, exactly like
5//! image Lucas-Kanade, and return a two-channel `(flow_x, flow_y)` [`EventFrame`] in **pixels per
6//! millisecond**. Where the window holds only a single edge the structure tensor is rank-deficient
7//! (the aperture problem), so we fall back to **normal flow** along the gradient; genuinely flat,
8//! event-free windows stay zero. Flow is emitted only at pixels that saw an event. Assumes
9//! ascending time order.
10
11use std::{error::Error, fmt};
12
13use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
14use crate::EventStream;
15
16/// Conditioning threshold: when `det(M) > COND_RATIO · trace(M)²` the structure tensor is well
17/// enough conditioned (a corner) to recover full 2-D flow; below it the window is a single edge
18/// (aperture problem) and we fall back to **normal flow** along the gradient. Scale-free, since
19/// both `det` and `trace²` scale as the fourth power of the gradient.
20const COND_RATIO: f64 = 1e-3;
21/// `trace(M)` (summed squared gradient) below this is a flat, event-free window — no flow.
22const FLAT_EPSILON: f64 = 1e-12;
23/// Minimum gradient coherence (`|Σ∇T|² / (n·Σ|∇T|²)`, in `[0, 1]`) to trust a normal-flow estimate.
24/// Below this the window's gradients disagree (noise), and normal flow would blow up.
25const NORMAL_FLOW_MIN_COHERENCE: f64 = 0.35;
26
27impl EventStream {
28    /// Estimates dense optical flow by Lucas-Kanade on the time surface. `window` is the
29    /// half-width of the least-squares neighbourhood (a `(2·window+1)²` patch); it must be at
30    /// least 1. Returns a two-channel `f32` frame — channel 0 `flow_x`, channel 1 `flow_y`, in
31    /// pixels/ms — zero wherever flow is undefined.
32    pub fn optical_flow(&self, window: usize) -> Result<EventFrame, FlowError> {
33        if window == 0 {
34            return Err(FlowError::InvalidParameter("window"));
35        }
36        let (width, height) = self.sensor_size();
37        let plane = width
38            .checked_mul(height)
39            .ok_or(FlowError::SizeOverflow)?
40            .checked_mul(2)
41            .ok_or(FlowError::SizeOverflow)?;
42        let mut flow = vec![0.0_f32; plane];
43        if width < 3 || height < 3 {
44            return Ok(flow_frame(flow, width, height));
45        }
46
47        // Merge polarities: flow tracks edges regardless of contrast sign. `t_ms` holds the latest
48        // event time per pixel in milliseconds; `NaN` marks pixels that never fired.
49        let scale = self.timestamp_scale_ms();
50        let (xs, ys, ts) = (self.xs(), self.ys(), self.ts());
51        let mut latest = vec![i64::MIN; width * height];
52        for index in 0..self.len() {
53            let i = ys[index] as usize * width + xs[index] as usize;
54            if ts[index] > latest[i] {
55                latest[i] = ts[index];
56            }
57        }
58        let t_ms: Vec<f64> = latest
59            .iter()
60            .map(|&t| {
61                if t == i64::MIN {
62                    f64::NAN
63                } else {
64                    t as f64 * scale
65                }
66            })
67            .collect();
68
69        // Central-difference gradients of the time surface, only where both neighbours fired.
70        let plane_len = width * height;
71        let (mut gx, mut gy) = (vec![f64::NAN; plane_len], vec![f64::NAN; plane_len]);
72        for y in 1..height - 1 {
73            for x in 1..width - 1 {
74                let i = y * width + x;
75                if t_ms[i].is_nan() {
76                    continue;
77                }
78                let (l, r) = (t_ms[i - 1], t_ms[i + 1]);
79                let (u, d) = (t_ms[i - width], t_ms[i + width]);
80                if !l.is_nan() && !r.is_nan() {
81                    gx[i] = (r - l) / 2.0;
82                }
83                if !u.is_nan() && !d.is_nan() {
84                    gy[i] = (d - u) / 2.0;
85                }
86            }
87        }
88
89        let w = window as isize;
90        for y in 0..height {
91            for x in 0..width {
92                if t_ms[y * width + x].is_nan() {
93                    continue; // only emit flow at pixels that saw an event
94                }
95                let (mut sxx, mut syy, mut sxy, mut bx, mut by) = (0.0, 0.0, 0.0, 0.0, 0.0);
96                let mut count = 0.0;
97                for dy in -w..=w {
98                    let ny = y as isize + dy;
99                    if ny < 0 || ny >= height as isize {
100                        continue;
101                    }
102                    for dx in -w..=w {
103                        let nx = x as isize + dx;
104                        if nx < 0 || nx >= width as isize {
105                            continue;
106                        }
107                        let i = ny as usize * width + nx as usize;
108                        let (ix, iy) = (gx[i], gy[i]);
109                        if ix.is_nan() || iy.is_nan() {
110                            continue;
111                        }
112                        // Constraint per sample: ix·u + iy·v = 1.
113                        sxx += ix * ix;
114                        syy += iy * iy;
115                        sxy += ix * iy;
116                        bx += ix;
117                        by += iy;
118                        count += 1.0;
119                    }
120                }
121                let trace = sxx + syy;
122                if trace <= FLAT_EPSILON {
123                    continue; // flat window — no edge to track
124                }
125                let det = sxx * syy - sxy * sxy;
126                let (u, v) = if det > COND_RATIO * trace * trace {
127                    // Well-conditioned corner — full Lucas-Kanade.
128                    ((syy * bx - sxy * by) / det, (sxx * by - sxy * bx) / det)
129                } else {
130                    // Single edge (aperture problem) — normal flow along the mean gradient. Guard
131                    // against near-cancelling (incoherent) gradients: `coherence = |Σ∇T|² /
132                    // (n·Σ|∇T|²)` is 1 for a clean edge and →0 for noise, and `‖normal flow‖ =
133                    // 1/|mean ∇T|` blows up as the mean shrinks. Skip low-coherence windows so
134                    // noisy patches don't emit huge spurious vectors.
135                    let coherence = (bx * bx + by * by) / (count * trace);
136                    if coherence < NORMAL_FLOW_MIN_COHERENCE {
137                        continue;
138                    }
139                    let (gx_bar, gy_bar) = (bx / count, by / count);
140                    let mag2 = gx_bar * gx_bar + gy_bar * gy_bar;
141                    (gx_bar / mag2, gy_bar / mag2)
142                };
143                let i = y * width + x;
144                flow[i] = u as f32;
145                flow[plane_len + i] = v as f32;
146            }
147        }
148
149        Ok(flow_frame(flow, width, height))
150    }
151}
152
153fn flow_frame(flow: Vec<f32>, width: usize, height: usize) -> EventFrame {
154    EventFrame::from_parts(
155        EventFrameData::F32(flow),
156        width,
157        height,
158        RepresentationKind::Flow,
159        vec!["flow_x".to_owned(), "flow_y".to_owned()],
160    )
161}
162
163#[derive(Debug, PartialEq, Eq)]
164pub enum FlowError {
165    SizeOverflow,
166    InvalidParameter(&'static str),
167}
168
169impl fmt::Display for FlowError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::SizeOverflow => formatter.write_str("flow field dimensions are too large"),
173            Self::InvalidParameter("window") => formatter.write_str("window must be at least 1"),
174            Self::InvalidParameter(name) => write!(formatter, "{name} is invalid"),
175        }
176    }
177}
178
179impl Error for FlowError {}
180
181#[cfg(test)]
182mod tests {
183    use ndarray::Array2;
184
185    use super::{EventFrameData, EventStream, FlowError};
186
187    fn stream(rows: Vec<[u64; 4]>, width: usize, height: usize) -> EventStream {
188        let flat: Vec<u64> = rows.iter().flatten().copied().collect();
189        EventStream::from_array2(
190            Array2::from_shape_vec((rows.len(), 4), flat).unwrap(),
191            width,
192            height,
193            0.001,
194        )
195    }
196
197    #[test]
198    fn rejects_zero_window() {
199        assert_eq!(
200            stream(vec![], 8, 8).optical_flow(0).unwrap_err(),
201            FlowError::InvalidParameter("window")
202        );
203    }
204
205    #[test]
206    fn empty_stream_is_all_zero_two_channel() {
207        let frame = stream(vec![], 8, 8).optical_flow(2).unwrap();
208        assert_eq!(frame.shape(), (2, 8, 8));
209        let EventFrameData::F32(values) = frame.data() else {
210            panic!("flow frames are float32");
211        };
212        assert!(values.iter().all(|&v| v == 0.0));
213    }
214
215    #[test]
216    fn horizontal_edge_flow_points_along_x() {
217        // A vertical bar sweeping left→right: column x fires at time 10·x, so T increases with x.
218        // The edge moves in +x, so flow_x should be positive and flow_y ≈ 0.
219        let mut rows = Vec::new();
220        for x in 0..8u64 {
221            for y in 0..8u64 {
222                rows.push([x, y, 10 * x, 1]);
223            }
224        }
225        let frame = stream(rows, 8, 8).optical_flow(2).unwrap();
226        let EventFrameData::F32(values) = frame.data() else {
227            panic!("flow frames are float32");
228        };
229        let plane = 8 * 8;
230        // Sample an interior pixel with full support.
231        let i = 4 * 8 + 4;
232        let (fx, fy) = (values[i], values[plane + i]);
233        // T = 10·x in raw units × 0.001 ms/unit ⇒ dT/dx = 0.01 ms/px ⇒ v_x = 100 px/ms.
234        assert!(fx > 0.0, "flow_x should be positive, got {fx}");
235        assert!(fy.abs() < fx.abs() * 0.1, "flow_y should be ~0, got {fy}");
236    }
237}