Saturday, 6 September 2014


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. 


  1. Thanks for your articles! Using the shader you posted which allows to convert RGB to HSV and back, i managed to properly fix the colors in damn Fable Anniversary!
    1st i analyzed HSV graphs in image editor and notticed that value ends at about 75% of entire range, while hue and saturation are pretty fine. Then i manipulated value curve (which was basically setting final control point to 75%) which stretched that 75% to 100%, image was finally fine. but soon after i realised that i dont know how i can reproduce this manipulation in shader, so it started to google for HSV conversion shaders and this brought me to your site. Before i reed this arcticle i just took your shader and wrote mine that includes yours:

    #include "ColorModel.h"

    #define startHUE 1.0
    #define startSAT 1.0
    #define startVAL 0.75
    #define finalHUE 1.0
    #define finalSAT 1.0
    #define finalVAL 1.0

    float4 HSVPass(float4 colorInput)
    // Fable Anniversary pixels brightness value is about 75%

    float3 colorHSV;
    float4 color;

    colorHSV = RGBtoHSV(colorInput.rgb);

    colorHSV.r = ((colorHSV.r/(startHUE))*(finalHUE));
    colorHSV.g = ((colorHSV.g/(startSAT))*(finalSAT));
    colorHSV.b = ((colorHSV.b/(startVAL))*(finalVAL));

    color.rgb = HSVtoRGB(colorHSV.rgb);
    color.a = colorInput.a;
    return saturate(color);

    and by changing startVAL to 0.75 (purpose of this is to tell initial value camparing to what considered to be a 100% aka 1.0) i did the trick exactly as i did in image editor:

    Also added finishVAL to manipulate post 1.0 value and know and tell shader exactly how much % of original value post initial correction i would have

    i decided to add HUE and SAT controls same way with start and finish values, and that allows to manipulate entire HSV very easy way. I use it in shader injectors for varios games now, results of manipulating only a value or saturation are much better than controlling gamma, brightness and contrast in many games that have them all messed up, limited in range and giving quite unpredictable and unpleasant results.

    Gonna try it with BF3 infamous by its extremely washed out color grading, GTA 4 which suffering of extreme desaturation and Witcher 2 with its extremely oversaturated "acid" colors where skin is absolutely RED.

    Thanks again for your help! Oh btw, im going to share that with public on steam and other gaming forums, and this must have included shader of yours, i wrote your copyrights in incudes like that:

    // Code borrowed from here
    // (c) by Chilli Ant aka IAN TAYLOR

    Are you ok with that?

  2. v00d00m4n, that's fine. As you may have guessed, I used to work in the games industry and was always annoyed when "artistic directors" would control the final brightness/contrast/saturation levels of the entire game arbitrarily. Don't they realise we've got those controls on our TVs/monitors?

    I'm intrigued by your injection of shaders into published games to get around the artistically-clamped ranges. Do you have a link to a description of this technique?

  3. Oh, sorry for late reply, well I used fxaa and smaa shader injectors and added custom shader to pack of shaders known as SweetFX, which is quite popular thing. Now they all got replaced by new more powerful (this time around it has access to depth buffer, automatically translates HLSL code to be compatible with d3d8, d9, d10, d11 and most wonderful thing - OpenGL, so yea typical directX hlsl code can be used in opengl game without porting) injector called Reshade, which you can fine here as well as budled version of SweetFX and few more shader packs you can find on forums there.

    It would be nice if you will join community and contribute some shaders to be used in game as post processing, im sure you have a lot of knowledge to share there :-)

    BTW, I was just going to post there more advanced version of what I posted here to promote it to adoption by some of those packs and decide to look at your site to find out what Y and L means in HCL and HCY because google not very aware of them, I don't really get the different between L and Y, both looks like Luma to me, aren't they?

    Af for artistic directors, yeah, I agree, their vision sometimes is pain in ass of every department, be it programming. or even game design and writing (that what I do actually), where their vision often makes no sense.

    Oh another thing, now im dealing with game overuse of ugly effects like lense flares and drity chromatic aberration and stuff like that with another technique - using helixmod or 3dmigoto which allows to dump and reinject edited shaders back to game, to disable that. Also using this method as trick to disable some annoying cluttering or immersion breaking UI and some bad color grading :-)