color balance rgb: global vibrance vs global chroma

The manual says that global vibrance

affects the chroma dimension of color over the entire image, prioritizing those colors with low chroma

How is this different from global chroma in the next tab? How does that “prioritization” work?

(Math is welcome, so are suggestions for papers to read).

from https://github.com/darktable-org/darktable/blob/master/src/iop/colorbalancergb.c line 573ff:

Linear chroma : distance to achromatic at constant luminance in scene-referred
const float chroma_boost = d->chroma_global + scalar_product(opacities, chroma);
const float vibrance = d->vibrance * (1.0f - powf(Ych[1], fabsf(d->vibrance)));
const float chroma_factor = fmaxf(1.f + chroma_boost + vibrance, 0.f);
Ych[1] *= chroma_factor;
1 Like

Thanks, this is helpful. So if

  • y = Ych[1]
  • V = the global vibrance control of this module

then chroma is scaled up by v = V \times (1 - |y|^V), in addition to the chroma boost. When |y| = 0, v = 1, so this effectively doubles chroma (ignoring other controls)? [Of course it is capped later.]

And then this declines as |y| increases… but I don’t know the scale for it. Does |y| top out at 1?

What I still don’t understand is what Ych[1] is, what its range is. Is it a color dimension?

I forgot that C/C++ indexes from 0s, so I guess Ych[1] is the chroma? And its domain is [0,1]?

Yes, that would be chroma. About the domain, I wouldn’t expect it to be on [0,1], since we have unbounded values at that part of the pipeline.

1 Like

The Ych values are a polar transformation of a special Yrg color space where Y is the luminance and r, g the chromaticity coordinates which define the color. ch is the polar representation of rg where c is the radius (chroma) and h is the polar angle (hue). Similar to what LCh is to Lab. From the definition of the color space, or the transformations, it is not immediately obvious that c would be constrained to [0, 1], but it is never less than zero.

My understanding is that r and g are bounded in Yrg, specifically 0 \le r, g, r+g \le 1, so the radius \sqrt{r^2 + g^2} is below 1 (and only attains it for extremes, eg r = 1 or g = 1. So it works like this:

@anon41087856, can you please clarify these things? Why was this particular transform chosen?

Yeah, if we disregard imaginary colours, that constraint of r and g components holds. One detail, the radius c is calculated with respect to the white point which has nonzero positive r and g coordinates, so yes, c should be always less than one.

Translating

const float chroma_boost = d->chroma_global + scalar_product(opacities, chroma);
const float vibrance = d->vibrance * (1.0f - powf(Ych[1], fabsf(d->vibrance)));
const float chroma_factor = fmaxf(1.f + chroma_boost + vibrance, 0.f);
Ych[1] *= chroma_factor;

into maths:

c_{in} = \sqrt{(r_{in} - r_{white})^2 + (g_{in} - g_{white})^2} \in [0; 1]
c_{boost} = v * (1 - c_{in}^{|v|})
c_{out} = c_{in} * (1 + c_{boost})

so c_{out} = c_{in} * \left(1 + v * \left(1 - c_{in}^{|v|}\right)\right) with v \in [-1; 1]

By design, Yrg is a normalized chromaticity space with

\begin{cases} r = \frac{R}{R + G + B} \leq 1 \\ g = \frac{G}{R + G + B} \leq 1 \\ b = \frac{B}{R + G + B} = 1 - r - g \leq 1 \\ Y = \alpha R + \beta G + \gamma B, \{\alpha, \beta, \gamma\} \in [0; 1] \end{cases}

So, by design, the chromaticity space is a triangular domain bounded by r = 0, g = 0, r + g = 1 over the (r, g) plane.

\{R, G, B\} \geq 0 are defined scene-referred from CIE LMS 2006 retina cone space by a matrix product such that Munsell hues are evenly spaced around the white point, so \begin{bmatrix}R\\G\\B\end{bmatrix} = [A] × \begin{bmatrix}L\\M\\S\end{bmatrix} where [A] is the 3×3 matrix that optimizes the hue linearity and even distribution of Munsell patches in Yrg space.

Going back to our vibrance transform:

  1. If v = 0, then c_{out} = c_{in}, so we have a no-op.
  2. If v = 1, then c_{out} = 2 c_{in} - c_{in}^2, so:
    \begin{cases} c_{out} > c_{in}, &\forall c_{in} \in ]0; 1[\\ c_{out} = 0, & \forall c_{in} = 0 \\ c_{out} = 1, & \forall c_{in} = 1 \end{cases}
  3. If v = -1, then c_{out} = c_{in}^2, so:
    \begin{cases} c_{out} < c_{in}, & \forall c_{in} \in ]0; 1[\\ c_{out} = 0, & \forall c_{in} = 0 \\ c_{out} = 1, & \forall c_{in} = 1 \end{cases}

which were the design constraints of the transform. The transform has been tuned such that \forall v \in [-1; 1] it degrades smoothly and respects the same boundary conditions as when v = \{-1; 0; 1\}. But it’s entirey made-up by limits analysis to assert the desired behaviour of affecting low-chroma colors more than high chroma ones under the smoothness and boundary constraints.

EDIT: @paperdigits @patdavid LaTeX support is the best thing that ever happened to this forum.

5 Likes

Thank @darix, I would’ve used MathML :wink:

1 Like

Thanks @darix !

1 Like

Thanks for the detailed explanation. I forgot about the possibility of v < 0. Updated graph:

That said, I don’t think that the |v| power is doing much there. Simply using 1 + v\cdot (1 - c_\text{in}) would give

which is almost the same, except for the low-ish chromas.

you can achieve the linear behaviour by using a parametric mask using a gradient for chroma in combination with a global chroma setting - so having the power in vibrance function ist quite a different thing.

The point is that for chroma above, say, 0.05, a linear approximation to the power function is pretty good, while above 0.1 it may be perceptually indistinguishable. (Of course it is not the same v though for the two mappings.)

This can also be seen by doing a Taylor expansion around c = 1 (I assume v > 0 to keep it simple, v < 0 is similar):

v (1 - c^v) \approx v^2 \cdot (1- c) + \frac{v^2(1-v)}{2} (1-c^2) + \dots

The second-order coefficient is \frac{v^2(1-v)}{2} < 2/27 \approx 0.07 for v \in [0,1]. In practice, this is just a wide limit, if you are looking at an L_1 norm or similar, you could always approximate the power version with a linear function pretty well (as the graphs show).

You are looking at parametric graphs, don’t forget all that ends up actually changing colors in a non-perceptual space, so the visual effect is not directly predictable from the graphs, and that special behaviour for low chromas is the whole point.

1 Like

Thanks for the explanation. This discussion has been very useful, now I understand this module much better.

This is in for a while. :smiley: Let's use math my nerds

Official documentation is here:

1 Like