The HSV Color Space and you!

When talking about Color Spaces most of us instantly think of the RGB space. Thats nice and all, since RGB is probably the most frequently used color space in programming… errr, don’t take that at face value, I don’t think I want to know what arcane color spaces the programmers of Adobe dive into when fiddling with the Photoshop source code. (Psst. Adobe-RGB is a thing!)
But for the most part, we tinker with the RGB color space (or RGBA / ARGB, if your color has to be really Alpha!)

And speaking of Alpha, there is a time when a man has to be a man, a woman a woman and RGB RGB. However, there are also times where a man has to be a woman and RGB has to be HSV, for example if we want to create nice saturation and hue shifts when post processing images. Like in the thumbnail! (Yes, it is thematically appropriate!)

Now I am not implying that HSV is the female equivalent of RGB, but I think a pseudo scientific study on the internet has proven that men are distinctly worse at differenciating hues than women…

So let us take a look at HSV, because HSV knows its hues.

HSV – Hue Saturation Value

HSV is a cylindrical Color Space. Which means, that while in RGB we would say something is not red, not blue but a 1/1 green, in HSV we would say : 120° in Hue, a Saturation of 1 and a Value of 1.

Illustration 1: By Jacob Rus – Own work, CC BY-SA 3.0,

I am sure most of you, hadn’t I just mentioned a green color, and hadn’t I giveth thou this illustration of the HSV Cylinder, would have no clue that HSV(120°,1,1) is indeed a green.

However, the HSV way of representing colors is much more intuitive to people that actually understand colors, like artists!
You have to admit, it’s easier to say: „Hey, lets desaturate that a bit!“, instead of: „Hey, let’s add and subtract colors from this color, so we end up with a flatter distribution of the RGB components while maintaining the dominant component!“, which essentially is desaturation.

HSV was developed in 1970 and derives from the RGB Color-Space. Today you can see HSV in all it’s glory in many Color-Pickers and in Image Editing software.

Converting RGB to HSV and back

As I mentioned before, there are tims where RGB has to be HSV. For example if we want to interpolate between hues for a fancy image effect or apply darkening or oversaturation to images. Since HSV is based on RGB, color values can be converted back and forth between those two systems with relative ease. Let’s begin by converting RGB to HSV, first in theory, then in code.

Converting RGB to HSV always follows the same basic steps.

Step 1. Calculate necessary values

$Cmax = max(r,g,b)$ The most dominant color value

$Cmin = min(r,g,b)$ The least dominant color value

$\Delta = Cmax - Cmin$ The delta of those two.
Step 2. Calculating the V component (Value)

The V Component is quite simply always equal to the most dominant color value.

$V=Cmax$
Step 3. Calculating the S component (Saturation)

The saturation is equal to the delta of the most and least dominant color divided by the most dominant color. However, if the most dominant color value is 0, which means we deal with the color black, S defaults to 0.

$s =\begin{Bmatrix} 0 & if Cmax = 0\\ \frac{\Delta }{Cmax} & if Cmax \neq 0 \end{Bmatrix}$

Step 4. Calculating the H component (Hue)

