Sharpen the pixels being exported, not the preview's - #780
Merged
Conversation
The unsharp mask multiplied its radius by the pipeline scale factor, so a 1 px radius became a ~6 px blur on a 24 MP export. The boost peaked at 8 px structure and read as contrast rather than sharpness at 1:1. Radius is now in output pixels on both the CPU and GPU paths, moving the peak to Nyquist. The L* noise gate came with it: at a true 1 px radius |L - blur| tops out near 1.0, so the old 1.5/2.0 closed the gate completely. Rescaled by the same factor it had been calibrated against. Export downscales now use INTER_AREA. OpenCV gives INTER_LANCZOS4 no prefilter on a shrink, so the CPU path was already producing plain bilinear and the GPU tiled path asked for it outright; neither was area-correct. The preview no longer shows export acutance faithfully at fit-to-window. Judge sharpening at 1:1, as in Lightroom.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause: sharpening landed on the wrong spatial frequency
apply_output_sharpeningmultiplied the USM radius byscale_factor = long_edge / 1600, sosharpen_radius = 1.0became a σ ≈ 6 px blur on a 24 MP export. Measured through the real export path (61 MP frame, GPU tiled), as gain over the unsharpened buffer:The peak moved from 8 px structure to Nyquist. The export was never under-sharpened — it was sharpened at the wrong scale for its own pixels, which reads as contrast rather than sharpness at 1:1.
Radius is now in output pixels on both paths (
lab/logic.py,gpu_engine.py,lab.wgsl), sharinggaussian_kernel_1das before so CPU and GPU stay bit-matched.The gate had to come with it
The L* noise gate was 1.5–2.0. At a true 1 px radius
|L - blur|tops out near 1.0 (p50 0.20, p99 1.03 on a real scan), so the gate closed completely and the first cut of this change produced no sharpening at all. Rescaled to 0.25/0.33 — the old value over the ~6× it had been implicitly calibrated against. Picked against measured alternatives:Best detail-to-grain trade of the three.
Second fix: resized exports were not area-correct
The GPU tiled path downscaled with
INTER_LINEAR, and the CPU path'sINTER_LANCZOS4is bit-identical to bilinear on an OpenCV downscale (measuredmaxdiff = 0.0on 4.13 — no prefilter on a shrink). Both now pickINTER_AREAwhen shrinking. AffectsPRINT/TARGET_PXonly; theORIGINALdefault never resampled.Ruled out
No hidden blur at defaults (only the lab USM touches pixels; CLAHE, chroma denoise, glow, halation, IR/dust, flat-field and carrier are all gated off). No
textureSampleanywhere on GPU. No proxy leaking into export — the source cache key includesfast_decode. Raw decode is clean: 16-bit, no FBDD, no median passes, no wavelet denoise,half_sizenever set on export. JPEG is explicitly 4:4:4.Left open
Demosaic is
AHDfor Bayer, the softest option in the build (DCB,AAHD,VNGalso available). A laplacian-variance sweep favoured AAHD ~2.6×, but that metric cannot separate detail from grain on a film scan and a flat-versus-detailed patch split did not resolve it. Wants a slanted-edge target before anything changes.Also noted, not touched:
_to_uint8_jit/_to_uint16_jittruncate instead of rounding, while the luma variants round. Half-LSB downward bias on RGB export, unrelated to sharpness.Tests
make allgreen, 3613 passed. Two regression tests added — the USM must boost fine detail over coarse (test_lab_logic.py), and a layout downscale must matchINTER_AREA(test_print_service.py). The relocation golden was regenerated; the characteristic-curve golden did not move, which is consistent since sharpen is not in the exposure kernel.test_unmasked_half_is_bit_identicalnow pins sharpening off: a genuine ±3 px USM support carries the mask edge a few rows past the boundary on a 48 px test frame, which would hide the leak that test looks for.Docs updated in the same change —
PIPELINE.md(radius units, gate values) andUSER_GUIDE.md(the preview no longer shows export acutance at fit-to-window; judge at 1:1).