16

How do I invert a color mapped image?

I have a 2D image which plots data on a colormap. I'd like to read the image in and 'reverse' the color map, that is, look up a specific RGB value, and turn it into a float.

For example: using this image: http://matplotlib.sourceforge.net/_images/mri_demo.png

I should be able to get a 440x360 matrix of floats, knowing the colormap was cm.jet

from pylab import imread
import matplotlib.cm as cm
a=imread('mri_demo.png')
b=colormap2float(a,cm.jet) #<-tricky part
JohanC
  • 71,591
  • 8
  • 33
  • 66
user448764
  • 171
  • 1
  • 5

4 Answers4

20

There may be better ways to do this; I'm not sure. If you read help(cm.jet) you will see the algorithm used to map values in the interval [0,1] to RGB 3-tuples. You could, with a little paper and pencil, work out formulas to invert the piecewise-linear functions which define the mapping.

However, there are a number of issues which make the paper and pencil solution somewhat unappealing:

  1. It's a lot of laborious algebra, and the solution is specific for cm.jet. You'd have to do all this work again if you change the color map. How to automate the solving of these algebraic equations is interesting, but not a problem I know how to solve.

  2. In general, the color map may not be invertible (more than one value may be mapped to the same color). In the case of cm.jet, values between 0.11 and 0.125 are all mapped to the RGB 3-tuple (0,0,1), for example. So if your image contains a pure blue pixel, there is really no way to tell if it came from a value of 0.11 or a value of, say, 0.125.

  3. The mapping from [0,1] to 3-tuples is a curve in 3-space. The colors in your image may not lie perfectly on this curve. There might be round-off error, for example. So any practical solution has to be able to interpolate or somehow project points in 3-space onto the curve.

Due to the non-uniqueness issue, and the projection/interpolation issue, there can be many possible solutions to the problem you pose. Below is just one possibility.

Here is one way to resolve the uniqueness and projection/interpolation issues:

Create a gradient which acts as a "code book". The gradient is an array of RGBA 4-tuples in the cm.jet color map. The colors of the gradient correspond to values from 0 to 1. Use scipy's vector quantization function scipy.cluster.vq.vq to map all the colors in your image, mri_demo.png, onto the nearest color in gradient. Since a color map may use the same color for many values, the gradient may contain duplicate colors. I leave it up to scipy.cluster.vq.vq to decide which (possibly) non-unique code book index to associate with a particular color.

import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
import scipy.cluster.vq as scv

def colormap2arr(arr,cmap):    
    # http://stackoverflow.com/questions/3720840/how-to-reverse-color-map-image-to-scalar-values/3722674#3722674
    gradient=cmap(np.linspace(0.0,1.0,100))

    # Reshape arr to something like (240*240, 4), all the 4-tuples in a long list...
    arr2=arr.reshape((arr.shape[0]*arr.shape[1],arr.shape[2]))

    # Use vector quantization to shift the values in arr2 to the nearest point in
    # the code book (gradient).
    code,dist=scv.vq(arr2,gradient)

    # code is an array of length arr2 (240*240), holding the code book index for
    # each observation. (arr2 are the "observations".)
    # Scale the values so they are from 0 to 1.
    values=code.astype('float')/gradient.shape[0]

    # Reshape values back to (240,240)
    values=values.reshape(arr.shape[0],arr.shape[1])
    values=values[::-1]
    return values

arr=plt.imread('mri_demo.png')
values=colormap2arr(arr,cm.jet)    
# Proof that it works:
plt.imshow(values,interpolation='bilinear', cmap=cm.jet,
           origin='lower', extent=[-3,3,-3,3])
plt.show()

The image you see should be close to reproducing mri_demo.png:

alt text

(The original mri_demo.png had a white border. Since white is not a color in cm.jet, note that scipy.cluster.vq.vq maps white to to closest point in the gradient code book, which happens to be a pale green color.)

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • yes, this is essentially what I thought was possible. Your initial solution included reading a line from an image with the same color map, which may be helpful to people who say, scan a figure in, and want to do their own numerical analysis. I was getting stuck on the vector quantization - initially, it seemed like the best bet would be to cycle through each possible color in the lut and calculate a 3d distance from the actual pixel value - which I couldn't see how to do quickly without looping. Thanks! – user448764 Sep 16 '10 at 20:33
2

Here is a simpler approach, that works for many colormaps, e.g. viridis, though not for LinearSegmentedColormaps such as 'jet'.

The colormaps are stored as lists of [r,g,b] values. For lots of colormaps, this map has exactly 256 entries. A value between 0 and 1 is looked up using its nearest neighbor in the color list. So, you can't get the exact value back, only an approximation.

Some code to illustrate the concepts:

from matplotlib import pyplot as plt

def find_value_in_colormap(tup, cmap):
    # for a cmap like viridis, the result of the colormap lookup is a tuple (r, g, b, a), with a always being 1
    # but the colors array is stored as a list [r, g, b]
    # for some colormaps, the situation is reversed: the lookup returns a list, while the colors array contains tuples
    tup = list(tup)[:3]
    colors = cmap.colors
    if tup in colors:
        ind = colors.index(tup)
    elif tuple(tup) in colors:
        ind = colors.index(tuple(tup))
    else: # tup was not generated by this colormap
        return None
    return (ind + 0.5) / len(colors)

val = 0.3
tup = plt.cm.viridis(val)

print(find_value_in_colormap(tup, plt.cm.viridis))

This prints the approximate value:

0.298828125

being the value corresponding to the color triple.

To illustrate what happens, here is a visualization of the function looking up a color for a value, followed by getting the value corresponding to that color.

from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(-0.1, 1.1, 10000)
y = [ find_value_in_colormap(plt.cm.viridis(x), plt.cm.viridis) for x in x]

fig, axes = plt.subplots(ncols=3, figsize=(12,4))
for ax in axes.ravel():
    ax.plot(x, x, label='identity: y = x')
    ax.plot(x, y, label='lookup, then reverse')
    ax.legend(loc='best')
axes[0].set_title('overall view')
axes[1].set_title('zoom near x=0')
axes[1].set_xlim(-0.02, 0.02)
axes[1].set_ylim(-0.02, 0.02)
axes[2].set_title('zoom near x=1')
axes[2].set_xlim(0.98, 1.02)
axes[2].set_ylim(0.98, 1.02)
plt.show()

overview plot

For a colormap with only a few colors, a plot can show the exact position where one color changes to the next. The plot is colored corresponding to the x-values.

overview for tab10

Tian
  • 870
  • 3
  • 12
  • 24
JohanC
  • 71,591
  • 8
  • 33
  • 66
0

Hy unutbu,

Thanks for your reply, I understand the process you explain, and reproduces it. It works very well, I use it to reverse IR camera shots in temperature grids, since a picture can be easily rework/reshape to fulfill my purpose using GIMP.

I'm able to create grids of scalar from camera shots that is really usefull in my tasks.

I use a palette file that I'm able to create using GIMP + Sample a Gradient Along a Path. I pick the color bar of my original picture, convert it to palette then export as hex color sequence. I read this palette file to create a colormap normalized by a temperature sample to be used as the code book. I read the original image and use the vector quantization to reverse color into values. I slightly improve the pythonic style of the code by using code book indices as index filter in the temperature sample array and apply some filters pass to smooth my results.

from numpy import linspace, savetxt
from matplotlib.colors import Normalize, LinearSegmentedColormap
from  scipy.cluster.vq import vq

# sample the values to find from colorbar extremums
vmin = -20.
vmax = 120.
precision = 1.

resolution = 1 + vmax-vmin/precision
sample = linspace(vmin,vmax,resolution)

# create code_book from sample
cmap = LinearSegmentedColormap.from_list('Custom', hex_color_list)
norm = Normalize()
code_book = cmap(norm(sample))

# quantize colors
indices = vq(flat_image,code_book)[0]
# filter sample from quantization results **(improved)**
values = sample[indices]

savetxt(image_file_name[:-3]+'.csv',values ,delimiter=',',fmt='%-8.1f')

The results are finally exported in .csv

Most important thing is to create a well representative palette file to obtain a good precision. I start to obtain a good gradient (code book) using 12 colors and more. This process is useful since sometimes camera shots cannot be translated to gray-scale easily and linearly.

Thanks to all contributors unutbu, Rob A, scipy community ;)

0

The LinearSegmentedColormap doesn't give me the same interpolation if I don't it manually during my test, so I prefer to use my own :

As an advantage, matplotlib is not more required since I integrate my code within an existing software.

def codeBook(color_list, N=256):
    """
    return N colors interpolated from rgb color list
    !!! workaround to matplotlib colormap to avoid dependency !!!
    """
    # seperate r g b channel
    rgb = np.array(color_list).T
    # normalize data points sets
    new_x = np.linspace(0., 1., N)
    x = np.linspace(0., 1., len(color_list))
    # interpolate each color channel
    rgb = [np.interp(new_x, x, channel) for channel in rgb]
    # round elements of the array to the nearest integer.
    return np.rint(np.column_stack( rgb )).astype('int')