简体   繁体   中英

Python - PyQt: Memory issue with QThread

I want to build a Qt interface to control a camera acquisition.

What I want: Before going into the hardware communication, I am testing a GUI which control a "fake camera", a continuous loop which, if started, gives a random image every 100 ms. The image acquisition is running in a separate thread so that the user could interact with the GUI. The user can start and stop the acquisition via a button.

How I want to do it: My first attempt was to simply istanziate a QThread and call the run() method which would then contain an infinite loop with single image acquisitions interleaved by a QThread.sleep(0.1) . I noticed that after stopping and restarting the thread, the program was starting to lag and crashed after some time. By reading some posts around and the main Qt webpage , I then learned that the preferable way to do what I want is to:

subclass a QObject to create a worker. Instantiate this worker object and a QThread . Move the worker to the new thread.

Moreover, following the idea in this post, I added a QTimer object to iterate indefinitely the worker inside the thread, and I implement an active flag that just makes the thread run without doing anything if it's set to False . This solution seemed to work at the beginning. I can start, stop and restart the acquisition as many times as I want.

Problems:

1) The CPU is always taking quite some resources (about 30% in my case, according to windows task menager) when the camera is not acquiring.

2) Sometimes, after acquisition is started, the memory start to be filled, like if every new image is allocated in new memory (while it is suppose to be overwritten I guess), till the program becomes irresponsive and then crashes. The following image is what I see in task menager when this happens: 在此处输入图片说明 Red arrows correspond to the time the acquisition start.

Where am I doing wrong? Is it the right way to procede?

The code

from PyQt5 import QtCore, QtWidgets
import sys
import numpy as np
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg


class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('MyWindow')
        self._main = QtWidgets.QWidget()
        self.setCentralWidget(self._main) 

        # generate matplotlib canvas
        self.fig = matplotlib.figure.Figure(figsize=(4,4))
        self.canvas = FigureCanvasQTAgg(self.fig)
        self.ax = self.fig.add_subplot(1,1,1)
        self.im = self.ax.imshow(np.zeros((1000, 1000)), cmap='viridis')
        self.im.set_clim(vmin=0,vmax=1) 
        self.canvas.draw()

        # Add widgets and build layout
        self.startcambutton = QtWidgets.QPushButton('Start', checkable=True)
        self.startcambutton.released.connect(self.acquire)
        self.contincheck = QtWidgets.QCheckBox("Continuous")
        self.contincheck.clicked.connect(self.continuous_acquisition)
        self.contincheck.setChecked(True)
        layout = QtWidgets.QGridLayout(self._main)
        layout.addWidget(self.canvas, 0, 0)
        layout.addWidget(self.startcambutton, 1, 0)
        layout.addWidget(self.contincheck, 2, 0)

        # Initialize worker and timer and moveToThread
        self.fake_camera_thread = QtCore.QThread()
        self.fake_camera_timer = QtCore.QTimer()
        self.fake_camera_timer.setInterval(0)
        self.fake_camera_worker = FakeCamera(self)
        self.fake_camera_worker.moveToThread(self.fake_camera_thread)
        self.fake_camera_timer.timeout.connect(self.fake_camera_worker.acquire)
        self.fake_camera_thread.started.connect(self.fake_camera_timer.start)
        self.fake_camera_thread.finished.connect(self.fake_camera_worker.deleteLater)
        self.fake_camera_thread.finished.connect(self.fake_camera_timer.deleteLater)
        self.fake_camera_thread.finished.connect(self.fake_camera_thread.deleteLater)
        self.fake_camera_thread.start()

        self.camera_thread = self.fake_camera_thread
        self.camera = self.fake_camera_worker
        self.camera.image.connect(self.image_acquired)

    def continuous_acquisition(self):
        if self.contincheck.isChecked(): self.startcambutton.setCheckable(True)
        else: self.startcambutton.setCheckable(False)

    def acquire(self):
        if self.startcambutton.isCheckable() and not self.startcambutton.isChecked():
            self.startcambutton.setText('Start')
            self.contincheck.setEnabled(True)
        elif self.startcambutton.isCheckable() and self.startcambutton.isChecked():
            self.startcambutton.setText('Stop')
            self.contincheck.setDisabled(True)
        self.camera.toggle()

    @QtCore.pyqtSlot(object)
    def image_acquired(self, image):
        self.im.set_data(image)
        self.canvas.draw()


    def closeEvent(self, event):
        """ If window is closed """
        self.closeApp()
        event.accept() # let the window close

    def closeApp(self):
        """ close program """
        self.camera_thread.quit()
        self.camera_thread.wait()
        self.close()
        return



