Search code examples
pythonmatplotlibvisualizationimshow

matplotlib: 3 channel binary RGB image only shows black


I have three 2D datasets that I'm trying to plot as an RGB image using matplotlib. I thought this would be quite straightforward but I'm finding it very frustrating.

Here's the bit of code I'm currently using:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes

b = simg["B"].astype(bool).astype(int)
g = simg["G"].astype(bool).astype(int)
r = simg["R"].astype(bool).astype(int)

fig = plt.figure()
ax = RGBAxes(fig, [0.1, 0.1, 0.8, 0.8], pad=0.0)
ax.imshow_rgb(r, g, b, interpolation="none")

where simg is the dictionary with my single channel images in it, stored as (n, m) arrays with dtype = uint16. The original data is "almost binary", so I'm using the astypes to threshold it. For reference, each channel contains about 5-10% 1s after thresholding. This is working as intended so I don't think it's the problem, but I tried removing the astypes anyway and it doesn't fix the issue.

Here's the result of the above snippet:

nothing but black

In that screenshot I've zoomed way in, and the area I marqueed off should be a green pixel. This data is present in the image, as rolling the mouse over this pixel shows that it indeed has a value of [0, 1, 0] in the tkinter window.

I thought this might have something to do with the RGBAxes, so I tried a much simpler method of displaying this data:

rgb = np.dstack((r, g, b))
plt.imshow(rgb, interpolation="none")

This creates an (n, m, 3) array, which the imshow documentation says will be interpreted as an RGB image, but this also just plots a completely black image. I also tried all of the different available interpolation methods, and tried changing the plotting backend (currently tkinter, but I also tried Qt5).

The data plots just fine if I only plot one channel. For example:

plt.imshow(b, cmap="binary_r", interpolation="none")

results in the image:

working binary blue channel

which is exactly what I expect to see in that channel. So something about trying to plot the three channels at once is not working the way I expect it to.

I'd appreciate any guidance anyone can give here, and I'm happy to provide any additional information I can that might help. If I can't get this working I'll probably just try rolling my own solution using matplotlib.cm.ScalarMappable and manually combining the RGB channels, but if I go that route I'll also need to re-implement the features of RGBAxes (i.e. the separate, smaller R, G, and B plots beside the combined RGB image) which would be kind of tedious.


Solution

  • I've solved this problem. As is almost always the case, it was the part that I was confident was not the problem that was, in fact, the problem.

    Having a more careful look at the imshow documentation (linked in the question) reveals that imshow interprets (n, m, 3) arrays in two different ways depending on their data type. For arrays of floats, the data is assumed to be in the range [0.0, 1.0], but for integers the data is assumed to be in the range [0, 255]. So essentially it was plotting correctly, but because my data was integer data (both before thresholding using astype and after), I was essentially plotting colors as close to black as possible in 8-bit RGB without them actually being black. My monitor is not that great, and they were all indistinguishable from black for me.

    The easy solution is to simply change the type of the data to a float instead of an int after thresholding:

    b = simg["B"].astype(bool).astype(float)
    g = simg["G"].astype(bool).astype(float)
    r = simg["R"].astype(bool).astype(float)
    

    and this now results in an excellent three-channel RGB image as expected: working now