From 4780953b396443e5cfd47005cfc726e3fa27ce13 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 03:26:05 +0000 Subject: [PATCH] address review: smoothing radius covers every supported Smoothness (1000%, Smooth More, 240 fps); a frames x radius work cap bounds planning --- crates/track/src/stabilize.rs | 41 +++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/track/src/stabilize.rs b/crates/track/src/stabilize.rs index b992e77d6..f3d917b8b 100644 --- a/crates/track/src/stabilize.rs +++ b/crates/track/src/stabilize.rs @@ -440,7 +440,11 @@ fn remove_scale(b: &Homography, c: [f64; 2]) -> Homography { Homography::scale_about(1.0 / s, p0).then_after(b) } -const MAX_SMOOTH_RADIUS: usize = 1024; +/// Widest window: 3 sigma of the largest Smoothness (1000%) with Smooth More at its maximum, at 240 fps. +const MAX_SMOOTH_RADIUS: usize = 8640; +/// Most window taps (frames x radius) one smoothing pass does; past it the window narrows so planning +/// stays bounded (an hour at 60 fps with the widest setting is about 4.7e8). +const MAX_SMOOTH_WORK: usize = 1 << 30; /// Corrective transforms (source → stabilized, before framing) for every frame. pub fn corrections(a: &WarpAnalysis, s: &StabSettings) -> Vec { @@ -472,9 +476,8 @@ pub fn corrections(a: &WarpAnalysis, s: &StabSettings) -> Vec { } StabResult::SmoothMotion => { let sigma = s.sigma(); - // The window grows with the stored fps; capped (3 sigma of the widest setting at 240 fps - // is under 900 frames) so planning stays linear in the frame count. - let rad = ((3.0 * sigma).ceil() as usize).min(n.saturating_sub(1)).min(MAX_SMOOTH_RADIUS); + // 3 sigma covers every supported setting; the work cap only bites on extreme frame counts. + let rad = (((3.0 * sigma).ceil() as usize).min(MAX_SMOOTH_RADIUS)).min(n.saturating_sub(1)).min(MAX_SMOOTH_WORK / n); for t in 0..n { if sigma < 0.3 || rad == 0 { out.push(Homography::IDENTITY); @@ -720,16 +723,16 @@ mod tests { #[test] fn a_huge_stored_fps_does_not_widen_the_smoothing_window_past_the_cap() { - let (a, _) = shaky(3000); + let (a, _) = shaky(9500); let s = StabSettings { fps: 1e9, preserve_scale: false, ..Default::default() }; - // Window 1024 each side: one frame is smoothed over at most 2049 frames, not all 3000. + // Window 8640 each side: one frame is smoothed over at most 17281 frames, not all 9500. let c = corrections(&a, &s); - assert_eq!(c.len(), 3000); + assert_eq!(c.len(), 9500); assert!(c.iter().all(|h| h.0.iter().flatten().all(|v| v.is_finite()))); - // A jump at frame 2900 is outside frame 0's window (frames 0..=1024): its correction is + // A jump at frame 9400 is outside frame 0's window (frames 0..=8640): its correction is // unchanged. Smoothing over the whole clip would move it. - let (mut jumped, _) = shaky(3000); - let f = &mut jumped.frames[2900]; + let (mut jumped, _) = shaky(9500); + let f = &mut jumped.frames[9400]; f.t[0] += 80.0; f.s[2] += 80.0; f.h[2] += 80.0; @@ -738,11 +741,27 @@ mod tests { assert!((x - y).abs() < 1e-9, "{x} vs {y}"); } assert!( - c[2999].0.iter().flatten().zip(c2[2999].0.iter().flatten()).any(|(x, y)| (x - y).abs() > 1e-6), + c[9499].0.iter().flatten().zip(c2[9499].0.iter().flatten()).any(|(x, y)| (x - y).abs() > 1e-6), "the jump does move frames whose window holds it" ); } + #[test] + fn the_widest_supported_smoothness_keeps_its_full_window() { + // 1000% Smoothness, Smooth More at its maximum, 60 fps: sigma 720 frames, radius 2160. + let s = StabSettings { smoothness: 1000.0, crop_less_smooth_more: 100.0, fps: 60.0, preserve_scale: false, ..Default::default() }; + let (a, _) = shaky(3000); + let c = corrections(&a, &s); + let (mut jumped, _) = shaky(3000); + let f = &mut jumped.frames[2000]; + f.t[0] += 80.0; + f.s[2] += 80.0; + f.h[2] += 80.0; + let c2 = corrections(&jumped, &s); + // Frame 0 sees a jump 2000 frames away (the old 1024-frame cap could not). + assert!(c[0].0.iter().flatten().zip(c2[0].0.iter().flatten()).any(|(x, y)| (x - y).abs() > 1e-9)); + } + #[test] fn smoothing_removes_jitter_and_no_motion_locks() { let (a, path) = shaky(90);