Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw a large amont of signal faster in matplotlib embedded within Qt environment?

I'm trying to plot a large among of signal in a matplotlib figure which is embedded in a Qt environment. The plots are updated according to QScrollBar which modify the part of signals that I need to show. My issue is the update of the figure takes a pretty long time, especially because I have 250 signals to update. So, I'm seeking for a way to optimize the EEG_plot.update function to reduce its draw time. I don't know how I could use an animate function to speed up the process or something else. My concern is I need to update the time axis ticks and probably also the y axis label positions. The other thing is if the last segment that I need to plot does not correspond exactly with the window size chosen I need to plot only a part of the window (for instance the last segment will be 5s but the window size is 10s)

I give the entire script right below

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np


class Viewer(QMainWindow):
    def __init__(self, parent=None):
        super(Viewer, self).__init__()
        self.parent = parent
        #######################################
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.mainVBOX_param_scene = QVBoxLayout()
        self.mascene = plot(self)


        self.paramPlotV = QVBoxLayout()
        self.horizontalSliders  = QScrollBar(Qt.Horizontal)
        self.horizontalSliders.setFocusPolicy(Qt.StrongFocus)
        self.horizontalSliders.valueChanged.connect(self.update_plot)
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(1)


        self.paramPlot = QHBoxLayout()
        l_gain = QLabel('Gain')
        self.e_gain = QLineEdit('5')
        l_win = QLabel('Window')
        self.e_win = QLineEdit('10')
        l_spacing = QLabel('vertical spacing')
        self.e_spacing = QLineEdit('10')
        l_linewidth = QLabel('linewidth')
        self.e_linewidth = QLineEdit('1')

        self.e_gain.returnPressed.connect(self.update_plot)
        self.e_win.returnPressed.connect(self.udpate_plot_plus_slider)
        self.e_spacing.returnPressed.connect(self.update_plot)
        self.e_linewidth.returnPressed.connect(self.update_plot)

        self.paramPlot.addWidget(l_gain)
        self.paramPlot.addWidget(self.e_gain)
        self.paramPlot.addWidget(l_win)
        self.paramPlot.addWidget(self.e_win)
        self.paramPlot.addWidget(l_spacing)
        self.paramPlot.addWidget(self.e_spacing)
        self.paramPlot.addWidget(l_linewidth)
        self.paramPlot.addWidget(self.e_linewidth)

        self.paramPlotV.addWidget(self.horizontalSliders)
        self.paramPlotV.addLayout(self.paramPlot)

        self.mainVBOX_param_scene.addWidget(self.mascene)
        self.mainVBOX_param_scene.addLayout(self.paramPlotV)

        self.centralWidget.setLayout(self.mainVBOX_param_scene)

        self.Fs = 1024
        self.Sigs_dict = np.random.rand(250,105*self.Fs)
        self.t = np.arange(self.Sigs_dict.shape[1])/self.Fs
        self.parent.processEvents()
        self.update()

    def updateslider(self):
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(np.ceil(self.t[-1]/int(self.e_win.text()))-1)
        self.horizontalSliders.setPageStep(1)
        self.horizontalSliders.update()

    def udpate_plot_plus_slider(self):
        self.updateslider()
        self.mascene.update()

    def update_plot(self):
        self.mascene.update()

    def update(self):
        self.updateslider()
        self.mascene.modify_sigs()
        self.mascene.update()

class plot(QGraphicsView):
    def __init__(self, parent=None):
        super(plot, self).__init__(parent)
        self.parent = parent
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.figure = plt.figure(facecolor='white')#Figure()
        self.canvas = FigureCanvas(self.figure)

        self.widget = QWidget()
        self.widget.setLayout(QVBoxLayout())
        self.widget.layout().setContentsMargins(0, 0, 0, 0)
        self.widget.layout().setSpacing(0)
        self.scroll = QScrollArea(self.widget)
        self.scroll.setWidget(self.canvas)

        layout = QVBoxLayout()
        layout.addWidget(self.scroll)
        self.setLayout(layout)

    def modify_sigs(self):
        self.Sigs_dict = self.parent.Sigs_dict
        self.t = self.parent.t
        self.Fs= self.parent.Fs


    def update(self):
        win_num = self.parent.horizontalSliders.value()
        self.figure.clear()
        plt.figure(self.figure.number)
        plt.subplots_adjust(left=0.1, bottom=0.01, right=1, top=1, wspace=0.0 , hspace=0.0 )
        self.axes = plt.subplot(1, 1, 1)
        gain = float(self.parent.e_gain.text())
        win= float(self.parent.e_win.text())
        self.spacing = float(self.parent.e_spacing.text())
        linewidth = float(self.parent.e_linewidth.text())
        ts = int(win*(win_num) * self.Fs)
        te = ts + int(win * self.Fs)
        if te > len(self.t):
            te=len(self.t)
        for i in range(self.Sigs_dict.shape[0]):
            line, = plt.plot(self.t[ts:te], gain*(self.Sigs_dict[i,ts:te]-np.mean(self.Sigs_dict[i,ts:te]))+i*self.spacing, linewidth=linewidth  )


        self.axes.autoscale(enable=True, axis='both', tight=True)
        self.axes.set_ylim((-self.spacing,(self.Sigs_dict.shape[0]+1)*self.spacing))
        self.axes.set_xlim((ts/ self.Fs, ts / self.Fs + win ))

        self.axes.set_yticks(np.arange(self.Sigs_dict.shape[0]) * self.spacing)
        self.axes.set_yticklabels([str(n) for n in np.arange(self.Sigs_dict.shape[0])])



        self.canvas.setGeometry(0, 0, self.parent.width()-100, (self.parent.height()-100)*self.spacing)
        self.canvas.draw_idle()


