HSV color transforms
Preliminary
All of the operations here require multiplication of matrices and vectors. Since MathML isn't very universal and I hate using images to represent things, I'll just use standard HTML tables to lay them out. Hopefully your web browser supports theinline-table layout, and a simple multiplication of a matrix and a vector will look like this:
| Ax | Ay | Az |
| Bx | By | Bz |
| Cx | Cy | Cz |
| Vx |
| Vy |
| Vz |
| AxVx+ AyVy + AzVz |
| BxVx+ ByVy + BzVz |
| CxVx+ CyVy + CzVz |
(If each thing ends up on its own line, just pretend they line up horizontally. Maybe in the future I'll just replace them with images. Incidentally, Safari is the only browser I've seen which does this right, and even then it still stacks some tables vertically for some reason.)
Matrix multiplication, like numerical multiplication, is associative, but unlike numerical mutiplication, is not commutative. This means that if you have multiple matrix operations to perform to a single vector, like v'=ABCDv, you can combine them together into a single master matrix where Z=ABCD and then v'=Zv. This will be useful later on.
Step 1: Convert RGB → YPbPr
RGB values aren't very convenient for doing complex transforms on, especially hue. The math for doing a hue rotation on RGB is nasty. However, the math for doing a hue rotation on YPbPr is very easy. So what's YPbPr? It's a simple color space like YUV, where the primary axis is the the average of the three channels, and the other two axes are the offsets of the blue and red channels from the brightness channel.Strictly-speaking, Y isn't the luminance, as this model doesn't account for the perceptual differences between red, green, and blue; as such this isn't the same Y as in the YUV color space. On that note, you can use YUV instead of YPbPr and the math will work out nearly the same (although the hue transform will generate slightly different results); I'll leave it up to the reader to find the appropriate transforms for that.
The conversion is pretty straightforward, with
Pb = b - Y = 2b/3 - r/3 - g/3
Pr = r - Y = 2r/3 - g/3 - b/3
| 1/3 | 1/3 | 1/3 |
| -1/3 | -1/3 | 2/3 |
| 2/3 | -1/3 | -1/3 |
Step 2: Hue
Now that our color is in this simple format, doing a hue transform is pretty simple - we're just rotating the color around the Y axis. The math for this is as follows (where H is the hue transform amount, in degrees):S = sin(H*π/180)
Th=
| 1 | 0 | 0 |
| 0 | C | -S |
| 0 | S | C |
| 1 | 0 | 0 |
| 0 | cos(H*π/180) | -sin(H*π/180) |
| 0 | sin(H*π/180) | cos(H*π/180) |
Step 3: Saturation
Saturation is just the distance between the color and the gray (Y) axis; you just scale the Pb and Pr channels. So its matrix is:| 1 | 0 | 0 |
| 0 | S | 0 |
| 0 | 0 | S |
Step 4: Value
Finally, the value transformation is a simple scaling of the color as a whole:| V | 0 | 0 |
| 0 | V | 0 |
| 0 | 0 | V |
Step 5: Convert back to RGB
To convert YPbPr back to RGB, it's also pretty simple; we just reverse the equations from above:b=Y+Pb
g=3Y-r-b=Y-Pr-Pb
| 1 | 0 | 1 |
| 1 | -1 | -1 |
| 1 | 1 | 0 |
Pulling it all together
The sum total transform Thsv is TYPbPrTHTSTVTrgb, which you get by multiplying all the above matrices together. Because matrix multiplication is associative, it's easiest to work out THTSTV first, which is| V | 0 | 0 |
| 0 | VScos(H*π/180) | -VSsin(H*π/180) |
| 0 | VSsin(H*π/180) | VScos(H*π/180) |
a = V*S*cos(H*π/180)/3
b = V*S*sin(H*π/180)/3
| 3k | 0 | 0 |
| 0 | 3a | -3b |
| 0 | 3b | 3a |
a=V*S*cos(H*π/180)/3
b=V*S*sin(H*π/180)/3
Thsv =
| k+2a | -2b | k-a-b |
| -k+a+3b | 3a-b | -k+a-2b |
| 2k-2a | 2b | 2k+a+b |
So show me the code already!
Okay, fine. Here's some simple C++ code to do an HSV transformation on a singleColor (where Color is a struct containing three members, r, g and b with obvious meanings, in whatever data format you want):
Color TransformHSV(
const Color &in, // color to transform
float H, // hue shift (in degrees)
float S, // saturation shift (scalar)
float V // value multiplier (scalar)
)
{
float k = V/3;
float a = V*S*cos(H*M_PI/180)/3;
float b = V*S*sin(H*M_PI/180)/3;
Color ret;
ret.r = (k+2*a)*in.r - 2*b*in.g + (k-a-b)*in.b;
ret.g = (-k+a+3*b)*in.r + (3*a-b)*in.g + (-k+a+2*b)*in.b;
ret.b = (2*k-2*a)*in.r + 2*b*in.g + (2*k+a+b)*in.b;
return ret;
}
What if I want to use someone else's color transform matrix?
Let's say you want to perfectly replicate the color transformation done by some other library or image-manipulation package (say, Flash, for example) but don't know the exact colorspace they use for their intermediate transformation. Well, for any affine transformation (i.e. not involving gamma correction or whatnot), this is actually pretty simple. First, just run the unit colors of red, green, and blue through that color filter, and then those become the rows of your transformation matrix. How convenient is that?Code for that would look something like this:
Color TransformByExample(
const Color &in, // color to transform
const Color &r, // pre-transformed red
const Color &g, // pre-transformed green
const Color &b // pre-transformed blue
)
{
Color ret;
ret.r = in.r*r.r + in.g*r.g + in.b*r.b;
ret.g = in.r*g.r + in.g*g.g + in.b*g.b;
ret.rb= in.r*b.r + in.g*b.g + in.b*b.b;
return ret;
}
And what about gamma correction?
Okay, the various code and equations from above assume linear gamma. Dealing with a non-linear gamma isn't too hard, though; just convert the colors to linear, then convert them back to gamma-space values when you're done. (If you're doing this, hopefully you're storing your values as floats.) For reference, here's how you do that:float LinearToGamma(float value, float gamma) { return pow(value, 1/gamma); }
float GammaToLinear(float value, float gamma) { return pow(value, gamma); }