Skip to content

Filters

ColorAide implements a number of filters with each filter being provided as a plugin. Filters simply apply some logic to transform a color in some specific way. Filters can be used to lighten colors, adjust saturation, or completely change the color. Filters can even be used to simulate things like color vision deficiencies.

W3C Filter Effects

The W3C Filter Effects Module Level 1 specification outline a number of filters for use in SVG and CSS. ColorAide implements all the filters that directly apply to colors. By default, filters are applied in the Linear sRGB color space, but can be applied in sRGB if requested. All other color spaces will throw an error.

Normal

Brightness

Saturate

Contrast

Opacity

Invert

Hue Rotate

Sepia

Grayscale

To apply a specific filter in ColorAide, just call the filter() method with the name of the filter you wish to use. If an amount is not provided, the default according to the W3C spec will be used instead.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors = Color.steps(inputs, steps=10, space='srgb')
>>> Steps(colors)
[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]
>>> Steps([c.filter('brightness', 0.5).clip() for c in colors])
[color(srgb-linear 0.5 0 0 / 1), color(srgb-linear 0.5 0.07796 0 / 1), color(srgb-linear 0.5 0.27286 0 / 1), color(srgb-linear 0.5 0.5 0 / 1), color(srgb-linear 0.04542 0.20186 0 / 1), color(srgb-linear 0 0.04579 0.04542 / 1), color(srgb-linear 0 0 0.5 / 1), color(srgb-linear 0.01595 0 0.20539 / 1), color(srgb-linear 0.11038 0.01225 0.19066 / 1), color(srgb-linear 0.4275 0.11161 0.4275 / 1)]
>>> Steps([c.filter('saturate', 0.5).clip() for c in colors])
[color(srgb-linear 0.6065 0.1065 0.1065 / 1), color(srgb-linear 0.66224 0.24021 0.16224 / 1), color(srgb-linear 0.8016 0.57446 0.3016 / 1), color(srgb-linear 0.964 0.964 0.464 / 1), color(srgb-linear 0.19943 0.35587 0.15401 / 1), color(srgb-linear 0.03601 0.0818 0.08143 / 1), color(srgb-linear 0.036 0.036 0.536 / 1), color(srgb-linear 0.03413 0.01818 0.22357 / 1), color(srgb-linear 0.15637 0.05825 0.23666 / 1), color(srgb-linear 0.62914 0.31325 0.62914 / 1)]
>>> Steps([c.filter('contrast', 0.8).clip() for c in colors])
[color(srgb-linear 0.9 0.1 0.1 / 1), color(srgb-linear 0.9 0.22474 0.1 / 1), color(srgb-linear 0.9 0.53658 0.1 / 1), color(srgb-linear 0.9 0.9 0.1 / 1), color(srgb-linear 0.17267 0.42298 0.1 / 1), color(srgb-linear 0.1 0.17326 0.17267 / 1), color(srgb-linear 0.1 0.1 0.9 / 1), color(srgb-linear 0.12552 0.1 0.42862 / 1), color(srgb-linear 0.2766 0.1196 0.40506 / 1), color(srgb-linear 0.78399 0.27858 0.78399 / 1)]
>>> Steps([c.filter('opacity', 0.5).clip() for c in colors])
[color(srgb-linear 1 0 0 / 0.5), color(srgb-linear 1 0.15593 0 / 0.5), color(srgb-linear 1 0.54572 0 / 0.5), color(srgb-linear 1 1 0 / 0.5), color(srgb-linear 0.09084 0.40373 0 / 0.5), color(srgb-linear 0 0.09158 0.09084 / 0.5), color(srgb-linear 0 0 1 / 0.5), color(srgb-linear 0.0319 0 0.41077 / 0.5), color(srgb-linear 0.22076 0.0245 0.38133 / 0.5), color(srgb-linear 0.85499 0.22323 0.85499 / 0.5)]
>>> Steps([c.filter('invert', 1).clip() for c in colors])
[color(srgb-linear 0 1 1 / 1), color(srgb-linear 0 0.84407 1 / 1), color(srgb-linear 0 0.45428 1 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.90916 0.59627 1 / 1), color(srgb-linear 1 0.90842 0.90916 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.9681 1 0.58923 / 1), color(srgb-linear 0.77924 0.9755 0.61867 / 1), color(srgb-linear 0.14501 0.77677 0.14501 / 1)]
>>> Steps([c.filter('hue-rotate', 90).clip() for c in colors])
[color(srgb-linear 0 0.356 0 / 1), color(srgb-linear 0 0.48932 0 / 1), color(srgb-linear 0 0.82259 0.20639 / 1), color(srgb-linear 0 1 0.856 / 1), color(srgb-linear 0 0.37753 0.52519 / 1), color(srgb-linear 0.09084 0.05913 0.14404 / 1), color(srgb-linear 1 0 0.144 / 1), color(srgb-linear 0.41077 0 0.04084 / 1), color(srgb-linear 0.38133 0.01908 0 / 1), color(srgb-linear 0.85499 0.31483 0 / 1)]
>>> Steps([c.filter('sepia', 1).clip() for c in colors])
[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]
>>> Steps([c.filter('grayscale', 1).clip() for c in colors])
[color(srgb-linear 0.2126 0.2126 0.2126 / 1), color(srgb-linear 0.32412 0.32412 0.32412 / 1), color(srgb-linear 0.6029 0.6029 0.6029 / 1), color(srgb-linear 0.9278 0.9278 0.9278 / 1), color(srgb-linear 0.30806 0.30806 0.30806 / 1), color(srgb-linear 0.07205 0.07205 0.07205 / 1), color(srgb-linear 0.0722 0.0722 0.0722 / 1), color(srgb-linear 0.03644 0.03644 0.03644 / 1), color(srgb-linear 0.09199 0.09199 0.09199 / 1), color(srgb-linear 0.40315 0.40315 0.40315 / 1)]

