1

I'm a novice using matplotlib as an embedded control in my PyQt4 application to display image data. I'd like to be able to allow the user to interactively draw a line on the image by clicking and dragging. I have it working but it is so slow as to be unusable, leading me to believe I'm not going about it the correct way. The only way I can get the line to appear is by forcing the canvas to redraw each time the mouse moves (I suspect this is the cause of the slowdown).

For example, on the mouse down event I store the current coordinates and add a Line2D object to the plot as follows:

def onMouseMove(self, event):

    if self.drawingLine:

        self.lineStartX = event.xdata
        self.lineStopX = event.xdata
        self.lineStartY = event.ydata
        self.lineStopY = event.ydata

        self.line = Line2D([self.lineStartX, self.lineStopX], [self.lineStartY, self.lineStopY], linewidth = 1.5, color = 'r')

        self.axes.add_line(self.line)

Then, in my mouse move event I redraw the line as follows:

def onMouseMove(self, event):

    if self.drawingLine:

        self.lineStopX = event.xdata
        self.lineStopY = event.ydata

        # Adjust the line to the new endpoint:
        self.line.set_data([self.lineStartX, self.lineStopX], [self.lineStartY, self.lineStopY])

        # Force a redraw otherwise you don't see any changes:
        self.fig.canvas.draw()

As I've stated this approach is unusably slow and hence probably wrong. Can somebody please clue me in to what the proper approach is here? Thank you all in advance.

LKeene
  • 627
  • 1
  • 8
  • 22

2 Answers2

1

matplotlib is built to be flexible and to work with multiple different backends. It is very slow at real-time plotting. The problem is that your mouse move events are very rapid. Anything trying to keep up with the mouse movement will probably be slow. You need to call the plot less often. You can do this by checking the time in your mouse move function and trying to limit the plotting calls to whatever works.

import time

def onMouseMove(self, event):
    if self.drawingLine and time.time() - last_time > 0.03:  # Change the 0.03 to change how often you plot.
        last_time = time.time()
        ...

I highly suggest pyqtgraph. pyqtgraph has built in rate limiting signals that you can work with to do this.

Below is a basic example of how you can do this.

# Change the style to look like matplotlib
pyqtgraph.setConfigOption("background", QtGui.QColor.fromRgbF(230/255, 230/255, 234/255, 255/255))
pyqtgraph.setConfigOption("background", 'w')
pyqtgraph.setConfigOption("foreground", 'k')
pyqtgraph.setConfigOption("antialias", True)

# Create the widgets and plot items
glw = pyqtgraph.GraphicsLayoutWidget()
pg = glw.addPlot(0, 0)


class MyClass(object):
    ...

    ...
    def onMouseMove(self, event):
        if self.drawingLine:

            scene_pos = event[0]
            data_pos = pg.getViewBox().mapSceneToView(scene_pos)
            x, y = data_pos.x(), data_pos.y()

            self.lineStopX = x
            self.lineStopY = y

            # Adjust the line to the new endpoint:
            if not self.line:
                self.line = pg.plot(x=[], y=[])
            self.line.setData(x=[self.lineStartX, self.lineStopX], 
                              y=[self.lineStartY, self.lineStopY])

mouse_move_sig = pyqtgraph.SignalProxy(pg.scene().sigMouseMoved,
                                       rateLimit=60, slot=onMouseMove)
justengel
  • 6,132
  • 4
  • 26
  • 42
  • Introducing the time limit will lead to a lag in the dynamics of the line. So in order to prevent a lag, you introduce a lag. Doesn't sound like a good solution to me. – ImportanceOfBeingErnest Oct 02 '17 at 18:38
  • The lag is due to the time it takes matplotlib to draw the line. If you tell matplotlib to draw a line a million times the program will spend the entire time drawing the line over and over. Your program will become unresponsive. figure.draw_idle() helps with this to some degree, but as I said before matplotlib is really slow when it comes to real-time plotting. – justengel Oct 02 '17 at 18:49
  • The matplotlib way to do this is to use the FunctionAnimation https://matplotlib.org/2.0.0/examples/animation/animate_decay.html. basically you collect the data really fast then you call the plotting function on a separate timer where the interval of the timer is much slower. You eyes can only see and comprehend at a certain speed. You don't need to plot every miniscule change of the line. You just need to plot it at a rate that looks smooth to the user. I think the suggested minimum is 20-30 fps. Plotting and drawing on a screen is a very expensive operation that takes a lot of time. – justengel Oct 02 '17 at 18:51
  • My point is that the solution should not be to draw the complete canvas at low rate, but instead only draw the line at high rate, leaving the rest of the canvas as it is. See my answer on what I mean by that. – ImportanceOfBeingErnest Oct 02 '17 at 19:16
1

First off, you will already gain a little by using

self.fig.canvas.draw_idle()

instead of draw(). This redraws the canvas only when it's not currently beeing repainted, saving you a lot of draws.

If this is not enough, you would need to use the technique of blitting. Now since you don't have a minimal example, I will not provide any complete solution for this here, but e.g. the answer to this question, why is plotting with Matplotlib so slow?, has an example of that. The idea is to store the background, and only redraw the part that changes (here the line).

background = fig.canvas.copy_from_bbox(ax.bbox)
# then during mouse move
fig.canvas.restore_region(background)
line.set_data(...)
ax.draw_artist(line)
fig.canvas.blit(ax.bbox)
# only after mouse has stopped moving
fig.canvas.draw_idle()

This technique is also used internally by some matplotlib widgets, e.g. matplotlib.widgets.Cursor to let the lines follow the cursor quickly.

This brings me to the last point, which is: You don't need to reinvent the wheel. There is a matplotlib.widgets.RectangleSelector, which by defaut draws a rectangle for selection. But you may use its drawtype='line' argument, to change the selection to a line, together with the argument blit=True this should already give you what you need - you will just need to add the code to finally draw a line once the selection is finished.

Note that in the newest matplotlib version, there is even a matplotlib.widgets.PolygonSelector, which may directly be what you need.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712