Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Respect array alpha with interpolation_stage='rgba' in _Imagebase::_make_image #28437

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

Merged
merged 5 commits into from
Feb 21, 2025

Conversation

chaoyihu
Copy link
Contributor

PR summary

This PR hopefully closes #28382, making sure that array alpha is respected in _Imagebase::_make_image when param interpolation_stage is set to 'rgba'.

PR checklist

@chaoyihu
Copy link
Contributor Author

chaoyihu commented Jul 4, 2024

Hi @timhoffm, may I request your review on this pr?

Sorry for the tagging - I'm not sure how to request a review when there's no suggested reviewers on the GitHub interface.

Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be clarified and the tests expanded a bit. What happens if A has an alpha channel and alpha is also a matrix for interpolation_stage='rgba'? We have a behaviour for interpolation_stage='data' so this PR should have the same behaviour if possible, and it'll need a test.

@chaoyihu
Copy link
Contributor Author

I tested with all possible combinations, each repeated with interpolation_stage as data and rgba, respectively:

  1. When the image does not have alpha channel:
    a. alpha not specified: default to 1.0.
    b. scalar alpha: broadcast.
    c. array alpha: concatenate.
  2. When the image has an alpha channel:
    a. alpha not specified: nothings needs to be done.
    b. scalar alpha: multiply the alpha channel with user-specified scalar.
    c. array alpha: replace the alpha channel with user-specified array.

The expected outcomes seem to align with existing test cases. I've observed conflicts when experimenting with different behaviors, for example, test_image::test_image_composite_alpha fails when I change 2.b. to replacing instead of multiplying.

I'm assuming the code is fine since it passed all checks, but please let me know if I'm missing anything.