Illustration 2: By Jacob Rus [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0) or GFDL (http://www.gnu.org/copyleft/fdl.html)], from Wikimedia Commons

Now comes the dicey part. Since the Hue value is denoted in degrees, we will have to somehow calculate at which angle our color resides. The common approach to this is to determine which color is the most dominant color and take it’s angle as an offset. ( Red = 0, Green = 120, Blue = 240 )

We then calculate the delta between the two remaining color values and divide this by the delta of our maximum and minimum value (d). The result is the angle offset from our base color, however relative to only one of the cylinders segments. (Where a result of 0.5 would equal 30°, since one segment has 60° in total, as shown by illustration 2). This means, that in order to get the actual hue value, we need to multiply our result by 60.

There is however another special case. If our maximum color value is zero, the H component is undefined, which for us means H is -1. Note that r g and b stand for the components of our RGB color.

The resulting formula looks something like this:

$H = \begin{Bmatrix} \frac{g-b}{\Delta }& if Cmax = r \\ 2+\frac{b-r}{\Delta }& if Cmax = g\\ 4+\frac{r-g}{\Delta } & if Cmax = b\\ -1 & if Cmax = 0 \end{Bmatrix}$

Let us take a look at some code for this. This is the Cg (nvidia) shading language btw and should be almost 1 to 1 translatable to HLSL or C / C++.

// Helper function to convert rgb values to hsv space
float3 rgb2hsv(float3 i) {
float h, s, v;

// Step 1
float Cmax = max(i.r, max(i.g, i.b));
float Cmin = min(i.r, min(i.g, i.b));
float d = Cmax - Cmin;

// Step 2
v = Cmax;

// Step 3
if (Cmax != 0)
s = d / Cmax;
else {
s = 0;
// h = -1 is part of step 4, but we can conveniently set it here and return
h = -1;
return float3(h, s, v);
}

// Step 4
if (i.r == Cmax)
h = (i.g - i.b) / d;
else if (i.g == Cmax)
h = 2 + (i.b - i.r) / d;
else
h = 4 + (i.r - i.g) / d;

h *= 60;

// Hue has to be positive
if (h < 0) {
h += 360;
}

return float3(h,s,v);
}

Similarly Converting HSV to RGB can be performed as follows. Note that in the following h s and v are the components of our HSV color.

Step 1. Find the Chroma

The Chroma is the value of our HSV color multiplied by it’s saturation

$C = v * s$

Step 2. Find the first two RGB components

First we bring our hue back from cylinder representation back to the segment representation by dividing it by 60. This will tell us in which cylinder segment our hue value is located. Next, we calculate the base value of the second largest component using this formula.

$h_{0} = \frac{h}{60}$

$c_{1} = C * (1-\left | mod(h_{0},2)-1 \right |)$

The next step is to find out which component of our RGB color is the most dominant one. We can tell which one that is by looking at the cylindrical representation of the HSV space. If our hue value is inside the first segment, the dominant color is red. So all values of h0 from 0 to 1 mean that the RGB color’s R component is the most dominant component. We already know the base value of the second largest RGB component, but we don’t yet know whether it’s the G or B component. Looking back at Illustration 2 it’s strikingly easy to tell. The second segment of the cylinder houses all yellow colors. In RGB space, yellow is the result of adding red and green, which you can also tell by the fact that the very next segment does indeed contain all the green colors. This means that if h0 ranges from 0 to 1, the second largest component is the green component.

Following this basic scheme we can create a template rgb0.

$c_{0} = C$

And given the colors  r g b

$rgb_{0} =\begin{Bmatrix} (c_{0},c_{1},0) & if 0 \leq h_{0} < 1\\ (c_{1},c_{0},0) & if 1 \leq h_{0} < 2\\ (0, c_{0},c_{1}) & if 2 \leq h_{0} < 3\\ (0, c_{1},c_{0}) & if 3 \leq h_{0} < 4\\ (c_{1},0,c_{0}) & if 4 \leq h_{0} < 5\\ (c_{0},0,c_{1})& if 5 \leq h_{0} < 6\\ (0,0,0) & if h = -1 \end{Bmatrix}$

Step 3. Ramp up the brightness

We are almost there, all that’s left to do is to calculate a brightness offset, which conveniently is also equal to the smallest RGB component.

This brigthness is calculated by subtracting our chroma from our color’s value.

$\Delta = v - C$

Lastly, increment all components of the rgb color by said brightness. In this case r0 g0 b0 are components of our template rgb0.

$rgb = (r_{0} + \Delta , g_{0} + \Delta, b_{0} + \Delta)$

Here’s some code.

// Helper function to convert hsv values to rgb space
float3 hsv2rgb(float3 i) {
// for convenience
float h = i.x;
float s = i.y;
float v = i.z;
// no saturation, grayscale
if (s == 0)
return float3(v, v, v);

// Step 1
float chroma = v * s;

// Step 2
float h0 = h / 60.0;
float sndCol = chroma*(1 - abs(h0 % 2 – 1));

float3 rgb0;
if (h == -1)
rgb0 = float3(0, 0, 0);
else if (0 <= h0 && h0 < 1)
rgb0 = float3(chroma, sndCol, 0);
else if (1 <= h0 && h0 < 2)
rgb0 = float3(sndCol, chroma, 0);
else if (2 <= h0 && h0 < 3)
rgb0 = float3(0, chroma, sndCol);
else if (3 <= h0 && h0 < 4)
rgb0 = float3(0, sndCol, chroma);
else if (4 <= h0 && h0 < 5)
rgb0 = float3(sndCol, 0, chroma);
else if (5 <= h0 && h0 < 6)
rgb0 = float3(chroma, 0, sndCol);

// Step 3
float d = v - chroma;

return float3(rgb0.r + d, rgb0.g + d, rgb0.b + d);
}

Changing saturation without HSV

I mentioned that we (non inclusive we if you are a woman. You are quite obviously not a man in that case, unless you want to then I guess welcome to the club!) men are bad with hues. But we can easily tell differences in saturation! One more thing that we have in common with the RGB color space, really. When researching this topic I stumbled over an alarming amount of people that suggested converting colors from RGB to HSV and back to manipulate saturation. And while of course that works, it is by no means efficient, if all you want to do is make your image a bit less saturated, especially since saturation changes can be performed on rgb values just as easily.

Imagine your RGB colors as an equalizer visualization. (The thing that goes up and down in your favorite media player when the bass drops). What we as humans percieve as „saturated“, is how drastically the dominant color stands out compared to that color’s lesser components. A very spiky curve would appear relatively saturated, while a flat line is just a gray without any color to it.

You can probably already tell that saturation has a lot to do with perception, since a dark red brown can either be the natural color of bricks or an oversaturated roof. A deep blue can be the natural color of the ocean or an oversaturated shallow stream.

Knowing this, we can manipulate the saturation of an RGB color by maintaining it’s most dominant color, but weakening the two lesser components, while still mainting the relative amounts of he latter two to each other. We do this by way of simple linear interpolation, where the color amount lost each step is relative to the current colors starting point and it’s distance to the most dominant color. Since the distance between the most dominant color and itself is always zero, this component will never lose any intensity, the other two however will gradually go down towards zero or towards the most dominant components value, resulting in a flat distribution of RGB values and thus in a grayscale picture.

Have some code:

// Changes the Saturation of an RGB color
fixed3 ChangeSaturationBy(fixed3 rgb, float sat) {

// determine the dominant color
fixed cmax = max(rgb.r, max(rgb.g, rgb.b));

// lower the non dominant colors, maintaining relative distance to each other
return fixed3(
saturate(cmax + (rgb.r - cmax) * sat),
saturate(cmax + (rgb.g - cmax) * sat),
saturate(cmax + (rgb.b - cmax) * sat)
);
}