Noise reduction
Pipeline
flowchart TD
RGB[Linear RGB] --> Split["Y = 0.2126R + 0.7152G + 0.0722B<br/>Cb = B - Y, Cr = R - Y"]
Split --> PerCh["For each channel (Y, Cb, Cr)"]
PerCh --> Atrous["À trous decomposition<br/>5 levels, B3-spline kernel<br/>gap schedule: 1, 2, 4, 8, 16"]
Atrous --> Bands["5 detail bands + residual"]
Bands --> Sigma["Estimate sigma per channel<br/>MAD of finest band / 0.6745"]
Sigma --> Thresh["Soft-threshold each level<br/>t = sigma * scale[k] * strength<br/>scale = [1.0, 1.0, 1.2, 1.5, 2.0]"]
Thresh --> Recon["Reconstruct: residual + sum(bands)"]
Recon --> Combine["Recombine Y, Cb, Cr -> RGB"]
Combine --> Out["Clamp 0..1"]
The luminance, color, and detail sliders parameterize the threshold strengths: luminance and color map to a 0..3 multiplier on Y and on (Cb, Cr) respectively, while detail only protects the finest-scale luminance band by scaling its threshold down toward zero.
Noise reduction runs in linear RGB before the later gamma-space tone and detail work. AgX converts the image into one luminance channel and two chroma-difference channels, denoises each with a redundant à trous wavelet decomposition, soft-thresholds the wavelet detail bands, then reconstructs RGB.1 The redundant, non-decimated structure is the point: every wavelet level stays at full image resolution, so threshold decisions are translation-invariant and do not introduce the zippering or shift sensitivity that a decimated pyramid can produce.
How it works
The denoise pass has three user-facing controls:
luminance: how strongly to denoise the luminance branchcolor: how strongly to denoise the two chroma branchesdetail: how much of the finest luminance band to protect
All three parameters live in 0.0..=100.0. When they are all zero,
is_neutral() short-circuits the entire pass.
1. Split into luminance and chroma
AgX first rewrites linear RGB into one luminance-like channel and two chroma-difference channels:
Y = 0.2126 R + 0.7152 G + 0.0722 B
Cb = B - Y
Cr = R - Y
That separation is important because luminance noise and chroma noise do not look equally bad. Chroma blotches are usually more objectionable than monochrome grain, so AgX lets the color channels be smoothed independently from the luma channel instead of driving all three RGB channels with one shared threshold.
2. Build a five-level à trous stack
Each channel is decomposed independently into five detail bands plus one final residual. Every level uses the same separable B3-spline low-pass kernel:
[1/16, 4/16, 6/16, 4/16, 1/16]
At level k, the tap spacing is 2^k, so the kernel footprint grows
without downsampling the image:
- Convolve horizontally with the strided B3-spline kernel.
- Convolve that result vertically with the same strided kernel.
- Subtract the smoothed approximation from the previous approximation to get the detail band for that level.
- Reuse the smoothed approximation as the input to the next level.
Boundary handling is mirror reflection at the image edges. The CPU and GPU paths share the same kernel weights, gap schedule, and mirror logic.
3. Estimate the noise floor from the finest band
After level 0, AgX estimates one global noise sigma per channel from the median absolute deviation of that finest detail band:
sigma = median(|detail_0|) / 0.6745
This is a robust estimate for approximately Gaussian noise and is cheap enough to reuse across all coarser bands. AgX does not currently model signal-dependent sensor noise or spatially varying noise; the threshold schedule is driven by this single per-channel sigma.
4. Soft-threshold each wavelet band
Each detail band is shrunk toward zero with soft thresholding:
soft(x, t) = sign(x) * max(|x| - t, 0)
The threshold for each level is:
threshold(level) = sigma * level_scale[level] * strength
with fixed per-level scale factors:
[1.0, 1.0, 1.2, 1.5, 2.0]
The user sliders are mapped as follows:
luminanceandcolormap linearly from0..100to0.0..3.0threshold multipliers.detailmaps todetail_factor = 1.0 - detail / 100.0.- That
detail_factoris applied only to the level-0 luminance threshold. Atdetail = 100, the finest luma band gets zero thresholding; atdetail = 0, it gets the full threshold.
That last point is deliberate. AgX protects the finest-scale luminance detail because that is where edge crispness and texture live. Chroma bands are not given a matching protection term, because the algorithm leans toward removing color speckling more aggressively than monochrome grain.
5. Reconstruct the channel
Once all five detail bands have been thresholded, the denoised channel is reconstructed as:
residual + detail_0 + detail_1 + detail_2 + detail_3 + detail_4
The CPU path keeps the detail bands and residual explicitly, then sums them at the end. The GPU path accumulates thresholded detail bands as it goes and adds the final residual in a last pass. Both produce the same wavelet reconstruction model.
6. Write the denoised channels back to RGB
After denoising, AgX converts the channels back into RGB. The CPU path
clamps once after reconstructing the full (Y, Cb, Cr) triplet, while
the GPU path clamps after each sequential channel write-back.
Conceptually the relationship is still:
R = Y + Cr
B = Y + Cb
G = (Y - 0.2126 R - 0.0722 B) / 0.7152
The CPU implementation reconstructs RGB from the full denoised
(Y, Cb, Cr) triplet in one step, then clamps the final pixel. The GPU
path writes the channels back sequentially: it rescales RGB for the Y
pass, then updates Cb, then Cr, clamping after each pass. The two
paths are meant to produce near-identical output, but they do not use
the exact same write-back mechanism.
Why we chose it
AgX uses à trous denoising because it matches the shape of the pipeline well:
- It is isotropic and translation-invariant, so it behaves predictably on photographic texture and edges.
- The same fixed B3-spline filter bank works on CPU and GPU with almost identical math.
- Per-subband thresholding makes the user model simple: one luma strength, one chroma strength, one finest-detail protection term.
- It fits AgX's adjustment pipeline cleanly as a linear-space full-image pass, alongside dehaze.
AgX intentionally keeps the implementation conservative. There is no learned noise model, no local variance estimation, and no cross-channel joint thresholding. The pass is a fixed five-level stationary wavelet shrinkage stage whose behavior is meant to be stable, portable, and easy to reason about from preset values.
Parameters and constants
| Constant | Value | Role |
|---|---|---|
NR_MIN | 0.0 | Lowest accepted slider value |
NR_MAX | 100.0 | Highest accepted slider value |
| Neutral state | all params 0.0 | Skip the pass entirely |
NUM_LEVELS | 5 | Number of à trous detail bands |
| B3 kernel | [1/16, 4/16, 6/16, 4/16, 1/16] | Separable low-pass filter at every level |
| Gap schedule | 1, 2, 4, 8, 16 | Tap spacing per level |
LEVEL_SCALE | [1.0, 1.0, 1.2, 1.5, 2.0] | Per-band threshold multiplier |
| Sigma constant | 0.6745 | MAD-to-sigma conversion factor |
| Output clamp | [0.0, 1.0] | Keep RGB in valid linear range |
One subtle but important implementation detail: detail by itself does
not add denoising. If luminance == 0 and color == 0, the channel
strengths stay at zero, so the output remains unchanged even if
detail > 0.
Beyond the expected range: preset validation rejects luminance,
color, and detail outside 0.0..=100.0. The internal constants
(filter taps, gap schedule, level scaling) are part of the algorithm
itself rather than tuning knobs — they are listed for transparency,
not exposed for tweaking.
Preset-slider mapping
In preset TOML, noise reduction is serialized as:
[noise_reduction]
luminance = 40.0
color = 25.0
detail = 50.0
Those values map directly to NoiseReductionParams. The semantics are:
luminance = 0disables denoising onY;100maps to a threshold multiplier of3.0.color = 0disables denoising on bothCbandCr;100likewise maps to3.0.detail = 0gives the finest luminance band its full threshold;detail = 100suppresses level-0 luminance thresholding entirely.
The mapping is linear and intentionally narrow. Presets stay portable because all deeper algorithm constants remain fixed in code.
Source
- CPU (Rust):
crates/agx/src/adjust/denoise.rs - GPU dispatcher:
crates/agx/src/engine/gpu/stages/denoise.rs - GPU WGSL kernels:
References
Albert Bijaoui, Jean-Luc Starck, and Fionn Murtagh (1994). Multiscale Image Restoration by the À Trous Algorithm / Restauration des images multi-échelles par l'algorithme à trous. PDF: https://gretsi.fr/data/ts/pdf/1994_11_3_1863_1.pdf
See also
- Concept references: Detail (noise reduction entry), Color models
- API references: noise reduction
- Related explanations: Detail pass, Dehaze, Grain
- How-tos: Write your own preset