Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Efficient Matplotlib Redrawing

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 is displayed over a heatmap image and mouse clicks can add or remove scatter points.

My data is drawn in the background using pcolormesh(), so when I update the canvas using axis.figure.canvas.draw() both the scatter points and the background heatmap are redrawn. Given the size of the heatmap, this is too slow for a usable interface.

Is there a way of selectively redrawing just the scatter points without redrawing the background?

Example Code:

points = []  # Holds a list of (x,y) scatter points 

def onclick(event):
    # Click event handler to add points
    points.append( (event.x, event.y) )
    ax.figure.canvas.draw()

fig = plt.figure()
ax = plt.figure()
# Plot the background
ax.pcolormesh(heatmap_data)

# Add the listener to handle clicks
cid = fig.canvas.mpl_connect("button_press_event", onclick)
plt.show()
like image 764
Jordan Avatar asked Mar 26 '15 11:03

Jordan


People also ask

Is Pyqtgraph faster than Matplotlib?

matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as matplotlib, but runs much faster. Matplotlib is more aimed toward making publication-quality graphics, whereas pyqtgraph is intended for use in data acquisition and analysis applications.

Why does Matplotlib take so long?

It's your bottleneck. In your case, you don't need to re-draw things like the axes boundaries, tick labels, etc. 2) In your case, there are a lot of subplots with a lot of tick labels. These take a long time to draw.

How do I save Matplotlib figures with high resolution?

To save the file in pdf format, use savefig() method where the image name is myImagePDF. pdf, format="pdf". We can set the dpi value to get a high-quality image. Using the saving() method, we can save the image with format=”png” and dpi=1200.


1 Answers

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()
like image 63
Joe Kington Avatar answered Oct 04 '22 03:10

Joe Kington