Expose the Contrast Mask's spacer as Mask Blur - #884
Merged
Conversation
The blur was pinned at 4% of the analysis grid. It is not a strength but a frequency cut-off, setting the scale above which tones are masked, and measured per octave at gamma 0.5 the axis is wide: 1% attenuates the 8px band to 0.75 and the 64px band to 0.51, where 8% leaves everything under 64px above 0.93. Narrower compresses harder (global range 0.50x at 1% against 0.62x at 4%, on a floor of 0.50) and lifts shadows that sit beside something bright, which is the mask line on a real sheet. At the limit blur(val) -> val and the operator collapses into a plain (1-g) reduction, which is Grade. 4% stays the default as the conservative end. This is the evidence the radius was missing when it was cut from the first version: on smooth synthetic content sigma 40 against 150 moved micro-contrast by 0.05, which did not justify a control. Per-octave on a real scan it does. Named for the quantity rather than the darkroom object: Spacer invites a thickness in mm, and mm needs FilmFormat from user-editable EXIF, which was already rejected as a fake unit here. Unlike the gamma scalar the blur rebuilds the plane, so it keys the plane cache on both engines, and it reaches both plane builds: the live one and the single plane a tiled export shares across its tiles, which would otherwise have stayed at the default. Verified that moving it re-renders under an unchanged source hash on CPU and GPU, with parity still exact at a non-default value.
marcinz606
force-pushed
the
feat/mask-blur
branch
from
August 17, 2026 20:01
0cca6b5 to
d3dee22
Compare
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.
The Contrast Mask's blur was pinned at 4% of the analysis grid. This exposes it as Mask Blur, beside Contrast Mask in the Tone panel.
Why it earns a control now
When the radius was cut from the first version, the only evidence I had was smooth synthetic content, where sigma 40 against 150 moved micro-contrast by 0.05. That did not justify a slider. Measured per octave on a real scan it clearly does, because the blur is not a strength but a frequency cut-off: it sets the scale above which tones are masked.
Band energy against unmasked, at gamma 0.5:
Narrower compresses harder (global range 0.50× at 1% against 0.62× at 4%, on a theoretical floor of 0.50) and lifts shadows sitting next to something bright, which is the mask line a printer sees on the sheet. At the narrow limit
blur(val) → valand the operator collapses into a plain (1−g) reduction, which is Grade's job. 4% stays the default as the conservative end.Note the first metric I reached for was blind to this: an RMS Laplacian only sees 1–2px, and even 1% blur is ~10px on a 1000px render, so micro-contrast barely moved (0.94 to 0.98) across the whole range. The per-octave decomposition is what showed the axis.
Naming
Mask Blur rather than Spacer, Unsharpness or Mask Sharpness.
FilmFormatfrom user-editable EXIF, already rejected as a fake unit on this feature.Mask Blur is 62px, keeps the number a real per-cent, and needs no inversion.
Cache correctness
The gamma slider is a pure scalar on a cached plane, so the blur is the one part that genuinely rebuilds it. It now keys the plane cache in both engines and the GPU's mask texture. Verified by moving the blur under an unchanged source hash: CPU and GPU both re-render (8%→4% mean |Δ| 0.00242, 4%→1% 0.00618) and agree exactly, and parity at a non-default blur is 0.00000 max abs diff.
Dead without a mask, so it greys out at gamma 0, and it is global-only like the mask itself. It is deliberately not in
_global_only, since that tuple's contract is "enabled exactly when global" and this has a second gate.Verification
make allgreen, 3964 passed, 3 new tests (the cut-off behaviour, the cache rebuild under one source hash, and that it is inert with no mask). Driven headlessly through the real GUI on a real scan alongside the existing Contrast Mask checks. Both docs updated.