Saturday 6 September 2014

RGB/HSV in HLSL 6

It is often common to need to take a colour image and change some aspect of it: its hue, saturation, brightness or contrast.

You could simply convert the image to an appropriate colour space (e.g. HSV) perform the manipulations and then convert back to RGB. However, of the three components of the HSV colour space, it is least likely that you'll want to change the hue, which is trickiest component to convert. So can you change the saturation and value without performing the full round-trip conversions?

Remember that for RGB to HSV:

  float U = min(R, min(G, B));
  float V = max(R, max(G, B));
  float C = V - U;

  float S = C / (V + Epsilon);

The hue 'H' is given by the difference between two RGB channel values divided by 'V'.

So if we maintain the absolute differences between RGB channel values, we maintain the chroma 'C' (because the 'V' and 'U' values change by the same amount) but not necessarily the HSV saturation 'S' (because the denominator 'V' changes).

This implies we can easily change the chroma (or HSV saturation) by manipulating 'U' without any impact on the value 'V'. However, changing the value 'V' (or lightness) is not so easy without also changing the chroma (or HSV saturation).

Consider another formulation of HSV saturation (without the epsilon term):

  float S = C / V;
          = (V - U) / V;
          = 1 - (U / V);

It is dependent on the ratio of the minimum and maximum RGB channel values. So if we maintain this ratio, we do not affect the HSV saturation.

Anyway, let's start with what appears to be the easier modification first. Suppose we have an RGB triplet and we want to change its HSV saturation to 'S_wanted':

  float U = min(R, min(G, B));
  float V = max(R, max(G, B));

  float Q = S_wanted * V / (V - U + Epsilon);
  RGB = (V - RGB) * Q;

Above, 'Q' is equivalent to 'S_wanted / S' and 'Epsilon' is an appropriately tiny number, e.g. 1e-10.

Now the trickier modification. Suppose we have an RGB triplet and we want to change its HSV value to 'V_wanted' without changing its HSV saturation (or hue). We can multiply all channel values by
'V_wanted / V' and then apply the saturation modification algorithm above to restore the original saturation. But wait! The channel multiplication does not change the ratio of the minimum and maximum RGB channel values, so it therefore does not change the saturation after all. Only a simple multiplicative scaling is required:

  float V = max(R, max(G, B));
  RGB *= V_wanted / (V + Epsilon);

Please remember that we're talking about simplifying modifications in the HSV colour space here. HSL is another matter entirely.