PyQt5: Python crashes with SIGSEGV *sometimes* when sending pixmap via a signal from another thread

Tags: , , , ,



Background and Issue

I am trying to process streaming data from a camera. Python keeps crashing with this message though:

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

The crash happens sometimes while emitting the signal containing an image.

My code, shown below, follows this process:

  1. A QObject named CameraThread is instantiated within the GUI and is run by a QThread.
  2. CameraThread instantiates a class IngestManager and gives it to the data source. The data source will call IngestManager‘s write() method repeatedly, providing data.
  3. The worker threads process the data and sends it back to IngestManager via a callback method frame_callback
  4. IngestManager emits the signal. This is where it crashes sometimes.

What I’ve tried / Observations

I tried several ways to fix it, including passing the pyqtSignal to the worker threads themselves. I also reckon that sometimes, the threads finish and emit at the same time, but I’m not sure how to tackle this.

The crash happens much sooner the more I interact with the GUI, such as rapidly pressing a dummy button. It almost never happens if I don’t interact with the UI (but it still happens). I think I may need lock of some sorts.

How do I start solving this problem?


This is the code that connects to the data source, processes the data, and emits the signal.

import io
import threading

from PIL import Image
from PIL.ImageQt import ImageQt
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QPixmap


class CameraThread(QObject):
    signal_new_frame = pyqtSignal(QPixmap)

    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    @pyqtSlot()
    def run(self):
        with DataSource() as source:
            output = IngestManager(frame_signal=self.signal_new_frame)

            # The source continuously calls IngestManager.write() with new data
            source.set_ingest(output)


class IngestManager(object):
    """Manages incoming data stream from camera."""

    def __init__(self, frame_signal):
        self.frame_signal = frame_signal

        # Construct a pool of 4 image processors along with a lock to control access between threads
        self.lock = threading.Lock()
        self.pool = [ImageProcessor(self) for _ in range(4)]
        self.processor = None  # First "frame" intentionally dropped, else thread would see garbled data at start

    def write(self, buf):
        if buf.startswith(b'xffxd8'):  # Frame detected
            if self.processor:
                self.processor.event.set()  # Let waiting processor thread know a frame is here

            with self.lock:
                if self.pool:
                    self.processor = self.pool.pop()
                else:
                    # All threads popped and busy. No choice but to skip frame.
                    self.processor = None

        if self.processor:
            self.processor.stream.write(buf)  # Feed frame data to current processor

    def frame_callback(self, image):
        print('Frame processed. Emitting.')
        self.frame_signal.emit(image)


class ImageProcessor(threading.Thread):
    def __init__(self, owner: IngestManager):
        super(ImageProcessor, self).__init__()

        # Data Stuff
        self.stream = io.BytesIO()

        # Thread stuff
        self.event = threading.Event()
        self.owner = owner
        self.start()

    def run(self):
        while True:
            if self.event.wait(1):
                pil_image = Image.open(self.stream)
        
                # Image is processed here, then sent back
                # ...
                # ...
        
                q_image = ImageQt(pil_image)
                q_pixmap = QPixmap.fromImage(q_image)
        
                self.owner.frame_callback(q_pixmap)

                # Reset the stream and event
                self.stream.seek(0)
                self.stream.truncate()
                self.event.clear()
                
                # Return to available pool
                with self.owner.lock:
                    self.owner.pool.append(self)

And this is how the code above is used:

from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread
from Somewhere import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)
        self.showFullScreen()

        # Task and thread instantiated. Task is then assigned to thread.
        self._thread = QThread()
        self._cam_prev = CameraThread()
        self._cam_prev.moveToThread(self._thread)

        # Signals are connected.
        self._cam_prev.signal_new_frame.connect(self.update_image)  # UI signal to show image on QLabel
        self._thread.started.connect(self._cam_prev.run)  # Trigger task's run() when thread is ready
        self._thread.start()

    @pyqtSlot(QPixmap)
    def update_image(self, q_pixmap: QPixmap):
        self.q_cam_preview.setPixmap(q_pixmap)

Answer

Firstly, it’s not safe to use QPixmap outside the main thread. So you should use QImage instead.

Secondly, ImageQt shares the buffer from the Image passed to it. So if the buffer is deleted while the Qt image is still alive, a crash will very likely ensue. You might need to copy the Qt image to prevent this happening if you can’t keep hold of the PIL image for long enough.



Source: stackoverflow