1

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.

Michel Rouzic
  • 1,013
  • 1
  • 9
  • 22
  • See http://stackoverflow.com/a/141943/5987 – Mark Ransom Apr 22 '13 at 19:15
  • Thanks Mark, it seems to look about right in your image. Is that basically the same thing as I said in my 3rd paragraph except instead of splitting the excess in 2 until all the excess of every channel is spread out you split it in 3 and just clip whatever still exceeds (and also you do it in sRGB)? Also doesn't it bother you to do it in something as arbitrary as sRGB? I don't do anything in sRGB, it's all gotta be linear, I think if I don't know how to do it in linear colour space (or at least in not totally arbitrarily-defined colour space like sRGB) then I don't truly know how to do it. – Michel Rouzic Apr 22 '13 at 19:40
  • 1
    I don't split the excess evenly, although that may be hard to see in the formula. By multiplying the original values I retain their original proportions and thus the hue. The output of my (modified) code with your 3rd paragraph example is `(1.0, 0.6615, 0.5385)` - I should probably post the modified code as an answer. You're absolutely right that the results are better in a linear space, but I've found sRGB to be "good enough" most of the time. – Mark Ransom Apr 22 '13 at 20:09
  • Oh so it's proportional to conserve the hue (yeah I had a little bit of trouble full figuring out what it did hehe), that seems like a good idea. I think we're onto something here, I think the final piece of the puzzle would have to do with weighting (as in, maybe it doesn't go up straight like this until it hits the final white limit, maybe it's more like a curved shoulder, as in sensitometry curves?) – Michel Rouzic Apr 22 '13 at 20:16

3 Answers3

1

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).

Community
  • 1
  • 1
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • OK I tried it unfortunately this is kind of what I feared :-/, when things start going white it just looks like a linear gradient of white is being superimposed, with the relatively sharp start characteristic of linear gradients (see 3 examples of linear ramps using that algo here http://i.imgur.com/sKG69tH.png ). I guess it's not as much a problem in sRGB because it's more "eased in". Maybe I should try to see how making a colour brighter works in CIELab/CIEXYZ or something like that, though I wouldn't mind an even more scientific approach. – Michel Rouzic Apr 23 '13 at 16:51
  • As for the non-linear part I guess that's something that can be done by scaling all channels using a function when the maximum is over the threshold, the real question is, with what function and why. It seems like basically your function adds/mixes in a certain amount of white when one channel is over the threshold, I think somehow we can make this be not linear. – Michel Rouzic Apr 23 '13 at 16:53
  • @MichelRouzic, your example was very illuminating, thanks. I think the problem is that my algorithm treats all colors as having an equal impact on the outcome, while darker colors like blue should count for less than lighter colors like green. You can see that the discontinuity is most prevalent in the blue example while being least visible in the orange. – Mark Ransom Apr 23 '13 at 16:58
  • I think I've found a way to fix that problem. Instead of directly transferring values between channels I interpolate with the grey value of same perceptual luminosity until the value is in-gamut again. Thanks for getting me on that track! Also see the details of my solution: http://stackoverflow.com/questions/16154269/algorithm-to-make-overly-bright-hdr-colours-become-white/16366423#16366423 – Michel Rouzic May 03 '13 at 19:46
1

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.

Michel Rouzic
  • 1,013
  • 1
  • 9
  • 22
-1

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.

ElKamina
  • 7,747
  • 28
  • 43
  • Okay, so let's say you have {1.6, 0.5, 0.1}, what does it turn into (how do you compute it, how does it spread across channels, how does it become white)? Also I don't want to touch the linearity of the [0.0, 1.0] range, because that wouldn't make sense to touch it. – Michel Rouzic Apr 22 '13 at 18:48
  • Thanks for the elaboration, but it doesn't take you any closer to a white, does it? For the sake of simplicity imagine having a {6.0, 0.0, 0.0} red, you most definitely do not want {1.0, 0.0, 0.0} as a result but rather something pretty close to white, like maybe {1.0, 0.8, 0.8}. You've gotta have a way from channels to cross over. Just out of curiosity where is "f(x) = log(1+x) - log(1-x)" coming from, what does it represent? – Michel Rouzic Apr 22 '13 at 19:32
  • What do you mean by (6.0, 0.0, 0.0)? it is out of bounds right? It is irrelevant to even consider stuff that is out of range. If you use the transformation I mentioned, you will never go out of bounds. – ElKamina Apr 22 '13 at 20:03
  • The question is all about what to do with out of bounds values, and the problem with your transformation is that no matter how much you multiply a primary colour (like full red multiplied by 6, or 1,000 for that matter) it will never become white, which is not realistic. – Michel Rouzic Apr 22 '13 at 20:20
  • Crossing over to other primary colors is a risky proposition. There are models for digital overexposure, but they do not deal will crossing over. When you "digitally" overexpose red you get bright red, not white. You can create your own models with little effort (create three functions, one for each primary color and add them). Otherwise, you can restrict to cases where all the primary colors have intensity > 0 and when you overexpose it enough you will get white. – ElKamina Apr 22 '13 at 20:43
  • Well in that case those models are wrong (if you see the frequency response of color receptors in the human eye or of filters in cameras it's plain to see why), and having all channels have more than 0 isn't a satisfactory way to do it either (because then whether you have 0.000001 or 0.01 makes a huge difference when you multiply it by 10 or 100 whereas you couldn't see the difference otherwise). – Michel Rouzic Apr 22 '13 at 20:49
  • To quote a great man "All models are wrong, but some models are useful". You need to tweak it and change it until you can get something useful. – ElKamina Apr 22 '13 at 21:01