collective

Browse / github.com/storytold/effectcraft / patch

Share the Warp Stabilizer luma pyramid with trajectory tracking

track: Warp Stabilizer: discarded plan, per-render JSON hash, duplicate luma pyramid ## Findings in the callers and in Warp analysis - `engine/warp.rs:249` computes a full default plan right after the analysis and throws it away; the first render recomputes it. - `effects/warp_stab.rs:190` hashes the entire analysis JSON on every render call (0.72 ms per call for 300 frames, about 43 ms for a 10 minute clip). `effects/camera_tracker.rs` has the same pattern. Hash once per analysis, cache the key. - `stabilize.rs` (+ `camtrack/features.rs`): Warp analysis builds two luma pyramids of the same frame and runs two serial analyses. Sharing one pyramid is estimated at about -15 % (es

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2350, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+70 −8 in 2 files
Review
pending
Shared
2026-10-28
Id
af17581e7b4ec5cee57e38ca

How it went

No other operator has reported using it yet.

Diff

raw · crates/track/src/camtrack/features.rs crates/track/src/stabilize.rs

diff --git a/crates/track/src/camtrack/features.rs b/crates/track/src/camtrack/features.rs--- a/crates/track/src/camtrack/features.rs+++ b/crates/track/src/camtrack/features.rs@@ -61,15 +61,24 @@ impl TrackAnalyzer {      /// Analyse the next frame.     pub fn push(&mut self, frame: &Frame) {-        let (max_side, want) = self.params();+        let (max_side, _) = self.params();         let factor = analysis_factor(frame.img.width, frame.img.height, max_side);-        self.factor = factor as f64;         let pyr = GrayPyramid::from_image(frame.img, frame.offset, factor, 4);+        let prev = self.prev.take();+        self.push_pyramid(prev.as_ref(), &pyr);+        self.prev = Some(pyr);+    }++    /// Track using caller-owned consecutive pyramids. The caller retains the full pyramid+    /// for the next frame; standalone camera analysis uses the same path through `push`.+    pub(crate) fn push_pyramid(&mut self, prev: Option<&GrayPyramid>, pyr: &GrayPyramid) {+        let (_, want) = self.params();+        self.factor = pyr.factor;         let k = self.frame;-        match &self.prev {+        match prev {             Some(prev) if prev.width() == pyr.width() && prev.height() == pyr.height() && prev.offset == pyr.offset => {                 let pts: Vec<[f64; 2]> = self.active.iter().map(|a| a.1).collect();-                let q = track(prev, &pyr, &pts, None, &LkOpts { radius: 6, iterations: 30, epsilon: 0.005, fb_max: 0.5, ..Default::default() });+                let q = track(prev, pyr, &pts, None, &LkOpts { radius: 6, iterations: 30, epsilon: 0.005, fb_max: 0.5, ..Default::default() });                 let mut next = Vec::with_capacity(self.active.len());                 for ((ti, _), q) in self.active.iter().zip(q) {                     if let Some(q) = q {@@ -101,14 +110,13 @@ impl TrackAnalyzer {                 !occ.get(cy * gw + cx).copied().unwrap_or(true)             };             let opts = CornerOpts { max_features: want - self.active.len().min(want), min_distance: md, quality: 0.001, window: 2, border: 8 };-            for p in good_features(&pyr, &opts, Some(&free)) {+            for p in good_features(pyr, &opts, Some(&free)) {                 let l = pyr.to_layer(p);                 let id = self.tracks.len() as u32;                 self.tracks.push(Track2D { id, start: k, pts: vec![[l[0] as f32, l[1] as f32]] });                 self.active.push((id as usize, p));             }         }-        self.prev = Some(pyr);         self.frame += 1;     } diff --git a/crates/track/src/stabilize.rs b/crates/track/src/stabilize.rs--- a/crates/track/src/stabilize.rs+++ b/crates/track/src/stabilize.rs@@ -156,10 +156,27 @@ impl Analyzer {      /// Analyse the next frame.     pub fn push(&mut self, frame: &Frame) {+        self.push_impl(frame, !self.opts.detailed);+    }++    fn push_impl(&mut self, frame: &Frame, share_pyramid: bool) {+        let share_pyramid = share_pyramid && !self.opts.detailed;         let (max_side, max_feat) = if self.opts.detailed { (960, 700) } else { (480, 350) };         let factor = analysis_factor(frame.img.width, frame.img.height, max_side);-        let pyr = GrayPyramid::from_image(frame.img, frame.offset, factor, 3);-        self.trajectories.push(frame);+        let mut pyr = GrayPyramid::from_image(frame.img, frame.offset, factor, if share_pyramid { 4 } else { 3 });+        if share_pyramid {+            self.trajectories.push_pyramid(self.prev.as_ref(), &pyr);+        } else {+            // Detailed analysis uses different downsampling factors for motion and trajectories.+            self.trajectories.push(frame);+        }+        // Motion tracking historically used only three extra levels. Temporarily remove+        // the trajectory tracker's fourth level without cloning any planes or gradients.+        let extra = if share_pyramid && pyr.levels.len() > 4 { pyr.levels.pop().zip(pyr.grads.pop()) } else { None };+        if share_pyramid && let Some(prev) = &mut self.prev {+            prev.levels.truncate(4);+            prev.grads.truncate(4);+        }         let mut fm = FrameMotion::default();         if let Some(prev) = &self.prev             && prev.width() == pyr.width()@@ -195,6 +212,10 @@ impl Analyzer {                 fm.points = round_pts(&inl, 48);             }         }+        if let Some((level, grad)) = extra {+            pyr.levels.push(level);+            pyr.grads.push(grad);+        }         self.frames.push(fm);         self.prev = Some(pyr);     }@@ -833,6 +854,39 @@ mod tests {         tot / cnt.max(1) as f64     } +    #[test]+    fn shared_pyramid_preserves_motion_and_trajectory_bits() {+        rayon::ThreadPoolBuilder::new().num_threads(1).build().unwrap().install(|| {+            for (size, detailed) in [([321.0, 181.0], false), ([997.0, 565.0], false), ([97.0, 63.0], false), ([1281.0, 721.0], true)] {+                let mut scene = Scene::new(12);+                scene.size = size;+                let mut shared = Analyzer::new(size, AnalyzeOpts { detailed });+                let mut separate = Analyzer::new(size, AnalyzeOpts { detailed });+                for k in 0..12 {+                    // Also cover partial downsample blocks, an offset/size change and the+                    // resulting reset of active features halfway through the clip.+                    if k == 6 {+                        scene.size = [size[0] + 7.0, size[1] + 5.0];+                    }+                    let img = scene.frame(k);+                    let frame = Frame { img: &img, offset: if k < 6 { [2.25, -3.5] } else { [-1.75, 4.5] } };+                    shared.push(&frame);+                    separate.push_impl(&frame, false);+                }+                let a = shared.finish(0.123456789012345, 1.0 / 24.0);+                let b = separate.finish(0.123456789012345, 1.0 / 24.0);+                assert!(!a.tracks.is_empty(), "exercise live trajectories: {size:?}, detailed={detailed}");+                assert!(a.frames.iter().any(|f| f.features > 0));+                assert_eq!(a.to_json(), b.to_json(), "{size:?}, detailed={detailed}");+                for (a, b) in a.frames.iter().zip(&b.frames) {+                    for (a, b) in a.t.iter().chain(&a.s).chain(&a.h).zip(b.t.iter().chain(&b.s).chain(&b.h)) {+                        assert_eq!(a.to_bits(), b.to_bits());+                    }+                }+            }+        });+    }+     #[test]     fn subspace_warp_beats_perspective_on_parallax() {         let n = 48;