def main():
    app = QApplication(sys.argv)
    app.setStyle('Windows')
    ex = Viewer(app)
    ex.showMaximized()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()

Update

I made a new implementation where I try to update data instead of reploting all the figure each time (update_set_data function), I don't plot all the point of the curve (for instance if the number of point > 10000 points, I take only 50% of them) I used decimate = len(self.t[ts:te]) // 10000 + 1 to compute the decimation, and last I don't replot the figure when the user is draging the slider.

When I use the old version I get thos time to update the figure:

time old: 4.148899078369141
time old: 4.117990255355835
time old: 4.152893781661987

With the new version I get:

time new: 2.0400094985961914
time new: 2.0248610973358154
time new: 2.0305933952331543

I have to say, I expected more than a 50% time reduction. Does someone have idea to optimize this more?

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
import time

class Viewer(QMainWindow):
    def __init__(self, parent=None):
        super(Viewer, self).__init__()
        self.parent = parent
        #######################################
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.mainVBOX_param_scene = QVBoxLayout()
        self.mascene = plot(self)


        self.paramPlotV = QVBoxLayout()
        self.horizontalSliders  = QScrollBar(Qt.Horizontal)
        self.horizontalSliders.setFocusPolicy(Qt.StrongFocus)
        self.horizontalSliders.valueChanged.connect(self.sliderReleasedfun)
        self.horizontalSliders.sliderPressed.connect(self.sliderPressedfun)
        self.horizontalSliders.sliderMoved.connect(self.sliderMovedfun)
        self.horizontalSliders.sliderReleased.connect(self.sliderReleasedfun)
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(1)


        self.paramPlot = QHBoxLayout()
        l_gain = QLabel('Gain')
        self.e_gain = QLineEdit('5')
        l_win = QLabel('Window')
        self.e_win = QLineEdit('10')
        l_spacing = QLabel('vertical spacing')
        self.e_spacing = QLineEdit('10')
        l_linewidth = QLabel('linewidth')
        self.e_linewidth = QLineEdit('1')

        self.e_gain.returnPressed.connect(self.update_plot)
        self.e_win.returnPressed.connect(self.udpate_plot_plus_slider)
        self.e_spacing.returnPressed.connect(self.update_plot)
        self.e_linewidth.returnPressed.connect(self.update_plot)

        self.paramPlot.addWidget(l_gain)
        self.paramPlot.addWidget(self.e_gain)
        self.paramPlot.addWidget(l_win)
        self.paramPlot.addWidget(self.e_win)
        self.paramPlot.addWidget(l_spacing)
        self.paramPlot.addWidget(self.e_spacing)
        self.paramPlot.addWidget(l_linewidth)
        self.paramPlot.addWidget(self.e_linewidth)

        self.paramPlotV.addWidget(self.horizontalSliders)
        self.paramPlotV.addLayout(self.paramPlot)

        self.mainVBOX_param_scene.addWidget(self.mascene)
        self.mainVBOX_param_scene.addLayout(self.paramPlotV)

        self.centralWidget.setLayout(self.mainVBOX_param_scene)

        self.Fs = 1024
        self.Sigs_dict = np.random.rand(250,105*self.Fs)
        self.t = np.arange(self.Sigs_dict.shape[1])/self.Fs
        self.parent.processEvents()
        self.update()

    def sliderPressedfun(self):
        self.horizontalSliders.valueChanged.disconnect()

    def sliderMovedfun(self,e):
        self.horizontalSliders.setValue(e)

    def sliderReleasedfun(self):
        self.horizontalSliders.valueChanged.connect(self.movesliderfun)
        self.movesliderfun()

    def movesliderfun(self):
        t0 = time.time()

        self.horizontalSliders.setEnabled(False)
        self.update_data()
        self.horizontalSliders.setEnabled(True)
        print('time new:', time.time()-t0)

    def updateslider(self):
        self.horizontalSliders.setMinimum(0)
        self.horizontalSliders.setMaximum(np.ceil(self.t[-1]/int(self.e_win.text()))-1)
        self.horizontalSliders.setPageStep(1)
        self.horizontalSliders.update()

    def udpate_plot_plus_slider(self):
        self.updateslider()
        self.mascene.update()

    def update_plot(self):
        self.mascene.update()

    def update_data(self):
        self.mascene.update_set_data()

    def update(self):
        self.updateslider()
        self.mascene.modify_sigs()
        self.mascene.update()

