RGB to HSV color space conversion (C)

From LiteratePrograms

Jump to: navigation, search

The RGB color space, used directly by most computer devices, expresses colors as an additive combination of three additive primary colors of light: red, green, and blue. A commonly used color space that corresponds more naturally to human perception is the HSV color space, whose three components are hue, saturation, and value. The formulas used to convert RGB to HSV depend on which of the RGB components is largest and which is smallest, and are described at Transformation from RGB to HSV.


Contents

Interfaces and preparation

In our first implementation, designed for ease of understanding and accuracy, we use high-precision floating-point operations. The red, green, and blue channels are each specified by values between 0 and 1. The hue is specified by an angle in degrees between 0 and 360, and both saturation and value are specified by values between 0 and 1. Our function will receive a structure as input and return a structure as output:

<<color structure>>=
struct rgb_color {
    double r, g, b;    /* Channel intensities between 0.0 and 1.0 */
};
struct hsv_color {
    double hue;        /* Hue degree between 0.0 and 360.0 */
    double sat;        /* Saturation between 0.0 (gray) and 1.0 */
    double val;        /* Value between 0.0 (black) and 1.0 */
};
<<RGB to HSV conversion function>>=
struct hsv_color rgb_to_hsv(struct rgb_color rgb) {
    perform conversion
}

We will need to know the largest and smallest of the three RGB values:

<<find min and max RGB value>>=
double rgb_min, rgb_max;
rgb_min = MIN3(rgb.r, rgb.g, rgb.b);
rgb_max = MAX3(rgb.r, rgb.g, rgb.b);

The macros MIN3 and MAX3 find the minimum or maximum of three values and are defined as:

<<macros>>=
#define MIN3(x,y,z)  ((y) <= (z) ? \
                         ((x) <= (y) ? (x) : (y)) \
                     : \
                         ((x) <= (z) ? (x) : (z)))
#define MAX3(x,y,z)  ((y) >= (z) ? \
                         ((x) >= (y) ? (x) : (y)) \
                     : \
                         ((x) >= (z) ? (x) : (z)))

Converting to HSV

We will compute the values in the following order:

<<perform conversion>>=
struct hsv_color hsv;
find min and max RGB value
compute value
normalize value to 1
compute saturation
normalize saturation to 1
compute hue
return hsv;

The value (V in HSV) is the easiest component to compute, simply being rgb_max. If the value is zero, the color is black and we exit, arbitrarily assigning values to the other channels:

<<compute value>>=
hsv.val = rgb_max;
if (hsv.val == 0) {
    hsv.hue = hsv.sat = 0;
    return hsv;
}

Next, to simplify further calculations we divide all three channels by the HSV value to scale the color up to a color of value 1:

<<normalize value to 1>>=
/* Normalize value to 1 */
rgb.r /= hsv.val;
rgb.g /= hsv.val;
rgb.b /= hsv.val;
rgb_min = MIN3(rgb.r, rgb.g, rgb.b);
rgb_max = MAX3(rgb.r, rgb.g, rgb.b);

Division by zero is avoided because we would have returned if hsv.val = 0.

The saturation (S in HSV) is controlled by how widely separated the RGB values are. When the values are close together, the color will be close to gray. When they're far apart, the color will be more intense or pure:

<<compute saturation>>=
hsv.sat = rgb_max - rgb_min;
if (hsv.sat == 0) {
    hsv.hue = 0;
    return hsv;
}

If the saturation is zero the color is gray and we arbitrarily assign zero to hue and return.

We now normalize the color to give it both a value and saturation of 1:

<<normalize saturation to 1>>=
/* Normalize saturation to 1 */
rgb.r = (rgb.r - rgb_min)/(rgb_max - rgb_min);
rgb.g = (rgb.g - rgb_min)/(rgb_max - rgb_min);
rgb.b = (rgb.b - rgb_min)/(rgb_max - rgb_min);
rgb_min = MIN3(rgb.r, rgb.g, rgb.b);
rgb_max = MAX3(rgb.r, rgb.g, rgb.b);

Finally, hue, which determines whether the color is red, blue, green, yellow, and so on, is the most complex to compute. Red is at 0 degrees, green is at 120 degrees, and blue is at 240 degrees. The maximum RGB color controls our starting point, and the difference of the other two colors determines how far we move away from it, up to 60 degrees away (halfway to the next primary color):

<<compute hue>>=
/* Compute hue */
if (rgb_max == rgb.r) {
    hsv.hue = 0.0 + 60.0*(rgb.g - rgb.b);
    if (hsv.hue < 0.0) {
        hsv.hue += 360.0;
    }
} else if (rgb_max == rgb.g) {
    hsv.hue = 120.0 + 60.0*(rgb.b - rgb.r);
} else /* rgb_max == rgb.b */ {
    hsv.hue = 240.0 + 60.0*(rgb.r - rgb.g);
}

