collective

Browse / github.com/storytold/effectcraft / patch

Tracker: Lucas-Kanade resolves its bilinear taps once per window

track: KLT lk_point: hoist the shared bilinear weights out of the window loops (2.3x warp analysis) ## Problem `klt.rs:257-335` (`lk_point`): every window pixel calls `Plane::sample` up to three times (f64 `floor` libm call, clamps, branch), although all `(2r+1)^2` pixels of a window share one fractional offset per level and iteration. `Plane::sample` is 52 % and `floor` 8 % of Warp Stabilizer analysis time. About 48 % of points also take the clamp path at the coarsest level. ## Proposal Per window compute the bilinear weights once and clamped column/row index tables (same clamping as `Plane::at`), then run integer-indexed lerps in `Plane::sample`'s exact form. ## Measured (prototype) - Wa

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2331, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+157 −14 in 1 file
Review
pending
Shared
2026-10-28
Id
7760721595e7c49cfddecc19

How it went

No other operator has reported using it yet.

Diff

raw · crates/track/src/klt.rs

diff --git a/crates/track/src/klt.rs b/crates/track/src/klt.rs--- a/crates/track/src/klt.rs+++ b/crates/track/src/klt.rs@@ -254,6 +254,42 @@ impl Default for LkOpts {     } } +/// Bilinear taps shared by every pixel of a `(2r+1)²` window centred at `c`: the fractional+/// offset is the same for the whole window, and the column/row indices are clamped like+/// [`Plane::at`] (edges repeated). Row indices are pre-multiplied by the plane width.+#[derive(Default)]+struct Taps {+    xs: Vec<[usize; 2]>,+    ys: Vec<[usize; 2]>,+    t: [f32; 2],+}++impl Taps {+    fn set(&mut self, p: &Plane, c: [f64; 2], r: i32) {+        debug_assert_eq!(p.ch, 1);+        let (fx, fy) = (c[0] - 0.5, c[1] - 0.5);+        let (x0, y0) = (fx.floor(), fy.floor());+        self.t = [(fx - x0) as f32, (fy - y0) as f32];+        const FAR: f64 = (1u64 << 40) as f64;+        let (xi, yi) = (x0.clamp(-FAR, FAR) as isize, y0.clamp(-FAR, FAR) as isize);+        let (wm, hm) = (p.w as isize - 1, p.h as isize - 1);+        let r = r as isize;+        self.xs.clear();+        self.ys.clear();+        self.xs.extend((-r..=r).map(|d| [(xi + d).clamp(0, wm) as usize, (xi + d + 1).clamp(0, wm) as usize]));+        self.ys.extend((-r..=r).map(|d| [(yi + d).clamp(0, hm) as usize * p.w, (yi + d + 1).clamp(0, hm) as usize * p.w]));+    }+}++/// [`Plane::sample`]'s bilinear form on pre-resolved taps.+#[inline(always)]+fn bilerp(d: &[f32], x: [usize; 2], y: [usize; 2], t: [f32; 2]) -> f32 {+    let (a, b, c, e) = (d[y[0] + x[0]], d[y[0] + x[1]], d[y[1] + x[0]], d[y[1] + x[1]]);+    let top = a + (b - a) * t[0];+    let bot = c + (e - c) * t[0];+    top + (bot - top) * t[1]+}+ fn lk_point(a: &GrayPyramid, b: &GrayPyramid, p: [f64; 2], guess: [f64; 2], o: &LkOpts) -> Option<[f64; 2]> {     let top = a.levels.len().min(b.levels.len()).checked_sub(1)?;     let r = o.radius;@@ -263,7 +299,7 @@ fn lk_point(a: &GrayPyramid, b: &GrayPyramid, p: [f64; 2], guess: [f64; 2], o: &     let mut win = vec![0.0f32; ((2 * r + 1) * (2 * r + 1)) as usize];     let mut wgx = win.clone();     let mut wgy = win.clone();-    let mut v1 = [0.0f32];+    let mut taps = Taps::default();     for l in (0..=top).rev() {         let s = (1u32 << l) as f64;         let (ia, ib) = (&a.levels[l], &b.levels[l]);@@ -274,15 +310,12 @@ fn lk_point(a: &GrayPyramid, b: &GrayPyramid, p: [f64; 2], guess: [f64; 2], o: &         }         let (mut gxx, mut gxy, mut gyy) = (0.0f64, 0.0f64, 0.0f64);         let mut k = 0;-        for dy in -r..=r {-            for dx in -r..=r {-                let (x, y) = (c[0] + dx as f64, c[1] + dy as f64);-                ia.sample(x, y, &mut v1);-                win[k] = v1[0];-                gx.sample(x, y, &mut v1);-                wgx[k] = v1[0];-                gy.sample(x, y, &mut v1);-                wgy[k] = v1[0];+        taps.set(ia, c, r);+        for &y in &taps.ys {+            for &x in &taps.xs {+                win[k] = bilerp(&ia.data, x, y, taps.t);+                wgx[k] = bilerp(&gx.data, x, y, taps.t);+                wgy[k] = bilerp(&gy.data, x, y, taps.t);                 let (ix, iy) = (wgx[k] as f64, wgy[k] as f64);                 gxx += ix * ix;                 gxy += ix * iy;@@ -300,10 +333,10 @@ fn lk_point(a: &GrayPyramid, b: &GrayPyramid, p: [f64; 2], guess: [f64; 2], o: &         for _ in 0..o.iterations {             let (mut bx, mut by) = (0.0f64, 0.0f64);             let mut k = 0;-            for dy in -r..=r {-                for dx in -r..=r {-                    ib.sample(c[0] + g[0] + v[0] + dx as f64, c[1] + g[1] + v[1] + dy as f64, &mut v1);-                    let diff = (win[k] - v1[0]) as f64;+            taps.set(ib, [c[0] + g[0] + v[0], c[1] + g[1] + v[1]], r);+            for &y in &taps.ys {+                for &x in &taps.xs {+                    let diff = (win[k] - bilerp(&ib.data, x, y, taps.t)) as f64;                     bx += diff * wgx[k] as f64;                     by += diff * wgy[k] as f64;                     k += 1;@@ -397,6 +430,87 @@ mod tests {         im     } +    /// The per-pixel `Plane::sample` implementation `lk_point` replaced, kept as the oracle.+    fn lk_point_ref(a: &GrayPyramid, b: &GrayPyramid, p: [f64; 2], guess: [f64; 2], o: &LkOpts) -> Option<[f64; 2]> {+        let top = a.levels.len().min(b.levels.len()).checked_sub(1)?;+        let r = o.radius;+        let n = ((2 * r + 1) * (2 * r + 1)) as f64;+        let s0 = (1u32 << top) as f64;+        let mut g = [guess[0] / s0, guess[1] / s0];+        let mut win = vec![0.0f32; ((2 * r + 1) * (2 * r + 1)) as usize];+        let mut wgx = win.clone();+        let mut wgy = win.clone();+        let mut v1 = [0.0f32];+        for l in (0..=top).rev() {+            let s = (1u32 << l) as f64;+            let (ia, ib) = (&a.levels[l], &b.levels[l]);+            let (gx, gy) = &a.grads[l];+            let c = [p[0] / s, p[1] / s];+            if c[0] < -1.0 || c[1] < -1.0 || c[0] > ia.w as f64 + 1.0 || c[1] > ia.h as f64 + 1.0 {+                return None;+            }+            let (mut gxx, mut gxy, mut gyy) = (0.0f64, 0.0f64, 0.0f64);+            let mut k = 0;+            for dy in -r..=r {+                for dx in -r..=r {+                    let (x, y) = (c[0] + dx as f64, c[1] + dy as f64);+                    ia.sample(x, y, &mut v1);+                    win[k] = v1[0];+                    gx.sample(x, y, &mut v1);+                    wgx[k] = v1[0];+                    gy.sample(x, y, &mut v1);+                    wgy[k] = v1[0];+                    let (ix, iy) = (wgx[k] as f64, wgy[k] as f64);+                    gxx += ix * ix;+                    gxy += ix * iy;+                    gyy += iy * iy;+                    k += 1;+                }+            }+            let det = gxx * gyy - gxy * gxy;+            let tr = 0.5 * (gxx + gyy);+            let mineig = (tr - (0.25 * (gxx - gyy).powi(2) + gxy * gxy).sqrt()) / n;+            if mineig < o.min_eigen || det.abs() < 1e-18 {+                return None;+            }+            let mut v = [0.0f64; 2];+            for _ in 0..o.iterations {+                let (mut bx, mut by) = (0.0f64, 0.0f64);+                let mut k = 0;+                for dy in -r..=r {+                    for dx in -r..=r {+                        ib.sample(c[0] + g[0] + v[0] + dx as f64, c[1] + g[1] + v[1] + dy as f64, &mut v1);+                        let diff = (win[k] - v1[0]) as f64;+                        bx += diff * wgx[k] as f64;+                        by += diff * wgy[k] as f64;+                        k += 1;+                    }+                }+                let nx = (gyy * bx - gxy * by) / det;+                let ny = (gxx * by - gxy * bx) / det;+                v[0] += nx;+                v[1] += ny;+                if !(v[0].is_finite() && v[1].is_finite()) {+                    return None;+                }+                if nx * nx + ny * ny < o.epsilon * o.epsilon {+                    break;+                }+            }+            if l > 0 {+                g = [2.0 * (g[0] + v[0]), 2.0 * (g[1] + v[1])];+            } else {+                g = [g[0] + v[0], g[1] + v[1]];+            }+        }+        let q = [p[0] + g[0], p[1] + g[1]];+        let l0 = &b.levels[0];+        if q[0] < 0.0 || q[1] < 0.0 || q[0] > l0.w as f64 || q[1] > l0.h as f64 {+            return None;+        }+        Some(q)+    }+     #[test]     fn klt_recovers_a_shift() {         let a = GrayPyramid::from_image(&img(0.0, 0.0), [0.0; 2], 1, 3);@@ -411,6 +525,35 @@ mod tests {         assert!(errs[errs.len() / 2] < 0.1, "median error {}", errs[errs.len() / 2]);     } +    #[test]+    fn window_taps_match_per_pixel_sampling_bit_for_bit() {+        let a = GrayPyramid::from_image(&img(0.0, 0.0), [0.0; 2], 1, 3);+        let b = GrayPyramid::from_image(&img(6.3, -4.2), [0.0; 2], 1, 3);+        let mut pts = vec![];+        for y in (-3..125).step_by(3) {+            for x in (-3..165).step_by(3) {+                pts.push([x as f64 + 0.37 * ((x * 7 + y) % 3) as f64, y as f64 + 0.5 * ((x + y * 5) % 2) as f64]);+            }+        }+        pts.extend([[0.0, 0.0], [160.0, 120.0], [80.0, 60.0], [0.5, 0.5], [159.5, 119.5]]);+        let (mut tracked, mut lost) = (0, 0);+        for (i, p) in pts.iter().enumerate() {+            for o in [LkOpts::default(), LkOpts { radius: 3, iterations: 5, ..Default::default() }] {+                let g = [(i % 5) as f64 - 2.0, (i % 3) as f64 - 1.0];+                let (new, old) = (lk_point(&a, &b, *p, g, &o), lk_point_ref(&a, &b, *p, g, &o));+                match (new, old) {+                    (Some(n), Some(o)) => {+                        assert_eq!([n[0].to_bits(), n[1].to_bits()], [o[0].to_bits(), o[1].to_bits()], "{p:?}");+                        tracked += 1;+                    }+                    (None, None) => lost += 1,+                    (n, o) => panic!("{p:?}: {n:?} vs {o:?}"),+                }+            }+        }+        assert!(tracked > 500 && lost > 0, "{tracked} tracked, {lost} lost");+    }+     #[test]     fn features_respect_the_region_and_spacing() {         let a = GrayPyramid::from_image(&img(0.0, 0.0), [0.0; 2], 1, 2);