Is there an easy way to use Matteo Niccoli's perceptual color maps for 2D plots in Mathematica?

I've taken the liberty of uploading the RGB values for MyCarta's color schemes to pastebin. Mr. Niccoli provides these in CSV downloadable from his website, but I found that I had to change their format if I want Mathematica to read them during initialization.

(* Read in the numerical data *)
Get["https://pastebin.com/raw/gN4wGqxe"]
ParulaCM = With[{colorlist = RGBColor @@@ parulaColors},
   Blend[ colorlist, #]&
   ];
Cube1CM = With[{colorlist = RGBColor @@@ cube1Colors},
   Blend[ colorlist, #]&
   ];
CubeYFCM = With[{colorlist = RGBColor @@@ cubeYFColors},
   Blend[ colorlist, #]&
   ];
LinearLCM = With[{colorlist = RGBColor @@@ cube1Colors},
   Blend[ colorlist, #]&
   ];
JetCM = With[{colorlist = RGBColor @@@ jetColors},
   Blend[ colorlist, #]&
   ];

If you want to have these functions available without defining them every time you open Mathematica, then put the above text in your init.m file.

You can see the colorschemes via

BarLegend[{#,{0,1}}]&/@{JetCM,ParulaCM,Cube1CM,CubeYFCM,LinearLCM}

enter image description here

and in a simple 2D plot via

DensityPlot[Cos[x] Sin[y], {x, -10, 10}, {y, -10, 10}, 
 PlotRange -> All, ColorFunction -> #, 
 PlotPoints -> 75] & /@ {JetCM, ParulaCM, Cube1CM, CubeYFCM,LinearLCM}

enter image description here

Definitely read the MyCarta blog posts for more information about these color palettes, and why you might want to use them. Also see Matteo's answer below for more info


Here's my attempt at recreating the LinearLCM palette. It turned out to be very tricky, because it's not a linear problem. In addition the blog article and the paper cited are confusing the terms "luminance" and "lightness".

I started the process with trying to recreate the uniform-luminance rainbow palette on the left: http://www.cs.utah.edu/~gk/papers/vis02/talk/jpg/Image09.jpg

In the paper this was achieved by visually matching the luminance of six different colors equally spaced on the color wheel. I don't know why they used the HLS color space for this because in this color space the "luminance" information is not saved in a single channel; saturation also controls the luminance.

In Mathematica I found two ways to normalize the luminance of those colors. Because of the way Blend/Darker/Lighter functions work I couldn't find a way to fully automatize them. These two approaches produce different results so I will present each one separately.

First approach

For each of the six colors {Red, Yellow, Green, Cyan, Blue, Magenta} I converted them to the LAB color space where luminance is truly controlled with a single channel and calculated the adjustment needed to be made with Lighter (for Blue) or Darker (other colors). I could probably write a more complex recursion function to do this but I thought it would be quicker if I just plot the functions and determine the intersections manually with adjusting the intervals. Here's an example:

ListPlot[Table[{x, 
   ColorConvert[Darker[Magenta, x], "LAB"][[
     1]] - .5}, {x, .15, .18, .001}]]

luminance_adjusting_1

This way I obtained the adjustment values for each colors. Negative numbers mean that the color appears too light, and vice versa.

adj = {{Red, -.080},
   {Yellow, -.520},
   {Green, -.456},
   {Cyan, -.476},
   {Blue, +.398},
   {Magenta, -.173}};

I then interpolated between these reference points in the HSB color space, to create an adjustment function for the whole color wheel [1]:

adjif = Interpolation[{ColorConvert[#[[1]], "HSB"][[1]], #[[2]]} & /@ 
   adj, InterpolationOrder -> 1];

adjustment_interpolation_1

I could now create the luminance-neutral palette. I started with the un-adjusted hue palette and then applied the interpolated adjustments.

rainbow = 
 ColorConvert[
  Rasterize[
   DensityPlot[x, {x, 0, 1}, {y, 0, 1}, 
    ColorFunction -> (Blend[{Red, Yellow, Green, Cyan, Blue, 
         Magenta}, #] &), ColorFunctionScaling -> True, 
    ImageSize -> {1000, 50}, AspectRatio -> 1/Divide @@ {1000, 50}, 
    PlotRangePadding -> 0, Frame -> False]], "HSB"]
rainbowd = ImageData[rainbow][[1]]

rainbowad = 
 ImageResize[
  ImageAssemble[{Map[
     Image[With[{a = adjif[#[[1]]]}, 
        If[a < 0, Darker[Hue @@ #, -a], Lighter[Hue @@ #, a]]]] &, 
     rainbowd]}], ImageDimensions[rainbow]]

hue_palette

adjusted_hue_palette_1

The palette looks quite neutral in luminance, but there are some artifacts: the dark bands in yellow and cyan regions and a light band in the blue region. The luminance level can be also checked with converting the palette to LAB color space:

ListLinePlot[ImageData[ColorConvert[rainbowad, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

adjusted_hue_palette_luminance_1

This plot confirms the artifacts, but they are minor except at the right end of the palette. Either way it does not look too bad.

The final step is to apply a linear luminance gradient over the neutral-luminance palette, using the Lighter or Darker functions on each half of the palette.

grad = ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif[#[[1]]]}, 
         With[{b = 
            If[a < 0, Darker[Hue @@ #, -a], Lighter[Hue @@ #, a]]}, 
          If[#[[1]] < 5/12, Lighter[b, 1 - 12/5 #[[1]]], 
           Darker[b, 12/5 #[[1]] - 1]]]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

luminance_linear_gradient_1

This palette looks quite similar to the one in the paper. The yellow artifact is not noticeable anymore, and the other two have shifted a bit further apart toward green and purple. The luminance plot again confirms this.

ListLinePlot[ImageData[ColorConvert[grad, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

linear_gradient_palette_luminance_1

Second approach

Another way to obtain the interpolated adjustment function is to use GrayLevel instead of Darker and Lighter. Since Darker[color, amount] is the same as Blend[{color, Black}, amount], using GrayLevel we now have two variables: Blend[{color, GrayLevel[level]}, amount]. For calibration I decided to fix the amount (variable f in the code) and calculate the level (variable x in the code) values. This time I was able to calculate the adjustments automatically.

f = 2/3;
ctab = Table[
  dx = .1;
  x = 0;
  cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];
  While[Abs[cx - .5] > 10^-10,
   While[cx - .5 < 0, x += dx; 
    cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];];
   x -= dx; dx *= .1; 
   cx = ColorConvert[Blend[{c, GrayLevel[x]}, f], "LAB"][[1]];
   ];
  x, {c, {Red, Yellow, Green, Cyan, Blue, Magenta}}]
{0.547099, 0.218085, 0.296868, 0.273634, 0.655471, 0.510001}

With this I again created an interpolation function and applied the adjustments over the initial rainbow palette.

adjif2 = Interpolation[{ColorConvert[#[[1]], "HSB"][[1]], #[[2]]} & /@
    Transpose[{{Red, Yellow, Green, Cyan, Blue, Magenta}, ctab}], 
  InterpolationOrder -> 1];

adjustment_interpolation_2

rainbowad2 = 
 ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif2[#[[1]]]}, 
         Blend[{Hue @@ #, GrayLevel[a]}, f]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

adjusted_hue_palette_2

Now the adjustment function is noticeably different, with some peaks more pronounced, and other less pronounced. The palette obtained this way looks much more luminance-neutral than the one from the first approach. Artifacts are much less pronounced. The downside is a greater loss of saturation.

ListLinePlot[ImageData[ColorConvert[rainbowad2, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

adjusted_hue_palette_luminance_2

Again, I can now apply a linear luminance gradient over the luminance-neutral palette.

grad2 = ImageResize[
  ColorConvert[
   ImageAssemble[{Map[
      Image[With[{a = adjif2[#[[1]]]}, 
         With[{b = Blend[{Hue @@ #, GrayLevel[a]}, f]}, 
          If[#[[1]] < 5/12, Lighter[b, 1 - 12/5 #[[1]]], 
           Darker[b, 12/5 #[[1]] - 1]]]]] &, rainbowd]}], "RGB"], 
  ImageDimensions[rainbow]]

luminance_linear_gradient_2

The gradient is almost perfectly linear, but again at the loss of saturation.

ListLinePlot[ImageData[ColorConvert[grad2, "LAB"]][[1, All, 1]], 
 PlotRange -> {0, 1}]

linear_gradient_palette_luminance_2

Conclusion

As you can see, the problem is not linear. There saturation and apparent brightness of a color are linked together, and with changing one of them the other also changes. So it's difficult to accurately reproduce the exact palette from the article.


[1] Actually the palette does not cover the whole color wheel; the hue ranges from 0 (Red) to 0.8333 (Magenta).


As suggested by Sektor I am adding this as a separate answer.

DumpsterDoofus' asked in a comment about perceptual, divergent colour schemes (where "zero is special").

I am definitely not a supporter of the common red-white-blue divergent scheme (or any scheme with white in the middle). I’ve often noticed when switching between grayscale and red-white-blue, for example (or red-white-black), that with the latter the white in the middle obfuscates subtle data features maps, and dramatically hinders the perception of discontinuities, for example faults in geological or geophysical maps.

In a paper I read recently, available on his website, Moreland argues that the white in this colour scheme is a Mach band. Mach bands, discovered by the physicist Ernst Mach, are optical illusions where the contrast at the edges between adjoined areas of different lightness appears enhanced by a natural edge detection system in the human vision. In the red-white-blue colormap the Mach band is present because a region of high lightness is surrounded on either side by a ramp of decreasing lightness.

Moreland’s solution to eliminate the Mach band effect is to interpolate between red and blue using Msh, a newly defined, polar coordinate form of CIELab color space. His resulting color map is a very good one, and is available on his website (link 1) along with a few other ones that are worth looking at.

Peter Kovesi also has a great set of divergent color maps.

For my part, I’m working on an interpolation between yellow and blue in HSV color space that goes through achromatic gray. My idea is to keep Hue constant for half of the colormap, then switch Hue at the zero, and simultaneously vary Value between, say 70 and 0, then back to 70, using an ad-hoc nonlinear function. Saturation can be kept constant or varied as well. This colormap, along with a new grayscale that is symmetric on either ends of the intermediate gray, will be available on my Github page (github dot com slash mycarta) by the end of the year, and also added as ASCII csv files on my blog color palette page (mycarta dot wordpress dot com slash color-palettes).

I would also like to add a clarification on something that shrx writes in his answer. He mentions that both in the reference paper and in my bog post there's confusion between luminance and lightness. I clearly state that the original paper makes that confusion and that's why my solution in the post is to fix the palette by adjusting the lightness profile using LAB color space, the same that shrx used. I used L* for lightness in the figures where I use my adjusted colormap, and left luminance in the figures where I used the original colormap for consistency with the author's.