Sharpen the capture with a separable unsharp mask
Capture sharpening as a two-pass unsharp mask in the detail stage: blur along x, then along y, each pass applying a one-dimensional high-pass to luminance so the composite preserves a flat field exactly and matches the textbook kernel on any locally one-dimensional edge. The radius is stated in source pixels and converted once per render, so a radius tuned on a fit view is the radius the exported file gets. Below one render pixel the operation declines to draw rather than showing sharpening the file will not contain, and emits a single pass-through that still carries the output transform. The develop session now renders through render_detailed, which is what lets an active neighbourhood operation reach the screen at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-4
@@ -1055,11 +1055,20 @@ impl DevelopSession {
|
||||
/// The mask array is rasterised in source space at proxy size and sampled
|
||||
/// through the framing map, so one array is correct at every output size:
|
||||
/// a 256px thumbnail and a 24 MP export bind the same texture.
|
||||
///
|
||||
/// `space` is the output space `shader` was composed for, and it has to be
|
||||
/// passed rather than assumed because the **detail stage** is composed
|
||||
/// here too and the two halves must agree. When an edit has an active
|
||||
/// neighbourhood operation the fused pass stops at unclipped linear
|
||||
/// working values and the last detail pass performs the output transform;
|
||||
/// composing the fused half for Display P3 and the detail half for sRGB
|
||||
/// would encode the export in the wrong space, with nothing to notice it.
|
||||
fn render_with_masks(
|
||||
&mut self,
|
||||
shader: &dr_pipeline::operation::ComposedShader,
|
||||
w: u32,
|
||||
h: u32,
|
||||
space: dr_types::ColourSpace,
|
||||
) -> Result<(), String> {
|
||||
let ctx = self.ctx.clone();
|
||||
self.ensure_subject_fields(&ctx);
|
||||
@@ -1069,8 +1078,29 @@ impl DevelopSession {
|
||||
.then(|| self.masks.as_ref().and_then(|p| p.array()))
|
||||
.flatten();
|
||||
|
||||
// The neighbourhood stage, composed at the size actually being drawn.
|
||||
//
|
||||
// It has to be composed *per render* rather than cached with the edit,
|
||||
// because a kernel is the one thing in this pipeline that is not
|
||||
// scale-free: a sharpening radius is stated in source pixels and the
|
||||
// develop view renders at whatever the viewport needs (FR-DSP-1), so
|
||||
// the conversion is different for the canvas, the thumbnail and the
|
||||
// export. `render_scale` works the ratio out from the framing, which
|
||||
// is also what makes zooming to 1:1 restore an exact preview with no
|
||||
// second render path to maintain.
|
||||
//
|
||||
// Empty for every edit with no active neighbourhood operation — which
|
||||
// is almost all of them — and `render_detailed` then falls straight
|
||||
// through to the single masked dispatch this used to call.
|
||||
let scale = self.graph.render_scale(self.demosaiced.size(), (w, h));
|
||||
let detail = self.graph.compose_detail_for(scale, space);
|
||||
let colour_key = self
|
||||
.graph
|
||||
.invalidation()
|
||||
.through(dr_pipeline::Affects::Colour);
|
||||
|
||||
self.adjust
|
||||
.render_masked(&self.demosaiced, shader, w, h, masks)
|
||||
.render_detailed(&self.demosaiced, shader, w, h, masks, &detail, colour_key)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -1769,7 +1799,7 @@ impl DevelopSession {
|
||||
|
||||
// Rasterise the masks first: the shader addresses array slices by
|
||||
// index, so the array has to describe *this* stack before it is bound.
|
||||
self.render_with_masks(&shader, w, h)?;
|
||||
self.render_with_masks(&shader, w, h, dr_types::ColourSpace::Srgb)?;
|
||||
let texture = self.adjust.output().ok_or("nothing was rendered")?;
|
||||
|
||||
// The import is fallible on format and usage only, and both are fixed
|
||||
@@ -1893,7 +1923,7 @@ impl DevelopSession {
|
||||
let (w, h) = self.graph.output_size(sw, sh);
|
||||
|
||||
let shader = self.graph.compose_for(space);
|
||||
self.render_with_masks(&shader, w, h)?;
|
||||
self.render_with_masks(&shader, w, h, space)?;
|
||||
|
||||
let (pixels, rw, rh) = self.adjust.export_pixels().map_err(|e| e.to_string())?;
|
||||
dr_export::Frame::in_space(rw, rh, pixels, space).map_err(|e| e.to_string())
|
||||
@@ -1920,7 +1950,7 @@ impl DevelopSession {
|
||||
let (w, h) = fit(fw, fh, edge.max(1), edge.max(1));
|
||||
|
||||
let shader = self.graph.compose_for(dr_types::ColourSpace::Srgb);
|
||||
self.render_with_masks(&shader, w, h)?;
|
||||
self.render_with_masks(&shader, w, h, dr_types::ColourSpace::Srgb)?;
|
||||
|
||||
let (pixels, rw, rh) = self.adjust.export_pixels().map_err(|e| e.to_string())?;
|
||||
Ok((rw, rh, pixels))
|
||||
|
||||
Reference in New Issue
Block a user