Вы находитесь на странице: 1из 6

1/26/2011 HSV color transforms

busybee
Because the need for color manipulation comes up fairly often in computer graphics, Code
Clicker/
particularly transformations of hue, saturation, and value, and because some of this Web/
AudioCompress
math is a bit tricky, here's how to do HSV color transforms on RGB data using simple HSV color transforms
matrix operations. Forum | Site Map | Legal

Note that this isn't for converting between RGB and HSV; this is only about applying an
HSV-space modification to an RGB value and getting another RGB value out. There is no affine transformation to convert
between RGB and HSV, as HSV is not a linear color space.

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
the inline-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

=
Ax V x + Ay V y + AzV z

Bx Vx + By Vy + BzVz

Cx Vx + Cy Vy + CzVz

Conveniently, that also serves as the crash course on how to multiply a matrix and a vector. (To multiply two matrices, you
treat each column of the right-hand matrix as a separate vector and multiply each of them by the left-hand matrix, and
then stack the result vectors horizontally.)

(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.

A note about response curves

The math involved assumes that we are dealing with a linear color space. However, most displays use an exponential
curve. For this to behave completely accurately, you'll have to convert your colors to linear color before, and to
beesbuzz.biz/…/hsv_color_transforms.p… 1/6
1/26/2011 HSV color transforms
exponential color afterwards. A pretty close approximation (assuming a display gamma of 2.2) is as follows, where Vl is
the linear value, Ve is the exponential value, and M is the maximum value in the exponential color space; for traditional 8-
bit/channel images this is 255.

Vl = (Ve /M)2.2
Vg = M(Ve )0.455

Note that Vl will be in the range of 0 to 1 and will be a floating point value; if you are going to do this on a per-pixel basis
in real time you will almost certainly want to precompute this as a lookup table. If you absolutely require integer math the
whole way through you'll also need to scale that value.

For the remainder of the math, we don't care about the scale of the values, as long as they start at 0.

Step 1: Convert RGB → YIQ

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 YIQ is very easy; YIQ is a color space which uses the
perceptive-weighted brightness of the red, green and blue channels to provide a luminance (Y) channel, and places the
chroma values for red, green and blue roughly 120 degrees apart in the I-Q plane.

Note that there are many color spaces that you can use for this transform which have different hue-mapping
characteristics; strictly-speaking, there is no single natural "angle" between any given colors, and different effects can be
achieved by using different color spaces such as YPbPr or YUV. (An earlier version of this page used YPbPr for
simplicity, but that led to much confusion when people expected the 180-degree rotation of red to be cyan, which is the
case in YIQ but not in YPbPr.)

The transformation from RGB to YIQ is best expressed as a matrix, with the RGB value multiplied as a 1x3 vector on the
right:

T Y IQ =

0.299 0.587 0.114

0.596 -0.274 -0.321

0.211 -0.523 0.311

One convenient property of the YIQ color space is that it is essentially unit-independent, so we don't have to care about
what our range of colors is, as long as it starts at 0.

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):

U = cos(H*π/180)
W = sin(H*π/180)

Th =

beesbuzz.biz/…/hsv_color_transforms.p… 2/6
1/26/2011 HSV color transforms

1 0 0

0 U -W

0 W U
=
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 I and Q channels. So its matrix
is:

Ts =

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:

Tv =

V 0 0

0 V 0

0 0 V

Step 5: Convert back to RGB

To convert YIQ back to RGB, it's also pretty simple; we just use the inverse of the first matrix:

T rgb =

1 0.956 0.621

1 -0.272 -0.647

1 -1.107 1.705

Pulling it all together

beesbuzz.biz/…/hsv_color_transforms.p… 3/6
1/26/2011 HSV color transforms
The final composed transform Thsv is TrgbTHTSTVTYIQ (we compose the matrices from right-to-left since the original color
is on the right - matrix multiplication is non-commutative), 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 VSU -VSW

0 VSW VSU

where X and Y are the same as in TH, above.

From here we can more conveniently get the master transform:

0.299 0.587 0.114

0.596 -0.274 -0.321

0.211 -0.523 0.311


*
V 0 0

0 VSU -VSW

0 VSW VSU
*
1 0.956 0.621

1 -0.272 -0.647

1 -1.107 1.705
=
.299V+.701VSU+.168VSW .587V-.587VSU+.330VSW .114V-.114VSU-.497VSW

.299V-.299VSU-.328VSW .587V+.413VSU+.035VSW .114V-.114VSU+.292VSW

.299V-.3VSU+1.25VSW .587V-.588VSU-1.05VSW .114V+.886VSU-.203VSW

As a sanity check, we test the matrix with V=S=1 and H=0 (meaning U=1 and W=0) and as a result we get something
very close to the identity matrix (with a little divergence due to roundoff error).

So show me the code already!

Okay, fine. Here's some simple C++ code to do an HSV transformation on a single Color (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 multiplier (scalar)
float V // value multiplier (scalar)

