mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 21:18:11 -06:00
#323: windows: noise reduction and mask edge refinement follow the whole frame's size
An adversarial review found local::denoise and Refine Edges scaled with the window's own size, so their strength changed with pan and zoom. Frame::output_long gives the whole output's long edge to both; tests on a noisy source cover them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e0539db3f5
commit
244088498d
@@ -110,6 +110,13 @@ impl Frame {
|
||||
self.crop.rect = Rect::from_center(r.center(), r.width() * t, r.height() * t);
|
||||
}
|
||||
|
||||
/// Long edge, in pixels, of the whole output this frame draws (its own size unless it is a
|
||||
/// window of a larger one): what size-relative effects (noise reduction, edge refinement)
|
||||
/// must scale with, so a window matches the same pixels of the whole render.
|
||||
pub fn output_long(&self, w: usize, h: usize) -> usize {
|
||||
self.view.map_or(w.max(h), |v| v.full_w.max(v.full_h) as usize)
|
||||
}
|
||||
|
||||
/// This frame narrowed to `win` of its `full_w × full_h` output: the same mapping, but the
|
||||
/// output is only that window (the crop rectangle shrinks to it; flips are as drawn).
|
||||
pub fn window(&self, full_w: usize, full_h: usize, win: crate::PixelWindow) -> Frame {
|
||||
|
||||
@@ -446,7 +446,7 @@ fn render_impl(src: Src<'_>, info: &SourceInfo, s: &DevelopSettings, req: &Rende
|
||||
// Without a cache the resampled buffer is ours: work on it in place.
|
||||
let mut img = if shared.is_some() { (*sampled).clone() } else { Arc::unwrap_or_clone(sampled.clone()) };
|
||||
lin_cpu(&mut img, info, &plan);
|
||||
local::denoise(&mut img, s, src_long, w.max(h));
|
||||
local::denoise(&mut img, s, src_long, frame.output_long(w, h));
|
||||
Arc::new(img)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn evaluate_one(m: &Mask, frame: &Frame, w: usize, h: usize, img: &Rgb32f, l
|
||||
// Refine Edges: the mask's edges follow the photo's (window up to ~4 % of the long edge,
|
||||
// the width of a soft brush edge or gradient)
|
||||
let k = (m.refine / 100.0).clamp(0.0, 1.0) as f32;
|
||||
let sigma = (0.04 * w.max(h) as f32 * k).max(1.0);
|
||||
let sigma = (0.04 * frame.output_long(w, h) as f32 * k).max(1.0);
|
||||
let refined = guided_cross(log_l, &alpha, sigma, 0.02);
|
||||
for (a, r) in alpha.data.iter_mut().zip(&refined.data) {
|
||||
*a += (r - *a) * k.sqrt();
|
||||
|
||||
@@ -150,3 +150,51 @@ fn windows_outside_or_degenerate_are_clamped() {
|
||||
assert!(r.width >= 1 && r.height >= 1 && r.width <= W && r.height <= H, "{win:?}: {}×{}", r.width, r.height);
|
||||
}
|
||||
}
|
||||
|
||||
/// A textured, noisy source: what noise reduction and edge refinement act on.
|
||||
fn noisy_scene() -> Rgb32f {
|
||||
let base = scene();
|
||||
Rgb32f::from_fn(base.width, base.height, |x, y| {
|
||||
let mut v = (x as u32).wrapping_mul(0x8da6_b343) ^ (y as u32).wrapping_mul(0xd816_3841);
|
||||
v ^= v >> 13;
|
||||
v = v.wrapping_mul(0x5bd1_e995);
|
||||
v ^= v >> 15;
|
||||
let n = 1.0 + 0.25 * ((v & 0xffff) as f32 / 32768.0 - 1.0);
|
||||
base.data[y * base.width + x].map(|c| c * n)
|
||||
})
|
||||
}
|
||||
|
||||
fn check_noisy(name: &str, s: &DevelopSettings, win: PixelWindow, max_ok: u8, mean_ok: f64) {
|
||||
let src = noisy_scene();
|
||||
let f = render(&src, &SourceInfo::default(), s, &RenderRequest::fit(W, H)).image;
|
||||
let w = render(&src, &SourceInfo::default(), s, &RenderRequest { window: Some(win), ..RenderRequest::fit(W, H) }).image;
|
||||
let (max, mean) = compare(&f, &w, win);
|
||||
assert!(max <= max_ok && mean <= mean_ok, "{name}: max {max} (≤ {max_ok}), mean {mean:.3} (≤ {mean_ok})");
|
||||
}
|
||||
|
||||
// Given noise reduction, its strength follows the whole frame's size, not the window's
|
||||
#[test]
|
||||
fn noise_reduction_does_not_depend_on_the_window() {
|
||||
for (id, v) in [("detail.nrLuminance", 80.0), ("detail.nrColor", 80.0)] {
|
||||
let mut s = DevelopSettings::default();
|
||||
controls::set(&mut s, id, v);
|
||||
check_noisy(id, &s, MID, 3, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Given a mask with Refine Edges, the refinement width follows the whole frame's size
|
||||
#[test]
|
||||
fn mask_edge_refinement_does_not_depend_on_the_window() {
|
||||
use lightcraft_develop::{LocalAdjustments, Mask, MaskComponent, MaskOp, MaskShape};
|
||||
use lightcraft_geom::Point;
|
||||
let shape = MaskShape::Linear { start: Point::new(0.45, 0.5), end: Point::new(0.55, 0.5) };
|
||||
let mut s = DevelopSettings::default();
|
||||
s.masks.push(Mask {
|
||||
id: 1,
|
||||
components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape }],
|
||||
adjust: LocalAdjustments { exposure: 1.5, ..Default::default() },
|
||||
refine: 100.0,
|
||||
..Default::default()
|
||||
});
|
||||
check_noisy("refine", &s, MID, 3, 0.2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user