Snel live plotten in Matplotlib / PyPlot

Jarenlang heb ik geworsteld om efficiënt live plotten in matplotlib te krijgen, en tot op de dag van vandaag ben ik ontevreden.

Ik wil een functie redraw_figuredie het cijfer “live” bijwerkt (terwijl de code wordt uitgevoerd), en de laatste plots weergeeft als ik stop bij een breekpunt.

Hier is een democode:

import time
from matplotlib import pyplot as plt
import numpy as np
def live_update_demo():
    plt.subplot(2, 1, 1)
    h1 = plt.imshow(np.random.randn(30, 30))
    redraw_figure()
    plt.subplot(2, 1, 2)
    h2, = plt.plot(np.random.randn(50))
    redraw_figure()
    t_start = time.time()
    for i in xrange(1000):
        h1.set_data(np.random.randn(30, 30))
        redraw_figure()
        h2.set_ydata(np.random.randn(50))
        redraw_figure()
        print 'Mean Frame Rate: %.3gFPS' % ((i+1) / (time.time() - t_start))
def redraw_figure():
    plt.draw()
    plt.pause(0.00001)
live_update_demo()

Plots moeten live worden bijgewerkt wanneer de code wordt uitgevoerd, en we zouden de laatste gegevens moeten zien wanneer we stoppen bij een onderbrekingspunt na redraw_figure(). De vraag is hoe je redraw_figure()

het beste kunt implementeren

In de bovenstaande implementatie (plt.draw(); plt.pause(0.00001)), werkt het, maar is het erg traag (~3.7FPS)

Ik kan het implementeren als:

def redraw_figure():
    plt.gcf().canvas.flush_events()
    plt.show(block=False)

En het werkt sneller (~11FPS), maar plots zijn niet up-to-date wanneer je stopt bij breekpunten (bijvoorbeeld als ik een breekpunt plaats op de regel t_start = ..., de tweede plot verschijnt niet).

Vreemd genoeg, wat echt werkt, is de show twee keer bellen:

def redraw_figure():
    plt.gcf().canvas.flush_events()
    plt.show(block=False)
    plt.show(block=False)

Dat geeft ~11FPS en houdt plots up-to-data als je pauze op een regel.

Nu heb ik horen zeggen dat het “block”-sleutelwoord is verouderd. En dezelfde functie twee keer aanroepen lijkt sowieso een rare, waarschijnlijk niet-draagbare hack.

Dus wat kan ik in deze functie plaatsen die met een redelijke framesnelheid kan plotten, geen gigantische kludge is en bij voorkeur over backends en systemen werkt?

Enkele opmerkingen:

  • Ik gebruik OSX en gebruik de backend TkAgg, maar oplossingen voor elk backend/systeem zijn welkom
  • Interactieve modus “Aan” werkt niet, omdat deze niet live wordt bijgewerkt. Het wordt alleen bijgewerkt in de Python-console wanneer de interpreter wacht op gebruikersinvoer.
  • Een blogstelde de implementatie voor:

    def redraw_figure():
    fig = plt.gcf()
    fig.canvas.draw()
    fig.canvas.flush_events()

Maar op mijn systeem worden de plots helemaal niet opnieuw getekend.

Dus, als iemand een antwoord heeft, zou je mij en duizenden anderen direct heel blij maken. Hun geluk zou waarschijnlijk doorsijpelen naar hun vrienden en familieleden, en hun vrienden en familieleden, enzovoort, zodat je potentieel het leven van miljarden zou kunnen verbeteren.

Conclusies

ImportanceOfBeingErnest laat zien hoe je blit kunt gebruiken om sneller te plotten, maar het is niet zo eenvoudig als iets anders in de functie redraw_figurete zetten (je moet bijhouden welke dingen je opnieuw moet tekenen).


Antwoord 1, autoriteit 100%

Allereerst draait de code die in de vraag is gepost met 7 fps op mijn machine, met QT4Agg als backend.

Nu, zoals in veel berichten is gesuggereerd, zoals hierof hier, kan het gebruik van bliteen optie zijn. Hoewel dit artikelvermeldt dat blit sterke geheugenlekkage veroorzaakt, zou ik let daar niet op.

Ik heb je code een beetje aangepast en de framesnelheid vergeleken met en zonder het gebruik van blit. De onderstaande code geeft

  • 28 fps wanneer uitgevoerd zonder blit
  • 175 fps met blit

Code:

