7

I need to draw an image with matplotlib's imshow() and then mark some pixels with different colour. Just changing their value in the initial array won't work as I need to use colours not present in the colourmap I am using. So my initial intention was to plot a second generated array above the first image with most of it masked and the required pixels not masked and with some values (potentially different ones to use different colour for different coordinates). And it works nicely with the matplotlib's interactive viewer, but when saving into a file everything gets distorted probably because of this bug, which I reported in the same situation: https://github.com/matplotlib/matplotlib/issues/3057

Are there any other options to change colour of some pixels?

Phlya
  • 5,726
  • 4
  • 35
  • 54

2 Answers2

20

You've already suggested the easiest way of doing it (overlaying another image on top), but if that's not working quite how you want it to, there are other options.


Approach #1 - Manually render and composite the image


The most direct way is to just render your array to RGB using the colormap, and then change the pixels you want.

As a quick example:

import numpy as np
import matplotlib.pyplot as plt

data = np.arange(100).reshape(10, 10)

cmap = plt.cm.gray
norm = plt.Normalize(data.min(), data.max())
rgba = cmap(norm(data))

# Set the diagonal to red...
rgba[range(10), range(10), :3] = 1, 0, 0

plt.imshow(rgba, interpolation='nearest')
plt.show()

enter image description here

A disadvantage to this method is that you can't just call fig.colorbar(im), as you're passing in a pre-rendered rgb image. Therefore, if you need a colorbar, you'll have to use a proxy artist. It's easiest to just add an additional, invisible (not drawn, rather than transparent) artist with imshow(data, visible=False) and then base the colormap on that artist. As a quick example:

import numpy as np
import matplotlib.pyplot as plt

data = np.arange(100).reshape(10, 10)

cmap = plt.cm.gray
norm = plt.Normalize(data.min(), data.max())
rgba = cmap(norm(data))

# Set the diagonal to red
rgba[range(10), range(10), :3] = 1, 0, 0

fig, ax = plt.subplots()
ax.imshow(rgba, interpolation='nearest')

# Add the colorbar using a fake (not shown) image.
im = ax.imshow(data, visible=False, cmap=cmap)
fig.colorbar(im)

plt.show()

enter image description here

