mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 23:32:17 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user