Skip to content

Color Averaging

Color averaging is the process taking multiple colors and calculating an average color from them, essentially mixing all the colors together. This involves looking at each color channel of all colors under consideration and averaging and averaging each channel independently. Additionally, by default, transparency is taken into account by using premultiplication which weights the colors such that more opaque colors have a greater significance in the mixing vs more translucent colors.

Averaging under ColorAide can take as many colors as desired and will return a color that represents the average. This approach to mixing is not to be confused with interpolation which employs a different technique. One thing that sets it apart from interpolation is that when performing the operation, the order of the colors does not matter and will yield the same results even if the colors are shuffled.

Averaging can be used as a way to mix multiple colors into one color or simply determine what the overall average color is from a set of colors. Results are subject to the geometry of the color space in which the average is performed.

Rectangular Space Averaging

ColorAide, by default, averages colors in the rectangular Linear sRGB color spaces. For most, averaging in rectangular spaces would most likely be the common approach.

Average RGB

While linear sRGB is the default color space when averaging, other color spaces can be used. Results will vary due to the geometry of the color space being used.

>>> Color.average(['red', 'blue'])
color(srgb-linear 0.5 0 0.5 / 1)
>>> Color.average(['red', 'blue'], space='srgb')
color(srgb 0.5 0 0.5 / 1)
>>> Color.average(['red', 'blue'], space='oklab')
color(--oklab 0.53998 0.0962 -0.09284 / 1)

Averaging can be applied to any amount of colors.

>>> Color.average(['red', 'yellow', 'orange', 'green'])
color(srgb-linear 0.75 0.39803 0 / 1)

Cylindrical Space Averaging

ColorAide can also average colors in cylindrical spaces. When applying averaging in a cylindrical space, hues will be averaged taking the circular mean. Due the difference in approach, color averaging in a cylindrical space can be quite different.

Average HSL

To perform averaging in a cylindrical/polar space, simply specify the space when averaging.

>>> Color.average(['purple', 'green', 'blue'], space='hsl')
color(--hsl 240 1 0.33399 / 1)

Colors that are deemed achromatic will have their hue treated as undefined, even if the hue is defined. This is to ensure that the average color makes sense and isn't tainted by a non-functional hue.

>>> Color.average(['white', 'blue'], space='hsl')
color(--hsl 240 0.5 0.75 / 1)

It should be noted that when averaging colors with hues which are evenly distributed around color space, the result will produce an achromatic hue. When achromatic hues are produced during circular mean, the color will discard chroma/saturation information, producing an achromatic color.

>>> Color.average(['red', 'green', 'blue'], space='hsl')
color(--hsl none 0 0.41699 / 1)

Weighted Averaging

To allow for greater control and nuance of mixing multiple colors, ColorAide allows weights to be defined to adjust how much a specific color is mixed relative to other colors.

As an example, let's assume we wanted to mix orange and red but brighten it up with white. More specifically, let's say we want 4 times the amount of white for every 1 part of the other colors. We can simply specify weights intuitively as [1, 1, 4].

>>> Color.average(['orange', 'red', 'white'])
color(srgb-linear 1 0.45875 0.33333 / 1)
>>> Color.average(['orange', 'red', 'white'], [1, 1, 4])
color(srgb-linear 1 0.72938 0.66667 / 1)

Weighted Average

Regardless of how big or small the numbers are, they are scaled relative to the largest value, so internally, [1, 1, 4] and [0.25, 0.25, 1] are essentially the same.

>>> Color.average(['orange', 'red', 'white'], [1, 1, 4])
color(srgb-linear 1 0.72938 0.66667 / 1)
>>> Color.average(['orange', 'red', 'white'], [0.25, 0.25, 1])
color(srgb-linear 1 0.72938 0.66667 / 1)

If more weights are provided that there are colors, the only the weights sufficient to satisfy the number of colors is consumed.

>>> Color.average(['orange', 'red', 'white'], [1, 1, 4, 2, 1])
color(srgb-linear 1 0.72938 0.66667 / 1)

If more colors are provided than weights, colors without defined weights are assumed to be full weight.

>>> Color.average(['orange', 'red', 'white'], [0, 1])
color(srgb-linear 1 0.5 0.5 / 1)

Note

It should be noted that negative weights are not allowed and will be clipped to zero, which treats the colors as if it is not included at all.

Averaging with Transparency

ColorAide, by default, will account for transparency when averaging colors. Colors which are more transparent will have less of an impact on the average. This is done by premultiplying the colors before averaging, essentially weighting the color components where more opaque colors have a greater influence on the average.