Tip

filter() can output the results in any color space you need by setting out_space.

>>> Color('#07c7ed').filter('grayscale', 1, out_space='hsl')
color(--hsl none 0 0.71528 / 1)

Color Vision Deficiency Simulation

Color blindness or color vision deficiency (CVD) affects approximately 1 in 12 men (8%) and 1 in 200 women. CVD affects millions of people in the world, and many people have no idea that they are color blind and not seeing the full spectrum that others see.

CVD simulation allows those who do not suffer with one of the many different variations of color blindness, to simulate what someone with a CVD would see. Keep in mind that these are just approximations, and that a given type of CVD can be quite different from person to person in severity.

The human eye has 3 types of cones that are used to perceive colors. Each of these cones can become deficient, either through genetics, or other means. Each type of cone is responsible for perceiving different wavelengths of light. A CVD occurs when one or more of these cones are missing or not functioning properly. There are severe cases where one of the three cones will not perceive color at all, and there are others where the cones may just be less sensitive.

Dichromacy

Dichromacy is a type of CVD that has the characteristics of essentially causing the person to only have two functioning cones for perceiving colors. This essentially flattens the color spectrum into a 2D plane. Protanopia describes the CVD where the cone responsible for long wavelengths does not function, deuteranopia describes the CVD affecting the cone responsible for processing medium wavelengths, and tritanopia describes deficiencies with the cone responsible for short wavelengths.

Normal

Protanopia

Deuteranopia

Tritanopia

One misconception is that people with CVD have a color blindness for just red and green or something similar as that can often be how it is described, and while the statement is true that certain people with CVD may have trouble with red and green, they often can have trouble with other colors as well.

The LMS color space was created to mimic the response of the human eye. Each channel represents one of the 3 cones with each cone responsible for seeing light waves of different frequencies: long (L), medium (M), and short (S). Protanopia represents deficiencies with the L cone, deuteranopia with the M cone, and tritanopia with the S cone. Any color whose properties only vary in the properties specific to a person's deficient cone(s) will have the potential to cause confusion for that person.

Consider the example below. We generate 3 different color series, each specifically targeting a specific deficiency. This is done by generating a series of colors that have all properties equal except that they have variance in a different cone response. The first row varies only with the L cone response, the second only with the M cone response, and the third only with the S cone response. We then apply the filters for protanopia, deuteranopia, and tritanopia. We can see that while many of the colors are altered, the row that targets the deficient cone specific to the CVD all appear to be of the same color making it difficult to distinguish between any of them.

