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:
CameraThreadis instantiated within the GUI and is run by a
CameraThreadinstantiates a class
IngestManagerand gives it to the data source. The data source will call
write()method repeatedly, providing data.
IngestManageris to redirect the incoming data to waiting threads. This is the basis for this approach.
IngestManagervia a callback method
IngestManageremits the signal. This is where it crashes sometimes.
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)
Firstly, it’s not safe to use
QPixmap outside the main thread. So you should use
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.