>>> for i in range(12):
...     Color.average(
...         [f'color(srgb 0 1 0 / {i / 11})', 'color(srgb 0 0 1)']
...     )
... 
color(srgb-linear 0 0 1 / 0.5)
color(srgb-linear 0 0.08333 0.91667 / 0.54545)
color(srgb-linear 0 0.15385 0.84615 / 0.59091)
color(srgb-linear 0 0.21429 0.78571 / 0.63636)
color(srgb-linear 0 0.26667 0.73333 / 0.68182)
color(srgb-linear 0 0.3125 0.6875 / 0.72727)
color(srgb-linear 0 0.35294 0.64706 / 0.77273)
color(srgb-linear 0 0.38889 0.61111 / 0.81818)
color(srgb-linear 0 0.42105 0.57895 / 0.86364)
color(srgb-linear 0 0.45 0.55 / 0.90909)
color(srgb-linear 0 0.47619 0.52381 / 0.95455)
color(srgb-linear 0 0.5 0.5 / 1)

There are cases where this approach of averaging may not be desired and results are desired without considering transparency. If so, premultiplied can be disabled by setting it to False. While the average of transparency is still calculated, it can be discarded from the final result if desired.

It should also be noted that when a color is fully transparent, its color components will be ignored, regardless of the premultiplied parameter, as fully transparent colors provide no meaningful color information.

>>> for i in range(12):
...     Color.average(
...         [f'color(srgb 0 1 0 / {i / 11})', 'color(srgb 0 0 1)'],
...         premultiplied=False,
...     )
... 
color(srgb-linear 0 0 1 / 0.5)
color(srgb-linear 0 0.5 0.5 / 0.54545)
color(srgb-linear 0 0.5 0.5 / 0.59091)
color(srgb-linear 0 0.5 0.5 / 0.63636)
color(srgb-linear 0 0.5 0.5 / 0.68182)
color(srgb-linear 0 0.5 0.5 / 0.72727)
color(srgb-linear 0 0.5 0.5 / 0.77273)
color(srgb-linear 0 0.5 0.5 / 0.81818)
color(srgb-linear 0 0.5 0.5 / 0.86364)
color(srgb-linear 0 0.5 0.5 / 0.90909)
color(srgb-linear 0 0.5 0.5 / 0.95455)
color(srgb-linear 0 0.5 0.5 / 1)

Averaging with Undefined Values

When averaging with undefined values, ColorAide will not consider the undefined values in the average. In short, it will be treated as if there was no value contributing to the average. This is mainly provided for sane averaging of achromatic colors in cylindrical/polar color spaces. With that said, any channel that has manually specified a channel as undefined will be treated in this manner.

>>> Color.average(['white', 'color(srgb 0 0 1)'], space='hsl')
color(--hsl 240 0.5 0.75 / 1)

When averaging hues in a polar space, implied achromatic hues are also treated as undefined as counting such hues would distort the average in a non-meaningful way.

>>> Color.average(['hsl(30 0 100)', 'hsl(240 100 50 / 1)'], space='hsl')
color(--hsl 240 0.5 0.75 / 1)

As stated earlier, undefined logic is applied to any channel with undefined values. It should be noted that no attempt to carry forward the undefined values through conversion is made at this time. If conversion is required, the conversions will remove any undefined status unless the channel is an achromatic hues.

>>> for i in range(12):
...     Color.average(['darkgreen', f'color(srgb 0 none 0 / {i / 11})', 'color(srgb 0 0 1)'])
... 
color(srgb-linear 0 0.06372 0.5 / 0.66667)
color(srgb-linear 0 0.06095 0.47826 / 0.69697)
color(srgb-linear 0 0.05841 0.45833 / 0.72727)
color(srgb-linear 0 0.05607 0.44 / 0.75758)
color(srgb-linear 0 0.05392 0.42308 / 0.78788)
color(srgb-linear 0 0.05192 0.40741 / 0.81818)
color(srgb-linear 0 0.05006 0.39286 / 0.84848)
color(srgb-linear 0 0.04834 0.37931 / 0.87879)
color(srgb-linear 0 0.04673 0.36667 / 0.90909)
color(srgb-linear 0 0.04522 0.35484 / 0.93939)
color(srgb-linear 0 0.04381 0.34375 / 0.9697)
color(srgb-linear 0 0.04248 0.33333 / 1)

When premultiplied is enabled, premultiplication will not be applied to a color if its alpha is undefined as it is unknown how to weight the color. Instead, a color with undefined transparency will be treated with full weight.

>>> Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / none)', 'color(srgb 0 0 1)'])
color(srgb-linear 0 0.11443 0.33333 / 1)