Skip to content

Sat vs Sat & Brilliance vs Sat curve module - #21596

Open
MStraeten wants to merge 12 commits into
darktable-org:masterfrom
MStraeten:satcurvergb
Open

Sat vs Sat & Brilliance vs Sat curve module#21596
MStraeten wants to merge 12 commits into
darktable-org:masterfrom
MStraeten:satcurvergb

Conversation

@MStraeten

Copy link
Copy Markdown
Collaborator

inspired by the davinci resolve stat vs sat curve here a scene referred replacement for the old chroma vs chroma, lightness vs chroma curves in colorzones.
image

Saturation and brilliance handling is heavily borrowed from colorbalancergb module using design, implementation, review and documentation help by friendly claude, openai, glm agents.

documentation:

Technical Information:

description : remap saturation and brilliance with curves.
purpose : corrective or creative.
input : linear, RGB, scene-referred.
processing : darktable UCS / JzAzBz (perceptual).
output : linear, RGB, scene-referred.

details

The saturation curve module remaps the saturation and brilliance of an image as a function of their own input value, using two independent editable curves. Unlike a flat saturation or vibrance slider, each curve lets you decide exactly how much correction is applied at every point along the saturation range — for example, protecting already-vivid colors while boosting near-neutral ones, or the reverse.

introduction

Global saturation and vibrance controls apply a single coefficient (or a coefficient weighted by chroma) to the whole image. This is often too blunt an instrument: skin tones, skies and foliage usually sit at different points on the saturation range, and pushing all of them by the same amount rarely gives a natural result.

The saturation curve module solves this by plotting a curve over the full range of a perceptually normalized saturation axis (0 to 100% of the local gamut boundary) and letting you add, move or remove control nodes anywhere along it. A second, independent curve does the same for brilliance — the perceptual dimension that behaves like a saturation-aware exposure change. Both curves are read using the current pixel's own saturation as the lookup value, so the correction is entirely local to each pixel's starting saturation, not to a spatial mask.

general principles

Internally, each pixel's RGB value is converted into a perceptual color space and reduced to two numbers: a hue angle and a normalized saturation value between 0 (fully neutral) and 1 (at the color space's gamut boundary for that hue and lightness). This normalized value is looked up on the saturation and brilliance curves, and the resulting curve heights are converted into multiplicative correction factors, applied at constant hue.

Two perceptual formulas are available for this normalization, selectable in the module footer:
JzAzBz (2021) : The original saturation formula. Uses the JzAzBz uniform color space, which does not fully account for the Helmholtz-Kohlrausch effect and can behave less smoothly near black.
darktable UCS (2022) : The darktable Uniform Color Space, designed specifically for saturation manipulation. It accounts for the Helmholtz-Kohlrausch effect and includes a built-in, more accurate gamut-mapping model, giving smoother results across the lightness range. This is the default and recommended formula.

Colors that would fall outside the destination gamut after correction are soft-clipped at constant hue, retargeting them toward the nearest in-gamut saturation instead of hard-clipping. This allows stronger curve adjustments to be used safely without producing posterization or clipped colors near the gamut boundary.

module controls

The module is organized as two tabs, one per curve, plus a shared histogram/picker/formula row below the curve graph.

saturation tab

saturation curve : Plots the output saturation multiplier (y-axis) as a function of the input, normalized saturation (x-axis). The default curve is a flat line at 50%, which corresponds to a neutral, unity multiplier — moving a node above the midline increases saturation at that point on the input range, moving it below decreases it.

  • Left-click on the curve to add a node, drag an existing node to reposition it.
  • Right-click a node to reset it to neutral (with Ctrl) or remove it (without Ctrl, when more than two nodes are present).
  • Double-click anywhere on the graph to reset the active curve entirely.
  • Scroll over a selected node to nudge its value up or down (hold Ctrl for larger steps).

The background of the graph shows a log-compressed histogram of the image's own input saturation distribution, letting you see exactly which parts of the range are populated before deciding where to place nodes. A gradient strip below the graph shows the corresponding hue-less perceptual gradient for reference.

brilliance tab

brilliance curve : Works identically to the saturation curve, but its output multiplier is applied to the perceptual brilliance dimension instead of saturation. Brilliance changes act at constant hue and combine a luminance and chroma shift in a direction orthogonal to pure saturation, giving a result closer to a local, saturation-aware exposure adjustment.

