Efficient Matplotlib Redrawing

前端 未结 1 733
醉酒成梦
醉酒成梦 2020-12-29 09:35

I\'m using Matplotlib to allow the user to select interesting data points with mouseclicks, using a very similar method to this answer.

Effectively, a scatter plot i

相关标签:
1条回答
  • 2020-12-29 10:30

    Sure! What you want is blitting. If you weren't writing a gui, you could simplify some of this by using matplotlib.animation, but you'll need to handle it directly if you want things to be interactive.

    In matplotlib terms, you want a combination of fig.canvas.copy_from_bbox, and then alternately call fig.canvas.restore_region(background), ax.draw_artist(what_you_want_to_draw) and fig.canvas.blit:

    background = fig.canvas.copy_from_bbox(ax.bbox)
    
    for x, y in user_interactions:
        fig.canvas.restore_region(background)
        points.append([x, y])
        scatter.set_offsets(points)
        ax.draw_artist(scatter)
        fig.canvas.blit(ax.bbox)
    

    Simple Blitting Example: Adding Points

    In your case, if you're only adding points, you can actually skip saving and restoring the background. If you go that route, though, you'll wind up with some subtle changes to the plot due to antialiased points being repeatedly redrawn on top of each other.

    At any rate, here's the simplest possible example of the type of thing you're wanting. This only deals with adding points, and skips saving and restoring the background as I mentioned above:

    import matplotlib.pyplot as plt
    import numpy as np
    
    def main():
        fig, ax = plt.subplots()
        ax.pcolormesh(np.random.random((100, 100)), cmap='gray')
    
        ClickToDrawPoints(ax).show()
    
    class ClickToDrawPoints(object):
        def __init__(self, ax):
            self.ax = ax
            self.fig = ax.figure
            self.xy = []
            self.points = ax.scatter([], [], s=200, color='red', picker=20)
            self.fig.canvas.mpl_connect('button_press_event', self.on_click)
    
        def on_click(self, event):
            if event.inaxes is None:
                return
            self.xy.append([event.xdata, event.ydata])
            self.points.set_offsets(self.xy)
            self.ax.draw_artist(self.points)
            self.fig.canvas.blit(self.ax.bbox)
    
        def show(self):
            plt.show()
    
    main()
    

    Sometimes Simple is Too Simple

    However, let's say we wanted to make right-clicks delete a point.

    In that case, we need to be able to restore the background without redrawing it.

    Ok, all well and good. We'll use something similar to the pseudocode snippet I mentioned at the top of the answer.

    However, there's a caveat: If the figure is resized, we need to update the background. Similarly, if the axes is interactively zoomed/panned, we need to update the background. Basically, you need to update the background anytime the plot is drawn.

    Pretty soon you need to get fairly complex.


    More Complex: Adding/Dragging/Deleting Points

    Here's a general example of the kind of "scaffolding" you wind up putting in place.

    This is somewhat inefficient, as the plot gets drawn twice. (e.g. panning will be slow). It is possible to get around that, but I'll leave those examples for another time.

    This implements adding points, dragging points, and deleting points. To add/drag a point after interactively zooming/panning, click to zoom/pan tool on the toolbar again to disable them.

    This is a fairly complex example, but hopefully it gives a sense of the type of framework that one would typically build to interactively draw/drag/edit/delete matplotlib artists without redrawing the entire plot.

    import numpy as np
    import matplotlib.pyplot as plt
    
    class DrawDragPoints(object):
        """
        Demonstrates a basic example of the "scaffolding" you need to efficiently
        blit drawable/draggable/deleteable artists on top of a background.
        """
        def __init__(self):
            self.fig, self.ax = self.setup_axes()
            self.xy = []
            self.tolerance = 10
            self._num_clicks = 0
    
            # The artist we'll be modifying...
            self.points = self.ax.scatter([], [], s=200, color='red',
                                          picker=self.tolerance, animated=True)
    
            connect = self.fig.canvas.mpl_connect
            connect('button_press_event', self.on_click)
            self.draw_cid = connect('draw_event', self.grab_background)
    
        def setup_axes(self):
            """Setup the figure/axes and plot any background artists."""
            fig, ax = plt.subplots()
    
            # imshow would be _much_ faster in this case, but let's deliberately
            # use something slow...
            ax.pcolormesh(np.random.random((1000, 1000)), cmap='gray')
    
            ax.set_title('Left click to add/drag a point\nRight-click to delete')
            return fig, ax
    
        def on_click(self, event):
            """Decide whether to add, delete, or drag a point."""
            # If we're using a tool on the toolbar, don't add/draw a point...
            if self.fig.canvas.toolbar._active is not None:
                return
    
            contains, info = self.points.contains(event)
            if contains:
                i = info['ind'][0]
                if event.button == 1:
                    self.start_drag(i)
                elif event.button == 3:
                    self.delete_point(i)
            else:
                self.add_point(event)
    
        def update(self):
            """Update the artist for any changes to self.xy."""
            self.points.set_offsets(self.xy)
            self.blit()
    
        def add_point(self, event):
            self.xy.append([event.xdata, event.ydata])
            self.update()
    
        def delete_point(self, i):
            self.xy.pop(i)
            self.update()
    
        def start_drag(self, i):
            """Bind mouse motion to updating a particular point."""
            self.drag_i = i
            connect = self.fig.canvas.mpl_connect
            cid1 = connect('motion_notify_event', self.drag_update)
            cid2 = connect('button_release_event', self.end_drag)
            self.drag_cids = [cid1, cid2]
    
        def drag_update(self, event):
            """Update a point that's being moved interactively."""
            self.xy[self.drag_i] = [event.xdata, event.ydata]
            self.update()
    
        def end_drag(self, event):
            """End the binding of mouse motion to a particular point."""
            for cid in self.drag_cids:
                self.fig.canvas.mpl_disconnect(cid)
    
        def safe_draw(self):
            """Temporarily disconnect the draw_event callback to avoid recursion"""
            canvas = self.fig.canvas
            canvas.mpl_disconnect(self.draw_cid)
            canvas.draw()
            self.draw_cid = canvas.mpl_connect('draw_event', self.grab_background)
    
        def grab_background(self, event=None):
            """
            When the figure is resized, hide the points, draw everything,
            and update the background.
            """
            self.points.set_visible(False)
            self.safe_draw()
    
            # With most backends (e.g. TkAgg), we could grab (and refresh, in
            # self.blit) self.ax.bbox instead of self.fig.bbox, but Qt4Agg, and
            # some others, requires us to update the _full_ canvas, instead.
            self.background = self.fig.canvas.copy_from_bbox(self.fig.bbox)
    
            self.points.set_visible(True)
            self.blit()
    
        def blit(self):
            """
            Efficiently update the figure, without needing to redraw the
            "background" artists.
            """
            self.fig.canvas.restore_region(self.background)
            self.ax.draw_artist(self.points)
            self.fig.canvas.blit(self.fig.bbox)
    
        def show(self):
            plt.show()
    
    DrawDragPoints().show()
    
    0 讨论(0)
提交回复
热议问题