>>> confusing_colors = confusion_line(Color('orange'), 'l')
>>> Steps([c.clip() for c in confusing_colors])
[color(srgb 0 0.73785 0.0565 / 1), color(srgb 0.48453 0.72034 0.0466 / 1), color(srgb 0.66519 0.70225 0.03524 / 1), color(srgb 0.79774 0.68354 0.02349 / 1), color(srgb 0.90633 0.66414 0.01175 / 1), color(srgb 1 0.64398 0 / 1)]
>>> confusing_colors = confusion_line(Color('hotpink'), 'm')
>>> Steps([c.clip() for c in confusing_colors])
[color(srgb 1 0.40107 0.70629 / 1), color(srgb 0.90666 0.5039 0.70182 / 1), color(srgb 0.7985 0.58535 0.69731 / 1), color(srgb 0.66664 0.65439 0.69277 / 1), color(srgb 0.48742 0.71511 0.68818 / 1), color(srgb 0.04172 0.76977 0.68356 / 1)]
>>> confusing_colors = confusion_line(Color('seagreen'), 's')
>>> Steps([c.clip() for c in confusing_colors])
[color(srgb 0.18039 0.5451 0.34118 / 1), color(srgb 0.26982 0.51394 0.56225 / 1), color(srgb 0.33367 0.4802 0.70642 / 1), color(srgb 0.38543 0.44317 0.81991 / 1), color(srgb 0.42983 0.40182 0.91581 / 1), color(srgb 0.46916 0.35444 1 / 1)]
>>> confusing_colors = confusion_line(Color('orange'), 'l')
>>> Steps([c.filter('protan').clip() for c in confusing_colors])
[color(srgb-linear 0.44718 0.44718 0.00253 / 1), color(srgb-linear 0.44632 0.44632 0.00253 / 1), color(srgb-linear 0.44545 0.44545 0.00252 / 1), color(srgb-linear 0.44459 0.44459 0.00252 / 1), color(srgb-linear 0.44372 0.44372 0.00252 / 1), color(srgb-linear 0.44286 0.44286 0.00251 / 1)]
>>> confusing_colors = confusion_line(Color('hotpink'), 'm')
>>> Steps([c.filter('protan').clip() for c in confusing_colors])
[color(srgb-linear 0.23099 0.23099 0.46046 / 1), color(srgb-linear 0.28318 0.28318 0.45292 / 1), color(srgb-linear 0.33538 0.33538 0.44537 / 1), color(srgb-linear 0.38758 0.38758 0.43782 / 1), color(srgb-linear 0.43978 0.43978 0.43027 / 1), color(srgb-linear 0.49197 0.49197 0.42273 / 1)]
>>> confusing_colors = confusion_line(Color('seagreen'), 's')
>>> Steps([c.filter('protan').clip() for c in confusing_colors])
[color(srgb-linear 0.23224 0.23224 0.09438 / 1), color(srgb-linear 0.20829 0.20829 0.27557 / 1), color(srgb-linear 0.18435 0.18435 0.45676 / 1), color(srgb-linear 0.16041 0.16041 0.63795 / 1), color(srgb-linear 0.13646 0.13646 0.81914 / 1), color(srgb-linear 0.11252 0.11252 1 / 1)]
>>> confusing_colors = confusion_line(Color('orange'), 'l')
>>> Steps([c.filter('deutan').clip() for c in confusing_colors])
[color(srgb-linear 0.35631 0.35631 0.0158 / 1), color(srgb-linear 0.39626 0.39626 0.00984 / 1), color(srgb-linear 0.43622 0.43622 0.00387 / 1), color(srgb-linear 0.47617 0.47617 0 / 1), color(srgb-linear 0.51612 0.51612 0 / 1), color(srgb-linear 0.55607 0.55607 0 / 1)]
>>> confusing_colors = confusion_line(Color('hotpink'), 'm')
>>> Steps([c.filter('deutan').clip() for c in confusing_colors])
[color(srgb-linear 0.38725 0.38725 0.43764 / 1), color(srgb-linear 0.38833 0.38833 0.43756 / 1), color(srgb-linear 0.38941 0.38941 0.43748 / 1), color(srgb-linear 0.3905 0.3905 0.43739 / 1), color(srgb-linear 0.39158 0.39158 0.43731 / 1), color(srgb-linear 0.39266 0.39266 0.43723 / 1)]
>>> confusing_colors = confusion_line(Color('seagreen'), 's')
>>> Steps([c.filter('deutan').clip() for c in confusing_colors])
[color(srgb-linear 0.1906 0.1906 0.10046 / 1), color(srgb-linear 0.17799 0.17799 0.28 / 1), color(srgb-linear 0.16539 0.16539 0.45953 / 1), color(srgb-linear 0.15278 0.15278 0.63907 / 1), color(srgb-linear 0.14018 0.14018 0.8186 / 1), color(srgb-linear 0.12757 0.12757 0.99814 / 1)]
>>> confusing_colors = confusion_line(Color('orange'), 'l')
>>> Steps([c.filter('tritan').clip() for c in confusing_colors])
[color(srgb-linear 0.09504 0.41129 0.56924 / 1), color(srgb-linear 0.27752 0.40204 0.46424 / 1), color(srgb-linear 0.46584 0.38712 0.3939 / 1), color(srgb-linear 0.66496 0.36167 0.3878 / 1), color(srgb-linear 0.86409 0.33623 0.38171 / 1), color(srgb-linear 1 0.31078 0.37561 / 1)]
>>> confusing_colors = confusion_line(Color('hotpink'), 'm')
>>> Steps([c.filter('tritan').clip() for c in confusing_colors])
[color(srgb-linear 0.96312 0.16951 0.23789 / 1), color(srgb-linear 0.77356 0.24403 0.28965 / 1), color(srgb-linear 0.584 0.31855 0.34142 / 1), color(srgb-linear 0.39444 0.39306 0.39318 / 1), color(srgb-linear 0.22435 0.44862 0.56064 / 1), color(srgb-linear 0.05436 0.50409 0.7287 / 1)]
>>> confusing_colors = confusion_line(Color('seagreen'), 's')
>>> Steps([c.filter('tritan').clip() for c in confusing_colors])
[color(srgb-linear 0.06253 0.22391 0.30451 / 1), color(srgb-linear 0.06359 0.22288 0.30244 / 1), color(srgb-linear 0.06464 0.22186 0.30038 / 1), color(srgb-linear 0.0657 0.22083 0.29831 / 1), color(srgb-linear 0.06675 0.2198 0.29624 / 1), color(srgb-linear 0.06781 0.21877 0.29417 / 1)]

