8

I know how to map a number to a color from this post: Map values to colors in matplotlib

But I don't know how to decode the mapping to get my original color, assuming this is a one-to-one mapping, which it must be I figure.

I am encoding an image for visualization purposes, but I need to be able to decode it and read the original data values.

For reference, here are the Colormap docs: http://matplotlib.org/api/cm_api.html

Here's my try to the main answer below, which still isn't working right.

from PIL import Image
import numpy as np
import matplotlib
import matplotlib.cm as cm
values = [670, 894, 582, 103, 786, 348, 972, 718, 356, 692]
minima = 103
maxima = 972
norm = matplotlib.colors.Normalize(vmin=minima, vmax=maxima, clip=True)
mapper = cm.ScalarMappable(norm=norm, cmap=cm.gist_rainbow_r)
c = []
for i in range(10):
    c.append(mapper.to_rgba(values[i], bytes=True))
print(c) # [(75, 255, 0, 255), (255, 77, 0, 255), (0, 255, 64, 255), (255, 0, 191, 255), (255, 250, 0, 255), (0, 72, 255, 255), (255, 0, 40, 255), (151, 255, 0, 255), (0, 83, 255, 255), (108, 255, 0, 255)]

def get_value_from_cm(color, cmap, colrange):
    # color = matplotlib.colors.to_rgba(color)
    r = np.linspace(colrange[0], colrange[1], 10) # there are 10 values
    norm = matplotlib.colors.Normalize(colrange[0], colrange[1])
    mapvals = cmap(norm(r))[:, :4] # there are 4 channels: r,g,b,a
    distance = np.sum((mapvals - color) ** 2, axis=1)
    return r[np.argmin(distance)]

decoded_colors = []
for i in range(10):
    decoded_colors.append(get_value_from_cm(c[i], cm.gist_rainbow_r, colrange=[minima, maxima]))
print(decoded_colors) # [778.88888888888891, 778.88888888888891, 489.22222222222223, 103.0, 778.88888888888891, 392.66666666666669, 103.0, 778.88888888888891, 392.66666666666669, 778.88888888888891]
J. Doe
  • 985
  • 2
  • 10
  • 15
  • That question has already been asked a week ago [here](https://stackoverflow.com/questions/45064969/go-from-rgb-to-scalars-in-matplotlib-colormap) and also 4 years ago [here](https://stackoverflow.com/questions/14445102/invert-not-reverse-a-colormap-in-matplotlib). Both do not have an answer. That said, inverting the colormapping is possible, if (a) you know the data range it is mapping and (b) if you know the colormap that has been used, and (c) if the colormap is unambiguous. I just guess that people are too lazy to do the work, because usually that problem can be circumvented. – ImportanceOfBeingErnest Jul 18 '17 at 21:51
  • Decoding the image is an imprecise way to recover the data -- normally you save the original data (and the code needed to generate a published image) and access that. – cphlewis Jul 18 '17 at 21:53
  • I don't see anywhere in matplotlib's bad docs about the underlying mechanism for their mappings but if they are injective, all data (that was encoded) can be recovered. Do you know where I can find this info and why you are saying it is not? The reason for doing it this way is that we want an image to show a specific color scheme in 1d for values in an array and then using nothing but that image to get our original values. We can't control colors just using the raw values. – J. Doe Jul 18 '17 at 23:56

1 Answers1

8

Inverting the colormapping is possible, if
(a) you know the data range it is mapping and
(b) if you know the colormap that has been used, and
(c) if the colormap is unambiguous.

The following function would return the value given a color, a colormap and the range over which the colormap has been used.

import numpy as np
import matplotlib.colors
import matplotlib.pyplot as plt

def get_value_from_cm(color, cmap, colrange=[0.,1.]):
    color=matplotlib.colors.to_rgb(color)
    r = np.linspace(colrange[0],colrange[1], 256)
    norm = matplotlib.colors.Normalize(colrange[0],colrange[1])
    mapvals = cmap(norm(r))[:,:3]
    distance = np.sum((mapvals - color)**2, axis=1)
    return r[np.argmin(distance)]


b = get_value_from_cm(plt.cm.coolwarm(0.5), plt.cm.coolwarm, [0.,1.])
c = get_value_from_cm(np.array([1,0,0]), plt.cm.coolwarm)

print b                   # 0.501960784314
print plt.cm.coolwarm(b)
# (0.86742763508627452, 0.86437659977254899, 0.86260246201960789, 1.0)
print plt.cm.coolwarm(0.5)
#(0.86742763508627452, 0.86437659977254899, 0.86260246201960789, 1.0)

Note that this method involves an error, so you only get the closest value from the colormap and not the value that has initially been used to create the color from the map.

In the updated code from the question, you have the color defined as integers between 0 and 255 for each channel. You therefore need to first map those to the range 0 to 1.

from PIL import Image
import numpy as np
import matplotlib
import matplotlib.cm as cm
values = [670, 894, 582, 103, 786, 348, 972, 718, 356, 692]
minima = 103
maxima = 972
norm = matplotlib.colors.Normalize(vmin=minima, vmax=maxima, clip=True)
mapper = cm.ScalarMappable(norm=norm, cmap=cm.gist_rainbow_r)
c = []
for i in range(10):
    c.append(mapper.to_rgba(values[i], bytes=True))
print(c) # [(75, 255, 0, 255), (255, 77, 0, 255), (0, 255, 64, 255), (255, 0, 191, 255), (255, 250, 0, 255), (0, 72, 255, 255), (255, 0, 40, 255), (151, 255, 0, 255), (0, 83, 255, 255), (108, 255, 0, 255)]

def get_value_from_cm(color, cmap, colrange):
    color = np.array(color)/255. 
    r = np.linspace(colrange[0], colrange[1], 256) 
    norm = matplotlib.colors.Normalize(colrange[0], colrange[1])
    mapvals = cmap(norm(r))[:, :4] # there are 4 channels: r,g,b,a
    distance = np.sum((mapvals - color) ** 2, axis=1)
    return r[np.argmin(distance)]

decoded_colors = []
for i in range(10):
    decoded_colors.append(get_value_from_cm(c[i], cm.gist_rainbow_r, colrange=[minima, maxima]))
print(decoded_colors)
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thank you for the response. I tried to modify my code to work with that, but it's giving me some weird numbers. Maybe you can see the problem in my code? (Sorry, tried going to new line but it submitted) Please look at the updated post in a minute – J. Doe Jul 18 '17 at 23:29
  • I updated the answer with the solution to your problem. – ImportanceOfBeingErnest Jul 19 '17 at 07:05
  • thanks but the error does not work for me. i dont understand why there isnt a way to get back exact values – J. Doe Jul 19 '17 at 17:11
  • Sorry, you changed my original code to replace the 256 by the number 10 in the linspace and I blindly took that over for the second version. This needs to stay 256 of course. The error can then be as large as `(972-103)/256. = 3.3`; so you can recover the values with an error of ~+/-3. This is due to the resolution of the colormap, which has 256 colors in it. – ImportanceOfBeingErnest Jul 20 '17 at 08:40