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.