Michel Rouzic
Michel Rouzic

Reputation: 1043

Algorithm to make overly bright (HDR) colours become white?

You know how every colour eventually turns white in an image if it's bright enough or sufficiently over-exposed? I'm trying to figure out a function to do this to apply to generated HDR images, in a realistic and pleasing looking way (using idealised camera performance as a reference I guess).

The problem the algorithm/function I want to obtain should solve is, let's say you have an orange pixel with the (linear RGB) values {1.0, 0.2, 0.0}. Everything is fine if you multiply each value by a factor of 1.0 or less, but let's say you multiply that pixel by 6, now you get {6.0, 1.2, 0.0}, what do you do with your out of range red and green value of 6.0 and 1.2? You could clip them which would give you {1.0, 1.0, 0.0}, which sadly is what Photoshop and 3DS Max seem to do, but it looks so very wrong as now your formerly orange pixel is yellow (so if you start with any saturated hue (meaning at least one channel is 0.0) you always end up with either magenta, yellow or cyan) and it will never become white.

I considered taking half of the excess of one channel and splitting it equally between the other channels, so for example {1.6, 0.5, 0.1} would become {1.0, 0.8, 0.4} but it's too simplistic and not very realistic. I strongly doubt that an acceptable solution could be anywhere near this trivial.

I'm sure there must have been research done on the topic, but I cannot find any relevant literature and sensitometry doesn't seem to be quite what I'm looking for.

Upvotes: 1

Views: 495

Answers (3)

Michel Rouzic
Michel Rouzic

Reputation: 1043

I've found a way to do it based on Mark Ransom's suggestion with a twist. When the colour is out of gamut we compute the grey colour of equivalent perceptual luminosity then we linearly interpolate between the out-of-gamut input colour and that grey value to find the first in-gamut colour. Weighting each RGB channel to get the perceptual luminosity part is the tricky part seeing as the most commonly used formula from CIELab L = 0.2126*red + 0.7152*green + 0.0722*blue is quite blatantly wrong as it makes the blue way too bright. Instead I did some tests and chose the weights which looked the most correct to me, though these are not definite and you might want to tweak them, although for this particular problem this is perhaps not too crucial.

Or in fewer words the solution is to desaturate the out-of-gamut colour just enough that it might be in-gamut.

Here is my solution in C code. All variables are in floating point format.

Wr=0.125; Wg=0.68; Wb=0.195;        // these are the weights for each colour

max = MAXN(MAXN(red, grn), blu);    // max is the maximum value of the 3 colours

if (max > 1.)       // if the colour is out of gamut
{
    L = Wr*red + Wg*grn + Wb*blu;   // Luminosity of the colour's grey point

    if (L < 1.) // if the grey point is no brighter than white
    {
        // t represents the ratio on the line between the input colour
        // and its corresponding grey point. t is between 0 and 1,
        // a lower t meaning closer to the grey point and a
        // higher t meaning closer to the input colour
        t = (1.-L) / (max-L);

        // a simple linear interpolation between the
        // input colour and its grey point
        red = red*t + L*(1.-t);
        grn = grn*t + L*(1.-t);
        blu = blu*t + L*(1.-t);
    }
    else    // if it's too bright regardless of saturation
    {
        red = grn = blu = 1.;
    }
}

Here's what it looks like with a linear orange gradient: linear orange gradient

It does not use anything like arbitrary gamma which is good, the only mostly arbitrary thing has to do with the Luminosity weights, but I guess those are quite necessary.

Upvotes: 1

Mark Ransom
Mark Ransom

Reputation: 308206

Modifying the Python code I left in an answer on another question to work in the range [0.0-1.0]:

def redistribute_rgb(r, g, b):
    threshold = 1.0
    m = max(r, g, b)
    if m <= threshold:
        return r, g, b
    total = r + g + b
    if total >= 3 * threshold:
        return threshold, threshold, threshold
    x = (3 * threshold - total) / (3 * m - total)
    gray = threshold - x * m
    return gray + x * r, gray + x * g, gray + x * b

This should return acceptable results in either a linear or gamma-corrected color space, although linear will be better.

Multiplying each r,g,b value by the same amount retains their original proportions and thus the hue, up to the point where x=0 and you've achieved white. You've expressed interest in a non-linear response once clipping starts, but I'm not entirely sure how to work that in. The math was carefully chosen so that at least one of the returned values will be at the threshold, and none will be above.

Running this on your example of (1.6, 0.5, 0.1) returns (1.0, 0.6615, 0.5385).

Upvotes: 1

ElKamina
ElKamina

Reputation: 7807

You have to map it to some non-linear scale. For example: http://en.wikipedia.org/wiki/Gamma_correction .

Ex: Let y = f(x) = log(1+x) - log(1-x) define the "actual" luminescence.

The reverse function is x = g(y) = (e^y-1)/(e^y+1).

now, you have values x=1 and x=0.2. For the first case the corresponding y is infinity. Six times the infinity is still infinity. If you use function g, you get new x_new = 1.

For x=0.2, y = 0.4054651. After multiplying by 6, y_new = 2.432791 . The corresponding x_new = 0.8385876.

For x=0, x_new will still be 0 (I will leave the calculations to you).

So starting from (1.0, 0.2, 0.0) your new set of points are (1.0, 0.8385876, 0.0).

This is one example of mapping function. There are infinite number of them. Choose one that looks best to you.

Upvotes: -1

Related Questions