Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing pyqtgraph multiprocessing into a pyqt widget

I am trying to plot images on a GUI that I am designing in Python. The full program will be collecting image data from a camera and then displaying the images on the GUI. I have explored using matplotlib, but it was too slow for my application. I need the plot to update rather quickly (preferably as fast as I can acquisition from the camera, once ever half second or second). This is challenging with pyqt because it seems that even when using a QThread, the plot can only update on the main thread, which causes my program to stop and the GUI to be inaccessible to the user.

I read that pyqtgraph is suppose to be a much faster plotting library than matplotlib. So I gave it a try and like it, but it seems to have the same problem as matplotlib when displaying an image. It stops the entire GUI. I did some research and came across this question: Painting without paintEvent and one of the answers proposed the use of QtProcess(). Therefore, my question is whether it is possible (and if so, can you provide some example code) to implement QtProcess() into a GUI, i.e. how can I make a plot on a GUI run in a separate process? (Also, if there is a way to do this with matplotlib, that would be extremely helpful.)

Here is the simple example I designed to test out my problem. I took an example script from pyqtgraph's examples and imported a pyqtgraph plot into a pyqt widget. Then when the pushButton is pressed, the plot it displayed. If you run the script, you will notice that the first plot will take a long time to load (6 or 7 seconds). Press the button again and it seems to load much faster. I am doing all of the data generation in a QThread(), but the plotting seems to require the main thread to work even though it is done in the QThread. I want to do exactly the same thing as in this example, except use QtProcess() to handle the plotting.

Any other suggestions about plotting or possible alternatives are welcome. Thanks

GUI script:

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'GUI.ui'
#
# Created: Fri Jun 28 14:40:22 2013
#      by: PyQt4 UI code generator 4.9.5
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName(_fromUtf8("MainWindow"))
        MainWindow.resize(800, 534)
        self.centralwidget = QtGui.QWidget(MainWindow)
        self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
        self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget)
        self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
        self.gridLayout = QtGui.QGridLayout()
        self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
        self.scrollArea = QtGui.QScrollArea(self.centralwidget)
        self.scrollArea.setFrameShape(QtGui.QFrame.NoFrame)
        self.scrollArea.setFrameShadow(QtGui.QFrame.Plain)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName(_fromUtf8("scrollArea"))
        self.scrollAreaWidgetContents = QtGui.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 780, 514))
        self.scrollAreaWidgetContents.setObjectName(_fromUtf8("scrollAreaWidgetContents"))
        self.widgetPlot = QtGui.QWidget(self.scrollAreaWidgetContents)
        self.widgetPlot.setGeometry(QtCore.QRect(20, 10, 741, 451))
        self.widgetPlot.setObjectName(_fromUtf8("widgetPlot"))
        self.pushButtonPlot = QtGui.QPushButton(self.scrollAreaWidgetContents)
        self.pushButtonPlot.setGeometry(QtCore.QRect(340, 480, 75, 23))
        self.pushButtonPlot.setObjectName(_fromUtf8("pushButtonPlot"))
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.gridLayout.addWidget(self.scrollArea, 1, 0, 1, 1)
        self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8))
        self.pushButtonPlot.setText(QtGui.QApplication.translate("MainWindow", "Plot", None, QtGui.QApplication.UnicodeUTF8))

The wrapper:

import numpy as np
import scipy

from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg

import sys

from PyQt4.QtCore import * 
from PyQt4.QtGui import *

from GUI import Ui_MainWindow


class MyForm(QMainWindow):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self.imv = pg.ImageView()

        vbox = QVBoxLayout()
        vbox.addWidget(self.imv)
        self.ui.widgetPlot.setLayout(vbox)

        self.plot_thread = plotData()

        self.connect(self.ui.pushButtonPlot, SIGNAL("clicked()"), lambda : self.plot_thread.input(self.imv))

    def threadPlot(self):
        self.plot_thread.input(self.imv)


    def plot(self):
        ## Create random 3D data set with noisy signals
        self.img = scipy.ndimage.gaussian_filter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100
        self.img = self.img[np.newaxis,:,:]
        decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis]
        data = np.random.normal(size=(100, 200, 200))
        data += self.img * decay
        data += 2

        ## Add time-varying signal
        sig = np.zeros(data.shape[0])
        sig[30:] += np.exp(-np.linspace(1,10, 70))
        sig[40:] += np.exp(-np.linspace(1,10, 60))
        sig[70:] += np.exp(-np.linspace(1,10, 30))

        sig = sig[:,np.newaxis,np.newaxis] * 3
        data[:,50:60,50:60] += sig

        self.imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0]))


