简体   繁体   中英

Python Matplotlib: reduce render time for interactive plot

I've got the following code that produces a plot that can interactively be modified. Clicking / holding the left mouse button sets the marker position, Holding the right button and moving the mouse moves the plotted data in direction x and using the mouse wheel zooms in/out. Additionally, resizing the window calls figure.tight_layout() so that the size of the axes is adapted to the window size.

# coding=utf-8
from __future__ import division

from Tkinter import *

import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from numpy import arange, sin, pi

matplotlib.use('TkAgg')


class PlotFrame(Frame):
    def __init__(self, master, **ops):
        Frame.__init__(self, master, **ops)

        self.figure = Figure()
        self.axes_main = self.figure.add_subplot(111)
        for i in range(10):
            t = arange(0, 300, 0.01)
            s = sin(0.02 * pi * (t + 10 * i))
            self.axes_main.plot(t, s)

        self.plot = FigureCanvasTkAgg(self.figure, master=self)
        self.plot.show()
        self.plot.get_tk_widget().pack(fill=BOTH, expand=1)

        self.dragging = False
        self.dragging_button = None
        self.mouse_pos = [0, 0]

        self.marker = self.figure.axes[0].plot((0, 0), (-1, 1), 'black', linewidth=3)[0]

        self.plot.mpl_connect('button_press_event', self.on_button_press)
        self.plot.mpl_connect('button_release_event', self.on_button_release)
        self.plot.mpl_connect('motion_notify_event', self.on_mouse_move)
        self.plot.mpl_connect('scroll_event', self.on_mouse_scroll)
        self.plot.mpl_connect("resize_event", self.on_resize)

    def on_resize(self, _):
        self.figure.tight_layout()

    def axes_size(self):
        pos = self.axes_main.get_position()
        bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
        width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
        axis_size = [(pos.x1 - pos.x0) * width, (pos.y1 - pos.y0) * height]
        return axis_size

    def on_button_press(self, event):
        # right mouse button clicked
        if not self.dragging and event.button in (1, 3):
            self.dragging = True
            self.dragging_button = event.button
            self.mouse_pos = [event.x, event.y]
        # left mouse button clicked
        if event.button == 1 and event.xdata is not None:
            self.move_marker(event.xdata)

    def on_button_release(self, event):
        if self.dragging and self.dragging_button == event.button:
            self.dragging = False

    def on_mouse_move(self, event):
        if self.dragging and self.dragging_button == 3:
            dx = event.x - self.mouse_pos[0]
            self.mouse_pos = [event.x, event.y]
            x_min, x_max = self.figure.axes[0].get_xlim()
            x_range = x_max - x_min
            x_factor = x_range / self.axes_size()[0]
            self.figure.axes[0].set_xlim([x_min - dx * x_factor, x_max - dx * x_factor])
            self.plot.draw()
        elif self.dragging and self.dragging_button == 1:
            self.move_marker(event.xdata)

    def on_mouse_scroll(self, event):
        if event.xdata is None:
            return
        zoom_direction = -1 if event.button == 'up' else 1
        zoom_factor = 1 + .4 * zoom_direction
        x_min, x_max = self.figure.axes[0].get_xlim()
        min = event.xdata + (x_min - event.xdata) * zoom_factor
        max = event.xdata + (x_max - event.xdata) * zoom_factor
        self.figure.axes[0].set_xlim([min, max])
        self.plot.draw()

    def move_marker(self, x_position):
        y_min, y_max = self.figure.axes[0].get_ylim()
        self.marker.set_data((x_position, x_position), (y_min, y_max))
        self.plot.draw()


if __name__ == '__main__':
    gui = Tk()
    vf = PlotFrame(gui)
    vf.pack(fill=BOTH, expand=1)
    gui.mainloop()

The implementation works fine, but rendering is really slow when displaying a lot of lines. How can I make rendering faster? As you can see in the implementation above, the whole plot is drawn completely every time anything changes which shouldn't be necessary. My thoughts on this:

  • Resizing the window: draw everything
  • Zooming: draw everything
  • Moving the marker: just redraw the marker (one line) instead of drawing everything
  • Moving the plot in x direction: move the pixels currently displayed in the plot left/right and only draw pixels that are moved into the visible area

Drawing everything when resizing/zooming is fine for me, but I really need faster drawing of the latter two modifications. I already looked into matplotlib's animations, but as far as I understood, they won't help in my case. Any help is greatly appreciated, thanks!

The solution seems to be to cache elements that get redrawn as you said:

One major thing that gets redrawn is the background:

    # cache the background
    background = fig.canvas.copy_from_bbox(ax.bbox)

After caching restore it using restore region then just re-draw the points/line at every call you need

        # restore background
        fig.canvas.restore_region(background)

        # redraw just the points
        ax.draw_artist(points)

        # fill in the axes rectangle
        fig.canvas.blit(ax.bbox)

To optimize drawing blitting can be used. With it only given artists (those that were changed) will be rendered instead of the whole figure.

Motplotlib uses that technique internally in the animation module. You can use Animation class in it as a reference to implement the same behaviour in your code. Look at the _blit_draw() and several related functions after it in the sources.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM