Fast Live Plotting в Matplotlib/PyPlot

В течение многих лет я изо всех сил пытался получить эффективный живой сюжет в matplotlib, и по сей день я остаюсь неудовлетворенным.

Мне нужна функция redraw_figure, которая обновляет цифру "вживую" (по мере запуска кода) и будет отображать последние графики, если я остановлюсь в точке останова.

Вот демон-код:

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()

Графики должны обновляться в реальном времени при запуске кода, и мы должны увидеть последние данные при остановке в любой точке останова после redraw_figure(). Вопрос в том, как наилучшим образом реализовать redraw_figure()

В реализации выше (plt.draw(); plt.pause(0.00001)) она работает, но очень медленная (~ 3.7FPS)

Я могу реализовать его как:

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

И он работает быстрее (~ 11FPS), но графики не обновляются, когда вы останавливаетесь на контрольных точках (например, если я ставлю точку останова на строку t_start = ..., второй график не появится).

Как ни странно, то, что на самом деле работает, вызывает шоу дважды:

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

Что дает ~ 11FPS и сохраняет графики до данных, если ваш разрыв в любой строке.

Теперь я слышал, что ключевое слово "block" устарело. И вызов одной и той же функции дважды кажется странным, вероятно, не-переносным хаком в любом случае.

Итак, что я могу поместить в эту функцию, которая будет строить с разумной частотой кадров, не является гигантским kludge и предпочтительно будет работать через бэкэнды и системы?

Некоторые примечания:

  • Я нахожусь в OSX и использую TkAgg бэкэнд, но решения на любом бэкэнд/системе приветствуются
  • Интерактивный режим "On" не будет работать, потому что он не обновляется вживую. Он просто обновляется, когда на консоли Python, когда интерпретатор ждет ввода пользователя.
  • A блог предложил реализацию:

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

Но, по крайней мере, в моей системе, которая вообще не перерисовывает графики.

Итак, если у кого-то есть ответ, вы бы прямо сделали меня и тысячи других счастливыми. Их счастье, вероятно, просочится к их друзьям и родственникам, их друзьям и родственникам и т.д., Чтобы вы могли улучшить жизнь миллиардов.

Выводы

ImportanceOfBeingErnest показывает, как вы можете использовать blit для более быстрого построения графика, но это не так просто, как положить что-то другое в функцию redraw_figure (вам нужно следить за тем, что нужно перерисовывать).

Ответ 1

Прежде всего, код, отправленный в вопросе, работает с 7 fps на моей машине, а QT4Agg - в качестве бэкэнд.

Теперь, как было предложено во многих сообщениях, например здесь или здесь, используя blit может быть вариантом. Хотя в этой статье упоминается, что blit вызывает сильную утечку памяти, я не мог этого заметить.

Я немного изменил ваш код и сравнил частоту кадров с использованием blit и без него. В приведенном ниже коде

  • 18 кадров в секунду при запуске без blit
  • 28 fps с blit

код:

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)

    fig.canvas.draw()   # note that the first draw comes before setting data 

    h1 = ax1.imshow(X, vmin=-1, vmax=1, interpolation="None", cmap="RdBu")

    h2, = ax2.plot(x, lw=3)
    text = ax2.text(0.8,1.5, "")
    ax2.set_ylim([-1,1])


    if blit:
        # cache the background
        axbackground = fig.canvas.copy_from_bbox(ax1.bbox)
        ax2background = fig.canvas.copy_from_bbox(ax2.bbox)

    t_start = time.time()
    k=0.
    for i in np.arange(1000):
        h1.set_data(np.sin(X/3.+k)*np.cos(Y/3.+k))
        h2.set_ydata(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(h1)
            ax2.draw_artist(h2)

            # 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()


        plt.pause(0.000000000001) 
        #plt.pause calls canvas.draw(), as can be read here:
        #http://bastibe.de/2013-05-30-speeding-up-matplotlib.html
        #however with Qt4 (and TkAgg??) this is needed. It seems,using a different backend, 
        #one can avoid plt.pause() and gain even more speed.


live_update_demo(True) # 28 fps
#live_update_demo(False) # 18 fps

Update:
Для более быстрого построения графика можно использовать pyqtgraph.
Как пишет документация pyqtgraph: "Для построения графика pyqtgraph не так полно/зрелый, как matplotlib, но работает намного быстрее".

Я поместил приведенный выше пример в pyqtgraph. И хотя это выглядит некрасиво, он работает с частотой 250 кадров в секунду на моей машине.

Подводя итог,

  • matplotlib (без blitting): 18 fps
  • matplotlib (с blitting): 28 fps
  • pyqtgraph: 250 fps

код pyqtgraph:

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_())