Skip to content

Color Averaging

Color averaging is the process of calculating an average color from a set of other colors by taking the mean of each color channel.

Averaging under ColorAide can take as many colors as desired and will return a color that represents the average. This is not to be confused with interpolation which employs a different technique, but in certain situations, it can sort of function like mixing multiple colors.

Rectangular Space Averaging

ColorAide, by default, averages in rectangular color spaces, the default being Linear sRGB. If desired, other color spaces can be used, such as perceptually uniform spaces like Oklab.

>>> 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 is not restricted to any certain amount of colors.

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

Cylindrical Space Averaging

ColorAide can average colors in rectangular spaces and cylindrical spaces. When applying averaging in a cylindrical space, hues will be averaged taking the circular mean.

Cylindrical averaging may not provide as good of results as using rectangular spaces, but is provided to provide a sane approach if a cylindrical space is used.

>>> Color.average(['orange', 'yellow', 'red'])
color(srgb-linear 1 0.45875 0 / 1)
>>> Color.average(['orange', 'yellow', 'red'], space='hsl')
color(--hsl 33.227 1 0.5 / 1)

Because calculations are done in a cylindrical space, the averaged colors can be different than what is acquired with rectangular space averaging.

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

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.

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

If you'd like to average the channels without taking transparency into consideration, simply set premultiplied to False.

>>> 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.5 0.5 / 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. This is mainly provided for averaging cylindrical colors, particularly achromatic colors.

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

Implied achromatic hues are only considered undefined if powerless is enabled. This is similar to how interpolation works. By default, explicitly defined hues are respected if working directly in the averaging color space.

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

While undefined logic is intended to handle achromatic hues, this logic will be applied to any channel. It should be noted that no attempt to carry forward the undefined values through conversion is made at this time. 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.

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