beesbuzz.biz/…/hsv_color_transforms.p… 4/6
1/26/2011 HSV color transforms
)
{
float VSU = V*S*cos(H*M_PI/180);
float VSW = V*S*sin(H*M_PI/180);

Color ret;
ret.r = (.299*V+.701*VSU+.168*VSW)*in.r
+ (.587*V-.587*VSU+.330*VSW)*in.g
+ (.114*V-.114*VSU-.497*VSW)*in.b;
ret.g = (.299*V-.299*VSU-.328*VSW)*in.r
+ (.587*V+.413*VSU+.035*VSW)*in.g
+ (.114*V-.114*VSU+.292*VSW)*in.b;
ret.b = (.299*V-.3*VSU+1.25*VSW)*in.r
+ (.587*V-.588*VSU-1.05*VSW)*in.g
+ (.114*V+.886*VSU-.203*VSW)*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
float m // Maximum value for a channel
)
{
Color ret;
ret.r = (in.r*r.r + in.g*r.g + in.b*r.b)/m;
ret.g = (in.r*g.r + in.g*g.g + in.b*g.b)/m;
ret.b = (in.r*b.r + in.g*b.g + in.b*b.b)/m;
return ret;
}

(Really, I only bring it up because it was part of the conversation which led me to write up this guide to begin with.)

A note about gamut and response

The YIQ color space as used here (as well as in NTSC) attempts to keep the perceptive brightness the same, regardless
of channel. This leads to some fairly major problems with the response range; for example, since green is about twice as
bright as red, rotating pure green to red would produce a super-red - and meanwhile, the value on some channels can
also be pulled down past 0. This is not a flaw in the algorithm, so much as a fundamental flaw in how color works to begin
with, and a disconnect between the intuitive notion of how "hue" works vs. the actual physics involved. (In a sense, a hue
"rotation" is a pretty ridiculous thing to even try to do to begin with; while red and blue shift are very real phenomena,
there is no actual fundamental property of light or color which places red, green and blue at 120-degree intervals apart
from each other.)

The easy solution to this issue is to always clamp the color values between 0 and 1. A more correct solution is to actually
keep track of where the colors are beyond the range and spread that excess energy (or lack thereof) across the image
(i.e. to neighboring pixels), similar to HDR rendering. As more display devices move to floating-point-based color
representation, this will become easier to deal with. However, at present (November 2009), no major image formats
support anything other than a clamped integer color space (although some high-definition video formats support so-
beesbuzz.biz/…/hsv_color_transforms.p… 5/6
1/26/2011 HSV color transforms
called "deep color" and at least the concept of superwhite/superblack).

In many cases, if you're making heavy use of hue transformation (in e.g. a game), you actually want to be dealing with
your art assets stored as HSV and then just use that to produce your RGB values to begin with.

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, float max) { return max*pow(value/max, 1/gamma); }
float GammaToLinear(float value, float gamma, float max) { return max*pow(value/max, gamma); }

In this case, both the linear and gamma colors will be stored in the same range (0..max). In real-world real-time
implementations you will probably do this transformation using either a pair of lookup tables or a polynomial
approximation so you don't have to do multiple pow() calls per pixel.

What about Photoshop's HSL adjustments?

As far as I can tell, Photoshop does not use a colorspace rotation to do its HSL adjustments. The best I can tell is that it
does some sort of ad-hoc "intuitive" approach where it likely uses the lowest channel value to determine the saturation,
and uses the ratio between the remaining two channels to determine the "angle" on the traditional 6-spoke print color
wheel (red/yellow/green/cyan/blue/magenta). This provides perfect numeric values based on peoples' expectations of
hues vs. RGB channels, but it actually makes for some pretty terrible chroma adjustment which tends to only work well on
solid colors. Even smooth gradients between two colors are completely fouled up by this approach.

So, what I'm saying is that Photoshop's HSL transform mechanism isn't really something that should be emulated since
its utility is limited to begin with (at least for general-purpose image processing; it's great for graphic design and number-
oriented pixel art, I suppose).

I haven't verified this for myself, but apparently the YIQ-based algorithm described here is what Flash, as well as video
editing tools such as Final Cut, Premiere, and so on, use for their color processing; given that it originates from real-life
NTSC video processing equipment (which encode all color information in YIQ), this makes sense.

Acknowledgments

Wikipedia's article on YIQ provided the transformation matrices to convert between RGB and YIQ.

Ricky Haggett had used the previous YPbPr version of the algorithm which caused me to notice a major error in the
math, and also inspired me to correct this to use YIQ, even though the math involved is much more complex but comes
closer to what people expect from hue transformation, even if a 180-degree rotation still isn't the same as just doing
(255-r,255-g,255-b).

Chris Healer found a mistake in my transcription of TYIQ.

beesbuzz.biz/…/hsv_color_transforms.p… 6/6

Вам также может понравиться