By default, ColorAide uses the Brettel 1997 method to simulate tritanopia as it is the only option that has decent accuracy for tritanopia. Viénot, Brettel, and Mollon 1999 approach is used to simulate protanopia and deuteranopia as it is not only faster than Brettel, but it handles extreme cases a little better. Machado 2009 has its strengths as well which we will cover in Anomalous Trichromacy.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors = Color.steps(inputs, steps=10, space='srgb')
>>> Steps(colors)
[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]
>>> Steps([c.filter('protan').clip() for c in colors])
[color(srgb-linear 0.11238 0.11238 0.00401 / 1), color(srgb-linear 0.25079 0.25079 0.00338 / 1), color(srgb-linear 0.59678 0.59678 0.00182 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.36856 0.36856 0 / 1), color(srgb-linear 0.08129 0.08129 0.09047 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00358 0.00358 0.4109 / 1), color(srgb-linear 0.04655 0.04655 0.38211 / 1), color(srgb-linear 0.29423 0.29423 0.85752 / 1)]
>>> Steps([c.filter('deutan').clip() for c in colors])
[color(srgb-linear 0.29275 0.29275 0 / 1), color(srgb-linear 0.40303 0.40303 0 / 1), color(srgb-linear 0.67871 0.67871 0 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.31213 0.31213 0.00699 / 1), color(srgb-linear 0.06477 0.06477 0.09289 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00934 0.00934 0.41006 / 1), color(srgb-linear 0.08195 0.08195 0.37694 / 1), color(srgb-linear 0.40818 0.40818 0.84088 / 1)]
>>> Steps([c.filter('tritan').clip() for c in colors])
[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]

