1

I'm trying to program a Hue/Saturation/Lightness filter for an image (in C++). The RGB->HSL conversions work fine, but my issue comes when accounting for the fact that each pixel has a different initial saturation and lightness.

For each pixel, I've calculated the source HSL and am given the HSL values as the filter input (in the range [0, 1]). I'd like to make 0.5 the default for all (the output is the same as the input), 0.0 for saturation means grayscale or completely black for lightness, and 1.0 means fully saturated or completely white. Unlike saturation and lightness, the hue part is easy: just add and wrap in the range [0, 1].

So then comes the task: how do we transform the saturation and lightness values of each pixel based on the filter inputs?

One solution that came to mind:

Out.H = Wrap(In.H + Filter.H - 0.5f, 0.0f, 1.0f);
Out.S = std::clamp(In.S * Filter.S * 2.0f, 0.0f, 1.0f);
Out.L = std::clamp(In.L * Filter.L * 2.0f, 0.0f, 1.0f);

Where Out is the output, In is the source image, and Filter is the filter settings. But if the source image has a lot of contrast to it, the initially light parts of the image clip quickly (get clamped to solid 1.0f), losing detail while the darker parts may still be barely visible. So instead, I figured perhaps there'd be some curves that could be applied to it that would prevent this clipping while still letting 0.5f return the original value. I thought of using a power function and calculating the power that would be needed. I came up with:

f(x) = x ^ log_1/2(a)

Where a is the value at the input pixel (the function's value at x = 0.5f). For instance: f(x) = x ^ log_1/2(.2)

This function, f(x) = x ^ log_1/2(0.2), passes through (0,0), (0.5, 0.2), and (1, 1). So if the saturation of the input pixel was 0.2f and the filter's saturation input from the user was x, running it through this function would work. You'd need to replace the 0.2 in the function with whatever the input pixel's saturation is. Supposedly, the same thing could be done for lightness. Thus, the code becomes:

Out.H = Wrap(In.H + Filer.H - 0.5f, 0.0f, 1.0f);
Out.S = pow(In.S, log(Filter.S) / log(0.5f));
Out.L = pow(In.L, log(Filter.L) / log(0.5f));

This works well for images with mid-range saturation and lightness. But, if it's initially very high or very low, this approach can mean there's a drop-off of the function very close to 0 or 1. For instance, if the saturation is initially 0.97, the function looks like:

f(x) = x ^ log_1/2(.97)

(With a point at (0.5, 0.97), I forgot to mark it).

This means you'd have to turn down the saturation to something like .001 before you start to see any actual difference. I've come across this problem with this test image:

Original (Saturation = 0.5):

Saturation = 0.001

Saturation = 0.0

At this point I'm stuck. Is there a way to adjust the saturation and lightness without losing detail or clipping? Perhaps there's another method of curving that can be used? Is there a standard way of doing it that I've failed to find? Thanks in advance.

Graphs made with Desmos online graphing calculator.

TheTrueJard
  • 471
  • 4
  • 18
  • Or is the first method mentioned (plain multiplication) indeed the "standard" way to do it, meaning clipping is normal? – TheTrueJard Dec 26 '17 at 03:51

1 Answers1

0

Okay, I still don't know about any "standard" way of accomplishing this, but I found that trigonometric curves work relatively well even for strongly saturated images.

Using the two functions a*sin(x*PI) where {0 <= x <= 0.5} and 1+(a-1)*sin(x*PI) where {.5 < x <= 1.0}, one can create curves as follows:

Initial saturation (a) = 0.2:

Trig 0.2

Initial saturation = 0.97:

Trig 0.97

Code:

Out.H = Wrap(In.H + Filter.H - 0.5f, 0.0f, 1.0f);
Out.S = (Filter.S <= 0.5f) ? (In.S * sin(Filter.S * PI)) : (1.0f - (1.0f - In.S) * sin(Filter.S * PI));
Out.L = (Filter.L <= 0.5f) ? (In.L * sin(Filter.L * PI)) : (1.0f - (1.0f - In.L) * sin(Filter.L * PI));

Clearly this is not perfect, but it seems to be sufficient for now, and allows for much cleaner image adjustment than the power function. It also means that the different pixels are losing and gaining saturation at different rates than before, perhaps differently from most HSL adjustment filters. If anyone happens to know the "standard" or usual way of handling thing, please, share!

TheTrueJard
  • 471
  • 4
  • 18