class plot(QGraphicsView):
    def __init__(self, parent=None):
        super(plot, self).__init__(parent)
        self.parent = parent
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.figure = plt.figure(facecolor='white')#Figure()
        self.canvas = FigureCanvas(self.figure)

        self.widget = QWidget()
        self.widget.setLayout(QVBoxLayout())
        self.widget.layout().setContentsMargins(0, 0, 0, 0)
        self.widget.layout().setSpacing(0)
        self.scroll = QScrollArea(self.widget)
        self.scroll.setWidget(self.canvas)

        layout = QVBoxLayout()
        layout.addWidget(self.scroll)
        self.setLayout(layout)
        self.win=10

    def modify_sigs(self):
        self.Sigs_dict = self.parent.Sigs_dict
        self.t = self.parent.t
        self.Fs= self.parent.Fs

    def update_set_data(self):
        win_num = self.parent.horizontalSliders.value()
        gain = float(self.parent.e_gain.text())
        win= float(self.parent.e_win.text())
        if not self.spacing == float(self.parent.e_spacing.text()):
            self.spacing = float(self.parent.e_spacing.text())
            spacing = True
        else:
            spacing = False
        self.linewidth = float(self.parent.e_linewidth.text())

        ts = int(self.win * (win_num) * self.Fs)
        te = ts + int(self.win * self.Fs)
        if te > len(self.t):
            diff = te - len(self.t)
            ts = ts - diff
            te = len(self.t)

        decimate = len(self.t[ts:te]) // 10000 + 1

        for i in range(self.Sigs_dict.shape[0]):
            self.Lines[i].set_data(self.t[ts:te:decimate], gain*(self.Sigs_dict[i,ts:te:decimate]-np.mean(self.Sigs_dict[i,ts:te:decimate]))+i*self.spacing )
            self.Lines[i].set_linewidth(self.linewidth)

        if spacing:
            self.axes.set_ylim((-self.spacing,(self.Sigs_dict.shape[0]+1)*self.spacing))
            self.axes.set_yticks(np.arange(self.Sigs_dict.shape[0]) * self.spacing)
            self.axes.set_yticklabels([str(n) for n in np.arange(self.Sigs_dict.shape[0])])



        self.axes.set_xlim((ts/ self.Fs, ts / self.Fs + win ))
        # self.canvas.draw_idle()
        self.canvas.draw()

    def update(self):
        win_num = self.parent.horizontalSliders.value()
        self.figure.clear()
        plt.figure(self.figure.number)
        plt.subplots_adjust(left=0.1, bottom=0.01, right=1, top=1, wspace=0.0 , hspace=0.0 )
        self.axes = plt.subplot(1, 1, 1)
        gain = float(self.parent.e_gain.text())
        win= float(self.parent.e_win.text())
        self.spacing = float(self.parent.e_spacing.text())
        linewidth = float(self.parent.e_linewidth.text())
        ts = int(self.win * (win_num) * self.Fs)
        te = ts + int(self.win * self.Fs)
        if te > len(self.t):
            diff = te - len(self.t)
            ts = ts - diff
            te = len(self.t)
        decimate = len(self.t[ts:te]) // 10000 + 1
        self.Lines = []
        for i in range(self.Sigs_dict.shape[0]):
            line, = plt.plot(self.t[ts:te:decimate], gain*(self.Sigs_dict[i,ts:te:decimate]-np.mean(self.Sigs_dict[i,ts:te:decimate]))+i*self.spacing, linewidth=linewidth  )
            self.Lines.append(line)

        self.axes.autoscale(enable=True, axis='both', tight=True)
        self.axes.set_ylim((-self.spacing,(self.Sigs_dict.shape[0]+1)*self.spacing))
        self.axes.set_xlim((ts/ self.Fs, ts / self.Fs + win ))

        self.axes.set_yticks(np.arange(self.Sigs_dict.shape[0]) * self.spacing)
        self.axes.set_yticklabels([str(n) for n in np.arange(self.Sigs_dict.shape[0])])



        self.canvas.setGeometry(0, 0, self.parent.width()-100, (self.parent.height()-100)*self.spacing)
        self.canvas.draw_idle()


def main():
    app = QApplication(sys.argv)
    app.setStyle('Windows')
    ex = Viewer(app)
    ex.showMaximized()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()
like image 398
ymmx Avatar asked Nov 06 '22 13:11

ymmx


1 Answers

You can try this with multi threading, so that you can break the whole code in many sub code and your all code will run in same time with multi threading

like image 95
Nj Nafir Avatar answered Nov 14 '22 08:11

Nj Nafir