import time
from matplotlib import pyplot as plt
import numpy as np
def live_update_demo(blit = False):
    x = np.linspace(0,50., num=100)
    X,Y = np.meshgrid(x,x)
    fig = plt.figure()
    ax1 = fig.add_subplot(2, 1, 1)
    ax2 = fig.add_subplot(2, 1, 2)
    img = ax1.imshow(X, vmin=-1, vmax=1, interpolation="None", cmap="RdBu")
    line, = ax2.plot([], lw=3)
    text = ax2.text(0.8,0.5, "")
    ax2.set_xlim(x.min(), x.max())
    ax2.set_ylim([-1.1, 1.1])
    fig.canvas.draw()   # note that the first draw comes before setting data 
    if blit:
        # cache the background
        axbackground = fig.canvas.copy_from_bbox(ax1.bbox)
        ax2background = fig.canvas.copy_from_bbox(ax2.bbox)
    plt.show(block=False)
    t_start = time.time()
    k=0.
    for i in np.arange(1000):
        img.set_data(np.sin(X/3.+k)*np.cos(Y/3.+k))
        line.set_data(x, np.sin(x/3.+k))
        tx = 'Mean Frame Rate:\n {fps:.3f}FPS'.format(fps= ((i+1) / (time.time() - t_start)) ) 
        text.set_text(tx)
        #print tx
        k+=0.11
        if blit:
            # restore background
            fig.canvas.restore_region(axbackground)
            fig.canvas.restore_region(ax2background)
            # redraw just the points
            ax1.draw_artist(img)
            ax2.draw_artist(line)
            ax2.draw_artist(text)
            # fill in the axes rectangle
            fig.canvas.blit(ax1.bbox)
            fig.canvas.blit(ax2.bbox)
            # in this post http://bastibe.de/2013-05-30-speeding-up-matplotlib.html
            # it is mentionned that blit causes strong memory leakage. 
            # however, I did not observe that.
        else:
            # redraw everything
            fig.canvas.draw()
        fig.canvas.flush_events()
        #alternatively you could use
        #plt.pause(0.000000000001) 
        # however plt.pause calls canvas.draw(), as can be read here:
        #http://bastibe.de/2013-05-30-speeding-up-matplotlib.html
live_update_demo(True)   # 175 fps
#live_update_demo(False) # 28 fps

Bijwerken:
Voor sneller plotten kan men overwegen om pyqtgraphte gebruiken.
Zoals de pyqtgraph-documentatiehet stelt: “Voor plotten is pyqtgraph lang niet zo compleet/volwassen als matplotlib, maar werkt veel sneller.”

Ik heb het bovenstaande voorbeeld overgezet naar pyqtgraph. En hoewel het er een beetje lelijk uitziet, draait het met 250 fps op mijn machine.

Dat samenvattend,

  • matplotlib (zonder blitting): 28 fps
  • matplotlib (met blitting): 175 fps
  • pyqtgraph: 250 fps

pyqtgraph-code:

import sys
import time
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import pyqtgraph as pg
class App(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(App, self).__init__(parent)
        #### Create Gui Elements ###########
        self.mainbox = QtGui.QWidget()
        self.setCentralWidget(self.mainbox)
        self.mainbox.setLayout(QtGui.QVBoxLayout())
        self.canvas = pg.GraphicsLayoutWidget()
        self.mainbox.layout().addWidget(self.canvas)
        self.label = QtGui.QLabel()
        self.mainbox.layout().addWidget(self.label)
        self.view = self.canvas.addViewBox()
        self.view.setAspectLocked(True)
        self.view.setRange(QtCore.QRectF(0,0, 100, 100))
        #  image plot
        self.img = pg.ImageItem(border='w')
        self.view.addItem(self.img)
        self.canvas.nextRow()
        #  line plot
        self.otherplot = self.canvas.addPlot()
        self.h2 = self.otherplot.plot(pen='y')
        #### Set Data  #####################
        self.x = np.linspace(0,50., num=100)
        self.X,self.Y = np.meshgrid(self.x,self.x)
        self.counter = 0
        self.fps = 0.
        self.lastupdate = time.time()
        #### Start  #####################
        self._update()
    def _update(self):
        self.data = np.sin(self.X/3.+self.counter/9.)*np.cos(self.Y/3.+self.counter/9.)
        self.ydata = np.sin(self.x/3.+ self.counter/9.)
        self.img.setImage(self.data)
        self.h2.setData(self.ydata)
        now = time.time()
        dt = (now-self.lastupdate)
        if dt <= 0:
            dt = 0.000000000001
        fps2 = 1.0 / dt
        self.lastupdate = now
        self.fps = self.fps * 0.9 + fps2 * 0.1
        tx = 'Mean Frame Rate:  {fps:.3f} FPS'.format(fps=self.fps )
        self.label.setText(tx)
        QtCore.QTimer.singleShot(1, self._update)
        self.counter += 1
if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    thisapp = App()
    thisapp.show()
    sys.exit(app.exec_())

Antwoord 2, autoriteit 13%

Hier is een manier om live plotten te doen: maak de plot als een afbeeldingsarray en teken de afbeelding vervolgens naar een multithreaded scherm.

Voorbeeld met een pyformula-scherm (~30 FPS):

import pyformulas as pf
import matplotlib.pyplot as plt
import numpy as np
import time
fig = plt.figure()
screen = pf.screen(title='Plot')
start = time.time()
for i in range(10000):
    t = time.time() - start
    x = np.linspace(t-3, t, 100)
    y = np.sin(2*np.pi*x) + np.sin(3*np.pi*x)
    plt.xlim(t-3,t)
    plt.ylim(-3,3)
    plt.plot(x, y, c='black')
    # If we haven't already shown or saved the plot, then we need to draw the figure first...
    fig.canvas.draw()
    image = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
    image = image.reshape(fig.canvas.get_width_height()[::-1] + (3,))
    screen.update(image)
#screen.close()

voer hier de afbeeldingsbeschrijving in

Disclaimer: ik ben de beheerder van pyformula’s

Other episodes