Skip to content

[Bug]: alpha array-type not working with RGB image in imshow() #26092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
patquem opened this issue Jun 8, 2023 · 25 comments · May be fixed by #26520
Open

[Bug]: alpha array-type not working with RGB image in imshow() #26092

patquem opened this issue Jun 8, 2023 · 25 comments · May be fixed by #26520
Labels
Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues Good first issue Open a pull request against these issues if there are no active ones! status: confirmed bug topic: color/alpha topic: images

Comments

@patquem
Copy link
Contributor

patquem commented Jun 8, 2023

Bug summary

Hi,
Whereas alpha = constant works with RGB image, this is not the case when working with an array-type for alpha.
(In my real case, I can only pass standard imshow parameters like alpha to the function of the library I use, and not a RGBA array).
Patrick

Code for reproduction

import numpy as np
import matplotlib.pyplot as plt
from skimage.color import gray2rgb

arr = np.random.random((10, 10))
arr_rgb = gray2rgb(arr)

alpha = np.ones_like(arr)
alpha[:5] = 0.2

plt.figure()
plt.tight_layout()
plt.subplot(121)
plt.title("Expected outcome")
plt.imshow(arr, alpha=alpha, cmap='gray')
plt.subplot(122)
plt.title("Actual outcome")
plt.imshow(arr_rgb, alpha=alpha)
plt.show()

Actual outcome

image

Expected outcome

image

Additional information

No response

Operating system

Windows

Matplotlib Version

3.7.1

Matplotlib Backend

TkAgg

Python version

Python 3.10.11

Jupyter version

No response

Installation

pip

@rcomer
Copy link
Member

rcomer commented Jun 8, 2023

Here is an example without skimage (fixed by #28437):

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

arr = np.random.random((10, 10))
cmap = plt.get_cmap('gray')
norm = mcolors.Normalize()
arr_rgb = cmap(norm(arr))[:, :, :3]

alpha = np.ones_like(arr)
alpha[:5] = 0.2

plt.subplot(121)
plt.title("Expected outcome")
plt.imshow(arr, alpha=alpha, cmap='gray')

plt.subplot(122)
plt.title("Actual outcome")
plt.imshow(arr_rgb, alpha=alpha)

plt.show()

I get this with both v3.7.1 and main:
test

The docstring for imshow does suggest that alpha may be an array, but it also says the array should be the same shape as the image so I assume that only means for the (M, N) case. So I'm not sure whether this is a bug or a documentation issue. ETA: though I'm not sure why the current behaviour would be desired - if you didn't want alpha to have an effect then you would just not pass it.

@patquem
Copy link
Contributor Author

patquem commented Jun 8, 2023

hello @rcomer,
Thanks for replying.
The library I use (Orix) manipulates complex object and allows to plot data thanks to this function : plot_map()
The plotting is based on a RGB array (issue from a complex object) and I wish to pass alpha to highlight some area of my map. I can't act directly on the RGB array.
Patrick

@tacaswell tacaswell added this to the v3.7.2 milestone Jun 8, 2023
@tacaswell
Copy link
Member

This is a bug. imshow basically has two orthogonal code paths, one for if the user passes us a ndim==2 array (which goes through color mapping) and one for for if eth user passes us a ndim==3 array (which more-or-less just goes out).

I suspect that when we implemented the "alapha-as-array" we forgot about the RGB(A) case.

@tacaswell
Copy link
Member

Yeah, the issue is in

else:
if A.ndim == 2: # _interpolation_stage == 'rgba'
self.norm.autoscale_None(A)
A = self.to_rgba(A)
if A.shape[2] == 3:
A = _rgb_to_rgba(A)
alpha = self._get_scalar_alpha()
output_alpha = _resample( # resample alpha channel
self, A[..., 3], out_shape, t, alpha=alpha)
output = _resample( # resample rgb channels
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
output[..., 3] = output_alpha # recombine rgb and alpha
# output is now either a 2D array of normed (int or float) data
# or an RGBA array of re-sampled input
output = self.to_rgba(output, bytes=True, norm=False)
# output is now a correctly sized RGBA array of uint8
# Apply alpha *after* if the input was greyscale without a mask
if A.ndim == 2:
alpha = self._get_scalar_alpha()
alpha_channel = output[:, :, 3]
alpha_channel[:] = ( # Assignment will cast to uint8.
alpha_channel.astype(np.float32) * out_alpha * alpha)
where we do apply alpha, but are asking for the scalar alpha not the array alpha.

#14889 is where this feature came in and where we picked up the explicit call to _get_scalar_alpha.

That said, I think the fix is to add logic to

if A.shape[2] == 3:
A = _rgb_to_rgba(A)
where we promote RGB -> RGBA to take into account a possibly array-like alpha.

I suspect that we have the same bug in

self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))
which is handling the fully un-sampled case and have confirmed we see the same bug in
if A.ndim == 2: # _interpolation_stage == 'rgba'
self.norm.autoscale_None(A)
A = self.to_rgba(A)

