HSL is another way to describe color with 3 parameters. RGB is the way computer
screens work, but not very intuitive. HSL is more intuitive, but you need to
convert it to RGB before you can draw a pixel with it. The nicest application of
this color model is that you can easily create rainbow gradients or change the
color, lightness or saturation of an image with this color model.
HSL color obviously has the parameters H, S and L, or Hue, Saturation and
Lightness.
Hue indicates the color sensation of the light, in other words if the
color is red, yellow, green, cyan, blue, magenta, ... This representation looks
almost the same as the visible spectrum of light, except on the right is now the
color magenta (the combination of red and blue), instead of violet (light with a
frequency higher than blue):
Hue works circular, so it can be represented on a circle instead. A hue of 360°
looks the same again as a hue of 0°.
Saturation indicates the degree to which the hue differs from a neutral
gray. The values run from 0%, which is no color, to 100%, which is the fullest
saturation of a given hue at a given percentage of illumination. The more the
spectrum of the light is concentrated around one wavelength, the more saturated
the color will be.
Lightness indicates the illumination of the color, at 0% the color is
completely black, at 50% the color is pure, and at 100% it becomes white. In HSL
color, a color with maximum lightness (L=255) is always white, no matter what
the hue or saturation components are. Lightness is defined as (maxColor+minColor)/2
where maxColoris the R, G or B component with the maximum value, and minColor
the one with the minimum value.
In this tutorial, Hue, Saturation and Lightness will be presented as numbers
between 0255 instead, so that the HSL model has the same 24 bits as the RGB
model.
The HSL color model is for example used in Paint Shop Pro's color picker.
The HSV Color
Model
The HSV color model (sometimes also called HSB), uses the parameter Value
instead of Lightness. Value works different than Lightness, in that the color
with maximum value (V=255) can be any color like red, green, yellow, white,
etc..., at it'smaximum brightness. Value is defined as maxColor, where maxColor
is the R, G or B component with the maximum value. So the colors red (255,0,0)
and white (255,255,255) both have a Value of 255 indeed.
In HSL, the Lightness showed the following behavior when increased:
In HSV, Value does the following:
The Hue and Saturation parameters work very similar to the ones in HSL. HSV is
generally better at representing the saturation, while HSL is better at
representing the brightness. However, HSV is again better to decrease the
brightness of very bright images.
We can compare the HSL and HSV model a bit better by comparing their plots:
Here is the plot of HSL (left) and HSV (right) with S=255, Hue on the horizontal
axis, and Lightness/Value on the vertical axis (maximum lightness at the top):
While the top of the HSL curve is white because white is the color with maximum
brightness, the top of the HSV curve contains all colors, because the saturation
is 255 and in HSV, saturation 255 has to be a color while white should have 0
saturation. The top of the HSV curve is the same as the center horizontal line
of the HSL curve, and the complete HSV curve is exactly the same as the bottom
half of the HSL picture.
Here is the plot of HSL (left) and HSV (right) with L=255 and V=255
respectively, Hue on the horizontal axis, and Saturation on the vertical axis
(maximum saturation at the bottom):
The HSL curve is completely white, because white is the only color with L=255 in
the HSL model. The HSV curve, now shows all colors that have one or more of
their color components equal to 255. The HSV picture here, is exactly the same
as the top half of the previous HSL picture where S=255.
And here's the plot of HSL (left) and HSV (right) with L=128 and V=128
respectively, and again Hue on the horizontal axis, and Saturation on the
vertical axis (maximum saturation at the bottom):
Color
Model Conversions
To draw the plots given above, color model conversion functions have to be used:
first you describe the color as HSL or HSV, but to plot it on screen, it has to
be converted to RGB first. Transformations from RGB to HSL/HSV are handy as
well, for example if you load an RGB image and want to change it hue, you have
to convert it to HSL or HSV first, then change the hue, and then change it back
to RGB.
The color model conversion formulas are already in QuickCG, in the QuickCG.cpp
file.
RGB to HSL
The following function converts from RGB color to HSL color. You give it a
ColorRGB and it returns a ColorHSL. Both ColorRGB and ColorHSL are simple
structs with 3 integers, the only difference is their names. The RGBtoHSL
function calculates the values for ColorHSL.
First the variables r, g, b, h, s, l are declared as floating point numbers.
Internally, the function works with floating point numbers between 0.0 and 1.0,
for better precision. At the end of the function, the results can very easily be
converted back to integers from 0255. The function can also easily be modified
to work with other ranges, e.g. if you'd want to use 16 bit per color channel,
or represent Hue as a value between 0° and 360°.
ColorHSL RGBtoHSL(ColorRGB colorRGB)
{
float r, g, b, h, s, l; //this function works with floats between 0 and 1
r = colorRGB.r / 256.0;
g = colorRGB.g / 256.0;
b = colorRGB.b / 256.0;

Then, minColor and maxColor are defined. Mincolor is the value of the color
component with the smallest value, while maxColor is the value of the color
component with the largest value. These two variables are needed because the
Lightness is defined as (minColor + maxColor) / 2.
float maxColor = max(r, max(g, b));
float minColor = min(r, min(g, b));

If minColor equals maxColor, we know that R=G=B and thus the color is a shade of
gray. This is a trivial case, hue can be set to anything, saturation has to be
set to 0 because only then it's a shade of gray, and lightness is set to R=G=B,
the shade of the gray.
//R == G == B, so it's a shade of gray
{
h = 0.0; //it doesn't matter what value it has
s = 0.0;
l = r; //doesn't matter if you pick r, g, or b
}

If minColor is not equal to maxColor, we have a real color instead of a shade of
gray, so more calculations are needed:
 Lightness (l) is now set to it's definition of (minColor + maxColor)/2.
 Saturation (s) is then calculated with a different formula depending if
light is in the first half of the second half. This is because the HSL model
can be represented as a double cone, the first cone has a black tip and
corresponds to the first half of lightness values, the second cone has a
white tip and contains the second half of lightness values.
 Hue (h) is calculated with a different formula depending on which of the
3 color components is the dominating one, and then normalized to a number
between 0 and 1.
else
{
l = (minColor + maxColor) / 2;
if(l < 0.5) s = (maxColor  minColor) / (maxColor + minColor);
else s = (maxColor  minColor) / (2.0  maxColor  minColor);
if(r == maxColor) h = (g  b) / (maxColor  minColor);
else if(g == maxColor) h = 2.0 + (b  r) / (maxColor  minColor);
else h = 4.0 + (r  g) / (maxColor  minColor);
h /= 6; //to bring it to a number between 0 and 1
if(h < 0) h ++;
}

Finally, H, S and L are calculated out of h,s and l as integers between 0 and
255 and "returned" as the result. Returned, because H, S and L were passed by
reference to the function.
ColorHSL colorHSL;
colorHSL.h = int(h * 255.0);
colorHSL.s = int(s * 255.0);
colorHSL.l = int(l * 255.0);
return colorHSL;
}