Comment on lines 1608 to 1609
im1 = np.random.rand(3, 3, 3)
im_concat_arr_a = np.concatenate((im1, np.expand_dims(arr_a, axis=-1)), axis=-1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is more easy to read than concat/expand_dims:

Suggested change
im1 = np.random.rand(3, 3, 3)
im_concat_arr_a = np.concatenate((im1, np.expand_dims(arr_a, axis=-1)), axis=-1)
im_rgb = np.random.rand(ny, nx, 3)
im_rgba = np.empty(ny, nx, 4)
im_rgba[..., :3] = im_rgb
im_rgba[..., 3] = array_alpha

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to avoid creating variables that are only used once.
How about this?

    im_rgb = np.random.rand(ny, nx, 3)
    im_concat_arr_a = np.concatenate(  # combine rgb channels with array alpha
        (im_rgb, array_alpha.reshape((3, 3, 1))), axis=-1
    )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my new commit made this outdated. Do you still prefer using temporary variables? If so I'd be happy to change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion. I find it slightly more readable to say "the first three components are im_rgb, the fourth component is array_alpha rather than "is a concantenation of im_rgb and array_alpha along the most inner axis.

Note that, you can write array_alpha.reshape((3, 3, 1) as array_alpha[..., np.newaxis], which IMHO makes the intent a bit more clear. also concatenate(..., axis=-1) is equivalent to dstack(...) in this case. So

# combine rgb channels with array alpha
im_concat_arr_a = np.dstack((im_rgb, array_alpha[..., np.newaxis]))  

would also be an option.

I'll leave it to you to decide which way you want to go.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be checked, but I think and explicit alpha should overwrite existing alpha rather than scaling it.

Copy link
Contributor Author

@chaoyihu chaoyihu Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this will cause conflicts:

I've observed conflicts when experimenting with different behaviors, for example, test_image::test_image_composite_alpha fails when I change 2.b. to replacing instead of multiplying.

test_image::test_image_composite_alpha creates color stripes with fading alpha peaking at 1.0 at the center, and applied scalar alpha to the stripe at different locations, for example:

    arr = np.zeros((11, 21, 4))
    arr[:, :, 0] = 1
    arr[:, :, 3] = np.concatenate(
        (np.arange(0, 1.1, 0.1), np.arange(0, 1, 0.1)[::-1]))
    ax.imshow(arr, extent=[1, 2, 5, 0], alpha=0.3)

And the baseline image uses a combined alpha, see below.
If the alpha channel was overwritten by a scalar, the fading effects would have been removed.
image_composite_alpha

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok there is currently a different behavior between scalar and array alpha if the image already has an alpha.

The documentation is not clear about the intended behavior:

The alpha blending value, between 0 (transparent) and 1 (opaque). If alpha is an array, the alpha blending values are applied pixel by pixel, and alpha must have the same shape as X.

My point is that this behavior is quite surprising and inconsistent:

  • scalar alpha: multiply the alpha channel with user-specified scalar.
  • array alpha: replace the alpha channel with user-specified array.

In all other cases, when we have a rgba color and an explicit alpha, the explicit alpha overwrites the rgba alpha; effectively through https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.to_rgba.html / https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.to_rgba_array.html

Additionally, I would have expected that scalar and array behave the same - either the scalar scales all elements and then the array also scales element-wise, or the both the scalar and the array overwrite.

Since tests cover the current behavior, this PR should be fine in that it does not introduce a behavior change. OTOH I'm wondering whether this behavior is really intended. The test and alpha handling came in in #1955, but this PR only states to implement missing alpha handling. There has been no discussion whether overwriting or multiplying is the right choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that this behavior is quite surprising and inconsistent

The documentation is not clear about the intended behavior

It should be possible to make the behaviors consistent. Forcing scalar alpha to overwrite causes conflicts, while multiplying array alpha pixel-wise does not seem to break any existing test case in test_image.py. Not sure if it has any side effects since I haven't ran the full test suite. Do you want me to try to change it this way?

This is a design choice so it's up to you. Either way, I can clarify the behavior in the docstring of imshow.

when we have a rgba color and an explicit alpha, the explicit alpha overwrites the rgba alpha; effectively through matplotlib.colors.to_rgba / matplotlib.colors.to_rgba_array

I see two methods of the same name to_rgba in the code base:

  • colors.to_rgba: If alpha is given, force the alpha value of the returned RGBA tuple to alpha.
  • cm.ScalarMappable.to_rgba: If the last dimension is 4, the alpha kwarg is ignored; it does not replace the preexisting alpha.

The second one is directly invoked in image.py thus looks more relevant to me. It shows yet another different behavior, but I think it is specific to this method for utility purposes and does not imply any design consideration about alpha handling.

I'm not sure about to_rgba_array, but intuitively I think it can be used as a setter and not for visualization purposes, so replacing preexisting alpha makes sense in its case.

The test and alpha handling came in in #1955, but this PR only states to implement missing alpha handling.

I think this PR and its related test case should be deemed correct - intended or not, because it does not cause conflicts anyway, either if we choose to keep the current behavior, or multiply array alpha.

Copy link
Member

@timhoffm timhoffm Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jklymak @anntzer I would like your opinion as image/interpolation experts.

The underlying problem here is that for RGB and RGBA images the kwarg alpha has been ignored if it was an array. The solution for RGB is straight forward (just use the given alpha array). However, what is the expected behavior for RGBA images, i.e. imshow(rgba_array, alpha=alpha)?

Should the kwarg alpha replace the intrinsic alpha channel? Generally I would say yes, this is how alpha also works in other cases (which is coded in color.to_rgba_array). The problem is that a scalar alpha kwarg does not exhibit this behavior, instead imshow(rgba_array, alpha=0.5) will scale the intrinsic alpha channel rather than replacing it.

Basically we have three options:

  • array alpha replaces intrinsic alpha - we live with the API inconsistency (scalar alpha scales, array alpha replaces)
  • array alpha scales intrinsic alpha - this is consistent with the existing scalar alpha for images, but different to the usual behavoir of alpha kwargs.
  • array alpha and scalar alpha replace intrinsic alpha, and intrinsic - this means an API change for scalar alpha on images.

This is the current behavior:

image

Code
N = 20
img_data = np.random.rand(N, N)
rgb_data = np.random.rand(N, N, 3)
rgba_data = np.random.rand(N, N, 4)
alpha = np.ones_like(img_data)
alpha[:, 0:N//2] = 0.2


with plt.rc_context({'image.interpolation_stage': 'rgba'}):
    fig, axs = plt.subplots(3, 3, figsize=(6, 6))
    for ax in axs.flat:
        ax.set(xticks=[], yticks=[])
    # no alpha
    axs[0, 0].imshow(img_data)
    axs[0, 1].imshow(rgb_data)
    axs[0, 2].imshow(rgba_data)
    # scalar alpha
    axs[1, 0].imshow(img_data, alpha=0.2)
    axs[1, 1].imshow(rgb_data, alpha=0.2)
    axs[1, 2].imshow(rgba_data, alpha=0.2)
    # array alpha
    axs[2, 0].imshow(img_data, alpha=alpha)
    axs[2, 1].imshow(rgb_data, alpha=alpha)
    axs[2, 2].imshow(rgba_data, alpha=alpha)
    
    axs[0, 0].set_title('scalar data')
    axs[0, 1].set_title('RGB data')
    axs[0, 2].set_title('RGBA data')
    axs[0, 0].set_ylabel('no alpha')
    axs[1, 0].set_ylabel('scalar alpha')
    axs[2, 0].set_ylabel('array alpha')
```

</details>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can live with that. Then let's take this PR basically as is so that we can release it as a bugfix in 3.10.1, which is the important part. We can then decide how to change/migrate the current behavior for RGBA and scaler alpha.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. But I know @tacaswell and @anntzer have thoughts on this as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree taking this as is.

I however disagree that multiplying in the scalar case for already RGBA data is inconsistent. In the case of lines the layer we add when rendering has variable alpha if anti-aliasing is turned on. When we set the alpha on a line we expect the result to be the combination of the AA alpha + the user alpha.

By analogy, if the user provides us an RGBA image with non-unifrom alpha and provides an artist-level alpha the expectation would be that they are combined not replaced.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a consistency PoV I have a mild preference for scaling, which at least is internally consistent within imshow() (e.g. if we decide to also handle e.g. alpha=[[scalar]], a 1x1 array which is broadcastable to a 2D array, our life will be easier if we choose to scale) even though it is not consistent with other functions of matplotlib (but that's a secondary concern IMO).
But I can't say that solution is really great either, so I don't feel very strongly either way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here, versus an antialiased line, is that the user has access to the alpha channel. I still think simpler is better. If someone sets alpha with a scalar and they meant it to be multiplicative, they can go back to their original alpha array and do the multiplication easily enough.

Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right to me. I think it probably needs a very short change note under doc/api/next_api_changes/behavior/ before merging. Thanks for digging into this!

@jklymak
Copy link
Member

jklymak commented Jul 12, 2024

#28061 went in so unfortunately you now have merge clashes, and will need to rebase. However I think its pretty straight forward, but let us know if you have difficulties - if you havent' done these before, it'd be worth backing up your branch...

Comment on lines -578 to -514
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICS, these two calls were the only ones using the _resample(..., alpha=...) parameter. If it is not needed anymore, we should remove it.

@ksunden
Copy link
Member

ksunden commented Feb 20, 2025

We are looking to include this in the 3.10.1 release, so would like to move forward.

I think the only outstanding comments on this are rewriting the change note and a question as to whether an internal function requires a parameter after this change. My leaning is to accept the rewritten change note (with or without the additional comment in the proposed change above) and not touch the internal function at this time.

I intend to merge the proposed change note rewrite in approximately 20 hours unless there is a) dissent expressed or b) someone else beats me to it. (I'd have said 24, but would like to get the release going earlier in my work day if I'm doing it tomorrow...)

@ksunden
Copy link
Member

ksunden commented Feb 20, 2025

I'm noting that there was a failed windows test, for which the logs have expired. I'm about 95% sure that is an unrelated flaky test or two, but the tests will be rerun on merging the rewritten change note anyway, so not going to worry about it too much until then.

@tacaswell tacaswell force-pushed the imshow-alpha-not-respected branch from 11390cd to a1247ce Compare February 20, 2025 22:03
@tacaswell
Copy link
Member

I took the liberty of rebasing to fix the docs build (circle CI runs on the branch tip (and gets its own config from the branch tip) where as GHA runs on a "synthetic" merge of the branch into the target branch. To make all our CI look "the same" we do the merge as part of the CI workflow, however this means when we bump the minimum python past what we are using on circle you we get "can not install Matplotlib" errors).

@anntzer
Copy link
Contributor

anntzer commented Feb 20, 2025

Per the above: mild preference for scaling rather than replacing preexisting alphas with the array alpha (sorry for not commenting earlier), but won't block either way.

@ksunden ksunden merged commit 44770ae into matplotlib:main Feb 21, 2025
40 checks passed
meeseeksmachine pushed a commit to meeseeksmachine/matplotlib that referenced this pull request Feb 21, 2025
ksunden added a commit that referenced this pull request Feb 21, 2025
…437-on-v3.10.x

Backport PR #28437 on branch v3.10.x (Respect array alpha with interpolation_stage='rgba' in _Imagebase::_make_image)
@ksunden ksunden mentioned this pull request Mar 3, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: interpolation_stage="rgba" does not respect array-alpha
6 participants