Feature #11959

Color balance in HSV mode

Added by Aurélien PIERRE over 2 years ago. Updated over 1 year ago.

Target version:
Start date:
Due date:
% Done:


Estimated time:
Affected Version:
git master branch
hardware architecture:


The color balance module in darktable allows to adjust the color cast for highlights, mid-tones, and shadows but in RGB mode and with weird labels (lift, gain, gamma). This makes it quite tricky to understand what you do, firstly because the labels don't mean anything to most users, then because RGB is not the most intuitive way to adjust colors.

Meanwhile, Capture One has a much praised color balance tool, with explicit labels and color wheels, allowing fast color grading :

While the wheels feel unnecessary and are probably patented, the idea of having 3 simple HSV sliders to adjust main/highlights/mid-tones/shadows would enable users to play with color-grading and achieve cinematic looks more easily.

153412-hue-shift.jpg (950 KB) 153412-hue-shift.jpg Cyan shift Aurélien PIERRE, 01/26/2018 03:52 PM
153412.jpg (1.84 MB) 153412.jpg Original Aurélien PIERRE, 01/26/2018 03:52 PM
153412-hue-shift.jpg (1010 KB) 153412-hue-shift.jpg Red shift Aurélien PIERRE, 01/26/2018 03:55 PM


#1 Updated by Tobias Ellinghaus over 2 years ago

  1. The terms "lift", "gamma" and "gain" are the industry standard for that module. There is no way we will change that to something less precise.
  2. The standard for that very module is to work in sRGB space. I would be surprised if Capture One used HSV for it.
  3. Color wheels are nice indeed, and there is some incomplete work to have them in dt, too. I just never found a nice UI for the whole thing that still allowed precise adjustments. And the color wheels are not enough, you still need sliders, otherwise there is no way to achieve negative numbers.

#2 Updated by Aurélien PIERRE over 2 years ago

1. I never saw them elsewhere, and I know they confuse many users. Maybe just a contextual label to translate them into simpler words would be enough.
2. The point is to have a GUI in HSV (with HSV sliders/cursors), the actual color space used to perform the modification on pixels doesn't matter.
3. As I said, color wheels feel like a design candy for hipsters, 3 sliders could do just as fine for that purpose.

#3 Updated by Tobias Ellinghaus over 2 years ago

With three sliders you mean one for highlights, one for midtones and one for shadows? I don't see how that would work, you can't set the same data that we have in 4 sliders right now with a single one. And as I said, the way the module currently works is basically what every program used for color grading video uses. Sometimes in the form of sliders, more often as color wheels with extra sliders.

#4 Updated by Aurélien PIERRE over 2 years ago

No, the three sliders would be for hue/saturation/value and you replicate them for highlights/mid-tones/shadows (and maybe for the main, although it sort of overlaps the white balance feature, but with a different interface). So that's 9 to 12 sliders in total. With 9 sliders, it would look exactly like the current color balance UI, only the settings would change.

The wheels are convenient to set-up saturation and hue as once in 2D, using polar coordinates (saturation:= radius, hue:= angle, I guess), but as a general paradigm, I prefer independent 1D coordinates with sliders.

#5 Updated by Tobias Ellinghaus over 2 years ago

  • Status changed from New to Closed: won't fix

I don't see any benefit in that but the big drawback that the module no longer works as color balance usually does.

#6 Updated by Aurélien PIERRE over 2 years ago

The benefit is well displayed here : Color-grading in RGB is non-sense, it's a lot of guess and trial work to get the correct color-cast without messing the saturation along or affecting the luminance as well.

I tried to reproduce the result achieved in the video with the color zones module, in luma mode, it did quite poorly and shifted all the tints of a certain amount, instead of shifting them toward a certain tint. The color-shift module does a similar job but with less control and an overall over-cooked result.

Similarly as the local-contrast module, it would be possible to have both modes (RGB/HSV) in the color-balance module.

#7 Updated by Roman Lebedev over 2 years ago

Aurélien PIERRE wrote:


I guess what you want to do, is to actually implement the change you are proposing (just a proof-of-concept, without caring about legacy history stacks for the moment)

#8 Updated by Tobias Ellinghaus over 2 years ago

I was questioning HSV sliders, not color wheels.

#9 Updated by Roman Lebedev over 2 years ago

Tobias Ellinghaus wrote:

I was questioning HSV sliders, not color wheels.

Yep, i was talking about the algo, not the UI, since i mentioned legacy params.

#10 Updated by Tobias Ellinghaus over 2 years ago

The algo is trivial, just do hsv->rgb in commit_params and keep using the current process(). The problem is finding a good UI and coding it. The one from Capture One has some nice features, even though it's not as capable as our current UI. Adapting it to support the whole feature set of the current one is the hard part. At the moment I lack any great ideas how to make it work without having as many sliders as we currently have plus the additional color wheels.

#11 Updated by Aurélien PIERRE over 2 years ago

The Capture One UI is just a combination of 2 controls (hue + saturation) into a single one with 2 axis (a wheel = angle + radius) + another redundant fine saturation control with a slider. You can achieve exactly the same with only 3 sliders, especially as the benefit of the wheel is not obvious compared to the sliders and both offer the same functionnality.

It may not be that trivial, because you don't want to shift every hue of the same amount, but rather retarget them to another hue, so it's not a translation but a selective minimization.

Put into maths, the naive way to do it is :

original_hue + (target_hue - original_hue) * amount