If desired, any of the three available methods can be used.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors = Color.steps(inputs, steps=10, space='srgb')
>>> Steps(colors)
[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]
>>> Steps([c.filter('tritan', method='brettel').clip() for c in colors])
[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]
>>> Steps([c.filter('tritan', method='vienot').clip() for c in colors])
[color(srgb-linear 1 0 0 / 1), color(srgb-linear 1 0.13398 0.13398 / 1), color(srgb-linear 1 0.46891 0.46891 / 1), color(srgb-linear 1 0.85924 0.85924 / 1), color(srgb-linear 0.14923 0.3469 0.3469 / 1), color(srgb-linear 0.00011 0.09147 0.09147 / 1), color(srgb-linear 0 0.14076 0.14076 / 1), color(srgb-linear 0 0.05782 0.05782 / 1), color(srgb-linear 0.16915 0.07473 0.07473 / 1), color(srgb-linear 0.76363 0.31216 0.31216 / 1)]
>>> Steps([c.filter('tritan', method='machado').clip() for c in colors])
[color(srgb-linear 1 0 0.00473 / 1), color(srgb-linear 1 0.06673 0.11254 / 1), color(srgb-linear 1 0.42955 0.38203 / 1), color(srgb-linear 1 0.8524 0.6961 / 1), color(srgb-linear 0.08307 0.36867 0.27955 / 1), color(srgb-linear 0 0.09865 0.09092 / 1), color(srgb-linear 0 0.1476 0.3039 / 1), color(srgb-linear 0 0.05813 0.12498 / 1), color(srgb-linear 0.20711 0.06178 0.13387 / 1), color(srgb-linear 0.90348 0.26694 0.41821 / 1)]

Anomalous Trichromacy

While Dichromacy is probably the more severe case with only two functional cones, a more common CVD type is anomalous trichromacy. In this case, a person will have three functioning cones, but not all of the cones function with full sensitivity. Sometimes, the sensitivity can be so low, that their ability to perceive color may be close to someone with dichromacy.

While dichromacy may be considered a severity 1, a given case of anomalous trichromacy could be anywhere between 0 and 1, where 0 would be no CVD.

Like dichromacy, the related deficiencies are named in a similar manner: protanomaly (reduced red sensitivity), deuteranomaly (reduced green sensitivity), and tritanomaly (reduced blue sensitivity).

Normal

Protanomaly 0.5

Protanomaly 0.7

Protanomaly 0.9

To represent anomalous trichromacy, ColorAide leans on the Machado 2009 approach which has a more nuanced approach to handling severity levels below 1. This research associated with this method did not really focus on tritanopia though, and Brettel is still a better choice for tritanopia. Instead of relying on the Machado approach for tritanomaly, we instead just use linear interpolation between the severity 1 results and the severity 0 (no CVD) results.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors = Color.steps(inputs, steps=10, space='srgb')
>>> Steps(colors)
[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]
>>> Steps([c.filter('protan', 0.3).clip() for c in colors])
[color(srgb-linear 0.63032 0.06918 0 / 1), color(srgb-linear 0.70293 0.20796 0 / 1), color(srgb-linear 0.88443 0.5549 0 / 1), color(srgb-linear 1 0.95923 0 / 1), color(srgb-linear 0.24525 0.36562 0 / 1), color(srgb-linear 0.03392 0.08521 0.09141 / 1), color(srgb-linear 0 0.04077 1 / 1), color(srgb-linear 0 0.01895 0.41633 / 1), color(srgb-linear 0.11396 0.05262 0.3851 / 1), color(srgb-linear 0.56082 0.29269 0.85987 / 1)]
>>> Steps([c.filter('protan', 0.5).clip() for c in colors])
[color(srgb-linear 0.45806 0.09279 0 / 1), color(srgb-linear 0.56403 0.22475 0 / 1), color(srgb-linear 0.82893 0.55464 0 / 1), color(srgb-linear 1 0.9391 0 / 1), color(srgb-linear 0.31598 0.35011 0 / 1), color(srgb-linear 0.04973 0.08304 0.09151 / 1), color(srgb-linear 0 0.0609 1 / 1), color(srgb-linear 0 0.02798 0.42051 / 1), color(srgb-linear 0.06528 0.06444 0.38853 / 1), color(srgb-linear 0.42566 0.32032 0.86561 / 1)]
>>> Steps([c.filter('protan', 0.9).clip() for c in colors])
[color(srgb-linear 0.20388 0.11298 0 / 1), color(srgb-linear 0.3583 0.23687 0 / 1), color(srgb-linear 0.74433 0.54658 0 / 1), color(srgb-linear 1 0.90752 0 / 1), color(srgb-linear 0.41835 0.33104 0 / 1), color(srgb-linear 0.07305 0.08116 0.09129 / 1), color(srgb-linear 0 0.09248 1 / 1), color(srgb-linear 0 0.04159 0.42961 / 1), color(srgb-linear 0 0.07967 0.39681 / 1), color(srgb-linear 0.22933 0.35303 0.88092 / 1)]