I am going to label this as good first issue as it is a clear bug (array alpha should work with RGB input). I would say this is a medium difficulty because the image processing code is a bit complicated. Most of that complexity is there for a good reason so be prepared to understand a majority of the _make_image function., The only dicey thing to work out is what to do in the case of the user passing both and RGBA array and an array alpha: Do we error, discard one (and warn), or blend them?

The patch:

diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py
index 135934a244..0ca26bdabf 100644
--- a/lib/matplotlib/image.py
+++ b/lib/matplotlib/image.py
@@ -553,9 +553,11 @@ class _ImageBase(martist.Artist, cm.ScalarMappable):
             else:
                 if A.ndim == 2:  # _interpolation_stage == 'rgba'
                     self.norm.autoscale_None(A)
-                    A = self.to_rgba(A)
-                if A.shape[2] == 3:
+                    A = self.to_rgba(A, alpha=self.get_alpha())
+                elif A.shape[2] == 3:
                     A = _rgb_to_rgba(A)
+                    if alpha := self.get_alpha() is not None:
+                        A[:, :, 3] = self.get_alpha()
                 alpha = self._get_scalar_alpha()
                 output_alpha = _resample(  # resample alpha channel
                     self, A[..., 3], out_shape, t, alpha=alpha)

fixes at least the obvious cases, but does not address the "what about RGBA" issue and I think will double apply the alpha in scalar cases.

I don't have time today to chase through those details or write tests.

@tacaswell tacaswell added status: has patch patch suggested, PR still needed Good first issue Open a pull request against these issues if there are no active ones! Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues labels Jun 8, 2023
@github-actions
Copy link

github-actions bot commented Jun 8, 2023

Good first issue - notes for new contributors

This issue is suited to new contributors because it does not require understanding of the Matplotlib internals. To get started, please see our contributing guide.

We do not assign issues. Check the Development section in the sidebar for linked pull requests (PRs). If there are none, feel free to start working on it. If there is an open PR, please collaborate on the work by reviewing it rather than duplicating it in a competing PR.

If something is unclear, please reach out on any of our communication channels.

@rcomer
Copy link
Member

rcomer commented Jun 8, 2023

The only dicey thing to work out is what to do in the case of the user passing both and RGBA array and an array alpha: Do we error, discard one (and warn), or blend them?

Looks like we have a precedent that alpha overrides the existing alpha channel on an rgba tuple.

if alpha is not None:
c = c[:3] + (alpha,)

Edit: OTOH we also have precedent for ignoring alpha.
https://matplotlib.org/stable/api/cm_api.html#matplotlib.cm.ScalarMappable.to_rgba

@greglucas
Copy link
Contributor

Oh the fun, there is also a precedent that it doesn't for ScalarMappables too :)

If the last dimension is 3, the *alpha* kwarg (defaulting to 1)
will be used to fill in the transparency. If the last dimension
is 4, the *alpha* kwarg is ignored; it does not
replace the preexisting alpha. A ValueError will be raised

@tacaswell
Copy link
Member

tacaswell commented Jun 8, 2023

And via

