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 0-255 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 0-255. 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) / (maxColor-minColor); 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: