diff --git a/doc/api/next_api_changes/behavior/28437-CH.rst b/doc/api/next_api_changes/behavior/28437-CH.rst new file mode 100644 index 000000000000..6121dfec8163 --- /dev/null +++ b/doc/api/next_api_changes/behavior/28437-CH.rst @@ -0,0 +1,8 @@ +*alpha* parameter handling on images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When passing and array to ``imshow(..., alpha=...)``, the parameter was silently ignored +if the image data was a RGB or RBGA image or if :rc:`interpolation_state` +resolved to "rbga". + +This is now fixed, and the alpha array overwrites any previous transparency information. diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index f71d49db8ad8..ddb4fd610b12 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -501,17 +501,27 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if A.ndim == 2: # interpolation_stage = 'rgba' self.norm.autoscale_None(A) A = self.to_rgba(A) - alpha = self._get_scalar_alpha() - if A.shape[2] == 3: - # No need to resample alpha or make a full array; NumPy will expand - # this out and cast to uint8 if necessary when it's assigned to the - # alpha channel below. - output_alpha = (255 * alpha) if A.dtype == np.uint8 else alpha - else: - 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) + alpha = self.get_alpha() + if alpha is None: # alpha parameter not specified + if A.shape[2] == 3: # image has no alpha channel + output_alpha = 255 if A.dtype == np.uint8 else 1.0 + else: + output_alpha = _resample( # resample alpha channel + self, A[..., 3], out_shape, t) + output = _resample( # resample rgb channels + self, _rgb_to_rgba(A[..., :3]), out_shape, t) + elif np.ndim(alpha) > 0: # Array alpha + # user-specified array alpha overrides the existing alpha channel + output_alpha = _resample(self, alpha, out_shape, t) + output = _resample( + self, _rgb_to_rgba(A[..., :3]), out_shape, t) + else: # Scalar alpha + if A.shape[2] == 3: # broadcast scalar alpha + output_alpha = (255 * alpha) if A.dtype == np.uint8 else alpha + else: # or apply scalar alpha to existing alpha channel + output_alpha = _resample(self, A[..., 3], out_shape, t) * alpha + output = _resample( + self, _rgb_to_rgba(A[..., :3]), out_shape, t) output[..., 3] = output_alpha # recombine rgb and alpha # output is now either a 2D array of normed (int or float) data diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index d8ac3786c395..7437651d2b86 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1711,3 +1711,49 @@ def test_resample_dtypes(dtype, ndim): axes_image = ax.imshow(data) # Before fix the following raises ValueError for some dtypes. axes_image.make_image(None)[0] + + +@pytest.mark.parametrize('intp_stage', ('data', 'rgba')) +@check_figures_equal() +def test_interpolation_stage_rgba_respects_alpha_param(fig_test, fig_ref, intp_stage): + axs_tst = fig_test.subplots(2, 3) + axs_ref = fig_ref.subplots(2, 3) + ny, nx = 3, 3 + scalar_alpha = 0.5 + array_alpha = np.random.rand(ny, nx) + + # When the image does not have an alpha channel, alpha should be specified + # by the user or default to 1.0 + im_rgb = np.random.rand(ny, nx, 3) + im_concat_default_a = np.ones((ny, nx, 1)) # alpha defaults to 1.0 + im_rgba = np.concatenate( # combine rgb channels with array alpha + (im_rgb, array_alpha.reshape((ny, nx, 1))), axis=-1 + ) + axs_tst[0][0].imshow(im_rgb) + axs_ref[0][0].imshow(np.concatenate((im_rgb, im_concat_default_a), axis=-1)) + axs_tst[0][1].imshow(im_rgb, interpolation_stage=intp_stage, alpha=scalar_alpha) + axs_ref[0][1].imshow( + np.concatenate( # combine rgb channels with broadcasted scalar alpha + (im_rgb, scalar_alpha * im_concat_default_a), axis=-1 + ), interpolation_stage=intp_stage + ) + axs_tst[0][2].imshow(im_rgb, interpolation_stage=intp_stage, alpha=array_alpha) + axs_ref[0][2].imshow(im_rgba, interpolation_stage=intp_stage) + + # When the image already has an alpha channel, multiply it by the + # scalar alpha param, or replace it by the array alpha param + axs_tst[1][0].imshow(im_rgba) + axs_ref[1][0].imshow(im_rgb, alpha=array_alpha) + axs_tst[1][1].imshow(im_rgba, interpolation_stage=intp_stage, alpha=scalar_alpha) + axs_ref[1][1].imshow( + np.concatenate( # combine rgb channels with scaled array alpha + (im_rgb, scalar_alpha * array_alpha.reshape((ny, nx, 1))), axis=-1 + ), interpolation_stage=intp_stage + ) + new_array_alpha = np.random.rand(ny, nx) + axs_tst[1][2].imshow(im_rgba, interpolation_stage=intp_stage, alpha=new_array_alpha) + axs_ref[1][2].imshow( + np.concatenate( # combine rgb channels with new array alpha + (im_rgb, new_array_alpha.reshape((ny, nx, 1))), axis=-1 + ), interpolation_stage=intp_stage + )