PyQt5:Python 当通过来自另一个线程的信号发送像素图时,SIGSEGV *有时* 崩溃
PyQt5: Python crashes with SIGSEGV *sometimes* when sending pixmap via a signal from another thread
背景和问题
我正在尝试处理来自相机的流数据。 Python 尽管此消息不断崩溃:
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
当发射包含图像的信号时,有时 会发生崩溃。
我的代码(如下所示)遵循以下过程:
- 一个名为
CameraThread
的 QObject
在 GUI 中被实例化并且被 QThread
. 运行
CameraThread
实例化一个class IngestManager
给数据源。数据源会反复调用IngestManager
的write()
方法,提供数据。
IngestManager
的作用是将传入的数据重定向到等待线程。 This is the basis for this approach.
- 工作线程处理数据并通过回调方法将其发送回
IngestManager
frame_callback
IngestManager
发出信号。这是它崩溃的地方有时。
我的尝试/观察结果
我尝试了几种方法来修复它,包括将 pyqtSignal
传递给工作线程本身。我还认为有时线程会同时完成和发出,但我不确定如何解决这个问题。
我与 GUI 交互越多,例如快速按下虚拟按钮,崩溃发生得越快。如果我不与 UI 交互,它几乎永远不会发生(但它仍然会发生)。我想我可能需要某种锁。
如何开始解决这个问题?
这是连接到数据源、处理数据和发出信号的代码。
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'\xff\xd8'): # 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)
上面的代码是这样使用的:
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)
首先,在主线程外使用QPixmap
是不安全的。所以你应该改用QImage
。
其次,ImageQt
共享传递给它的 Image
的缓冲区。因此,如果在 Qt 图像仍然存在时删除缓冲区,很可能会发生崩溃。如果您不能长时间保留 PIL 图像,您可能需要 copy the Qt image 来防止这种情况发生。
背景和问题
我正在尝试处理来自相机的流数据。 Python 尽管此消息不断崩溃:
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
当发射包含图像的信号时,有时 会发生崩溃。
我的代码(如下所示)遵循以下过程:
- 一个名为
CameraThread
的QObject
在 GUI 中被实例化并且被QThread
. 运行
CameraThread
实例化一个classIngestManager
给数据源。数据源会反复调用IngestManager
的write()
方法,提供数据。IngestManager
的作用是将传入的数据重定向到等待线程。 This is the basis for this approach.
- 工作线程处理数据并通过回调方法将其发送回
IngestManager
frame_callback
IngestManager
发出信号。这是它崩溃的地方有时。
我的尝试/观察结果
我尝试了几种方法来修复它,包括将 pyqtSignal
传递给工作线程本身。我还认为有时线程会同时完成和发出,但我不确定如何解决这个问题。
我与 GUI 交互越多,例如快速按下虚拟按钮,崩溃发生得越快。如果我不与 UI 交互,它几乎永远不会发生(但它仍然会发生)。我想我可能需要某种锁。
如何开始解决这个问题?
这是连接到数据源、处理数据和发出信号的代码。
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'\xff\xd8'): # 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)
上面的代码是这样使用的:
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)
首先,在主线程外使用QPixmap
是不安全的。所以你应该改用QImage
。
其次,ImageQt
共享传递给它的 Image
的缓冲区。因此,如果在 Qt 图像仍然存在时删除缓冲区,很可能会发生崩溃。如果您不能长时间保留 PIL 图像,您可能需要 copy the Qt image 来防止这种情况发生。