mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
effects: fix the grain-size drift from the crop halo
The crop kept 3 px of halo, which makes the blur exact inside a box, but the lag-1 correlation also reads the right neighbour's high-pass residual — one column outside the box, still polluted by the crop edge when another box covers it (adjacent boxes are the norm with a step of size/2). The size estimate drifted by ~5e-5 in the parity test; a 4 px halo removes it. The parity test now holds std to 1e-6 and size to 1e-7 (was 5 % for size), and the boxed statistics still cost 1.8 / 3.1 ms at 1080p / 4K.
This commit is contained in:
@@ -87,9 +87,10 @@ pub(crate) fn grain_stats_boxes(img: &Image, boxes: &[(usize, usize, usize)]) ->
|
||||
if w < 3 || h < 3 {
|
||||
return GrainStats::default();
|
||||
}
|
||||
// The Gaussian's three box passes at sigma 1.5 have radius 1 each, so a 3 px halo is enough
|
||||
// for the high-pass residual of every pixel inside a box.
|
||||
const HALO: usize = 3;
|
||||
// The Gaussian's three box passes at sigma 1.5 have radius 1 each, so 3 px of halo make the
|
||||
// residual exact inside a box; one more column covers the right neighbour used by the lag-1
|
||||
// correlation.
|
||||
const HALO: usize = 4;
|
||||
let mut mask = vec![false; w * h];
|
||||
for &(x, y, s) in boxes {
|
||||
for yy in y..y.saturating_add(s).min(h) {
|
||||
@@ -418,9 +419,9 @@ mod tests {
|
||||
let masked = grain_stats_masked(&img, Some(&mask));
|
||||
let boxed = grain_stats_boxes(&img, &boxes);
|
||||
for k in 0..3 {
|
||||
assert!((masked.std[k] - boxed.std[k]).abs() < 1e-5, "{k}: {masked:?} vs {boxed:?}");
|
||||
assert!((masked.std[k] - boxed.std[k]).abs() < 1e-6, "{k}: {masked:?} vs {boxed:?}");
|
||||
}
|
||||
assert!((masked.size - boxed.size).abs() <= 0.05 * masked.size.max(1.0), "size: {masked:?} vs {boxed:?}");
|
||||
assert!((masked.size - boxed.size).abs() <= 1e-7, "size: {masked:?} vs {boxed:?}");
|
||||
// A denser grid where every box overlaps its neighbours (mask dedup across boxes).
|
||||
let img2 = noisy(96, 64, 0.15, 21);
|
||||
let mut boxes2 = Vec::new();
|
||||
@@ -441,9 +442,9 @@ mod tests {
|
||||
let masked2 = grain_stats_masked(&img2, Some(&mask2));
|
||||
let boxed2 = grain_stats_boxes(&img2, &boxes2);
|
||||
for k in 0..3 {
|
||||
assert!((masked2.std[k] - boxed2.std[k]).abs() < 1e-5, "{k}: {masked2:?} vs {boxed2:?}");
|
||||
assert!((masked2.std[k] - boxed2.std[k]).abs() < 1e-6, "{k}: {masked2:?} vs {boxed2:?}");
|
||||
}
|
||||
assert!((masked2.size - boxed2.size).abs() <= 0.05 * masked2.size.max(1.0), "size: {masked2:?} vs {boxed2:?}");
|
||||
assert!((masked2.size - boxed2.size).abs() <= 1e-7, "size: {masked2:?} vs {boxed2:?}");
|
||||
// No boxes measures the whole image, like no mask.
|
||||
assert_eq!(grain_stats_boxes(&img, &[]), grain_stats(&img));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user