The same interaction (add / move / remove nodes, reset, scroll-to-nudge) applies to this curve. Switching tabs does not reset the other curve — both are always active simultaneously and can be combined.

shared controls

color picker : Samples the normalized saturation from the selected point and overlays them on the active curve's graph as a vertical guide. Use this to locate where a specific subject (skin, sky, foliage) sits on the input axis before shaping the curve around it.

show saturation mask : Toggles a grayscale preview of the normalized input saturation used by both curves, displayed in place of the image. Brighter areas correspond to higher input saturation. Use this to understand which regions of the image will be affected by a given section of the curve before committing to an edit. The mask reflects the module's own input, so it is unaffected by any correction the curves themselves apply.

saturation formula : Selects between the JzAzBz and darktable UCS normalization models described in general-principles. This setting affects both curves and is not meant to be changed regularly, which is why it is placed in the shared control row rather than duplicated per tab.

Usage guide

Basic workflow

When first enabled, both curves default to a flat, neutral line and the image is unchanged.

  1. Pick a starting point. Use the color picker on a subject you want to target (e.g. skin or sky) to see where its saturation falls on the graph.
  2. Enable the saturation mask briefly to confirm which regions of the image correspond to which part of the curve's x-axis.
  3. Add a node near the picked position and drag it up to boost, or down to reduce, saturation in that range. The histogram overlay shows how much of the image will be affected.
  4. Refine the shape. Add further nodes to protect one end of the range (for example, keeping already-vivid colors near the right edge untouched) while adjusting another.
  5. Switch to the brilliance tab if the corrected colors need a luminance/chroma trim at constant hue to look balanced — this is often useful after a strong saturation boost, to avoid an artificial-looking result.

Choosing a formula

Leave the formula on darktable UCS unless you are matching the behavior of an older edit or style that was built using the JzAzBz model. Switching formulas changes how input saturation is normalized and will therefore change the effect of existing curve nodes — re-check the result after switching.

FAQ

why does the curve reference a normalized value instead of raw chroma?
Raw chroma or saturation values are not bounded consistently across hues and lightness levels — the same numeric chroma can be barely visible for one hue and heavily clipped for another. By normalizing input to the local gamut boundary (0–1) for the current hue and lightness, the curve behaves consistently across the whole image, and a node placed at, say, 80% always means "80% of the way to the most saturated color physically displayable at that hue and lightness."

how is this different from color balance RGB's perceptual saturation grading?
Color balance RGB applies a single perceptual saturation and brilliance coefficient, optionally split between shadows, mid-tones and highlights via luminance masks. The saturation curve module instead lets the correction vary continuously as a function of the pixel's own saturation, independent of its luminance. The two modules are complementary and can be used together — luminance-based masking in color balance RGB, saturation-based shaping here.

internal processing
The following is the internal order of operations within the module, for each active pixel:

  1. Convert from pipeline RGB to XYZ, then to the selected perceptual space (JzAzBz or darktable UCS JCH),
  2. Compute the hue angle and the saturation normalized to the local gamut boundary,
  3. Look up the saturation and brilliance curves at that normalized value to obtain two correction factors,
  4. Apply the saturation factor at constant hue, soft-clipping against the gamut boundary,
  5. Apply the brilliance factor at constant hue, in the direction orthogonal to saturation,
  6. Soft-clip the result again against the destination gamut,
  7. Convert back to pipeline RGB.

caveats

Because the normalization depends on the working color profile's gamut, changing the working RGB space mid-session will change how existing curve nodes behave, since 80% of the gamut boundary under one profile is not the same absolute saturation as under another. If you change the working profile, re-check the curves against the histogram overlay.

The saturation mask preview shows the module's input saturation, not the corrected output — it is a diagnostic aid for shaping the curve, not a preview of the final result.

@MStraeten MStraeten added the feature: new new features to add label Jul 23, 2026
@TurboGit

Copy link
Copy Markdown
Member

If I read this correctly there is no guided-filter, right? I remember that during the development of the color equalizer we had quite some issues with artifacts without a proper filter to smooth things. Won't this happen here too?

@MStraeten

Copy link
Copy Markdown
Collaborator Author

as long as smooth curves are used there's no need i my opinion. But might be considered for steep changes.

@MStraeten
MStraeten marked this pull request as draft July 23, 2026 18:42
@TurboGit

