Texture and Clarity

Three independent sharpening passes applied in sequence to the luminance of an image, each isolating a different spatial scale: Texture works on the finest detail with a band-pass response, Unsharp Mask hits the small-to-medium scale with a classical Gaussian high-pass plus optional noise-suppressing threshold, and Clarity adds mid-scale local contrast through an edge-preserving bilateral filter with a midtone mask. All three run on Rec. 709 luminance so chroma is bit-for-bit preserved; mono images operate on the single channel directly. Live preview with instant before/after toggle; all stages fully invertible by setting their amount to zero.

1. The three stages

Each stage runs independently with its own enable condition (|amount| < 0.001 → skip), so you can use just one, two, or all three. They apply in order:

source -> Texture -> Unsharp Mask -> Clarity -> result

which matters a little: Texture lifts the fine detail that USM and Clarity then operate on, so the composition is cumulative rather than orthogonal. Doing the same amount of sharpening in a different order would give subtly different results. In practice the order chosen here (fine → medium → broad) is the usual sharpening-pipeline convention and the one that tends to produce the most natural-looking results on astronomical imagery.

2. Texture

Band-pass sharpening by difference of Gaussians (DoG): the image is blurred at two scales (σ and 2σ), their difference isolates the frequency band between them, and that band is added back scaled by the amount.

3. Unsharp Mask

The classical high-pass sharpening operation: blur the image at one scale, subtract to isolate the detail above that scale, add it back scaled by the amount. A soft threshold can suppress sharpening of low-amplitude differences so background noise isn't amplified along with real detail.

4. Clarity

Mid-scale local contrast enhancement using an edge-preserving bilateral filter as the base layer, with a midtone-weighted mask so the effect concentrates where it's most visually effective (mid-tones) and doesn't crush shadows or blow highlights.

The Clarity Mask group below has one more slider:

5. Before / after toggle

The ⇆ Toggle Before / After button is checkable: on shows the original cached pixmap, off shows the latest processed result. Switching is instantaneous because both pixmaps are cached — no re-processing happens, so you can flip back and forth as fast as you can click. Moving any slider automatically pops the button out of its “showing original” state so you see the new result immediately. The main use is calibrating the amount sliders: toggle a few times to see whether the sharpening is doing something real or just magnifying noise.

6. Preview panel

Right side of the dialog. Scrollable, pannable, zoomable:

The preview is debounced at 150 ms — small slider nudges coalesce into one computation so dragging the slider doesn't queue up a backlog. Each computation runs on a background QThread with cancellation support: changing a slider mid-computation cancels the in-flight preview and starts a new one as soon as you stop moving.

7. Document mask integration

If the active document carries a mask, the processed result is blended with the untouched original through that mask so only masked regions receive the sharpening effect. The blend is standard src × (1−m) + out × m, after all three sharpening stages have run. Both the live preview and the Apply result respect the mask.

8. Active-document tracking

The dialog listens for the main window's currentDocumentChanged signal, so switching to a different open image updates the preview to that image's current state without closing and re-opening the dialog. Slider settings carry across the document change — useful for applying the same sharpening recipe to several frames in a row.

9. Apply / Reset / Cancel and the preset grip

Apply runs the final full-resolution computation and commits as an undoable edit named “Texture and Clarity”. All eight slider values are recorded as the preset so Replay Last Action reproduces the exact run. The dialog closes on Apply. Reset returns all sliders to their defaults (every amount at zero, radii at their defaults, mask strength at 1.0). Cancel closes without applying.

The grip at the bottom-left of the controls column carries the current slider state and can be dragged onto the canvas to make a preset shortcut, or dropped directly on an image to apply those settings headlessly. Double-clicking the shortcut tile reopens this dialog with those values seeded. The dialog's window geometry is also persisted via QSettings so it opens where you last left it.

10. Technical details

All processing on float32 in [0, 1]. OpenCV provides the Gaussian and bilateral filter kernels; everything else is numpy. NaN inputs are nan-safe-converted to zero at each stage boundary.

10.1 Texture: difference of Gaussians

For slider amount a and radius σ (the inner Gaussian), Texture computes:

T(x) \;=\; x \;+\; 2a \,\cdot\, \left(G_{\sigma}(x) - G_{2\sigma}(x)\right)

where Gσ is a 2-D Gaussian blur of sigma σ (OpenCV's GaussianBlur). The difference Gσ − G2σ is a band-pass response peaking at spatial scale roughly σ and falling off at both finer and coarser scales. Multiplying by 2a and adding back scales that band without touching the DC level or the frequencies below 2σ. The output is clipped to [0, 1] to prevent overshoot. The kernel sizes are set to 2·round(3σ) + 1 and forced odd so OpenCV accepts them.

10.2 Unsharp Mask

Classical USM with an optional soft threshold:

U(x) \;=\; x \;+\; a \,\cdot\, (x - G_{\sigma}(x))

The high-pass detail d = x − Gσ(x) isolates everything above spatial scale σ, and adding it back scaled by a enhances those frequencies. When threshold t > 0, each pixel's detail is weighted by:

w(d) \;=\; \mathrm{clip}\!\left(\frac{|d| - t}{t},\;0,\;1\right)\,,\qquad d = x - G_{\sigma}(x)

so differences below t get zero weight (not sharpened), differences between t and 2t ramp linearly from 0 to 1, and differences above 2t get full sharpening. The soft ramp avoids a visible edge at the threshold that a hard cutoff would introduce. In practice you want t just above your background noise floor, which for a stacked astronomical frame is in the 0.003–0.010 range.

10.3 Clarity: bilateral local contrast + midtone mask

Clarity uses an edge-preserving bilateral base layer rather than a Gaussian, so local contrast is enhanced within smooth regions without ringing across edges (which is where USM gets ugly on nebula boundaries). For slider amount a and radius r:

C(x) \;=\; x \;+\; a \,\cdot\, (x - B_{\sigma}(x)) \,\cdot\, M(x)

where Bσ is a bilateral filter with sigma_space = 10r and sigma_color = 0.1 (fixed; the bilateral's colour sigma controls which pixels get averaged together, and 0.1 matches typical astronomical dynamic range). The midtone mask M(x) is a parabolic window peaking at mid-grey:

M(x) \;=\; \mathrm{clip}\!\left(1 - 4(x - 0.5)^2,\;0,\;1\right)

which has M(0) = 0, M(0.5) = 1, M(1) = 0, and smoothly interpolates between. The midtone strength slider blends this with a flat all-ones mask:

M'(x) \;=\; s\,M(x) \;+\; (1 - s)\,\cdot\, 1

so s = 1 gives the classic Lightroom-style midtones-only Clarity, s = 0 applies the effect uniformly everywhere, and intermediate values interpolate.

10.4 Adaptive-resolution bilateral

The bilateral filter cost scales badly with sigma_space. For large radii (sigma_space > 10, i.e. slider radius > 1.0), SASpro downsamples the image before filtering by a factor scale = clip(5 / sigma_space, 0.1, 1.0), applies the bilateral at the proportionally scaled sigma_space, then upsamples the base layer back with linear interpolation. The visual quality cost is negligible because the bilateral at large sigma_space is already smoothing finer than the downsample factor, but the runtime cost is cut by the square of the downscale ratio — a 10× radius becomes roughly 25× faster.

10.5 Luminance-only processing with chroma preservation

For RGB inputs, SASpro computes luminance using the Rec. 709 standard coefficients:

L \;=\; 0.2126\,R \;+\; 0.7152\,G \;+\; 0.0722\,B

runs all three sharpening stages on L, then applies the resulting ratio uniformly to all three channels:

(R', G', B') \;=\; (R, G, B) \,\cdot\, \frac{L_{\mathrm{new}}}{L}

which preserves chroma exactly — the hue and saturation of every pixel is bit-for-bit the same before and after, and only the luminance structure changes. A tiny +ε is added to the denominator to avoid division by zero on pure black pixels. Mono inputs skip this step entirely and run the sharpening directly on the single channel.

10.6 Document mask gating

If the active document carries a mask m ∈ [0, 1], the final composite is:

\mathrm{out} \;=\; (1 - m)\,\mathrm{src} \;+\; m\,\mathrm{processed}

applied after all three sharpening stages. The mask is resampled to the processed image's dimensions by nearest-neighbour (OpenCV INTER_NEAREST) and broadcast to match channel count. Both the live preview and the final Apply respect this mask identically.

10.7 Threading and cancellation

Each preview computation runs on a dedicated QThread (TextureClarityWorker) that owns a cancel flag checked between stages. When a slider moves, the 150 ms debounce timer restarts; when it fires, any still-running worker has its cancel flag set, is given up to 300 ms to notice, then a fresh worker is spawned with the new parameters. This keeps the UI responsive even on 24+ MP frames where a single pass can take over a second.

Credits. The bilateral filter is Tomasi & Manduchi, Bilateral Filtering for Gray and Color Images (ICCV 1998); difference-of-Gaussians edge/band-pass detection traces to Marr & Hildreth, Theory of edge detection (1980). The unsharp mask is older still — it was a darkroom photographic technique by the 1930s, formalised digitally in the image-processing textbooks of the 1980s and 1990s. The Rec. 709 luminance coefficients are from ITU-R Recommendation BT.709. The implementations here are from OpenCV (GaussianBlur, bilateralFilter). SASpro's own contribution is the pipeline composition: the three stages in sequence, the luminance-ratio chroma preservation path for RGB, the adaptive-resolution downsample-filter-upsample path for large-radius bilateral, the midtone-vs-uniform mask blend slider, the soft-ramp USM threshold, the debounced live-preview worker with cancellation, and the cached before/after instant toggle.