# 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. – Stu 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. – Stu 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. – Stu 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. – Stu Apr 18 '12 at 18:02

You can do the usual (w0*Color1.hue + w1*Color2.hue), just ensure it approaches from the proper angle (a few comparisons and swappings will do). But yeah, hue angle blending is inherently less efficient than RGB.

Considering a and b being blended hues, and the weight being the amount of movement from a to b (all between 0 and 1):

float hue_blend(float a, float b, float weight) {
if (b < a) {
swap(a, b);
weight = 1.0 - weight;
}
if (b-a > 0.5) a += 1.0;
float r = a + (b-a)*weight;
if (r > 1.0) r -= 1.0;
return r;
}

• Note the specific question here is how to extend this to more than two colours. How can we "ensure it approaches from the proper angle" when our colours form a triangle wrapping around both sides of the colour wheel? – DMGregory Jul 26 at 8:37
• Cant you express it using several hue_blend calls? I.e. if r + g + b = 1, then (r + g)*m = 1, therefore m = 1/(r + g); mrx + mgy with first hue_blend, then apply the second. using blue weight. – SmugLispWeenie Jul 26 at 9:59
• If you do that, then doing a blend with identical hue-weight combinations, blending the pairs in a different order, could give you a different result. The chosen order of pairwise blend operations adds a bias, so the weights aren't in full control of the outcome. – DMGregory Jul 26 at 11:36
• Then I'm afraid it appears to be impossible to do in normal HSV. Color spaces are all about trade-offs. – SmugLispWeenie Jul 26 at 14:40
• There are already two working strategies proposed in the existing answers. Here's another: think of the hues as point masses at the rim of a disc that pivots as its center. The blended hue is the point on the rim of the disc toward which it tips according to the net torque summed across all the point masses. So I don't agree that we can conclude this is "impossible" – DMGregory Jul 26 at 14:55