[Bug]: Doesn't show 1-channel image by using matplotlib.image.AxesImage.set_data()
Bug summary
I could not plot a 1-channel image while I was trying to populate a Subplots figure, although I can visualize it by using plt.imshow().
Code for reproduction
fig, axes = plt.subplots(3, 2, num=0)
# fig.canvas.mpl_connect('key_press_event', on_press)
h, w = image_size
h, w = int(h), int(w)
ax1 = axes[0][0].imshow(np.zeros((h, w, 3), dtype=np.float32))
ax2 = axes[0][1].imshow(np.zeros((h, w, 3), dtype=np.float32))
ax3 = axes[1][0].imshow(np.zeros((h, w, 3), dtype=np.float32))
ax4 = axes[1][1].imshow(np.zeros((h, w, 3), dtype=np.float32))
ax5 = axes[2][0].imshow(np.zeros((h, w), dtype=np.float32))
ax6 = axes[2][1].imshow(np.zeros((h, w), dtype=np.float32))
image_np = <(h,w) uint8 or float32>
image_pil = Image.fromarray(image_np).convert('RGB')
...
# ax5.set_data(image_np) # Fails!
# ax5.set_data(image_pil) # Works!
plt.imshow(image_np) # Works!
plt.draw()
plt.pause(1e-9)
Actual outcome

Expected outcome
I used the Paint to generate the following image to demonstrate what I was expecting.