#12 Updated by Aurélien PIERRE over 2 years ago

I began prototyping algos to adjust the hue :

Although I'm not happy with the luma masks (designed from scratch, I don't know what's used in dt), the algorithm works well for the main hue. The core is :

def gaussian_weights(source, target, sigma):
    return np.exp(-(source - target)**2 / (2 * sigma**2)) / (sigma * np.sqrt(2 * np.pi))

def hue(source, target, amount):
    """Move the hue of source closer to the target, 
    assuming source and target angles between [-pi; pi],
    according to their distance

    if amount != 0:
        sigma = np.pi
        x = np.cos(source) + amount * (np.cos(target) - np.cos(source)) * np.pi * gaussian_weights(np.cos(source), np.cos(target), sigma)
        y = np.sin(source) + amount * (np.sin(target) - np.sin(source)) * np.pi * gaussian_weights(np.sin(source), np.sin(target), sigma)

        return np.arctan2(y, x)

    else :
        return source

def normal2rad(theta):
    """Remap the hue channel to [-pi; pi] radians assuming source is in [0;1]""" 

    # Rescale [0; 1] to [0; 2 pi]
    theta = theta * 2 * np.pi

    # Remap [pi; 2 pi] to [-pi; 0]
    negative = theta > np.pi # Boolean array : put 1 where theta > pi
    theta[negative] = - (2 * np.pi - theta[negative]) 

    return theta

def rad2normal(theta):
    """Remap the hue channel to [0; 1] assuming source is in [-pi; pi] radians""" 

    # Remap [-pi; 0] to [0; 2 pi]
    negative = theta < 0 # Boolean array : put 1 where theta < 0
    theta[negative] = 2 * np.pi + theta[negative]

    # Rescale to [0;1]
    theta = theta / (2 * np.pi)

    return theta

then, the execution : 

# Convert hue to radians
H = normal2rad(H)

# Apply hue remapping at full-throttle
H_bis = hue(H, np.pi, 1)

# Convert hue back to normal [0;1]
H_bis = rad2normal(H_bis)

#13 Updated by Aurélien PIERRE over 2 years ago

Some results without luma masking. What do you think ?

#14 Updated by Tobias Ellinghaus over 2 years ago

I don't really understand what that code is supposed to do. How would you, for example, apply a gamma correction to the red channel with that? Or lower the blacks/dark part of the blue channel?

#15 Updated by Aurélien PIERRE over 2 years ago

apply a gamma correction to the red channel with that? Or lower the blacks/dark part of the blue channel?

I don't, for that you can use the current module in RGB. What I do in my code is to remap the current hue to another target without breaking the tonal variations.

For example, let's say you want a teal/orange color grading (orange in highlights, cyan in shadows). Now you have to tweak the gains on the 3 RGB channels on shadows and highlights for hours because none of these colors are pure RGB values, and RGB values affect luminance, color and saturation altogether.

With my code, you just set-up a hue of 180° in shadows, around 45° in highlights, and that's it. To avoid unnatural look, the tints are shifted accordingly to their distance to the target (the farther the target is from the source, the less the algo shifts the hue of the source), so that the pixels that already have the target hue are untouched as well as those whose color is opposite (180° on the chroma wheel) to the target (like, shifting blue to red would be weird, so it's shifted to magenta instead).

The shifts use gaussian weights on the hue distance between the source and the target.

#16 Updated by Tobias Ellinghaus over 2 years ago

Ok, then I did understand it correctly. What you suggest is basically a new module as that is not color balance any longer. Please watch to understand what color balance is and how it's being used.

#17 Updated by Aurélien PIERRE over 2 years ago

What I suggest is achieving the same purpose in a different way. A color balance just balances colors, the way it does it only affect us geeks. The interface it displays affects its use. The core principle of design is the interface directs the use, and even if HSV is not the good old way to do it, it is very efficient, especially to achieve natural results in just a few settings tweaks. We could perfectly have both modes in the same module, the same way Profile denoising or Local contrast have. If I hadn't been an hardcore dt user since v0.7, I could switch to Capture One just for that feature.

The novelty of my approach, if I dare say so, is that hues are remapped according to their distance to the target hue, which does not mess up complementary colors and respects the original gradation. So you can go all the way without messing your picture, that's pretty dummy-proof.

Anyway, I will work on this if nobody wants to, although I think it would be a just a warm-up for experienced dt devs whereas I'm not a C dev at all.

Dropping this for future reference :

#18 Updated by Roman Lebedev over 2 years ago

  • Target version set to 2.6.0

#21 Updated by Roman Lebedev almost 2 years ago

  • % Done changed from 100 to 70
  • Status changed from Fixed to Patch attached

#22 Updated by Aurélien PIERRE almost 2 years ago

I meant I asked for the feature, I did the feature, so I it's fixed on my end. I don't know if you are interested to merge it, so the rest is not up to me.

#23 Updated by Aurélien PIERRE over 1 year ago

  • % Done changed from 70 to 0
  • Status changed from Patch attached to Closed: upstream

#24 Updated by Roman Lebedev over 1 year ago

(FYI, "Closed: upstream" means: not darktable's bug/problem, but of e.g. some other repository/library/etc (indirectly) used by darktable)

#25 Updated by Aurélien PIERRE over 1 year ago

  • % Done changed from 0 to 100
  • Status changed from Closed: upstream to Fixed

Thanks for the info, Roman. Still learning… ;-)

Also available in: Atom PDF

Go to top