mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 23:32:17 +00:00
Tracking: face skin search visits only window pixels; stabilizer smoothing window capped (#3022)
* track: face skin search visits only window ring pixels; cap stabilizer smoothing window * address review: stabilizer window test asserts the cap with a far jump * address review: smoothing radius covers every supported Smoothness (1000%, Smooth More, 240 fps); a frames x radius work cap bounds planning
This commit is contained in:
@@ -558,16 +558,19 @@ fn fit_from_face(face: &Face, topo: &Topology, offset: [f64; 2]) -> Option<FaceF
|
||||
fn nearest_skin(skin: &[bool], size: (usize, usize), center: (usize, usize), radius: i64) -> Option<(usize, usize)> {
|
||||
let (w, h) = size;
|
||||
let (ci, cj) = center;
|
||||
// The radius comes from mask geometry: past the window's larger side every ring is outside it.
|
||||
let radius = radius.min(w.max(h) as i64 + 1);
|
||||
for r in 0..radius {
|
||||
for dj in -r..=r {
|
||||
for di in -r..=r {
|
||||
if di.abs() != r && dj.abs() != r {
|
||||
continue;
|
||||
}
|
||||
// Only the ring's own pixels: the full rows at the top and bottom, the two ends between.
|
||||
let step = if dj.abs() == r { 1 } else { (2 * r).max(1) };
|
||||
let mut di = -r;
|
||||
while di <= r {
|
||||
let (i, j) = (ci as i64 + di, cj as i64 + dj);
|
||||
if i >= 0 && j >= 0 && (i as usize) < w && (j as usize) < h && skin[j as usize * w + i as usize] {
|
||||
return Some((i as usize, j as usize));
|
||||
}
|
||||
di += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1187,6 +1190,16 @@ pub fn measure(l: &[[f64; 2]; N], reference_height: f64) -> [f64; 14] {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_skin_search_stays_within_the_window_for_a_huge_radius() {
|
||||
let mut skin = vec![false; 64 * 64];
|
||||
skin[60 * 64 + 5] = true;
|
||||
// A radius from hostile mask geometry costs no more than one that covers the window.
|
||||
assert_eq!(nearest_skin(&skin, (64, 64), (32, 32), i64::MAX), Some((5, 60)));
|
||||
assert_eq!(nearest_skin(&skin, (64, 64), (5, 60), 3), Some((5, 60)));
|
||||
assert_eq!(nearest_skin(&skin, (64, 64), (0, 0), 10), None);
|
||||
}
|
||||
|
||||
/// Pearson correlation.
|
||||
fn corr(a: &[f64], b: &[f64]) -> f64 {
|
||||
let n = a.len() as f64;
|
||||
|
||||
@@ -440,6 +440,12 @@ fn remove_scale(b: &Homography, c: [f64; 2]) -> Homography {
|
||||
Homography::scale_about(1.0 / s, p0).then_after(b)
|
||||
}
|
||||
|
||||
/// 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<Homography> {
|
||||
let n = a.frames.len();
|
||||
@@ -470,7 +476,8 @@ pub fn corrections(a: &WarpAnalysis, s: &StabSettings) -> Vec<Homography> {
|
||||
}
|
||||
StabResult::SmoothMotion => {
|
||||
let sigma = s.sigma();
|
||||
let rad = ((3.0 * sigma).ceil() as usize).min(n.saturating_sub(1));
|
||||
// 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);
|
||||
@@ -714,6 +721,47 @@ mod tests {
|
||||
s / (p.len() - 2) as f64
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_stored_fps_does_not_widen_the_smoothing_window_past_the_cap() {
|
||||
let (a, _) = shaky(9500);
|
||||
let s = StabSettings { fps: 1e9, preserve_scale: false, ..Default::default() };
|
||||
// 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(), 9500);
|
||||
assert!(c.iter().all(|h| h.0.iter().flatten().all(|v| v.is_finite())));
|
||||
// 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(9500);
|
||||
let f = &mut jumped.frames[9400];
|
||||
f.t[0] += 80.0;
|
||||
f.s[2] += 80.0;
|
||||
f.h[2] += 80.0;
|
||||
let c2 = corrections(&jumped, &s);
|
||||
for (x, y) in c[0].0.iter().flatten().zip(c2[0].0.iter().flatten()) {
|
||||
assert!((x - y).abs() < 1e-9, "{x} vs {y}");
|
||||
}
|
||||
assert!(
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user