class FakeCamera(QtCore.QObject):
    image = QtCore.pyqtSignal(object)

    def __init__(self, parent):
        QtCore.QObject.__init__(self)
        self.parent = parent
        self.active = False

    def toggle(self):
        self.active = not self.active

    def acquire(self):
        """ this is the method running indefinitly in the associated thread """
        if self.active:
            self.new_acquisition()

    def new_acquisition(self):
        noise = np.random.normal(0, 1, (1000, 1000))
        self.image.emit(noise)
        if not self.parent.startcambutton.isChecked():
            self.active = False
        QtCore.QThread.sleep(0.1)



if __name__ == '__main__':
    app = QtCore.QCoreApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    mainGui = MyWindow()
    mainGui.show()
    app.aboutToQuit.connect(app.deleteLater)
    app.exec_()

QThread.sleep() only accepts whole arguments, when passing a floating it will round it and in your case 0.1 that will be rounded to 0 so there is no pause, so the signal will be continuously issued but the painting takes a while so the data it will be stored in a queue for it increases the memory. On the other hand if a QTimer is going to call a task continuously it is better to live in the thread of the object that handles the task so that it is enough that the QTimer is the son of FakeCamera. Another improvement is the use of the decorator @QtCore.pyqtSlot() since the connection is given in C++ making it more efficient. And finally I have improved the design since FakeCamera should not interact directly with the GUI because if you want to use it with another GUI you will have to modify a lot of code, instead it is better to create slots.

from PyQt5 import QtCore, QtWidgets
import numpy as np
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg


class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('MyWindow')
        self._main = QtWidgets.QWidget()
        self.setCentralWidget(self._main) 

        # generate matplotlib canvas
        self.fig = matplotlib.figure.Figure(figsize=(4,4))
        self.canvas = FigureCanvasQTAgg(self.fig)
        self.ax = self.fig.add_subplot(1,1,1)
        self.im = self.ax.imshow(np.zeros((1000, 1000)), cmap='viridis')
        self.im.set_clim(vmin=0,vmax=1) 
        self.canvas.draw()

        # Add widgets and build layout
        self.startcambutton = QtWidgets.QPushButton('Start', checkable=True)
        self.startcambutton.released.connect(self.acquire)
        self.contincheck = QtWidgets.QCheckBox("Continuous")
        self.contincheck.toggled.connect(self.startcambutton.setCheckable)
        self.contincheck.setChecked(True)
        layout = QtWidgets.QGridLayout(self._main)
        layout.addWidget(self.canvas, 0, 0)
        layout.addWidget(self.startcambutton, 1, 0)
        layout.addWidget(self.contincheck, 2, 0)

        # Initialize worker and timer and moveToThread
        fake_camera_thread = QtCore.QThread(self)
        self.fake_camera_worker = FakeCamera()
        self.fake_camera_worker.moveToThread(fake_camera_thread)
        self.startcambutton.toggled.connect(self.fake_camera_worker.setState)
        self.fake_camera_worker.image.connect(self.image_acquired)
        fake_camera_thread.started.connect(self.fake_camera_worker.start)
        fake_camera_thread.finished.connect(self.fake_camera_worker.deleteLater)
        fake_camera_thread.finished.connect(fake_camera_thread.deleteLater)
        fake_camera_thread.start()

    @QtCore.pyqtSlot()
    def acquire(self):
        if self.startcambutton.isCheckable():
            text = "Stop" if self.startcambutton.isChecked() else "Start"
            self.startcambutton.setText(text)
            self.contincheck.setEnabled(not self.startcambutton.isChecked())

    @QtCore.pyqtSlot(object)
    def image_acquired(self, image):
        self.im.set_data(image)
        self.canvas.draw()


class FakeCamera(QtCore.QObject):
    image = QtCore.pyqtSignal(object)

    def __init__(self, parent=None):
        super(FakeCamera, self).__init__(parent)
        self.active = False
        self.fake_camera_timer = QtCore.QTimer(self, interval=0)
        self.fake_camera_timer.timeout.connect(self.acquire)

    @QtCore.pyqtSlot()
    def start(self):
        self.fake_camera_timer.start()

    @QtCore.pyqtSlot(bool)
    def setState(self, state):
        self.active = state

    @QtCore.pyqtSlot()
    def toggle(self):
        self.active = not self.active

    @QtCore.pyqtSlot()
    def acquire(self):
        """ this is the method running indefinitly in the associated thread """
        if self.active:
            self.new_acquisition()
        QtCore.QThread.msleep(100)

    def new_acquisition(self):
        noise = np.random.normal(0, 1, (1000, 1000))
        self.image.emit(noise)


if __name__ == '__main__':
    import sys
    app = QtCore.QCoreApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    mainGui = MyWindow()
    mainGui.show()
    app.aboutToQuit.connect(app.deleteLater)
    sys.exit(app.exec_())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM