# How do you blend multiple colors in HSV (polar) color-space?

In RGB color space, you can do a weighted multiple-color blend by just doing:

Start with R = G = B = 0. Then we perform a blend at index i using a set of colors C, and a set of normalized weights w like so:

``````R += w[i] * C[i].r
G += w[i] * C[i].g
B += w[i] * C[i].b
``````

But I'd like to interpolate the colors in the HSV color-space instead, so that saturation and brightness are uniform across the interpolation. I know I can blend saturation and brightness in the same way as above, but the HUE component is an angle around a continuous circle, since HSV is essentially a polar coordinate system.

Blending only two HSV colors makes sense to me, you just find the shortest arc around the circle and interpolate between the two hues. But when you attempt to blend more than 2 colors, it becomes a bit of a puzzle.

You have to handle anomalous cases, like 4 equally-weighted colors with a hue at 0, 90, 180, and 270 degrees. They basically cancel each other out, so any hue will do.

Any ideas would be greatly appreciated.

-
Have you considered converting from HSV to RGB, modifying the RGB value and then converting back to HSV? – user22592 Oct 18 '14 at 14:21

I'll quote my answer to this question, since it's basically the same thing - he's averaging compass angles while you're averaging hue angles, but the procedure's the same either way.

You could convert each angle to a 2D vector and sum the vectors, then convert the result back to an angle.

In pseudocode:

``````totalVector = [0, 0]
for each angle:
vector = [cos(angle), sin(angle)]
totalVector += vector
if length(totalVector) < aSmallNumber:
# error, angles are all over the place so there's no meaningful average
avgAngle = atan2(totalVector.y, totalVector.x)
``````

That being said, I'm not sure the specification of blending hue while preserving saturation is all that reasonable. As you say, it's possible to pick hues that are distributed around the circle so that a proper blend between them should be a very desaturated color. Using the above approach, the output hue will be extremely sensitive to the inputs in such a case - if you shift one of the inputs just a little the output hue will change quite a lot.

-
Thanks for the answer, I've tried this myself before, but this produces a linear NLerp-style interpolation, not a SLerp. What this means is if you have hues that are nearly 180 degrees appart, such as 0 and 179, the blend between them should be smooth and uniform along the arc between them, but instead is heavily biased toward 0 and toward 179. – Toxikman Apr 11 '12 at 17:10
@Toxikman Yeah, that's true. If you have just two hues, of course it's easy to find the shortest distance around the circle and lerp the hue directly to get smooth angular interpolation. For more hues I still think the problem is kind of ill-specified; what would you want to happen in a case like the 0, 90, 180, 270 you mentioned? – Nathan Reed Apr 11 '12 at 17:21
In the diametrically opposed examples, almost any answer will do, so long as the interpolated result is smooth as you smoothly vary the weights of the contributors. For example, in the case of 0, 180, if both weights are equal, either 90 or 270 will work, so long as it is consistent. – Toxikman Apr 11 '12 at 19:55
In that case, doesn't standard weighted averaging of the hue angles do the job? That will certainly be smooth with respect to the weights. It won't necessarily be smooth with respect to the inputs - e.g. if one of the inputs flips from 360 to 0 it'll change the way the interpolation goes. But it sounds like you're more interested in varying the weights than varying the inputs, anyway. – Nathan Reed Apr 11 '12 at 20:24
Standard weighted averaging doesn't consider that the angles are continuous on a circle. For example, if I have values 10, 10, 350 with the same weight, the standard weighted average will be ~123, but the correct hue is ~3. – Toxikman Apr 12 '12 at 1:12

Ah ha! This is a tricky one, but I found an algorithm that works, but it's not very optimal (yet). The algorithm is a generalized form of quaternion SLerp. It requires quaternion math and some iteration to approach the correct answer. Here is my implementation:

First you create N quaternions, all using the same vector (like 0,1,0) with the angle as the hue. Then you pass them and the weights into this function (C++):

``````void Quaternion::SetBlend( uint count, const Quaternion quats[], const float weights[] )
{
assert( count >= 2 );
// Iterate to find the weighted mean
operator = ( quats[0] ); // initial estimate
for( int i = 0; i < ITERATIONS; i++ )
{
Quaternion qmi = Invert();
Quaternion qe(0,0,0,0);
for( uint n = 0; n < count; n++ )
qe += weights[n] * (qmi * quats[n]).Log();
*this *= qe.Exp();
}
}
``````

When you get the angle from the resulting quaternion, it is the weighted mean. This can produce a good approximation with `ITERATIONS` set to even 1 or 2. Now, obviously there's a lot of zeros and extra math in here that can be optimized away, but at least I have a solution that works and I can optimize it from here.

-

This is from the hip but i wonder if you could get some mileage from barycentric coordinates - where you treat each color as a point on a polygon and convert from the barycentric coordinates back into "world space" to get the blended color.

You might get a more "flat" blend since you are blending across a flat polygon instead of a sphere, but for colors that are somewhat close on the (hyper?)sphere, i would think it would approximate the right answer (and maybe you could normalize the blended answer and scale it back to the radius of the sphere).

Totally shooting from the hip, sorry if this is obvious or non useful :P

-
That's essentially what Nathan Reed proposed in another answer, but it does not produce correct results when the hues are close to 180 degrees appart. – Toxikman Apr 18 '12 at 18:02