The Brettel and Viénot approach can be used for severities below 1 as well, but, like Brettel with tritanopia, they will employ simple linear interpolation between a severity 1 case ans the actual color. It is probably debatable as to whether this approach is sufficient or not.

Usage Details

To use filters, a filter name must be given, followed by an optional amount. If an amount is omitted, suitable default will be used. The exact range a given filter accepts varies depending on the filter. If a value exceeds the filter range , the value will be clamped.

Filters Name Default
Brightness brightness 1
Saturation saturate 1
Contrast contrast 1
Opacity opacity 1
Invert invert 1
Hue rotation hue-rotate 0
Sepia sepia 1
Grayscale grayscale 1
Protan protan 1
Deutan deutan 1
Tritan tritan 1

All of the filters that are supported allow filtering in the Linear sRGB color space and will do so by default. Additionally, the W3C filter effects also support filtering in the sRGB color space. The CVD filters are specifically designed to be applied in the Linear sRGB space, and cannot be used in any other color space.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors = Color.steps(inputs, steps=10, space='srgb')
>>> Steps(colors)
[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]
>>> Steps([c.filter('sepia', 1, space='srgb-linear').clip() for c in colors])
[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]
>>> Steps([c.filter('sepia', 1, space='srgb').clip() for c in colors])
[color(srgb 0.393 0.349 0.272 / 1), color(srgb 0.72473 0.64492 0.50235 / 1), color(srgb 0.98106 0.87359 0.68035 / 1), color(srgb 1 1 0.806 / 1), color(srgb 0.64467 0.57456 0.44736 / 1), color(srgb 0.32034 0.28556 0.22236 / 1), color(srgb 0.189 0.168 0.131 / 1), color(srgb 0.20429 0.18153 0.14152 / 1), color(srgb 0.45304 0.40295 0.31398 / 1), color(srgb 0.93524 0.83226 0.64837 / 1)]

Processing Lots of Colors

One logical application for filters is to apply them directly to images. If you are performing these operations on millions of pixels, you may notice that ColorAide, with all of its convenience, may not always be the fastest. There is a cost due to the overhead of convenience and a cost due to the pure Python approach as well. With that said, there are tricks that can dramatically make things much faster in most cases!

functools.lru_cache is your friend in such cases. We actually process all the images on this page with ColorAide to demonstrate the filters. The key to making it a quick and painless process was to cache repetitive operations. When processing images, it is highly likely that you will be performing the same operations on thousands of identical pixels. Caching the work you've already done can speed this process up exponentially.

There are certainly some images that could be constructed in such a way to elicit a worse case scenario where the cache would not be able to compensate as well, but for most images, caching dramatically reduces processing time.

We can crawl the pixels in a file, and using a simple function like below, we will only process a pixel once (at least until our cache fills and we start having to overwrite existing colors).

@lru_cache(maxsize=1024 * 1024)
def apply_filter(name, amount, space, method, p, fit):
    """Apply filter."""

    has_alpha = len(p) > 3
    color = Color('srgb', [x / 255 for x in p[:3]], p[3] / 255 if has_alpha else 1)
    if method is not None:
        # This is a CVD filter that allows specifying the method
        color.filter(name, amount, space=space, in_place=True, method=method)
    else:
        # General filter.
        color.filter(name, amount, space=space, in_place=True)
    # Fit the color back into the color gamut and return the results
    return tuple([int(x * 255) for x in color.fit(method=fit)[:3 if has_alpha else -1]])

When processing a 4608x2456 image (15,925,248 pixels) during our testing, it turned a ~7 minute process into a ~25 second process*. Using gamut mapping opposed to simple clipping only increases time by to about ~56 seconds. The much smaller images shown on this page process much, much faster.

The full script can be viewed here.

* Tests were performed using the Pillow library. Results may vary depending on the size of the image, pixel configuration, number of unique pixels, etc. Cache size can be tweaked to optimize the results.