output_alpha = _resample( # resample alpha channel
self, A[..., 3], out_shape, t, alpha=alpha)
output = _resample( # resample rgb channels
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
output[..., 3] = output_alpha # recombine rgb and alpha
if you pass an RGBA array and a scalar alpha they get combined so we have precedent for 3 of the 4 options!

import numpy as np
import matplotlib.pyplot as plt
a = np.zeros([2, 2, 4])
a[:, :, 0] = 1
a[:, :, 3] = np.linspace(0, 1, 4).reshape(2, 2)
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(a)
ax2.imshow(a, alpha=.5)
plt.show()

@rcomer
Copy link
Member

rcomer commented Jun 10, 2023

So we should add one that raises, to complete the set 🤪

More seriously, I think it’s probably more important for imshow to be consistent with itself than consistent with other things, in which case it should also blend for array alpha. (Though I have not followed exactly what that _resample function does so I don’t know how difficult it would be.)

@dhiganthrao
Copy link

Hello there! I'd like to contribute to this issue. I'm new to contributing to Matplotlib, so please pardon me if I ask any silly questions! I'd just like to clarify what the issue is, and the general sketch of the solution:

When displaying images using Matplotlib, if the provided image is of 2 dimensions (grayscale image) and an alpha parameter is provided, the image is plotted with the given alpha parameter being used to make certain pixels of the image opaque by a factor: if a scalar value of alpha is provided, the entire image is affected. If an array is provided for alpha, then each pixel of the image is affected by the corresponding value in the alpha array.

However, if the provided image is of 3 dimensions (RGB image) and an array is provided for alpha, the image isn't affected in any way. If a scalar value is provided, the entire image is affected as is what happened in the last case.

If the provided image is of 4 dimensions (RGBA), and a value for alpha is provided, no matter what the value is (scalar or array), the image isn't affected: it uses the preexisting alpha array present in the image.

We'd like to change this behavior: the case for grayscale images works well, so we don't need to change that. For cases where the dimensions of the image are > 2, we'd like to add cases covering all bases: if the image is RGB, add cases where the display function can accept array values for alpha, and if the image is RGBA, decide on a way to let the user know that there are two alpha values: one already provided in the image, and one provided by them.

The way to start would be by examining the code snippets @/tacaswell listed, and making the corresponding changes.

Please pardon me if I've made any mistake in my clarification! Thanks for patiently reading through this.

@tacaswell
Copy link
Member

The dimensions are either 2 or 3, and in the case of 3, may have length 3 or 4 in the last dimension (we should fail on 4D input!).

The goal is to make RGB and RGBA input work with both scalar alpha and array alpha (like the the color-mapped case) with alpha blending in the RGBA case.

@dhiganthrao
Copy link

All right, sounds good! I'll get to work on this, if that's fine.

@NikosNikolaidis02
Copy link

Hello,

I am currently working on this issue.

I would like to ask if we have a problem if the array is RGB and not RGBA? Should we change the _rgb_to_rgba function in order to work for numpy-array-type alpha or just add condition on image.py on lines 557 to 558 ?

NikosNikolaidis02 pushed a commit to NikosNikolaidis02/matplotlib that referenced this issue Jun 18, 2023
@QuLogic QuLogic modified the milestones: v3.7.2, v3.7.3 Jul 5, 2023
@stevezhang1999
Copy link
Contributor

Just want to clarify that if both RGBA input and parameter alpha is provided for imshow(), should just ignore alpha or combine them? Ignoring it makes more sense to me.

I' m a little bit of confused about the code below - is the code desired? It looks a bit weird for me that we are passing alpha to _resample() with separated RGB and alpha channel.

And via

output_alpha = _resample( # resample alpha channel
self, A[..., 3], out_shape, t, alpha=alpha)
output = _resample( # resample rgb channels
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
output[..., 3] = output_alpha # recombine rgb and alpha

if you pass an RGBA array and a scalar alpha they get combined so we have precedent for 3 of the 4 options!

import numpy as np
import matplotlib.pyplot as plt
a = np.zeros([2, 2, 4])
a[:, :, 0] = 1
a[:, :, 3] = np.linspace(0, 1, 4).reshape(2, 2)
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(a)
ax2.imshow(a, alpha=.5)
plt.show()

I can fix it or we can wait for new contributors as this one looks quite straightforward after so many discussions?

@tacaswell
Copy link
Member

I think we should combine them.

For better or worse, the scalar alpha case is blending so the array case should blend too. It would be very confusing if those two things were different.

If the user has an RGBA array they want to replace the alpha channel of, then doing

ax2.imshow(a[:, :, :3] alpha=alpha_array)

is possible (either choice we make), but if they want to blend then is a bit more awkward if we choose to unconditionally replace the alpha channel.

If this is interesting to you, please go ahead and do it @stevezhang1999 !

@stevezhang1999
Copy link
Contributor

I think we should combine them.

For better or worse, the scalar alpha case is blending so the array case should blend too. It would be very confusing if those two things were different.

If the user has an RGBA array they want to replace the alpha channel of, then doing

ax2.imshow(a[:, :, :3] alpha=alpha_array)

is possible (either choice we make), but if they want to blend then is a bit more awkward if we choose to unconditionally replace the alpha channel.

If this is interesting to you, please go ahead and do it @stevezhang1999 !

Is it common that we want to apply another alpha blending on a RGBA images? Now this looks less weird to me, but still a little bit confused about when we want to do it.

@AALAM98mod100 AALAM98mod100 linked a pull request Aug 14, 2023 that will close this issue
5 tasks
@AALAM98mod100
Copy link

AALAM98mod100 commented Aug 15, 2023

What I remain confused about is combining the alpha provided by the user and in RGBA image. How should I go about addressing this in the PR, need some guidance here.
What I'm confused about is the changes that need to be made here. Should I pass the full RGBA image in each of the calls to _resample and then combine then within the function?

output_alpha = _resample( # resample alpha channel
self, A[..., 3], out_shape, t, alpha=alpha)
output = _resample( # resample rgb channels
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
output[..., 3] = output_alpha # recombine rgb and alpha

As for the cm.ScalarMappable.to_rbga() function, I think the support for alpha arrays inherently exist. If a user passes alpha, it will simply get broadcast to the last dimension in the array at the line xx[:, :, 3] = alpha. Someone please correct me if I'm wrong on this assumption

try:
    if x.ndim == 3:
        if x.shape[2] == 3:
            if alpha is None:
                alpha = 1
            if x.dtype == np.uint8:
                alpha = np.uint8(alpha * 255)
            m, n = x.shape[:2]
            xx = np.empty(shape=(m, n, 4), dtype=x.dtype)
            xx[:, :, :3] = x
            xx[:, :, 3] = alpha
        elif x.shape[2] == 4:
            xx = x
        else:
            raise ValueError("Third dimension must be 3 or 4")

@QuLogic
Copy link
Member

QuLogic commented Aug 18, 2023

Is it common that we want to apply another alpha blending on a RGBA images? Now this looks less weird to me, but still a little bit confused about when we want to do it.

We discussed this on call earlier today. I've written a short script to exercise all cases (that we could think of).
Figure_1

test script
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np

fig, axs = plt.subplots(3, 4, figsize=(12, 10), layout='compressed')

for ax in axs.flat:
    ax.set(facecolor='red', xticks=[], yticks=[])

mapped = np.array([
    [0.1, 1.0],
    [1.0, 0.1]
])
rgb = np.repeat(mapped[:, :, np.newaxis], 3, axis=2)
rgba = np.concatenate(
    [
        rgb,
        [
            [[1.0], [0.9]],
            [[0.8], [0.7]],
        ]
    ],
    axis=2
)

alpha_scalar = 0.5
alpha_2d = np.full_like(mapped, alpha_scalar)

cmap_with_alpha = ListedColormap(
    np.concatenate([plt.cm.viridis.colors,
                    np.full((len(plt.cm.viridis.colors), 1), alpha_scalar)],
                   axis=1),
)

for ax, alpha, t in zip(axs, [None, alpha_scalar, alpha_2d], ['off', 'float', 'array']):
    ax[0].imshow(mapped, alpha=alpha)
    ax[0].set_title(f'2D, alpha={alpha_scalar} {t}')

    ax[1].imshow(mapped, cmap=cmap_with_alpha, alpha=alpha)
    ax[1].set_title(f'2D with {alpha_scalar} alpha cmap, alpha={alpha_scalar} {t}')

    ax[2].imshow(rgb, alpha=alpha)
    ax[2].set_title(f'RGB, alpha={alpha_scalar} {t}')

    ax[3].imshow(rgba, alpha=alpha)
    ax[3].set_title(f'RGBA, alpha={alpha_scalar} {t}')

plt.show()

The columns are the types of X input: colormapped 2D data, colormapped 2D data with a colormap that includes transparency, 3D RGB data, 3D RGBA data. The rows are no alpha, scalar alpha, and array alpha (both of 0.5).

In the second row with scalar alpha, we can see that it is applied by multiplying by the data's alpha (or 1 if data didn't have any), i.e, blending the two alphas. This is most clear with RGBA, which is neither equal to RGB-with-0.5-alpha, nor RGBA-without-alpha.

It is the bottom-right two cases that are inconsistent with that, and we agreed on the call that it should be made consistent, and that adding blending for those cases would be best.

What I remain confused about is combining the alpha provided by the user and in RGBA image. How should I go about addressing this in the PR, need some guidance here.
What I'm confused about is the changes that need to be made here. Should I pass the full RGBA image in each of the calls to _resample and then combine then within the function?

We should try to apply this in a manner that is consistent with the 2D colormapped case. If it does separate resampling of alpha with later combination, then we should do that.

@AALAM98mod100
Copy link

AALAM98mod100 commented Aug 18, 2023

Thank you for the plots above @QuLogic. They were super helpful. I have implemented the expected plot in #26520 but the alphas seems to be doubly applied when using imsave() instead of imshow(). I will need some guidance in the PR and see where else would I need to make changes

@stevezhang1999
Copy link
Contributor

Can you provide a code snippet about how you are using imsave() ? I'm not quite sure how this is related to imsave().

@AALAM98mod100
Copy link

I'm using the test script from QuLogic above as shown

...
for ax, alpha, t in zip(axs, [None, alpha_scalar, alpha_2d], ['off', 'float', 'array']):
    ax[0].imshow(mapped, alpha=alpha)
    ax[0].set_title(f'2D, alpha={alpha_scalar} {t}')

    ax[1].imshow(mapped, cmap=cmap_with_alpha, alpha=alpha)
    ax[1].set_title(f'2D with {alpha_scalar} alpha cmap, alpha={alpha_scalar} {t}')

    ax[2].imshow(rgb, alpha=alpha)
    ax[2].set_title(f'RGB, alpha={alpha_scalar} {t}')

    ax[3].imshow(rgba, alpha=alpha)
    ax[3].set_title(f'RGBA, alpha={alpha_scalar} {t}')

plt.savefig('imsave.png') # produces erroneous output 
plt.show() # produces correct output but erroneous after saving figure
  1. The image I get when I do plt.show()
    image
  2. The image I get when I save it
    image

@stevezhang1999
Copy link
Contributor

So I got you are refering to imsave in savefig. I will suggest starting from:

  • displaying the input of imsave to see if it looks correct
  • whether your modification is also presented in imsave and break something (a quick look tells me it might be to_rgba)

@saikarna913
Copy link
Contributor

Is this issue still open? I am willing to work on this issue.

@rcomer
Copy link
Member

rcomer commented Apr 9, 2025

The example from #26092 (comment) was fixed by #28437

Image

If I've understood correctly, the example from #26092 (comment) is still wrong on the bottom right as of 7e997ae

Image

@rcomer rcomer removed the status: has patch patch suggested, PR still needed label Apr 9, 2025
@rcomer
Copy link
Member

rcomer commented Apr 9, 2025

Removed the "has patch" label because the relevant code has been refactored since the patch was written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues Good first issue Open a pull request against these issues if there are no active ones! status: confirmed bug topic: color/alpha topic: images
Projects
None yet