mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:33:41 +00:00
fix(analysis): bound tracker regions, rescaled source frames and analysis schedules
This commit is contained in:
@@ -316,7 +316,7 @@ impl Session {
|
||||
if l.switches.adjustment {
|
||||
return Err("3D Camera Tracker needs footage, not an adjustment layer".into());
|
||||
}
|
||||
let times = analysis_times(c, l);
|
||||
let times = analysis_times(c, l, crate::warp::MAX_ANALYSIS_FRAMES);
|
||||
if times.len() < 2 {
|
||||
return Err("the layer must be at least two frames long".into());
|
||||
}
|
||||
@@ -518,8 +518,8 @@ pub fn placed(project: &Project, cid: ItemId, lid: LayerId, uid: Uid) -> Option<
|
||||
let k0 = solve.first_solved()?;
|
||||
let comp_size = [comp.width as f64, comp.height as f64];
|
||||
let world = solve.world(comp_size, solve.frames[k0].focal * scale);
|
||||
let mut times = analysis_times(comp, layer);
|
||||
times.truncate(solve.frames.len());
|
||||
// Only the frames the solve has: not the whole layer's schedule, then cut.
|
||||
let times = analysis_times(comp, layer, solve.frames.len());
|
||||
Some(Placed { solve, world, scale, times, comp_size })
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,9 @@ pub(crate) struct Work {
|
||||
pub times: Vec<Tick>,
|
||||
}
|
||||
|
||||
/// Most pixels a source frame is brought to for analysis (2 GiB as float RGBA).
|
||||
const MAX_SOURCE_PIXELS: u64 = 1 << 27;
|
||||
|
||||
/// The layer's source frame at comp time `t` at 100 % (layer pixels + offset = image pixels).
|
||||
/// Footage at its native size is used straight from the footage source (no copy unless its
|
||||
/// Cineon Settings convert it).
|
||||
@@ -223,6 +226,11 @@ pub(crate) fn source_frame(
|
||||
// Footage decoded at another size: bring it to layer pixels.
|
||||
let w = (buf.img.width as f64 / buf.scale).round().max(1.0) as u32;
|
||||
let h = (buf.img.height as f64 / buf.scale).round().max(1.0) as u32;
|
||||
// The scale comes from the footage's saved width alone: a decoded frame of another
|
||||
// aspect would give a height far beyond anything the layer shows.
|
||||
if u64::from(w) * u64::from(h) > MAX_SOURCE_PIXELS {
|
||||
return None;
|
||||
}
|
||||
let off = [buf.offset[0] / buf.scale, buf.offset[1] / buf.scale];
|
||||
return Some((Arc::new(effectcraft_raster::resample(&buf.img, w, h)), off));
|
||||
}
|
||||
|
||||
@@ -255,14 +255,19 @@ async fn run_work(w: WarpWork, shared: &WarpShared) {
|
||||
s.finished = true;
|
||||
}
|
||||
|
||||
/// Frames to analyse: every comp frame from the layer's In to its Out point (inside the comp).
|
||||
pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer) -> Vec<Tick> {
|
||||
/// Most frames one analysis schedules (4.8 hours at 60 fps): the schedule is made from saved
|
||||
/// timing, before any frame is analysed.
|
||||
pub(crate) const MAX_ANALYSIS_FRAMES: usize = 1 << 20;
|
||||
|
||||
/// Frames to analyse: every comp frame from the layer's In to its Out point (inside the comp),
|
||||
/// at most `limit` of them.
|
||||
pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer, limit: usize) -> Vec<Tick> {
|
||||
let fd = comp.frame_duration();
|
||||
let lo = comp.frame_rate.snap_nearest(layer.in_point.max(Tick::ZERO));
|
||||
let hi = layer.out_point.min(comp.duration);
|
||||
let mut t = lo;
|
||||
let mut v = vec![];
|
||||
while t < hi {
|
||||
while t < hi && v.len() < limit.min(MAX_ANALYSIS_FRAMES) {
|
||||
v.push(t);
|
||||
t += fd;
|
||||
}
|
||||
@@ -319,7 +324,7 @@ impl Session {
|
||||
if l.switches.adjustment {
|
||||
return Err("Warp Stabilizer needs footage, not an adjustment layer".into());
|
||||
}
|
||||
let times = analysis_times(c, l);
|
||||
let times = analysis_times(c, l, MAX_ANALYSIS_FRAMES);
|
||||
if times.len() < 2 {
|
||||
return Err("the layer must be at least two frames long".into());
|
||||
}
|
||||
@@ -429,3 +434,21 @@ impl Session {
|
||||
changed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod schedule_tests {
|
||||
use effectcraft_project::{Comp, LayerSource, Project, build};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use super::{MAX_ANALYSIS_FRAMES, analysis_times};
|
||||
|
||||
#[test]
|
||||
fn a_long_layer_schedules_at_most_the_limit() {
|
||||
let mut p = Project::default();
|
||||
// A million seconds at 1000 fps: 10⁹ frames.
|
||||
let c = Comp::new(4, 4, FrameRate::new(1000, 1), Tick::from_seconds_f64(1e6));
|
||||
let l = build::layer(&mut p, &c, "N", LayerSource::Null, (4, 4), None);
|
||||
assert_eq!(analysis_times(&c, &l, usize::MAX).len(), MAX_ANALYSIS_FRAMES);
|
||||
assert_eq!(analysis_times(&c, &l, 10).len(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@ pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer, di
|
||||
Direction::Forward | Direction::FrameForward => {
|
||||
let mut t = start.unwrap_or(now).clamp(lo, hi);
|
||||
let stop = if dir == Direction::FrameForward { t + fd } else { end.unwrap_or(hi).min(hi) };
|
||||
while t <= stop && t <= hi {
|
||||
while t <= stop && t <= hi && times.len() < crate::warp::MAX_ANALYSIS_FRAMES {
|
||||
times.push(t);
|
||||
t += fd;
|
||||
}
|
||||
@@ -487,7 +487,7 @@ pub(crate) fn analysis_times(comp: &effectcraft_project::Comp, layer: &Layer, di
|
||||
Direction::Backward | Direction::FrameBackward => {
|
||||
let mut t = end.unwrap_or(now).clamp(lo, hi);
|
||||
let stop = if dir == Direction::FrameBackward { t - fd } else { start.unwrap_or(lo).max(lo) };
|
||||
while t >= stop && t >= lo {
|
||||
while t >= stop && t >= lo && times.len() < crate::warp::MAX_ANALYSIS_FRAMES {
|
||||
times.push(t);
|
||||
t -= fd;
|
||||
}
|
||||
|
||||
@@ -405,6 +405,9 @@ const SMALL_MOTION: f64 = 1.0;
|
||||
/// Lowest coarse NCC score of the best match for which the fine stage may search only ±1.
|
||||
const RELIABLE_SCORE: f64 = 0.8;
|
||||
|
||||
/// Largest feature or search region side (layer pixels) a track point works with.
|
||||
pub const MAX_REGION_SIDE: f64 = 4096.0;
|
||||
|
||||
/// Per-point tracking state.
|
||||
#[derive(Clone, Debug)]
|
||||
struct PointState {
|
||||
@@ -445,6 +448,15 @@ impl PointState {
|
||||
}
|
||||
|
||||
fn new(spec: PointSpec, frame: &Frame, opts: &TrackOptions) -> PointState {
|
||||
// Regions come from saved projects and commands: each frame's crop and correlation
|
||||
// search grow with them, so they are kept to a side a real feature never exceeds.
|
||||
let side = |v: f64| if v.is_finite() { v.clamp(0.0, MAX_REGION_SIDE) } else { 0.0 };
|
||||
let spec = PointSpec {
|
||||
feature_size: spec.feature_size.map(side),
|
||||
search_size: spec.search_size.map(side),
|
||||
search_offset: spec.search_offset.map(|v| if v.is_finite() { v.clamp(-MAX_REGION_SIDE, MAX_REGION_SIDE) } else { 0.0 }),
|
||||
..spec
|
||||
};
|
||||
let c = [spec.center[0] + frame.offset[0], spec.center[1] + frame.offset[1]];
|
||||
let mut st = PointState { spec, center: c, shape: [1.0, 0.0], velocity: [0.0; 2], templates: vec![], levels: 0 };
|
||||
let range = st.range();
|
||||
|
||||
@@ -328,3 +328,14 @@ fn featureless_regions_stay_put() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_regions_larger_than_any_feature_are_bounded() {
|
||||
let img = Image::new(8, 8);
|
||||
// A search region of 10⁹ pixels a side (and a non-finite one) on an 8×8 frame: the crop
|
||||
// would hold 10¹⁸ samples.
|
||||
for size in [1e9, f64::INFINITY, f64::NAN] {
|
||||
let mut tr = Tracker::new(TrackOptions::default(), &[spec([4.0, 4.0], size, size)], &Frame::new(&img));
|
||||
let _ = tr.step(&Frame::new(&img));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user