We always subtract in the direction that causes the hue to move toward the larger of the two smaller channel values. Note that moving down from red requires handling of wraparound to ensure that we keep the angle in the 0 to 360 range; this isn't necessary, but the range would be -60 degrees to 300 degrees without it.

Sample code

We can demonstrate the algorithm with this small sample program that takes three RGB components on the command line and outputs the HSV components:

<<rgb_to_hsv.c>>=
#include <stdlib.h>
#include <stdio.h>
macros
color structure
RGB to HSV conversion function
int main(int argc, char* argv[]) {
    struct rgb_color rgb;
    struct hsv_color hsv;
    rgb.r = atof(argv[1]);
    rgb.g = atof(argv[2]);
    rgb.b = atof(argv[3]);
    hsv = rgb_to_hsv(rgb);
    printf("Hue: %f\nSaturation: %f\nValue: %f\n\n", hsv.hue, hsv.sat, hsv.val);
    return 0;
}

Here's a sample invocation corresponding to the light greyish-blue color shown to the right below:

$ ./rgb_to_hsv 0.4392157 0.6745098 0.71372549
Hue: 188.571430
Saturation: 0.384615
Value: 0.713725

Efficient integer version

When converting many colors, for example during certain image format conversions where this needs to be done for each pixel, an optimized implementation is useful. Much of the algorithm is the same, except that we work primarily with integers and we avoid the normalization steps by folding them into subsequent computation. Note that unlike the first version, which is always precise enough to convert back to the same 24-bit RGB color, this one can lose some information.

First, here are our data structures, modified to use integers:

<<integer color structure>>=
struct rgb_color {
    unsigned char r, g, b;    /* Channel intensities between 0 and 255 */
};
struct hsv_color {
    unsigned char hue;        /* Hue degree between 0 and 255 */
    unsigned char sat;        /* Saturation between 0 (gray) and 255 */
    unsigned char val;        /* Value between 0 (black) and 255 */
};

The type of rgb_min and rgb_max similarly changes:

<<integer find min and max RGB value>>=
unsigned char rgb_min, rgb_max;
rgb_min = MIN3(rgb.r, rgb.g, rgb.b);
rgb_max = MAX3(rgb.r, rgb.g, rgb.b);

The value is calculated the same way as before, but instead of normalizing the value to 1, we divide by the value when computing saturation:

<<integer compute saturation>>=
hsv.sat = 255*long(rgb_max - rgb_min)/hsv.val;
if (hsv.sat == 0) {
    hsv.hue = 0;
    return hsv;
}

Note that we also have to scale up by 255, since a quotient of values in the range 0 to 255 will be in the range 0 to 1, and we want our answer in the range 0 to 255. We have to multiply before dividing to avoid truncation.

Finally, we fold the normalization of saturation into the hue computation by dividing the differences by rgb_max - rgb_min:

<<integer compute hue>>=
/* Compute hue */
if (rgb_max == rgb.r) {
    hsv.hue = 0 + 43*(rgb.g - rgb.b)/(rgb_max - rgb_min);
} else if (rgb_max == rgb.g) {
    hsv.hue = 85 + 43*(rgb.b - rgb.r)/(rgb_max - rgb_min);
} else /* rgb_max == rgb.b */ {
    hsv.hue = 171 + 43*(rgb.r - rgb.g)/(rgb_max - rgb_min);
}

Note that, to fit the hues in the 0 to 255 range, we've moved green to 85 and blue to 171, and changed the 60 factor to 43. We no longer need to check if hue is less than zero; C's wraparound semantics do just what we want. There is also no need to adjust for the value normalization, since we're taking quotients which produce fractions between 0 and 1.

The overall conversion function now looks like this:

<<integer RGB to HSV conversion function>>=
struct hsv_color rgb_to_hsv(struct rgb_color rgb) {
    struct hsv_color hsv;
    integer find min and max RGB value
    compute value
    integer compute saturation
    integer compute hue
    return hsv;
}

Here's the sample program for this version:

<<rgb_to_hsv_int.c>>=
#include <stdlib.h>
#include <stdio.h>
macros
integer color structure
integer RGB to HSV conversion function
int main(int argc, char* argv[]) {
    struct rgb_color rgb;
    struct hsv_color hsv;
    rgb.r = (unsigned char)atoi(argv[1]);
    rgb.g = (unsigned char)atoi(argv[2]);
    rgb.b = (unsigned char)atoi(argv[3]);
    hsv = rgb_to_hsv(rgb);
    printf("Hue: %d\nSaturation: %d\nValue: %d\n\n", hsv.hue, hsv.sat, hsv.val);
    return 0;
}
$ ./rgb_to_hsv 112 172 182
Hue: 135
Saturation: 98
Value: 182
Download code
Views