matplotlib icon indicating copy to clipboard operation
matplotlib copied to clipboard

[Bug]: Doesn't show 1-channel image by using matplotlib.image.AxesImage.set_data()

Open nicolasrosa opened this issue 4 years ago • 13 comments

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

matplotlib_bug

Expected outcome

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

matplotlib_fix_paint

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

nicolasrosa avatar Apr 08 '22 21:04 nicolasrosa

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.

tacaswell avatar Apr 09 '22 17:04 tacaswell

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()

nicolasrosa avatar Apr 09 '22 18:04 nicolasrosa

@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?

mylasem avatar Apr 09 '22 18:04 mylasem

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()

nicolasrosa avatar Apr 09 '22 18:04 nicolasrosa

Hello @mylasem, go on. Let me know if I can help you with something.

nicolasrosa avatar Apr 09 '22 18:04 nicolasrosa

@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.

mylasem avatar Apr 09 '22 18:04 mylasem

@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).

nicolasrosa avatar Apr 09 '22 19:04 nicolasrosa

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:

  1. given an RGB image, show it
  2. 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.

tacaswell avatar Apr 09 '22 21:04 tacaswell

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.

jklymak avatar Apr 10 '22 07:04 jklymak

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.

tacaswell avatar Apr 10 '22 17:04 tacaswell

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.

jklymak avatar Apr 10 '22 19:04 jklymak

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()

so

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)

so

tacaswell avatar Apr 10 '22 22:04 tacaswell

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!

github-actions[bot] avatar Dec 10 '25 02:12 github-actions[bot]