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:
- A
QObject
namedCameraThread
is instantiated within the GUI and is run by aQThread
. CameraThread
instantiates a classIngestManager
and gives it to the data source. The data source will callIngestManager
‘swrite()
method repeatedly, providing data.- The role of
IngestManager
is to redirect the incoming data to waiting threads. This is the basis for this approach.
- The role of
- The worker threads process the data and sends it back to
IngestManager
via a callback methodframe_callback
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)
Advertisement
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.