How to fix rainbows and other bad colormaps using Python

Yep, colormaps again!

In my 2014 tutorial on The Leading Edge I showed how to Evaluate and compare colormaps (Jupyter notebook here). The article followed an extended series of posts (The rainbow is dead…long live the rainbow!) and then some more articles on rainbow-like colormap artifacts (for example here and here).

Last year, in a post titled Unweaving the rainbow, Matt Hall described our joint attempt to make a Python tool for recovering digital data from scientific images (and seismic sections in particular), without any prior knowledge of the colormap. Please check our GitHub repository for the code and slides, and watch Matt’s talk (very insightful and very entertaining) from the 2017 Calgary Geoconvention below:

One way to use the app is to get an image with unknown, possibly awful colormap, get the data, and re-plot it with a good one.

Matt followed up on colormaps with a more recent post titled No more rainbows! where he relentlessly demonstrates the superiority of perceptual colormaps for subsurface data. Check his wonderful Jupyter notebook.

So it might come as a surprise to some, but this post is a lifesaver for those that really do like rainbow-like colormaps. I discuss a Python method to equalize colormaps so as to render them perceptual.  The method is based in part on ideas from Peter Kovesi’s must-read paper – Good Colour Maps: How to Design Them – and the Matlab function equalisecolormap, and in part on ideas from some old experiments of mine, described here, and a Matlab prototype code (more details in the notebook for this post).

Let’s get started. Below is a time structure map for a horizon in the Penobscot 3D survey (offshore Nova Scotia, licensed CC-BY-SA by dGB Earth Sciences and The Government of Nova Scotia). Can you clearly identify the discontinuities in the southern portion of the map? No?

equalization_before_horizon

OK, let me help you. Below I am showing the map resulting from running a Sobel filter on the horizon. Penobscop_sobel

This is much better, right? But the truth is that the discontinuities are right there in the original data; some, however, are very hard to see because of the colormap used (nipy spectral, one of the many Matplotlib cmaps),  which introduces perceptual artifacts, most notably in the green-to-cyan portion.

In the figure below, in the first panel (from the top) I show a plot of the colormap’s Lightness value (obtained converting a 256-sample nipy spectral colormap from RGB to Lab) for each sample; the line is coloured by the original RGB colour. This erratic Lightness profile highlights the issue with this colormap: the curve gradient changes magnitude several times, indicating a nonuniform perceptual distance between samples.

In the second panel, I show a plot of the cumulative sample-to-sample Lightness contrast differences, again coloured by the original RGB colours in the colormap. This is the best plot to look at because flat spots in the cumulative curve correspond to perceptual flat spots in the map, which is where the discontinuities become hard to see. Notice how the green-to-cyan portion of this curve is virtually horizontal!

That’s it, it is simply a matter of very low, artificially induced perceptual contrast.

Solutions to this problem: the obvious one is to Other NOT use this type of colormaps (you can learn much about which are good perceptually, and which are not, in here); a possible alternative is to fix them. This can be done by re-sampling the cumulative curve so as to give it constant slope (or constant perceptual contrast). The irregularly spaced dots at the bottom (in the same second panel) show the re-sampling locations, which are much farther apart in the perceptually flat areas and much closer in the more dipping areas.

The third panel shows the resulting constant (and regularly sampled) cumulative Lightness contrast differences, and the forth and last the final Lightness profile which is now composed of segments with equal Lightness gradient (in absolute value).

equalization_pictorialHere is the structure map for the Penobscot horizon using the nipy spectum before and after equalization on top of each other, to facilitate comparison. I think this method works rather well, and it will allow continued use of their favourite rainbow and rainbow-like colormaps by hard core aficionados.

 

equalize

If you want the code to try the equalization, get the noteboook on GitHub.

Geophysical tutorial – How to evaluate and compare colormaps in Python

These below are two copies of a seismic horizon from the open source Penobscot 3D seismic survey  coloured using two different colormaps (data from Hall, 2014).

horizon_comparison

Figure 1

Do you think either of them is ‘better’?  If yes, can you explain why? If you are unsure and you want to learn how to answer such questions using perceptual principles and open source Python code, you can read my tutorial Evaluate and compare colormaps (Niccoli, 2014), one of the awesome Geophysical Tutorials from The Leading Edge. In the process you will learn many things, including how to calculate an RGB colormap’s intensity using a simple formula:

