46

I was given a data set that is essentially an image, however each pixel in the image is represented as a value from -1 to 1 inclusive. I am writing an application that needs to take these -1 to 1 grayscale values and map them to the associated RGB value for the MATLAB "Jet" color scale (red-green-blue color gradient).

I am curious if anyone knows how to take a linear value (like -1 to 1) and map it to this scale. Note that I am not actually using MATLAB for this (nor can I), I just need to take the grayscale value and put it on the Jet gradient.

Thanks, Adam

Adam Shook
  • 709
  • 1
  • 7
  • 10
  • So you want to end up with a B&W image encoded with a color format? – Pubby Oct 09 '11 at 20:45
  • 1
    What do the values mean? For example, is -1 pure black? – Jim Rhodes Oct 09 '11 at 20:48
  • I have a C++ class that converts from a wavelength (in nanometers) to RGB. Is this what you want ? (of course, you would use -1=380nm=violet and 1=780nm=red to cover the whole scale). It seems to me that the 'jet' color scale is more about the visible spectrum than about Red-Green-Blue. Anyway I can post the code if it can help. –  Oct 09 '11 at 20:49
  • The data itself is a 530x530 covariance matrix. Each value of the matrix is represented from -1 to 1 and is traditionally colored with a gradient from dark red (1) to green-sh (0) to dark blue (-1). Here is a link that may help describe the gradient: http://blogs.mathworks.com/images/loren/73/colormapManip_14.png The data is often used in MATLAB and it can automatically apply this single value to the gradient. However, I need to use it in my real-time C++ graphics application while still preserving the proper color scheme. – Adam Shook Oct 09 '11 at 20:56
  • Sounds like the most straightforward method would be to calculate hue (in degrees) as something like hue = 120 - 120 * data (to give you a range of 240 = blue to 0 = red) and then assume full saturation and value and convert to RGB. Unless you want to check if (data < 0) then RGB is over one range else if (data > 0) then RGB is over another range. – tinman Oct 09 '11 at 21:19
  • [D. Borland and R.M. Taylor II, **Rainbow Color Map (Still) Considered Harmful**, *IEEE Computer Graphics and Applications* 27(2):14-17, 2007](https://pdfs.semanticscholar.org/ee79/2edccb2c88e927c81285344d2d88babfb86f.pdf). It's been long enough since we've known that this color map is **really** misleading. Please look for a different color map. Might I suggest [anyone of these](https://peterkovesi.com/projects/colourmaps/)? – Cris Luengo Oct 31 '18 at 22:20

7 Answers7

87

Consider the following function (written by Paul Bourke -- search for Colour Ramping for Data Visualisation):

/*
   Return a RGB colour value given a scalar v in the range [vmin,vmax]
   In this case each colour component ranges from 0 (no contribution) to
   1 (fully saturated), modifications for other ranges is trivial.
   The colour is clipped at the end of the scales if v is outside
   the range [vmin,vmax]
*/

typedef struct {
    double r,g,b;
} COLOUR;

COLOUR GetColour(double v,double vmin,double vmax)
{
   COLOUR c = {1.0,1.0,1.0}; // white
   double dv;

   if (v < vmin)
      v = vmin;
   if (v > vmax)
      v = vmax;
   dv = vmax - vmin;

   if (v < (vmin + 0.25 * dv)) {
      c.r = 0;
      c.g = 4 * (v - vmin) / dv;
   } else if (v < (vmin + 0.5 * dv)) {
      c.r = 0;
      c.b = 1 + 4 * (vmin + 0.25 * dv - v) / dv;
   } else if (v < (vmin + 0.75 * dv)) {
      c.r = 4 * (v - vmin - 0.5 * dv) / dv;
      c.b = 0;
   } else {
      c.g = 1 + 4 * (vmin + 0.75 * dv - v) / dv;
      c.b = 0;
   }

   return(c);
}

Which, in your case, you would use it to map values in the range [-1,1] to colors as (it is straightforward to translate it from C code to a MATLAB function):

c = GetColour(v,-1.0,1.0);

This produces to the following "hot-to-cold" color ramp:

color_ramp

It basically represents a walk on the edges of the RGB color cube from blue to red (passing by cyan, green, yellow), and interpolating the values along this path.

color_cube


Note this is slightly different from the "Jet" colormap used in MATLAB, which as far as I can tell, goes through the following path:

#00007F: dark blue
#0000FF: blue
#007FFF: azure
#00FFFF: cyan
#7FFF7F: light green
#FFFF00: yellow
#FF7F00: orange
#FF0000: red
#7F0000: dark red

Here is a comparison I did in MATLAB:

%# values
num = 64;
v = linspace(-1,1,num);

%# colormaps
clr1 = jet(num);
clr2 = zeros(num,3);
for i=1:num
    clr2(i,:) = GetColour(v(i), v(1), v(end));
end

Then we plot both using:

figure
subplot(4,1,1), imagesc(v), colormap(clr), axis off
subplot(4,1,2:4), h = plot(v,clr); axis tight
set(h, {'Color'},{'r';'g';'b'}, 'LineWidth',3)

jet hot_to_cold

Now you can modify the C code above, and use the suggested stop points to achieve something similar to jet colormap (they all use linear interpolation over the R,G,B channels as you can see from the above plots)...

svenevs
  • 833
  • 9
  • 24
Amro
  • 123,847
  • 25
  • 243
  • 454
  • MATLAB Jet follows this path as per [http://bugs.launchpad.net/inkscape/+bug/236508](this) .11 steps in BLUE line from (0, 0, 127) to (0, 0, 255), BLUE - GREEN line from (0, 14, 255) to (0, 255, 255) in 18 steps, GREEN - YELLOW line from (14, 255, 240) to (255, 255, 0) in 18 steps, YELLOW - RED line from (255, 240, 0) to (255, 0, 0) in 18 steps and finally in the red line (240, 0, 0) to (127, 0, 0) in 11 steps. Might have minor error in step count. – habla2019 Nov 01 '18 at 17:32
  • 1
    keep in mind MATLAB interpolates values along the path mentioned in the answer above (see ramp plot) and returns a colormap of any length, the values are not hard-coded at a fixed length. – Amro Nov 01 '18 at 21:36
  • I think, this is really just a complicated way of saying V is mapped onto H in the HSV model, and S=V=1 . What the code basically does is to convert from HSV color space to RGB space with S=V=1. If you walk on the upper outer rim of the HSV color space, this amounts to walking on the RGB cube in the path shown above. Of course, eventually one has to convert from HSV to RGB space. But I feel that doing that explicitly is better programming style. – Gab Nov 21 '19 at 19:36
  • @Gabriel sort of, yes. But to get the same results above as the the cold-to-hot ramp, we don't map the values over the entire H range, only two-thirds of it (i.e from 0° to 240° on the HSV cylinder), otherwise it would include blue-to-magenta-to-red shades: https://i.imgur.com/ojnma2S.png, https://pastebin.com/w2Fdzxf4 – Amro Nov 23 '19 at 12:18
  • @Amro: yes, that is understood that it only walks from 240 deg to 0 deg. Still, I think, it would be much better to do the conversion explicitly , and describing it that way, than doing all this stuff implicitly. – Gab Nov 25 '19 at 07:56
  • @Gabriel true but the HSV2RGB trick is a specific case and won't necessarily work to generate the "Jet" colormap (see the stop-points mentioned above). You would need to do some kind of piecewise interpolation similar to the code in the beginning of the post: https://imgur.com/a/RZneAtV, https://pastebin.com/w0NsHyvr – Amro Nov 25 '19 at 12:40
  • I see, thanks for pointing it out. A path of the Jet colormap on the RGB cube and the HSV cone might be helpful, but I don't have the time right now. On the RGB cube it seems to look more like some meandering, while on the HSV cone it mostly might look a bit like a saw-tooth on the mantle. – Gab Nov 26 '19 at 19:16
30

I hope this is what you're looking for:

double interpolate( double val, double y0, double x0, double y1, double x1 ) {
  return (val-x0)*(y1-y0)/(x1-x0) + y0;
}
double blue( double grayscale ) {
  if ( grayscale < -0.33 ) return 1.0;
  else if ( grayscale < 0.33 ) return interpolate( grayscale, 1.0, -0.33, 0.0, 0.33 );
  else return 0.0;
}
double green( double grayscale ) {
  if ( grayscale < -1.0 ) return 0.0; // unexpected grayscale value
  if  ( grayscale < -0.33 ) return interpolate( grayscale, 0.0, -1.0, 1.0, -0.33 );
  else if ( grayscale < 0.33 ) return 1.0;
  else if ( grayscale <= 1.0 ) return interpolate( grayscale, 1.0, 0.33, 0.0, 1.0 );
  else return 1.0; // unexpected grayscale value
}
double red( double grayscale ) {
  if ( grayscale < -0.33 ) return 0.0;
  else if ( grayscale < 0.33 ) return interpolate( grayscale, 0.0, -0.33, 1.0, 0.33 );
  else return 1.0;
}

I'm not sure if this scale is 100% identical to the image you linked but it should look very similar.

UPDATE I've rewritten the code according to the description of MatLab's Jet palette found here

double interpolate( double val, double y0, double x0, double y1, double x1 ) {
    return (val-x0)*(y1-y0)/(x1-x0) + y0;
}

double base( double val ) {
    if ( val <= -0.75 ) return 0;
    else if ( val <= -0.25 ) return interpolate( val, 0.0, -0.75, 1.0, -0.25 );
    else if ( val <= 0.25 ) return 1.0;
    else if ( val <= 0.75 ) return interpolate( val, 1.0, 0.25, 0.0, 0.75 );
    else return 0.0;
}

double red( double gray ) {
    return base( gray - 0.5 );
}
double green( double gray ) {
    return base( gray );
}
double blue( double gray ) {
    return base( gray + 0.5 );
}
Ilya Denisov
  • 868
  • 8
  • 25
  • This is fairly close but not 100%. It will work for the time being. I may just have to sample a texture to get the exact values... Thank you! – Adam Shook Oct 09 '11 at 21:39
  • 2
    I've found some info about MatLab's Jet palette: https://bugs.launchpad.net/inkscape/+bug/236508 – Ilya Denisov Oct 11 '11 at 07:52
19

The other answers treat the interpolation as a piecewise linear function. This can be simplified by using a clamped triangular basis function for interpolation. We need a clamp function that maps its input to the closed unit interval:

clamp(x)=max(0, min(x, 1))

And a basis function for interpolation:

N(t) = clamp(1.5 - |2t|)

Then the color becomes:

r = N(t - 0.5), g = N(t), b = N(t + 0.5)

Plotting this from -1 to 1 gives:

Plot of RGB values from -1 to 1

Which is the same as provided in this answer. Using an efficient clamp implementation:

double clamp(double v)
{
  const double t = v < 0 ? 0 : v;
  return t > 1.0 ? 1.0 : t;
}

and ensuring your value t is in [-1, 1], then jet color is simply:

double red   = clamp(1.5 - std::abs(2.0 * t - 1.0));
double green = clamp(1.5 - std::abs(2.0 * t));
double blue  = clamp(1.5 - std::abs(2.0 * t + 1.0));

As shown in the above link on implementing clamp, the compiler may optimize out branches. The compiler may also use intrinsics to set the sign bit for std::abs eliminating another branch.

"Hot-to-Cold"

A similar treatment can be used for the "hot-to-cold" color mapping. In this case the basis and color functions are:

N(t) = clamp(2 - |2t|)

r(t)=N(t-1), g(t) = N(t), b(t) = N(t+1)

And the hot-to-cold plot for [-1, 1]:

Hot-to-cold plot

OpenGL Shader Program

Eliminating explicit branches makes this approach efficient for implementing as an OpenGL shader program. GLSL provides built-in functions for both abs and clamp that operate on 3D vectors. Vectorizing the color calculation and preferring built-in functions over branching can provide significant performance gains. Below is an implementation in GLSL that returns the RGB jet color as a vec3. Note that the basis function was modified such that t must lie in [0,1] rather than the range used in the other examples.

vec3 jet(float t)
{
  return clamp(vec3(1.5) - abs(4.0 * vec3(t) + vec3(-3, -2, -1)), vec3(0), vec3(1));
}
Joshua Fraser
  • 356
  • 1
  • 3
  • 6
  • The OpenGL Shader Program works correctly, thanks for the sharing. Where do you find this convention method? Any external links? – ollydbg23 Aug 08 '22 at 10:14
  • I don't understand your question. The OpenGL shader program is a variation of the method I describe above. All of the content in my answer—including the shader program—is my original work. – Joshua Fraser Aug 12 '22 at 02:12
  • Hi, thanks for the reply, your original work is really nice, it help me to create a 3D view of a heatmap. – ollydbg23 Aug 12 '22 at 02:35
3

I'm not really sure why there are so many complex answers to this simple equation. Based on the MatLab JET Hot-to-Cold color map chart and graph plot posted above in Amro's comment (thank you), the logic is very simple to calculate the RGB values using high-speed/basic math.

I use the following function for live-rendering normalized data to display spectrograms and it's incredibly fast and efficient with no complex math outside double precision multiplication and division, simplified by ternary logic chaining. This code is C# but very easily ported to almost any other language (sorry PHP programmers, you're out of luck thanks to abnormal ternary chain order).

public byte[] GetMatlabRgb(double ordinal)
{
    byte[] triplet = new byte[3];
    triplet[0] = (ordinal < 0.0)  ? (byte)0 : (ordinal >= 0.5)  ? (byte)255 : (byte)(ordinal / 0.5 * 255);
    triplet[1] = (ordinal < -0.5) ? (byte)((ordinal + 1) / 0.5 * 255) : (ordinal > 0.5) ? (byte)(255 - ((ordinal - 0.5) / 0.5 * 255)) : (byte)255;
    triplet[2] = (ordinal > 0.0)  ? (byte)0 : (ordinal <= -0.5) ? (byte)255 : (byte)(ordinal * -1.0 / 0.5 * 255);
    return triplet;
}

The function takes an ordinal range from -1.0 to 1.0 per the JET color specification, though this function does no sanity checking if you're outside that range (I do that before my call here).

So make sure you do sanity/bounds checking prior to calling this function or simply add your own limiting to cap the value when you implement it yourself.

This implementation does not take luminosity into consideration so may not be considered a purist implementation but gets you in the ballpark fairly well and is much faster.

Billal Begueradj
  • 20,717
  • 43
  • 112
  • 130
tpartee
  • 548
  • 7
  • 20
1

Java(Processing) code that will generate Jet and HotAndCold RGB. I created this code following the RGB distribution scheme in the post of Amro above.

color JetColor(float v,float vmin,float vmax){
       float r=0, g=0, b=0;
       float x = (v-vmin)/(vmax-vmin);
       r = 255*constrain(-4*abs(x-0.75) + 1.5,0,1);
       g = 255*constrain(-4*abs(x-0.50) + 1.5,0,1);
       b = 255*constrain(-4*abs(x-0.25) + 1.5,0,1);
       return color(r,g,b);
    }

color HeatColor(float v,float vmin,float vmax){
       float r=0, g=0, b=0;
       float x = (v-vmin)/(vmax-vmin);
       r = 255*constrain(-4*abs(x-0.75) + 2,0,1);
       g = 255*constrain(-4*abs(x-0.50) + 2,0,1);
       b = 255*constrain(-4*abs(x) + 2,0,1);
       return color(r,g,b);
    }
    //Values are calculated on trapezoid cutoff points in format y=constrain(a(x-t)+b,0,1)
    //Where a=((delta)y/(delta)x), t=x-offset value to symetric middle of trapezoid, and b=y-a(x-t) for the last peak point (x,y)
1

Seems like you have hue values of an HSL system and the saturation and lightness are implicit. Search for HSL to RGB conversion on the internet and you will find a lot of explanations, code etc. (Here is one link)

In your particular case, though, let's assume you are defaulting all color saturations to 1 and lightness to 0.5. Here is the formula you can use to get the RGB values:

Imagine for every pixel, you have h the value you read from your data.

hue = (h+1.0)/2;  // This is to make it in range [0, 1]
temp[3] = {hue+1.0/3, hue, hue-1.0/3};
if (temp[0] > 1.0)
    temp[0] -= 1.0;
if (temp[2] < 0.0)
    temp[2] += 1.0;

float RGB[3];
for (int i = 0; i < 3; ++i)
{
    if (temp[i]*6.0 < 1.0)
        RGB[i] = 6.0f*temp[i];
    else if (temp[i]*2.0 < 1.0)
        RGB[i] = 1;
    else if (temp[i]*3.0 < 2.0)
        RGB[i] = ((2.0/3.0)-temp[i])*6.0f;
    else
        RGB[i] = 0;
}

And there you have the RGB values in RGB all in the range [0, 1]. Note that the original conversion is more complex, I simplified it based on values of saturation=1 and lightness=0.5

Why this formula? See this wikipedia entry

Shahbaz
  • 46,337
  • 19
  • 116
  • 182
0

This probably isn't exactly the same, but it may be close enough for your needs:

if (-0.75 > value) {
    blue = 1.75 + value;
} else if (0.25 > value) {
    blue = 0.25 - value;
} else {
    blue = 0;
}

if ( -0.5 > value) {
    green = 0;
} else if (0.5 > value) {
    green = 1 - 2*abs(value);
} else {
    green = 0;
}

if ( -0.25 > value) {
    red = 0;
} else if (0.75 > value) {
    red = 0.25 + value;
} else {
    red = 1.75 - value;
}
IronMensan
  • 6,761
  • 1
  • 26
  • 35