Copy link
Copy Markdown
Member

@MStraeten
My main concern/question was about how this is different from Color Balance RGB. You have the answer, the first masking is based on luminance and even if we add parametric masking based on saturation, the effect still depends on the luminance of the original pixel, right? Also the parametric mask is linear whereas the curve can get any shape.

@MStraeten

Copy link
Copy Markdown
Collaborator Author

the mask is based on saturation, not luminance.

Main difference to colorbalancergb in combination with a chroma channel parametric mask is the shape of the correction. parametric masks have 4 control points to define the area/intensitycurve of the saturation change.
So it's nothing that can't be done also with a number of instances of colorbalancergb and Cz parametric masks.

In my experiments with guided filter i derive these based on the saturation mask to mitigate saturation noise effects.
Using a fast guided filter as in toneequal but based on saturation i'm able to avoid noise when doing massive brilliance changes:
image
But i wasn't able to get such harsh effects when changing staturation.

So guided filter seems to make sense for the brilliance curve but not for the saturation curve - but i'd expect this as an edge case since basic changes of brilliance are done with tone equalizer or colorbalancergb.

@MStraeten

Copy link
Copy Markdown
Collaborator Author

New feature: Edge‑aware mask control with guided filter

The module now computes an additional normalized saturation mask, derived from the same perceptually normalized saturation that drives the curves. Optionally, this mask is smoothed with a fast scalar guided filter, so the strength of the correction varies smoothly across saturation edges instead of switching abruptly.

Purpose of the guided filter mask

Instead of applying the curve effect with the same strength everywhere, the guided filter lets the module fade the correction in and out based on local saturation differences. This reduces typical artifacts such as halos or stair‑stepping at boundaries between saturated and neutral regions, especially when the curves are aggressive - especially when changing brilliance.

New controls in the module

In the “guided filter” section, the following parameters are available:

use guided filter

Enables spatial modulation. The correction is blended per pixel with a strength factor derived from the saturation mask, instead of always being fully applied.

filter radius

Sets the neighborhood radius that the guided filter uses for its statistics. Larger values produce a more strongly smoothed mask with broader influence; smaller values keep the modulation tighter around local structures.

edge feathering

Controls how strongly the filter follows edges. Lower values preserve harder edges and keep the mask transitions more tightly aligned to the underlying saturation structure, while higher values produce softer, less edge‑anchored transitions.

iterations

Specifies how many times the guided filter runs. More iterations increase smoothing and the effective reach of the filter, at the cost of additional computation.

mask quantization

Applies an optional quantization step to the saturation mask before smoothing, merging fine variations into broader levels. This reduces noise in the mask channel and makes transitions more robust against tiny saturation fluctuations.

Behaviour of the saturation mask preview

The show saturation mask toggle still displays a grayscale visualization of the normalized input saturation that the curves use as their x‑axis. When the guided filter is enabled, the preview instead shows the filtered mask, i.e. the actual strength map that later blends the corrected pixels with the original ones. This allows you to judge directly how smooth the transitions are and which areas of the image the guided filter influences, and by how much.

Internal processing

Inside the module, the guided‑filter path extends the processing pipeline as follows:

The module computes a scalar saturation mask from the perceptually normalized saturation.

It smooths this mask with an edge‑aware guided filter, either on CPU or GPU (OpenCL), using downsampling and resampling to keep the computation efficient at large resolutions.

The filtered mask then drives the blend between the untouched input image and the fully corrected image: a mask value of 0 keeps the pixel unchanged, a value of 1 applies the full curve correction, and intermediate values produce a smooth interpolation.

@MStraeten
MStraeten marked this pull request as ready for review July 24, 2026 21:12
@TurboGit

Copy link
Copy Markdown
Member

Ok, I need to test this, almost on top of my TODO list.

@TurboGit TurboGit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First try and I had lock.

I have opened the module, moved the saturation curve 2 or 3 times and nothing happens after that. Dt is locked somewhere.

Also, the main window do not update when moving the curve. You need to release the mouse button to see the effect. On other curve (tone curve, rgb curve) one just have to stop moving for some ms to see the update.

This module should be added into the scene referred module group presets.

Comment thread data/kernels/fast_guided_filter.cl Outdated
Comment thread data/kernels/satcurve.cl Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: new new features to add

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants