mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Point tracker: refine in a 3x3 neighbourhood when the motion was small
The fine stage searched a 5x5 neighbourhood per pyramid level and seed. When the best coarse match is reliable (NCC score >= 0.8) and lies within one top-level pixel of the motion prediction, a 3x3 neighbourhood is enough; otherwise the 5x5 search stays. Fixes #2349 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
9e68fc47f7
commit
4b31e21a1d
+12
-3
@@ -399,6 +399,12 @@ impl Crop {
|
||||
}
|
||||
}
|
||||
|
||||
/// Largest distance (in top-level pixels, per axis) between the best coarse match and the
|
||||
/// motion prediction for which the fine stage searches only ±1 pixel per level.
|
||||
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;
|
||||
|
||||
/// Per-point tracking state.
|
||||
#[derive(Clone, Debug)]
|
||||
struct PointState {
|
||||
@@ -499,7 +505,10 @@ impl PointState {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fine: refine each seed down the pyramid in a 5 × 5 neighbourhood.
|
||||
// Fine: refine each seed down the pyramid. A good motion prediction plus a reliable coarse
|
||||
// match only needs a 3 × 3 neighbourhood per level; otherwise keep 5 × 5.
|
||||
let near = cands[0].0 >= RELIABLE_SCORE && seeds.first().is_some_and(|c| (c[0] - cp[0]).abs() <= SMALL_MOTION && (c[1] - cp[1]).abs() <= SMALL_MOTION);
|
||||
let rr: i32 = if near { 1 } else { 2 };
|
||||
let mut best = (f64::MIN, sc);
|
||||
for seed in seeds {
|
||||
let mut c = seed;
|
||||
@@ -510,8 +519,8 @@ impl PointState {
|
||||
// Start from the projected match; neighbours must beat it (no drift on ties).
|
||||
let scorer = Scorer::new(pl, tp, c, shape);
|
||||
let mut lb = (scorer.at(0, 0), c);
|
||||
for j in -2..=2 {
|
||||
for i in -2..=2 {
|
||||
for j in -rr..=rr {
|
||||
for i in -rr..=rr {
|
||||
if i == 0 && j == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,27 @@ fn recovers_translation_path_subpixel() {
|
||||
assert!(worst < 0.25, "worst error {worst} px");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovers_after_sudden_jumps_the_prediction_misses() {
|
||||
// Alternating large steps defeat the velocity prediction, so the fine stage must keep its
|
||||
// wider neighbourhood; smooth frames in between use the narrow one.
|
||||
let path = |f: usize| {
|
||||
let t = f as f64;
|
||||
let jump = [0.0, 7.0, -5.0, 9.0, -8.0, 6.0, -7.0, 8.0];
|
||||
let j = jump[f % jump.len()];
|
||||
Pose { c: [120.0 + 1.5 * t + j, 100.0 + 1.1 * t - 0.6 * j], rot: 0.0, scale: 1.0 }
|
||||
};
|
||||
let f0 = frame(360, 260, path(0), 40.0, 0.02, 0, None);
|
||||
let mut tr = Tracker::new(TrackOptions::default(), &[spec(path(0).c, 31.0, 91.0)], &Frame::new(&f0));
|
||||
for f in 1..24 {
|
||||
let img = frame(360, 260, path(f), 40.0, 0.02, f as u32, None);
|
||||
let r = tr.step(&Frame::new(&img))[0];
|
||||
let e = dist(r.center, path(f).c);
|
||||
assert!(e < 0.3, "frame {f}: error {e} px");
|
||||
assert_eq!(r.status, Status::Tracked);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_offset_and_channels() {
|
||||
let pose = |f: usize| Pose { c: [120.0 + 3.3 * f as f64, 100.0 - 1.7 * f as f64], rot: 0.0, scale: 1.0 };
|
||||
|
||||
Reference in New Issue
Block a user