import numpy as np
ntnst = 0.2989 * rgb[:,0] + 0.5870 * rgb[:,1] + 0.1140 * rgb[:,2] # get the intensity
intensity = np.rint(ntnst) # rounds up to nearest integer

…and  how to display  the colormap as a colorbar with an overlay plot of the intensity as in Figure 2.

intensity_dots

Figure 2

 

Reference

Hall, M. (2014) Smoothing surfaces and attributes. The Leading Edge 33, no. 2, 128–129. Open access at: https://github.com/seg/tutorials#february-2014

Niccoli, M. (2014) Evaluate and compare colormaps. The Leading Edge 33, no. 8.,  910–912. Open access at: https://github.com/seg/tutorials#august-2014

New Matlab isoluminant colormap for azimuth data

I recently added to my Matlab File Exchange function, Perceptually improved colormaps, a colormap for periodic data like azimuth or phase. I am going to briefly showcase it using data from my degree thesis in geology, which I used before, for example in Visualization tips for geoscientists – Matlab. Figure 1, from that post, shows residual gravity anomalies in milligals.

data cube1_final_shading_slope

Figure 1

Often we’re interested in characterizing these anomalies by calculating the direction of maximum dip at each point on the surface, and for that direction display the azimuth, or dip azimuth.  I’ve done this for the surface of residual anomalies from Figure 1 and displayed the azimuth in Figure 2. Azimuth from 0 to 360 degrees are color-coded using Jet, Matlab’s standard colormap (until recently). Typically I do not trust azimuth values when the dip is close to zero because it is often contaminated by noise so I would use shading to de-saturate the colors where dip has the lowest values, but for ease of discussion I haven’t done so in this case.

Figure 2. Azimuth values color-coded with Jet.

Figure 2. Azimuth values color-coded with Jet.

There are two problems with Figure 2. First, the well-known problems with the jet colormap. For example, blue is too dark and blue areas appear as bands of constant colour. Yellow is much lighter than any other colour so we see artificial yellow edges that are not really present in the data. But there is an additional issue in Figure 2 because azimuths close in value to 0 and 360 degrees are colored with blue and red, respectively, instead of a single color as they should, causing an additional artificial edge.

In Figure 3 I recolored the map using a colormap that replicates those used in many geophysical software tools to display azimuth or phase data. This is better because it wraps around at 360 degrees but the perceptual issues are unresolved: in this case red, yellow and blue all appear as sharp perceptual edges.

standardAZ

Figure 3. Azimuth values color-coded with generic azimuth colormap.

 

isoAZ

Figure 4. Azimuth values color-coded with isoluminant azimuth colormap.

 

In Figure 4 I used my new colormap, called isoAZ (for isoluminant azimuth). This colormap is much better because not only does it wraps around at 360 degrees, but also lightness is held constant for all colors, which eliminates the perceptual anomalies. All the artificial yellow, red, and blue edges are gone, only real edges are left. This can be more easily appreciated in the figure below: if you hover with your mouse over it you are able to switch back and forth between Figure 3 and Figure 4.

isoAZstandardAZ

From an interpretation point of view, azimuths 180 degrees apart are of opposing colours, which is ideal for dip azimuth data because it allows us to easily recognize folds where dips of opposite direction are juxtaposed at an edge. One example is the sharp edge in the northwest quadrant of Figure 4, where magenta is juxtaposed to green. If you look at Figure 1 you see that there’s a relative high in this area (the edge in Figure 4) with dips of opposite direction on either side (East and West, or 0 and 360 degrees).

The colormap was created in the Lightness-Chroma-Hue color space, a polar transform of the Lab color space, where lightness is the vertical axis and at each value of lightness, chroma is the radial coordinate and hue the polar angle. One limitation of this approach is that due to theirregular  shape of the color gamut section at each lightness value, we can never exceed  chroma values  of about 38-40 (at lightness = 65 in Matlab; in Python, with extensive trial and error, I have not been able to go past 36 using the Scikit-image Color module), which make the resulting colors pale, pastely.

it creates For those that want to experiment with it further, I used just a few lines of code similar to the ones below:

radius = 38; % chroma
theta = linspace(0, 2*pi, 256)'; % hue
a = radius * cos(theta);
b = radius * sin(theta);
L = (ones(1, 256)*65)'; % lightness
Lab = [L, a, b];
RGB=colorspace('RGB<-Lab',Lab(end:-1:1,:));

This code is a modification from an example by Steve Eddins on a post on his Matlab Central blog. In Steve’s example the colormap cycles through the hues as lightness increases monotonically (which by the way is an excellent way to generate a perceptual rainbow). In this case lightness is kept constant and hue cycles through the entire 360 degrees and wraps around. Also, instead of using the Image Processing Toolbox, I used  Colorspace, a free function from Matlab File Exchange, for the color space transformations.

For data like fracture orientation where azimuths 180 degrees apart are equivalent it is better to stack two of these isoluminant colormaps in a row. In this way we place opposing colors 90 degrees apart, whereas color 180 degrees apart are the same. You can do it using Matlab commands repmat or vertcat, as below:

radius = 38; % chroma
theta = linspace(0, 2*pi, 128)'; % hue
a = radius * cos(theta);
b = radius * sin(theta);
L = (ones(1, 128)*65)'; % lightness
Lab = [L, a, b];
rgb=colorspace('RGB<-Lab',Lab(end:-1:1,:));
RGB=vertcat(rgb,rgb);

Parula: a new Matlab colormap

Steve Eddins of the Matwork just published a post announcing a new Matlab colormap to replace Jet. It is called Parula (more to come on his blog about this intriguing name).

parula

First impression: Parula looks good.

And while I haven’t had time to take it into Python to run a full perceptual test and into ImageJ for a colour blindness test, as a preliminary test I did convert it to grayscale with an online picture converting tool that uses the lightness information to perform the conversion (instad of just desaturating the colors) and the result shows monotonic changes in gray.

parula_compare

Looks promising… Full test to come.

NASA Worldview satellite image browser adopts MyCarta perceptual rainbow

I was thrilled this week to learn from Ryan Boller that his team at NASA’s ESDIS Project included MyCarta’s perceptual rainbow (the CubicYF) as one of the palettes for the Worldview satellite imagery browser.

If you’d like to try it, once on the viewer you can load an overlay and then you can choose from among several color palettes. The perceptual rainbow palette is listed here as “Rainbow 2”.

I am including below an example using the Land surface temperature for April 13 2013 from MODIS Aqua mission:

Land_surf_temp_130413

This is really exciting news as NASA’s adoption will increase the palette’s exposure and its chances of becoming more mainstream. This is also as close as I will ever get to realizing my childhood dream of becoming an astronaut. Thanks ESDIS, and thanks Ryan, on both accounts.

Color palettes for seismic structure maps and attributes

I created three color palettes for structure maps (seismic horizons, elevation maps, etcetera) and seismic attributes. To read about the palettes please check these previous blog posts:

The rainbow is dead…long live the rainbow! – Part 5 – CIE Lab linear L* rainbow
The rainbow is dead series – Part 7 – Perceptual rainbow palette – the method
The rainbow is dead series – Part 7 – Perceptual rainbow palette – the goodies

The palettes are available as plain ASCII files and also formatted for a number of platforms and software products:

Geosoft
Hampson-Russell
Kingdom
Madagascar
Matlab
OpendTect
Petrel
Seisware
Surfer
Voxelgeo

Please download them from my Color Palettes page and follow instructions therein.

Enjoy!

linearlfb

Image courtesy of Sergey Fomel of the Madagascar Development blog

Better Palettes

Thanks to @kwinkunks for the tip about this post

RELATED POSTS (MyCarta)

The rainbow is dead…long live the rainbow! – Part 1 —

The rainbow is dead…long live the rainbow! – Part 2: a rainbow puzzle —

A rainbow for everyone —

Is Indigo really a colour of the rainbow? —

Why is the hue circle circular at all?

The rainbow is dead…long live the rainbow! – The rainbow is dead…long live the rainbow! – Perceptual palettes, part 2: a rainbow puzzle

ROYGBIV or YOGRVIB?

If you are interested in the topic of color palettes for scientific data, and the rainbow in particular, I would say you ought to read this 2007 IEEE visualization paper by Borland and Taylor: Rainbow Color Map (Still) Considered Harmful. It clearly and elegantly illustrates why the rainbow palette should be avoided when displaying scientific data. I like Figure 1 in the paper in particular. The illustration shows how it is easy to order perceptually a set of 4 paint chips of different gray intensity, but not at all easy to order 4 paint chips colored red, green, yellow, and blue. The author’s argument is that the rainbow colors are certainly ordered, from shorter to longer wavelengths, but they are not perceptually ordered. In this post I wanted to extend the chips example to all 7 colors in the rainbow and try to demonstrate the point in a quantitative way.