Using an invisible imshow is the easiest way to make a proxy artist for this purpose, but if speed is a concern (or if it's somehow triggering the rendering bug you mentioned) you can also use any ScalarMappable. ScalarMappable is an abstract base class that's normally only used to inherit from for colorbar support. Because we don't need to draw anything, though, we can just use it directly.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.cm import ScalarMappable

data = np.arange(100).reshape(10, 10)

cmap = plt.cm.gray
norm = plt.Normalize(data.min(), data.max())
rgba = cmap(norm(data))

# Set the diagonal to red
rgba[range(10), range(10), :3] = 1, 0, 0

fig, ax = plt.subplots()
ax.imshow(rgba, interpolation='nearest')

# Add the colorbar using a ScalarMappable
im = ScalarMappable(norm, cmap)
im.set_array(data)
fig.colorbar(im)

plt.show()

Approach #2 - Abuse set_bad, set_over, or set_under


The set_bad, set_over and set_under methods of the colormap to allow you to flag pixels that are NaN's or out of the specified range of the colormap.

Therefore, another way to do what you want is to set those values to NaN and specify what the NaN color should be (set_bad.. By default it's transparent for most colormaps.).

If you have an integer array or already need to have transparent NaN pixels, you can similarly abuse set_over and set_under. In this case, you'll need to manually specify the vmin or vmax when you call imshow.

As a quick example of using/abusing set_bad to do this:

import numpy as np
import matplotlib.pyplot as plt

data = np.arange(100).reshape(10, 10).astype(float)

cmap = plt.cm.gray
cmap.set_bad((1, 0, 0, 1))

# Set the diagonal to NaN
data[range(10), range(10)] = np.nan

plt.imshow(data, cmap=cmap, interpolation='nearest')
plt.show()

enter image description here

One advantage to this method over the first one is that it's a bit easier to draw a colorbar. (The disadvantage is that this method is much less flexible.):

import numpy as np
import matplotlib.pyplot as plt

data = np.arange(100).reshape(10, 10).astype(float)

cmap = plt.cm.gray
cmap.set_bad((1, 0, 0, 1))

# Set the diagonal to NaN
data[range(10), range(10)] = np.nan

plt.imshow(data, cmap=cmap, interpolation='nearest')
plt.colorbar()
plt.show()

enter image description here

Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • Thank you for your suggestions, Joe, they seem very reasonable. Will these pixels be shown on a colorbar then in these cases? I don't need them there. – Phlya Nov 01 '14 at 21:01
  • After trying the first option I see that obviously the colorbar is hugely affected, as well as colours of the whole image. Probably it happens because the range of the values goes down to only (0, 1) after rendering to rgb, at least that's what I see on the labels of the colorbar, and I have a much higher range before. So the colours of some parts of the image become indistinguishable, although they are clearly different if I just call imshow() on my original data. – Phlya Nov 01 '14 at 21:31
  • 1
    And using `set_bad`, `set_under` and `set_over` is not a very good option as I would like to have a possibility to mark different pixels with different colours, and this limits their number to 3. And I might also need to use `vmin` and `vmax` for other purposes. – Phlya Nov 01 '14 at 21:35
  • By default, the added colors won't show up on the colorbar. However, making a colorbar for the first method requires a couple of extra steps, while you don't have to do anything extra for the second method. – Joe Kington Nov 01 '14 at 21:38
  • Oh, and if I do normalization as you suggest for the first option, then I get a blue square instead of my image, without it it looks more like it should. – Phlya Nov 01 '14 at 22:08
  • No worries! Thank you, this is definitely helpful, but why do you think the first method could change the colors on my picture? – Phlya Nov 01 '14 at 23:04
  • This is how it should look: https://yadi.sk/d/RMK1a4-ocSJj4 Here is how it looks after calling `cm(data)`: https://yadi.sk/d/Rm5A-Fm9cSJfY – Phlya Nov 01 '14 at 23:14
  • That's odd... Can you show a bit more of your code? Is it possible that you're inadvertently doing `rgb = norm(cmap(data))` instead of `rgb = cmap(norm(data))`? – Joe Kington Nov 02 '14 at 02:17
  • I am afraid the code is too long and complicate :( And as I said, in my case if I add normalization step, I just get a blue square instead of an image, so I am not using it. – Phlya Nov 02 '14 at 06:20
  • I'll try to make a short version of the code later today. – Phlya Nov 02 '14 at 11:10
  • Ah! Well, if you leave out any sort of normalization, that explains it. The colormap is expecting values between 0 and 1. The example I showed assumed there were no NaN's or Inf's during the normalization step. You'll need to call `nanmin` and `nanmax` if there are. – Joe Kington Nov 02 '14 at 14:07
  • Oh, yes, thank you! I had all kinds of not finite numbers in the array, so now using `data[np.isfinite(data)].min()` and similarly for `max()`, and now it works nicely with my data too! But Could you think of any other way to generate a colorbar? It seems that plotting an invisible image (except for taking a long time with a big dataset) triggers the same bug I referred to in my original question... – Phlya Nov 02 '14 at 15:55
  • Sure, you can also add a colorbar by "faking" a `ScalarMappable` instance. It should be much faster, and hopefully won't trigger the bug. I've added an example. – Joe Kington Nov 02 '14 at 16:21
  • Thanks a lot, Joe, this seems to be working perfectly for me now! – Phlya Nov 02 '14 at 17:02
0

To add to Joe's very nice answer, when supplying an array of rgba values to imshow, the z values read out by the mouse cursor now show tuples of rgba values, instead of the oroginal data values.

To work around this, we can overlay a transparent image onto the original one. We can then at the same time use this transparent image to attach a colorbar to the figure:

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

data = np.arange(100).reshape(10, 10)

cmap = plt.cm.gray
norm = plt.Normalize(data.min(), data.max())
rgba = cmap(norm(data))

# Set the diagonal to red...
rgba[range(10), range(10), :3] = 1, 0, 0

im = ax.imshow(rgba, interpolation='nearest')
im2 = ax.imshow(data, cmap='gray')
cbar = plt.colorbar(im2, ax=ax)
im2.set_alpha(0.0)

Note that it is important in this case to create the colorbar before the call to im2.set_alpha(0.0). If not, the colors in the colorbar will also be transparent (they follow the current alpha of the image).

In a case where it would not be possible to respect the creation order, one can set the opacity of the colors in the colorbar back to 1 using

cbar.set_alpha(1.0)
cbar.draw_all()
nvaytet
  • 61
  • 6