HSL to RGB
This is the opposite conversion, so this function will calculate the inverse of
the RGBtoHSL function.
First, internally the variables with small letters are defined as floating point
numbers between 0 and 1 again. Some temporary values for the calculations are
also declared.
ColorRGB HSLtoRGB(ColorHSL colorHSL)
{
float r, g, b, h, s, l; //this function works with floats between 0 and 1
float temp1, temp2, tempr, tempg, tempb;
h = colorHSL.h / 256.0;
s = colorHSL.s / 256.0;
l = colorHSL.l / 256.0;

Then follows a trivial case: if the saturation is 0, the color will be a
grayscale color, and the calculation is then very simple: r, g and b are all set
to the lightness.
//If saturation is 0, the color is a shade of gray
if(s == 0) r = g = b = l;

If the saturation is higher than 0, more calculations are needed again. red,
green and blue are calculated with the formulas defined in the code.
//If saturation > 0, more complex calculations are needed
else
{
//Set the temporary values
if(l < 0.5) temp2 = l * (1 + s);
else temp2 = (l + s)  (l * s);
temp1 = 2 * l  temp2;
tempr = h + 1.0 / 3.0;
if(tempr > 1) tempr;
tempg = h;
tempb = h  1.0 / 3.0;
if(tempb < 0) tempb++;
//Red
if(tempr < 1.0 / 6.0) r = temp1 + (temp2  temp1) * 6.0 * tempr;
else if(tempr < 0.5) r = temp2;
else if(tempr < 2.0 / 3.0) r = temp1 + (temp2  temp1) * ((2.0 / 3.0)  tempr) * 6.0;
else r = temp1;
//Green
if(tempg < 1.0 / 6.0) g = temp1 + (temp2  temp1) * 6.0 * tempg;
else if(tempg < 0.5) g = temp2;
else if(tempg < 2.0 / 3.0) g = temp1 + (temp2  temp1) * ((2.0 / 3.0)  tempg) * 6.0;
else g = temp1;
//Blue
if(tempb < 1.0 / 6.0) b = temp1 + (temp2  temp1) * 6.0 * tempb;
else if(tempb < 0.5) b = temp2;
else if(tempb < 2.0 / 3.0) b = temp1 + (temp2  temp1) * ((2.0 / 3.0)  tempb) * 6.0;
else b = temp1;
}

And finally, the results are returned as integers between 0 and 255.
ColorRGB colorRGB;
colorRGB.r = int(r * 255.0);
colorRGB.g = int(g * 255.0);
colorRGB.b = int(b * 255.0);
return colorRGB;
}

