Skip to content

FIX: make_image should not modify original array#29892

Merged
dstansby merged 1 commit into
matplotlib:mainfrom
rcomer:array-alpha
Apr 13, 2025
Merged

FIX: make_image should not modify original array#29892
dstansby merged 1 commit into
matplotlib:mainfrom
rcomer:array-alpha

Conversation

@rcomer

@rcomer rcomer commented Apr 9, 2025

Copy link
Copy Markdown
Member

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
Comment thread lib/matplotlib/tests/test_image.py Outdated
ax3.imshow(rgbau)


def test_imshow_rgba_multi_draw():

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

dstansby commented Apr 10, 2025

Copy link
Copy Markdown
Member

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

anntzer commented Apr 10, 2025

Copy link
Copy Markdown
Contributor

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

rcomer commented Apr 10, 2025

Copy link
Copy Markdown
Member Author

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

@jklymak

jklymak commented Apr 10, 2025

Copy link
Copy Markdown
Member

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
Copy Markdown
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

QuLogic commented Apr 11, 2025

Copy link
Copy Markdown
Member

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

jklymak commented Apr 11, 2025

Copy link
Copy Markdown
Member

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

@QuLogic

QuLogic commented Apr 11, 2025

Copy link
Copy Markdown
Member

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

@jklymak

jklymak commented Apr 11, 2025

Copy link
Copy Markdown
Member

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

@tacaswell

Copy link
Copy Markdown
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
Copy Markdown
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
@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