Method to add transparency to a saved BufferRegion?

Hey,
I have a question on how to add transparency to cached background-layers… I tried my best to come up with a feasible solution, but so far I’m a bit lost so I thought I’ll give it a try and ask if there’s a simple way to do what I want:

In short, I’m the developer of EOmaps, a library that uses matplotlib figures to analyze geographical data.

In order to overlay different plot-layers, I use blitting to save and restore previously fetched layers.
The layers are stored as BufferRegions using:
saved_layer = canvas.copy_from_bbox(bbox)
and restored via
canvas.restore_region(saved_layer, bbox=bbox, xy=xy).

Now my question is the following:
Is it possible to add transparency to the region restored via canvas.restore_region(...) ?
… to make myself clear, I want to store a non transparent layer and restore it with arbitrary transparency.

… here’s a gif to make it even more clear :smile: (I’d like to be able to set the transparency of the overlay)

transparent_blit

Any insights on this would be highly appreciated!
Cheers, Raphael

Can you convert to a RGBA image and use draw_image instead?
Thinking about it loud, I wonder, in fact, whether we could just (after deprecation) get rid of restore_region and BufferRegions and replace their uses by draw_image (having copy_from_bbox return an RGBA image instead).

Hey, thanks for the response!!

yes, converting the buffer to rgba should not be a problem…
I’d use something like this:

buf = saved_background.to_string_argb()
ncols, nrows = saved_background.get_extents()[2:]

argb = np.frombuffer(buf, dtype=np.uint8).reshape(nrows, ncols, 4)

however, it seems that canvas does not have a draw_image function…
what exactly do you mean by draw_image ?

Sorry, that method is on the (cached) renderer, i.e. fig.canvas.get_renderer() (after a draw has been made). See matplotlib.backend_bases — Matplotlib 3.5.3 documentation.

OK thanks! … always scares me a little when I have to look that deep into the matplotlib code-base :sweat_smile:

I found the function, but I can’t seem to get it working properly and the documentation is starting to get a bit sparse… (especially the gc argument puzzles me a bit…)

I crafted a minimal working example that shows how I do it currently and where I’d like to incorporate your suggestion… could you maybe help me out by quickly showing how to use draw_image in such a context?

import numpy as np
import matplotlib.pyplot as plt

f, ax = plt.subplots()
ax.plot([1,2,3], "ro-")
ax.plot([3,2,1], "go-")
f.canvas.draw()

buffer = f.canvas.copy_from_bbox(ax.bbox)

x = buffer.get_extents()
ncols, nrows = x[2] - x[0], x[3] - x[1]

argb = np.frombuffer(buffer, dtype=np.uint8).reshape((nrows, ncols, 4))
rgba = argb[...,(1,2,3,0)]

def on_draw(self):
    # this is how i currently restore partial regions
    # (but I can't figure out how to properly add transparency here)
    f.canvas.restore_region(buffer, (200, 200, 400, 400), (20, 50))

    # ------ how would I implement this with draw_image ???
    # (the code below does not seem to work)
    
    # gc = self.renderer.new_gc()
    # renderer.draw_image(gc, 0, 0, rgba)
    # gc.restore()
    
f.canvas.mpl_connect("draw_event", on_draw)

OK… i figured it out myself… the only thing that was missing in the above code was to set a proper clipping region :smile:

gc = self.renderer.new_gc()
gc.set_clip_rectangle(self.canvas.figure.bbox)
renderer.draw_image(gc, 0, 0, rgba)
gc.restore()

THANKS A LOT for pointing me into the right direction !!!

Soon it will be possible to transparently overlay arbitrary layers in EOmaps :partying_face:

transparent_blit_ready

1 Like

This looks super nice.
See also [MNT]: Replace BufferRegion/restore_region by (x, y, array) tuples and draw_image? · Issue #23882 · matplotlib/matplotlib · GitHub which this discussion has triggered.

@raphaelquast Trying to understand what your actual application is doing: are you rendering two figures and then blitting from one on to the other?

@tacaswell

basically yes… but I do all in one figure…
I use a rather extensive adaption of the BlitManager to create some kind of a “multi-layer interface” for a figure that caches already rendered layers so that I can use blitting to compare them without having to re-draw.

EOmaps is intended for analysis of very large datasets (and/or WebMap layers) so I try to avoid re-drawing as much as possible…

I guess it would be possible to use 2 separate figures for that, but then I would have to carefully connect the two so that they are always properly aligned despite of events such as resize/zoom/pan etc…

I was worried about exactly those issues with multiple figures!

I still do not see how you do the dynamic sub-setting with multiple layers though (or maybe restore_region is more flexible than I remember off the top of my head…).

@tacaswell
Not sure if I understand your question correctly…
Let me try to explain in principle what I do:

  1. draw something on the axis (e.g. the effective overlay)
  2. render & cache the figure
  3. clear the axis
  4. draw something again (e.g. the effective background layer)
  5. implement the on_draw callback as shown above to overlay the previously drawn figure on each draw event

Now, both restore_region and draw_image can be used to restore a subset of the cached render at an arbitrary position within the figure… so in the end I’m using a "motion_notify_event" to get the cursor position and update the position and size of the region drawn with restore_region (or draw_image) accordingly.

Finally I implemented callbacks that detect if a re-draw of the cached overlay is necessary (e.g. on resize/pan/zoom events)

Does this make it more clear?

Now, both restore_region and draw_image can be used to restore a subset of the cached render at an arbitrary position within the figure

Yes, that was the detail of the API I was not remembering.

That is all very clever!

Hey, I came across a follow-up question that I haven’t been able to answer so far…

Is there a way to obtain a matplotlib.backends._backend_agg.BufferRegion without the background patch of the axes? (e.g. so that the overlay of an initially mostly transparent image does not get a white background…)

(basically the same as when using .savefig(transparent=True) but simply setting the background-patch color to none as done in .savefig doesn’t seem to do the job when using canvas.copy_from_bbox()

Any help would be highly appreciated :slight_smile: Thanks!

Can you open an issue to track this on github? Off the top of my head it seems like it should work (assuming you correctly set the figure and the axes patch to transparent and triggered a new draw).

Thanks for the very quick response!
OK, I’ll put together a minimal example and open an issue !

@anntzer.lee

OK, I was nearly ready composing the issue when I realized that f.canvas.copy_from_bbox(ax.bbox) copies both the figure and the axes background patch… setting both to transparent now does the job nicely!

Thanks again for the quick response… all works again as expected! :partying_face:

The logic is that the canvas knows nothing about the artist structure, it just has a buffer of pixels that you are copying a sub-set of (which happens to be the bounding box of the Axes).