Here below is a 256-sample rainbow palette I created interpolating between the RGB values for the seven colors of the rainbow red, orange, yellow, green, blue, indigo, and violet (ROY G BIV):

On this palette I see a number of perceptual artifacts, the most notable ones being a sharp edge at the yellow and a flat zone at the green. The existence of these edges I tried to explain quantitatively in the first post of this series.

Now, to go back to the experiment, from the original RGB values for the non interpolated colors I created the 7 color chips below . Question: can you order them based on their perceived intensity?

I think if you have full color vision (more on the topic of rainbow and impaired color vision in the next section of this post) eventually you will be able to order them as I did.If not, try now below. In this new image I converted the color chips to gray chips using the values obtained in Matlab with this formula:

INT = (0.2989 * RGB(:,1) + 0.5870* RGB(:,2) + 0.1140 * RGB(:,3))';

Give it a try, then hover with your mouse over the image to read the intensity values.

roygbiv_intensityroygbiv_intensity_values

Not surprisingly, the values are not in any particular order. This reinforces the notion that although the rainbow colors are ordered by increasing wavelength (or decreasing in this case) , they are not perceptually ordered. (See this comment to my previous post). Below I rearranged the gray chips by increasing intensity.

And now I reconverted from gray to RGB colors and adjusted the distance between each pair of chips so that it is proportional to the intensity difference between the chips in the pair (I actually had to artificially change the value for green and orange so they would not overlap). That was an epiphany for me. And the name is funny too, BIV R GOY, or YOG R VIB…

I said that it was an epiphany because I realize the implications of trying to create a palette by interpolating through these colors with those distances. So I did it, and I am showing it below in the top color palette. We jumped out of the frying pan, into the fire! We went from perceptual artifacts that are inherent to the rainbow (reproduced in reverse order from blue to red to facilitate comparison as the bottom palette) to interpolation artifacts in the intensity ordered rainbow. Hopeless!

ROYGBIV puzzle

As if what I have shown in the previous section wasn’t scary enough, I took 7 squares and colored them using the same RGB values for Red, Orange, Yellow, Green, Blue, Indigo, and Violet. Then I used the Dichromacy plug-in in ImageJ to simulate how these colors would be seen by a viewer with Deuteranopia (the more common form of color vision deficiency). I then shuffled the squares in random order on a square canvas, and numbered them 1-7 in clockwise order.

Puzzle: can you pair the squares numbered 1 through 7 with the colors R though V? I will give away the obvious one, which is the yellow:

1=Y
2=?
3=?
4=?
5=?
6=?
7=?

Cannot do it? For the solution just hover over the image with your mouse. If you like the animation and would like to use it on your blog, twitter, Facebook, get the GIF file version here. Please be kind enough to link it back to this post.

roygbiv_random_deuteranoperoygbiv_random

Conclusion

When I tried myself I could not solve the puzzle, and that finally convinced me that trying to fix the rainbow was a hopeless cause. Even if we could, it would still confuse a good number of people (about 8% of male have one form or the other of color vision deficiency). From the next post on I will show what I got when I tried to create a better, more perceptual rainbow from scratch.

Related posts (MyCarta)

The rainbow is dead…long live the rainbow! – the full series

What is a colour space? reblogged from Colour Chat

Color Use Guidelines for Mapping and Visualization

A rainbow for everyone

Is Indigo really a colour of the rainbow?

Why is the hue circle circular at all?

A good divergent color palette for Matlab

Related topics (external)

Color in scientific visualization

The dangers of default disdain

Color tools

How to avoid equidistant HSV colors

Non-uniform gradient creator

Colormap tool

Color Oracle – color vision deficiency simulation – stand alone (Window, Mac and Linux)

Dichromacy –  color vision deficiency simulation – open source plugin for ImageJ

Vischeck – color vision deficiency simulation – plugin for ImageJ and Photoshop (Windows and Linux)

For teachers

NASA’s teaching resources for grades 6-9: What’s the Frequency, Roy G. Biv?