RGB to HSV
The function RGBtoHSV works very similar as the RGBtoHSL function, the only
difference is that now the variable V (Value) instead of L (Lightness) is used,
and Value is defined as maxColor. This can immediately be calculated at the
beginning of the function:
ColorHSV RGBtoHSV(ColorRGB colorRGB)
{
float r, g, b, h, s, v; //this function works with floats between 0 and 1
r = colorRGB.r / 256.0;
g = colorRGB.g / 256.0;
b = colorRGB.b / 256.0;
float maxColor = max(r, max(g, b));
float minColor = min(r, min(g, b));
v = maxColor;

Then, the saturation is calculated. If the color is black, the value of
saturation doesn't matter so it can be set to 0. This has to be done to avoid a
division by zero.
if(maxColor == 0)//avoid division by zero when the color is black
{
s = 0;
}
else
{
s = (maxColor  minColor) / maxColor;
}

Finally, the hue is calculated. If saturation is 0, the color is gray so hue
doesn't matter. Again this case is handled separately to avoid divisions by
zero.
if(s == 0)
{
h = 0; //it doesn't matter what value it has
}
else
{
if(r == maxColor) h = (g  b) / (maxColorminColor);
else if(g == maxColor) h = 2.0 + (b  r) / (maxColor  minColor);
else h = 4.0 + (r  g) / (maxColor  minColor);
h /= 6.0; //to bring it to a number between 0 and 1
if (h < 0) h++;
}

And finally, the results are returned as integers between 0 and 255.
ColorHSV colorHSV;
colorHSV.h = int(h * 255.0);
colorHSV.s = int(s * 255.0);
colorHSV.v = int(v * 255.0);
return colorHSV;
}

HSV to RGB
First the floating point numbers between 0 and 1 are declared again:
ColorRGB HSVtoRGB(ColorHSV colorHSV)
{
float r, g, b, h, s, v; //this function works with floats between 0 and 1
h = colorHSV.h / 256.0;
s = colorHSV.s / 256.0;
v = colorHSV.v / 256.0;

The trivial case for saturation = zero is handled:
//If saturation is 0, the color is a shade of gray
if(s == 0) r = g = b = v;

The HSV model can be presented on a cone with hexagonal shape. For each of the
sides of the hexagon, a separate case is calculated:
//If saturation > 0, more complex calculations are needed
else
{
float f, p, q, t;
int i;
h *= 6; //to bring hue to a number between 0 and 6, better for the calculations
i = int(floor(h)); //e.g. 2.7 becomes 2 and 3.01 becomes 3 or 4.9999 becomes 4
f = h  i; //the fractional part of h
p = v * (1  s);
q = v * (1  (s * f));
t = v * (1  (s * (1  f)));
switch(i)
{
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}

And again, the results are "returned" as integers between 0 and 255.
ColorRGB colorRGB;
colorRGB.r = int(r * 255.0);
colorRGB.g = int(g * 255.0);
colorRGB.b = int(b * 255.0);
return colorRGB;
}

HSL and
HSV Arithmetic
The functions given above are already in the QuickCG, and thanks to them we can
do HSL and HSV arithmetic on images.
The examples will be performed on the flower image again:
Changing Hue
It doesn't matter if you change hue with the HSL or HSV model, the results are
the same, so for no particular reason at all let's do it with HSL here.
The following code will load the BMP image, convert it to HSL, change the Hue by
adding a certain value to it, and convert it back to RGB to display it (put this
code in the main function in the main.cpp file):
ColorRGB image[200][133];
int main(int argc, char *argv[])
{
ColorRGB colorRGB;
ColorHSL colorHSL;
screen(200, 133, 0, "HSL and HSV Color");
loadBMP("pics/flower.bmp", image[0], 200, 133);
for(int x = 0; x < w; x++)
for(int y = 0; y < h; y++)
{
//store the color of the image in variables R, G and B
colorRGB = image[x][y];
//calculate H, S and L out of R, G and B
colorHSL = RGBtoHSL(colorRGB);
//change Hue
colorHSL.h += int(42.5 * 1);
colorHSL.h %= 255;
//convert back to RGB
colorRGB = HSLtoRGB(colorHSL);
//plot the pixel
pset(x, y, colorRGB);
}
redraw();
sleep();
return 0;
}

The Hue is modulo divided through 255, because it has to be between 0 and 255,
and it's circular, so a hue of 260 is the same as a hue of 5. The value 42.5 was
chosen to be 255/6, representing a hue shift of 60°. Here are screenshots of the
result for a hue shift of 0°, 60°, 120°, 180°, 240° and 300° (360° gives the
same result as 0° again):
You can also set Hue to a constant, to give the whole image the same color. For
example, here hue is set to 25, which is orange:
Changing
Saturation
You can change the saturation to make the image more colorful, or more like
pastel, or grayscale. This time, the results are slightly different if you use
the HSL or HSV color model.
For example, to increase the saturation by multiplying it with 2.5, change the
lines of the code that changed the hue to:
colorHSV.s = int(colorHSV.s * 2.5);
if(colorHSV.s > 255) colorHSV.s = 255;

If Saturation is higher than 255, it's truncated. On the left is the result if
you use HSL, on the right if you use HSV. The result is pretty similar to the
original image because the saturation in it was quite high already. Only the
background became a bit more green.
If you multiply it with 0.5 instead, you'll decrease the saturation by halving
it:
You can also decrease the saturation by substracting a value from it instead:
colorHSV.s = colorHSV.s  100;
if(colorHSV.s < 0) colorHSV.s = 0;

The background will be grayscale now, while the flower with it's high saturation
still has some color:
If saturation is set to 0, the image will be grayscale, in a different way if
you use HSL or HSV, and both are also different from the "average" formula to
grayscale an image:
And this is what you get if you set saturation equal to 128 for all pixels. The
flower and background look equally colorful now:
Changing
Brightness
Finally, HSL and HSV can also be used to change the brightness of an image.
Again, the HSL and HSV model will work differently. HSL gives bad result when
making an image with white or near white pixels darker.
You can also decrease the saturation by substracting a value from it instead:
colorHSV.l = 50;
if(colorHSV.l < 0) colorHSV.l = 0;

Here's the result for HSL on the left and HSV on the right:
And setting brightness equal to 192 for all pixels gives: