Tracking: bounded crop size, Roto chain span and resection copies (#2989)

* Tracking: bounded crop size, Roto chain span and resection copies

A stored pre-tracking blur or search area sized the padded crop with no
limit; both are held to what the frame can use (blur at most 100, as the
options command allows). A Roto Brush span from the project made one render
build a key for every frame of it, rescanning all strokes per frame; the
chain reaches at most 200,000 frames either side of the base and strokes
are grouped by frame once. The camera solver copied the whole
reconstruction for every unsolved frame, even frames seeing too few points
to resect; those are skipped first.

* address review: in_span uses the MAX_CHAIN-bounded span, forward key range cannot overflow
This commit is contained in:
Dylan M. Taylor
2026-11-11 14:39:47 -05:00
committed by GitHub
parent 094d59528b
commit 0e248459d2
4 changed files with 61 additions and 8 deletions
+4 -1
View File
@@ -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<usize> = (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<usize> =
(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<BaCam>)> = todo
+7 -1
View File
@@ -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();
+27 -6
View File
@@ -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<i64, u64> {
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<i64, Vec<&Stroke>> = 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);
}
+23
View File
@@ -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.