class plotData(QThread):
    def __init__(self,parent=None):
        QThread.__init__(self,parent)
        self.exiting = False
    def input(self, imv):
        self.imv = imv
        self.start()

    def collectImage(self):
        ## Create random 3D data set with noisy signals
        self.img = scipy.ndimage.gaussian_filter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100
        self.img = self.img[np.newaxis,:,:]
        decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis]
        data = np.random.normal(size=(100, 200, 200))
        data += self.img * decay
        data += 2

        ## Add time-varying signal
        sig = np.zeros(data.shape[0])
        sig[30:] += np.exp(-np.linspace(1,10, 70))
        sig[40:] += np.exp(-np.linspace(1,10, 60))
        sig[70:] += np.exp(-np.linspace(1,10, 30))

        sig = sig[:,np.newaxis,np.newaxis] * 3
        data[:,50:60,50:60] += sig

        self.imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0]))

    def run(self):

        self.collectImage()
        self.emit(SIGNAL("Done"))


app = QApplication(sys.argv)
myapp = MyForm()

myapp.show()
sys.exit(app.exec_())

My attempt at trying to use QtProcess() (unsuccessful, I do not know how to import a QtProcess() widget into a Qt GUI):

import numpy as np
import scipy

from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg

import sys

from PyQt4.QtCore import * 
from PyQt4.QtGui import *

from GUI import Ui_MainWindow


class MyForm(QMainWindow):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        self.proc = mp.QtProcess()

        self.remotepg = self.proc._import('pyqtgraph')
        self.win = self.remotepg.plot()

        self.imv = self.win.plot([1,4,2,3], [4,6,3,4], pen=None, symbol='o')

        vbox = QVBoxLayout()
        vbox.addWidget(self.imv)
        self.ui.widgetPlot.setLayout(vbox)

app = QApplication(sys.argv)
myapp = MyForm()

myapp.show()
sys.exit(app.exec_())
like image 448
Mink Avatar asked Dec 21 '22 03:12

Mink


1 Answers

There are three general approaches to keeping your GUI responsive while doing work in the background:

  1. Timers -- Everything operates within the GUI thread and your work is done by having a timer call some function in between GUI updates. This is the simplest and most common approach and is very likely to be adequate (if written correctly) using either pyqtgraph or matplotlib. The drawback of this approach is that if the work function takes a long time, the GUI will become unresponsive. Pulling data from a camera and displaying it should not cause you problems, though.
  2. Threads -- In the case that your work function becomes too long, you can move some of the work out to a separate thread. Importantly, you can NOT make changes to the GUI from a different thread. Do as much work as possible, then send the results to the GUI thread to be displayed.
  3. Processes -- This is more or less the same approach as using threads, but has the advantage that you may get better CPU utilization (read about the global interpreter lock in python), and the disadvantage that communication between processes can be more cumbersome than between threads. Pyqtgraph has built-in multiprocessing features to make this easier (see pyqtgraph/examples/RemoteSpeedTest.py)

This topic is discussed extensively elsewhere, so I'll leave it at that.

With that in mind, there are a few problems with the code you have posted:

  1. You are generating and displaying a 3D data set (100-frame video) with every frame update. This is the reason it appears to update so slowly. See pyqtgraph/examples/ImageItem.py and VideoSpeedTest.py for examples of video done correctly.
  2. You are calling setImage from the worker thread; this may work sometimes, but expect it to crash. A better approach is to generate the data in the worker, then send the data to the main GUI thread to be displayed. The Qt documentation discusses threading in depth. (but as I mentioned before, avoiding threads altogether would be even better)

Finally, one important rule to follow: If you are having performance issues, profile your code. You cannot fix the problem until you know what is causing it.

like image 150
Luke Avatar answered Mar 03 '23 11:03

Luke