Additional information
No response
Operating system
Ubuntu 20.04
Matplotlib Version
3.5.1
Matplotlib Backend
TkAgg
Python version
Python 3.8.10
Jupyter version
No response
Installation
pip
Are you getting a traceback? If so can you share that as well?
It is also super helpful if you can give us code that we can copy-paste to run (random data is OK!) rather than trying to fill in the lines you left as pseudo code. There is a good chance we may guess wrong and debug the wrong issue.
from matplotlib import pyplot as plt
import numpy as np
def print_info(var):
print(var.shape, var.dtype, np.min(var), np.max(var))
def main():
# --- Figure 1
fig, axes = plt.subplots(6,1)
# Initial data
ax0 = axes[0].imshow(np.ones((256, 512, 3))) # Works, (256, 512,3), float64
ax1 = axes[1].imshow(np.ones((256, 512, 1))) # Fails, (256, 512), float64
ax2 = axes[2].imshow(np.ones((256, 512))) # Fails, (256, 512), float64
ax3 = axes[3].imshow(np.ones((256, 512, 3)).astype(np.uint8)) # Works, (256, 512,3), uint8
ax4 = axes[4].imshow(np.ones((256, 512, 1)).astype(np.uint8)) # Fails, (256, 512), uint8
ax5 = axes[5].imshow(np.ones((256, 512)).astype(np.uint8)) # Fails, (256, 512), uint8
# --- Figure 2
fig2, axes2 = plt.subplots(9, 1)
# Initial data
ax0 = axes2[0].imshow(np.zeros((256, 512, 3)))
ax1 = axes2[1].imshow(np.zeros((256, 512, 1)))
ax2 = axes2[2].imshow(np.zeros((256, 512)))
ax3 = axes2[3].imshow(np.zeros((256, 512, 3)).astype(np.uint8))
ax4 = axes2[4].imshow(np.zeros((256, 512, 1)).astype(np.uint8))
ax5 = axes2[5].imshow(np.zeros((256, 512)).astype(np.uint8))
ax6 = axes2[6].imshow(np.zeros((256, 512, 3)).astype(np.uint8))
ax7 = axes2[7].imshow(np.zeros((256, 512, 1)).astype(np.uint8))
ax8 = axes2[8].imshow(np.zeros((256, 512)).astype(np.uint8))
# New data
np_float64_3 = np.random.rand(256, 512, 3) # 3-channel random image, float64
np_float64_1 = np.random.rand(256, 512, 1) # 1-channel random image, float64
np_float64_0 = np.random.rand(256, 512) # 0-channel random image, float64
np_uint8_3 = np_float64_3.astype(np.uint8)
np_uint8_1 = np_float64_1.astype(np.uint8)
np_uint8_0 = np_float64_0.astype(np.uint8)
np_uint8_3_255 = (np_float64_3*255).astype(np.uint8)
np_uint8_1_255 = (np_float64_1*255).astype(np.uint8)
np_uint8_0_255 = (np_float64_0*255).astype(np.uint8)
print_info(np_float64_3)
print_info(np_float64_1)
print_info(np_float64_0)
print_info(np_uint8_3)
print_info(np_uint8_1)
print_info(np_uint8_0)
print_info(np_uint8_3_255)
print_info(np_uint8_1_255)
print_info(np_uint8_0_255)
ax0.set_data(np_float64_3) # Works!
ax1.set_data(np_float64_1) # Fails!
ax2.set_data(np_float64_0) # Fails!
ax3.set_data(np_uint8_3) # Works!
ax4.set_data(np_uint8_1) # Fails!
ax5.set_data(np_uint8_0) # Fails!
ax6.set_data(np_uint8_3_255) # Works!
ax7.set_data(np_uint8_1_255) # Fails!
ax8.set_data(np_uint8_0_255) # Fails!
plt.show()
if __name__ == '__main__':
main()
@tacaswell @nicolasrosa This is my first time contributing and I am interested in fixing this issue. Mind if I look into this? Anything I should know?
I forgot doing in the code, but if you try to plot the proposed images using the following lines, it will work.
plt.figure()
plt.imshow(image)
plt.show()
Hello @mylasem, go on. Let me know if I can help you with something.
@nicolasrosa Thank you. Do you mind defining what image_size is in the code reproduction above? More context to image_np would be helpful as well since I am trying to reproduce the actual outcome you provided.
@nicolasrosa Thank you. Do you mind defining what image_size is in the code reproduction above? More context to image_np would be helpful as well since I am trying to reproduce the actual outcome you provided.
It was image_size=(1024,2048).
via https://github.com/matplotlib/matplotlib/pull/15090 I would expect this to work at a technical level (and the above code runs for me without exception). I'm still not quite sure what is distinguishing working and failing.
I think the issue here is that imshow actually does two entirely different things:
- given an RGB image, show it
- given a 2D array, colormap it to a falsecolor image and show that
In the first case we try to infer if the user gave use [0-1] float RGB(A) or [0-255] int RGB(A), but in the second we set up a mini-processing pipeline to go from the input data (which is a 2D array of floats) to a 4-channel RGBA image (which is what we need to pass to the backends). We have to map the full gamut for floats -> a finite range of integers. To make this possible while still having any contrast the normalization functions have some max and min and any input values in that range are mapped to [0, 1] which are in turn mapped to colors (see https://matplotlib.org/stable/tutorials/colors/colormapnorms.html , https://matplotlib.org/stable/tutorials/colors/colormaps.html , and https://github.com/matplotlib/matplotlib/pull/18487 for lots of details). If the user does not specifally specify the min/max, we infer it from looking at the initial data. When you set the data later we do not automatically update the auto-inferred normalization limits. In the case of constant input data I think we go with equal vmin/vmax, but would need to check that.
I think you may want to do
im = ax.imshow(np.ones(...), vmax=0, vmax=255)
which tells Matplotlib that it should use the range [0, 255] for the false color mapping.
https://matplotlib.org/stable/tutorials/introductory/images.html is also a good reference.
@mylasem The first step is to localize the bug so we can sort out what (if anything) we need to fix in the code or the documentation to prevent this problem in the future.
In the first case we try to infer if the user gave use [0-1] float RGB(A) or [0-255] int RGB(A),
... as pointed out a few times, this is theoretically impossible, so I don't think we should be doing this at all.
At the c++ level I am 95% sure we dispatch on type (via the _resample code path) and we have some logic to warn if the user is out of gamut https://github.com/matplotlib/matplotlib/blob/2aa4b7dca40dc01236aed601f2a071168ddd2dca/lib/matplotlib/image.py#L713-L731
I thought the thing that was impossible was to infer vmin/vmax to be INTMAX/INTMIN for 1-channel false-color data, for actual RGB(A) data that we are just showing infering the range on type is justifiable.
I think question is if we get [1,0,1] is that on a 256 range or 0-1? Heuristically you could assume if all your data was between 0-1 it was on the 0-1 range, but that isn't going to be always correct.
If we get [[[1, 0, 1]]] (so shaped (1, 1, 3) )we dispatch on type (because at ndim==3 we treat it as a direct RGB(A) image):
fig, axd = plt.subplot_mosaic([["float", "int"]])
axd["int"].imshow(np.array([[[1, 0, 1]]], dtype='uint8'))
axd["float"].imshow(np.array([[[1, 0, 1]]], dtype='float'))
for k, v in axd.items():
v.set_title(k)
plt.show()

If we get [[1, 0, 1]] (so shaped (1, 3)) we do not look at the types at all and scale from [0, 1]:
fig, axd = plt.subplot_mosaic([["float", "int"]])
axd["int"].imshow(np.array([[1, 0, 1]], dtype='uint8'))
axd["float"].imshow(np.array([[1, 0, 1]], dtype='float'))
for k, v in axd.items():
v.set_title(k)

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!