diff --git a/crates/track/src/camtrack/solver.rs b/crates/track/src/camtrack/solver.rs index 8e212127b..99e8a9920 100644 --- a/crates/track/src/camtrack/solver.rs +++ b/crates/track/src/camtrack/solver.rs @@ -932,7 +932,10 @@ fn solve_core(tracks: &CameraTracks, s: &SolveSettings, cancel: Option<&AtomicBo } lap("keyframe adjustment"); // Every other frame: resect from the points (nearest registered camera first). - let mut todo: Vec = (0..d.n).filter(|k| !r.cams.contains_key(k)).collect(); + // A frame seeing fewer than six reconstructed points cannot be resected (`register`): it is + // skipped before the reconstruction is copied for it. + let mut todo: Vec = + (0..d.n).filter(|k| !r.cams.contains_key(k) && d.by_frame[*k].iter().filter(|(ti, _)| r.pts.contains_key(ti)).nth(5).is_some()).collect(); todo.sort_by_key(|k| r.cams.keys().map(|c| c.abs_diff(*k)).min().unwrap_or(usize::MAX)); let base = r.clone(); let resected: Vec<(usize, Option)> = todo diff --git a/crates/track/src/lib.rs b/crates/track/src/lib.rs index 8867bfcb8..83e5c2e75 100644 --- a/crates/track/src/lib.rs +++ b/crates/track/src/lib.rs @@ -359,6 +359,9 @@ impl<'a> Scorer<'a> { } } +/// The largest pre-tracking blur (pixels), as the options command allows. +const MAX_BLUR: f64 = 100.0; + /// The pre-processed pixels around one point: a pyramid of a crop. Plane coordinates are image /// pixels minus `origin`, divided by `2^level`. struct Crop { @@ -369,7 +372,10 @@ struct Crop { impl Crop { /// Crop image pixels `[c - ext, c + ext]` (image pixel coordinates), pre-processed. fn new(frame: &Frame, opts: &TrackOptions, c: [f64; 2], ext: [f64; 2], levels: usize) -> Crop { - let sigma = opts.blur.max(0.0) / 2.0; + // A stored blur or search area past the frame adds only repeated edge pixels: both are + // held to what the frame can use, so a few project numbers cannot size a huge crop. + let sigma = opts.blur.clamp(0.0, MAX_BLUR) / 2.0; + let ext = [ext[0].clamp(0.0, frame.img.width as f64), ext[1].clamp(0.0, frame.img.height as f64)]; let margin = (sigma * 3.0).ceil() + 3.0 + if opts.enhance { 6.0 } else { 0.0 } + (1 << levels) as f64; let x0 = (c[0] - ext[0] - margin).floor(); let y0 = (c[1] - ext[1] - margin).floor(); diff --git a/crates/track/src/roto.rs b/crates/track/src/roto.rs index 3bab65dff..4ce4bfa95 100644 --- a/crates/track/src/roto.rs +++ b/crates/track/src/roto.rs @@ -80,6 +80,10 @@ impl StrokeKind { } } +/// The most frames a propagation chain reaches on either side of its base frame (about two +/// hours at 24 fps). +const MAX_CHAIN: i64 = 200_000; + /// One stroke: a polyline swept by a disc of `radius` layer pixels, on layer frame `frame`. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -151,8 +155,16 @@ impl RotoData { pub fn strokes_of(&self, frame: i64, object: u32) -> Vec<&Stroke> { self.strokes.iter().filter(|s| s.frame == frame && (s.object == object || matches!(s.kind, StrokeKind::Refine | StrokeKind::RefineErase))).collect() } + /// The stored span cut to at most MAX_CHAIN frames either side of the base: a project's + /// numbers must not make one render build billions of keys. Frames past it have no key. + fn bounded_span(&self, base: i64) -> [i64; 2] { + [self.span[0].max(base.saturating_sub(MAX_CHAIN)), self.span[1].min(base.saturating_add(MAX_CHAIN))] + } pub fn in_span(&self, f: i64) -> bool { - self.base.is_some() && f >= self.span[0] && f <= self.span[1] + self.base.is_some_and(|b| { + let span = self.bounded_span(b); + f >= span[0] && f <= span[1] + }) } /// Add a stroke: the first segmentation stroke sets the base frame and a span of /// `default_span` frames each side (clamped to `limits`); strokes outside the span extend it. @@ -181,10 +193,15 @@ impl RotoData { pub fn chain_keys(&self, seed: u64) -> BTreeMap { let mut out = BTreeMap::new(); let Some(base) = self.base else { return out }; + // The strokes by frame, grouped once (each frame looked through every stroke before). + let mut by_frame: BTreeMap> = BTreeMap::new(); + for s in &self.strokes { + by_frame.entry(s.frame).or_default().push(s); + } let key = |prev: u64, f: i64| { let mut h = prev ^ 0x9e37_79b9_7f4a_7c15; fnv(&mut h, &f.to_le_bytes()); - for s in self.strokes.iter().filter(|s| s.frame == f) { + for s in by_frame.get(&f).into_iter().flatten() { fnv(&mut h, serde_json::to_string(s).unwrap_or_default().as_bytes()); } h @@ -192,12 +209,16 @@ impl RotoData { let k0 = key(seed, base); out.insert(base, k0); let mut k = k0; - for f in base + 1..=self.span[1] { - k = key(k, f); - out.insert(f, k); + let span = self.bounded_span(base); + // `base + 1` only when there is a frame after the base (it overflows at i64::MAX). + if base < span[1] { + for f in base + 1..=span[1] { + k = key(k, f); + out.insert(f, k); + } } k = k0; - for f in (self.span[0]..base).rev() { + for f in (span[0]..base).rev() { k = key(k, f); out.insert(f, k); } diff --git a/crates/track/src/roto/tests.rs b/crates/track/src/roto/tests.rs index e50dbbce8..4b2a040b4 100644 --- a/crates/track/src/roto/tests.rs +++ b/crates/track/src/roto/tests.rs @@ -723,6 +723,29 @@ fn the_union_of_objects_is_their_largest_extent() { assert_eq!(FrameSeg::union(vec![seg(&[1]), odd]).matte, vec![0, 255, 0, 0]); } +#[test] +fn a_stored_span_reaches_a_bounded_number_of_frames() { + // A project can store any span: the chain stops MAX_CHAIN frames either side of the base. + let d = RotoData { base: Some(0), span: [i64::MIN, i64::MAX], strokes: vec![] }; + let k = d.chain_keys(1); + assert_eq!(k.len() as i64, 2 * super::MAX_CHAIN + 1); + assert_eq!(k.keys().next(), Some(&-super::MAX_CHAIN)); +} + +#[test] +fn in_span_matches_the_frames_that_have_keys_and_extreme_bases_do_not_overflow() { + let d = RotoData { base: Some(0), span: [i64::MIN, i64::MAX], strokes: vec![] }; + assert!(d.in_span(super::MAX_CHAIN) && !d.in_span(super::MAX_CHAIN + 1)); + assert!(d.in_span(-super::MAX_CHAIN) && !d.in_span(-super::MAX_CHAIN - 1)); + assert_eq!(d.source_of(super::MAX_CHAIN + 1), None); + assert!(d.chain_keys(1).keys().all(|f| d.in_span(*f))); + // A base at the end of i64: no forward frames, and no `base + 1` overflow. + let d = RotoData { base: Some(i64::MAX), span: [i64::MIN, i64::MAX], strokes: vec![] }; + let k = d.chain_keys(1); + assert_eq!(k.len() as i64, super::MAX_CHAIN + 1); + assert!(k.contains_key(&i64::MAX)); +} + /// A scene for the band re-cut: `rgb` of a `w`×`h` frame where the foreground is `inside`, with /// a background that turns red left of `x = 30` (far from most shapes), and a soft prior that is /// `inside` shifted by (3, 2) px with an edge `soft_edge` px wide.