CET perceptually uniform colormaps

Peter Kovesi at the Center for Exploration Targeting created a very useful set of perceptually uniform colormaps, many of which can replace the highly non-uniform colormaps provided with Python plotting programs. Here we will show how to use them via the new cetcolors python package, listing all the ones available and allowing you to evaluate how perceptually uniform they are for you, your particular monitor, etc. We will plot them using matplotlib via holoviews, but identical bokeh palettes are also provided, and both Bokeh palettes and Matplotlib colormaps are usable in datashader. Thus these colormaps can be used in any of those Python packages, as well as any other package that accepts a Python list of normalized RGB tuples or hex colors. See Peter's site to download versions for other non-Python packages.

We first make some utilities for sorting and visualizing colormaps:

In [1]:
import numpy as np
import holoviews as hv
import cetcolors as cc

hv.notebook_extension()

import param
with param.logging_level('CRITICAL'):
    hv.plotting.mpl.LayoutPlot.v17_layout_format = True
HoloViewsJS successfully loaded in this cell.
In [2]:
%opts Image [xaxis='bare' yaxis='bare' sublabel_format=None aspect=3 fontsize=13]

# Collapse all colormap aliases into a single string, and then sort by it
inverse = {}
for k,v in cc.cm.items():
    if not k[-2:] == "_r":
        inverse[v] = inverse.get(v, [])
        inverse[v].insert(0,k)
all_cms = {',  '.join(reversed(v)):k for (k,v) in inverse.items()}.items()
all_cms.sort()

aliased_cms=[(k,v) for (k,v) in all_cms if "," in k]

def lay_out_in_columns(_list, cols, spacer=None):
    """Given a list, reorder it into the number of columns specified, filling missing items with the given spacer."""
    rows = int(np.ceil(len(_list)*1.0/cols))
    padded = _list + [spacer] * (rows*cols-len(_list))
    return list(np.array(padded).reshape(cols,rows).T.flat)

xs,ys = np.meshgrid(np.linspace(0,1,80), np.linspace(0,1,10))

def colormaps(named_cms,cols=3,array=xs):
    images = [hv.Image(array, group=name)(style=dict(cmap=cm)) 
              for name,cm in named_cms]
    return hv.Layout(lay_out_in_columns(images,cols,hv.Empty())).display('all').cols(cols)

def colormaps_by_name(names,cols=3,array=xs):
    return colormaps({(k,v) for (k,v) in cc.cm.items() if k in names},cols,array)

Named colormaps

The full list of colormaps included will be shown below, but a useful subset of these maps that cover the various types have been given more convenient names, and we will focus on those first.

For Matplotlib users: After importing cetcolors, all the colormaps shown here will be available by string name prefixed with cet_, along with reversed versions suffixed with _r (not shown below). For instance, the cetcolors version of "hot" is available as "cet_hot" and "cet_hot_r" (as well as the more verbose names "cet_linear_kryw_0_100_c71" and "cet_linear_kryw_0_100_c71_r" as described below).

Linear (sequential) colormaps, for plotting magnitudes:

In [3]:
colormaps([i for i in aliased_cms if "linear" in i[0] and "diverging" not in i[0]])
Out[3]:

Diverging colormaps, for plotting magnitudes increasing or decreasing from a central point:

In [4]:
colormaps([i for i in aliased_cms if "diverging" in i[0]])
Out[4]:

Misc colormaps:

  • cyclic: for cyclic quantities like orientation or phase (where the highest and lowest values need the same color)
  • isoluminant: to highlight low spatial-frequency information
  • rainbow: to highlight local differences in sequential data
In [5]:
colormaps([i for i in aliased_cms if "cyclic" in i[0] or "rainbow" in i[0] or "isoluminant" in i[0]])
Out[5]:

Full list of available colormaps

This list includes all those above, plus other variants and alternatives. Each colormap has a name in the form:

category_huesequence_lightnessrange_cmeanchroma[_scolorshift_[rifreversed]]

along with any shorter alias that may be defined above.

In [6]:
colormaps(all_cms, cols=4)
Out[6]:

Testing perceptual uniformity

Peter Kovesi created a test image with a sine grating modulation of intensity, where modulation gain decreases from top to bottom, which helps evaluate perceptual uniformity of a colormap at a glance. The matplotlib maintainers use different definitions of perceptually uniform (uniformity in a different color space), but the new matplotlib perceptually uniform colormaps do well at Peter's test image:

In [7]:
%output size=200
%opts Image [xaxis='bare' yaxis='bare' sublabel_format=None aspect=4 fontsize=10]
sine=np.load("colourmaptest.npy")
In [8]:
colormaps([["viridis"]*2,["inferno"]*2],cols=1,array=sine)
Out[8]:

Here the sine grating for a uniform colormap should be visible as a fine-toothed comb with teeth that gradually become less visible from top to bottom. The further down the comb these teeth are visible, the higher the discriminability of magnitudes at that location in the colormap. Thus a perceptually uniform colormap, like the two above, should have teeth that visible at the same length for all colors.

You can also use these images to evaluate the overall level of discriminability provided by a given colormap -- the longer the visible area of teeth, the better this colormap allows you to discriminate fine differences in magnitude. Here the inferno map seems to have better discriminability than the viridis map, despite both being perceptually uniform.

The default colormaps that have traditionally been used with Matlab, Matplotlib, and HoloViews are clearly not perceptually uniform -- all the green and yellow areas are nearly indistinguishable:

In [9]:
colormaps([["hot"]*2,["jet"]*2],cols=1,array=sine)
Out[9]:

Thus those colormaps should be avoided if at all possible, to avoid generating misleading visualizations. Compare these two to the perceptually uniform versions provided by this package:

In [10]:
colormaps([("fire",cc.m_fire),("rainbow",cc.m_rainbow)],cols=1,array=sine)
Out[10]:

We can see the results for all the cetcolors colormaps below, which can be summarized as:

  • "linear" colormaps all work well by this criterion
  • "diverging" colormaps typically have discontinuities in perceptual discriminability around the central value
  • "cyclic" colormaps with repeating colors tend to have discontinuities at 1/4 and 3/4 of the way through the cycle, or at other locations if shifted.
  • "isoluminant" colormaps typically show no visible modulation, because without luminance cues humans can only discriminate low spatial-frequency modulations (i.e., much wider teeth would be needed for evaluating such colormaps)
  • Some of the "rainbow" colormaps seem to have a perceptual discontinuity around the color red, the reasons for which are not yet clear.
In [11]:
colormaps(all_cms,cols=1,array=sine)
Out[11]: