Skip to content

FIX: make_image should not modify original array #29892

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 1 commit into from
Apr 13, 2025

Conversation

rcomer
Copy link
Member

@rcomer rcomer commented Apr 9, 2025

PR summary

Fixes #29891. The problem is that we divide the RGB part of the array by its alpha on each draw. The simple fix is to make a copy but I assume for efficiency we should only copy when necessary.

  • If we didn't start with RGBA, we already made a copy
  • If alpha was an array, we already made a copy
  • If the alpha channel is one everywhere, it does not matter

PR checklist

@rcomer rcomer added Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. PR: bugfix Pull requests that fix identified bugs labels Apr 9, 2025
@jklymak jklymak requested a review from anntzer April 9, 2025 21:49
@@ -281,6 +281,17 @@ def test_imshow_alpha(fig_test, fig_ref):
ax3.imshow(rgbau)


def test_imshow_rgba_multi_draw():
Copy link
Member

Choose a reason for hiding this comment

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

You do this conditionally - may be helpful to test the conditions where you are not doing the copy to demonstrate that they work fine on redraw

@dstansby
Copy link
Member

dstansby commented Apr 10, 2025

This adds another copy of the input image, which for medium to large images is not neglegible. Running the script below I get a peak memory usage of ~450 MB, but with this PR I get a peak usage of ~580MB (as expected the difference is ~one copy of the input image, which is ~120MB). The performance of resizing the window is also much worse, I presume because it's making a copy of the array every time the window size changes. < ignore this

I guess the other way to fix this is to multiply in-place, then divide in place after the resample. That would avoid the extra memory, but if it's constantly happening during a window resize I could imagine it has performace implications, and perhaps floating point errors could accumulate pretty fast with all the multiplications and divisions?

I can't think of another option to solve this without going back to applying alpha in resampled space (ie undoing #29776). Perhaps @anntzer has some thoughts as the original PR author?

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
rgba = np.random.random((2000, 2000, 4))

fig, ax = plt.subplots()
ax.imshow(rgba)
fig.canvas.draw()

@rcomer rcomer added this to the v3.11.0 milestone Apr 10, 2025
@anntzer
Copy link
Contributor

anntzer commented Apr 10, 2025

The fix looks correct to me, sorry for introducing the bug.

re: performance:

  • It's clear to me that unpremultiplied filtering is wrong because agg explicitly clips the RGB values to no more than alpha after the premultiplication (Filter images in premultiplied alpha mode. #29776 (comment)), which only makes sense in premultiplied space; if we really don't want to premultiply we should then at least remove that clipping.
  • In an initial version I wrote the premultiplication by upcasting uint8 just to uint16 e52c150 for int input, which should save a lot of memory, but following review (Filter images in premultiplied alpha mode. #29776 (review)) I moved everything to float for simplicity. I could bring back these narrower types to save memory.
  • It may be possible to save memory by merging the premultiplication step into the C++ filtering code; it's probably doable but a quick check suggests it's still a bit of work.

Thoughts?

@rcomer
Copy link
Member Author

rcomer commented Apr 10, 2025

I guess we can add "input was uint8" to my list of reasons not to copy, since we already cast to float.

@jklymak
Copy link
Member

jklymak commented Apr 10, 2025

In my opinion #29776 should remain. The previous results were incorrect, and I think having correct results should trump efficiency.

We copy the user data in _normalize_image_array and set that to self._A. It seems to me the proper thing to do it recopy the user data, not make another version of our self._A. I'm not sure how inefficient that is, but doing that at the correct level would make it so we only ever have one extra copy.

@dstansby
Copy link
Member

👍 to doing the right thing at the expense of memory - at a glance I think there might be ways to save creating extra memory for the alpha channel, but for simplicity I think worth getting this in as is and then optimizing (if possible) later.

The problem is that we divide the RGB part of the array by its alpha
on each draw.  The simple fix is to make a copy but I assume for
efficiency we should only copy when necessary.

* If we didn't start with float RGBA, we already made a copy
* If alpha was an array, we already made a copy
* If the alpha channel is one everywhere, it does not matter
@QuLogic
Copy link
Member

QuLogic commented Apr 11, 2025

It may be possible to save memory by merging the premultiplication step into the C++ filtering code; it's probably doable but a quick check suggests it's still a bit of work.

Either #29453, or some not-yet-pushed version of it, does attempt to do some of this deduplication. I don't have time to look at that again until I finish the text stuff, but it may yet be a possibility to reduce the memory footprint, which can be done after this PR at least fixes the bug.

@jklymak
Copy link
Member

jklymak commented Apr 11, 2025

That's fine as long as we remember to remove the extra copy this introduces.

@QuLogic
Copy link
Member

QuLogic commented Apr 11, 2025

I thought #29776 removed a copy (from _rgb_to_rgba), so we should be net even with this PR? At least for now.

@jklymak
Copy link
Member

jklymak commented Apr 11, 2025

Sure? But we still will want to remove this copy?

@tacaswell
Copy link
Member

I just want to clarify the issue is that we are modifying our copy of the array not the object the user passed to us.

@dstansby
Copy link
Member

I just want to clarify the issue is that we are modifying our copy of the array not the object the user passed to us.

Yes, this is correct

@dstansby dstansby merged commit 349aa96 into matplotlib:main Apr 13, 2025
41 checks passed
@rcomer rcomer deleted the array-alpha branch April 13, 2025 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: bugfix Pull requests that fix identified bugs Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: images
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: image alpha re-applied each draw?
6 participants