pyqtgraph ImageView 在多线程时冻结

Posted

技术标签:

【中文标题】pyqtgraph ImageView 在多线程时冻结【英文标题】:pyqtgraph ImageView Freezes when multithreaded 【发布时间】:2019-11-22 13:21:27 【问题描述】:

我有多个通过 wifi 无线连接的摄像头,我正在尝试将数据流式传输到客户端,客户端会在 GUI 上显示流。

我的问题是 pyqtgraph ImageItems 似乎在大约 30 秒后停止重新绘制,或者如果我点击窗口外,或者如果我调整其中一张图像的控件。之后,我可以通过调整窗口大小来重新绘制图像,但这有点乏味。

我想也许 pyqtgraph 不是线程安全的,但我什至不知道我是否使用了真正的线程,因为我必须让所有东西都通过 Qt (QThread) 运行才能正常工作。

我发现一些论坛帖子显示了与此类似的问题。 The first one 重定向到 here 解释说你可以使用名为 NSAppSleepDisabled 的东西,但这似乎只在 OSX 上,我正在运行 Windows 10。The second one 解释说他们的整个 GUI 冻结,而我没有遇到这个问题,只有 ImageItem 冻结,其余的 GUI 都是响应式的。

这些是我的导入

import time
from multiprocessing import Queue
from threading import Lock
from queue import Empty

import pyqtgraph as pg
from PyQt5.QtCore import QThread

为了保持我的软件可扩展,我使用工作线程来管理传入的图像数据,为了保持 ImageView 的线程安全,我添加了一个锁:

graph_lock = Lock()

class WindowUpdater(QThread):

    stop_q = Queue()

    def __init__(self, cam_q: Queue, img_control: pg.ImageView, **kwargs):
        super().__init__(**kwargs)
        self.q = cam_q
        self.c = img_control

    def run(self) -> None:
        while self.stop_q.empty():
            try:
                timg = self.q.get_nowait()
                graph_lock.acquire(True)
                self.c.setImage(timg.frame)
                self.c.invalidate()
                graph_lock.release()
            except Empty:
                time.sleep(0.1)

然后主应用程序处理将这些线程链接到传入数据

app = QApplication(sys.argv)
window = QMainWindow()

grid = QGridLayout()
grid.setSpacing(10)
widg = QWidget()
widg.setLayout(grid)
window.setCentralWidget(widg)
window.show()

window.setGeometry(300, 300, 500, 400)
window.show()

threads = []

for i, h in enumerate(hosts):
    img = pg.ImageView(window)
    grid.addWidget(img, i, 0)

    img_item = pg.ImageItem()
    img.addItem(img_item)

    threads.append(WindowUpdater(img_queue, img)

for t in threads:
    t.start()

sys.exit(app.exec_())

其中hosts 是用于连接的主机名列表,img_queue 是特定于该主机的摄像头流的多处理队列。

有人知道为什么同时运行多个 pyqtgraph ImageView 或 ImageItem 实例会导致这里出现问题吗?

【问题讨论】:

【参考方案1】:

你不应该从另一个线程更新 GUI,因为 Qt 禁止它(1)。所以你必须使用信号来传递信息。

class WindowUpdater(QThread):
    imageChanged = pyqtSignal(np.ndarray)
    stop_q = Queue()

    def __init__(self, cam_q: Queue, **kwargs):
        super().__init__(**kwargs)
        self.q = cam_q

    def run(self) -> None:
        while self.stop_q.empty():
            try:
                timg = self.q.get_nowait()
                graph_lock.acquire(True)
                self.imageChanged.emit(timg.frame)
                graph_lock.release()
            except Empty:
                time.sleep(0.1)
# ...
for i, h in enumerate(hosts):
    img = pg.ImageView(window)
    grid.addWidget(img, i, 0)

    img_item = pg.ImageItem()
    img.addItem(img_item)
    thread = WindowUpdater(img_queue)
    thread.imageChanged.connect(img.setImage)
    threads.append(thread)
# ...

(1)GUI Thread and Worker Thread

【讨论】:

感谢您回答我的问题!有更快的选择吗? pyqtSignals太慢,Qt事件循环备份导致程序内存泄漏。 @iggy12345 1) 信号是否太慢?我想不是,你是怎么测量的? dt / size_of_image 之间的比例是多少?,一个GUI本身有很多任务,其中包括绘画所以你必须意识到你不会有无限的效率,每个图像都会以最大30ms的比例刷新屏幕问题. 2)QEvents不会导致内存泄漏,因为Qt会消除它们,我不知道你从哪里得到这些信息。 相机以 90 fps 运行,使用 cython 收集和处理每一帧并放入队列中,然后将图像展平并通过 WiFi 传输到客户端,所有这些,根据我的调试线以大约 50-100 fps 的速度读出,客户端有一个进程,它接收数据并使用 cython 将其转换回 2D numpy 数组,然后将其放入一个适当的队列中,该队列连接到您在上面看到的 WindowUpdater 类 使用 pyqtSignals,如果我以 90 fps 发出信号,这是 WindowUpdater 所做的,但只能以 30 fps 处理它,Qt 框架不会丢弃任何这些事件,所以事件会堆积起来,并且由于每个事件都包含一个完整的图像,因此程序在几分钟内就会消耗几乎整 GB 的 RAM (~900 MB) @iggy12345 如果你有一个从 50 FPS 到 100 FPS 的数据,你不能在屏幕上显示它,虽然硬件可以支持它,但人眼无法区分它,因为视图有效在 30 FPS,因此根据已知标准,屏幕以 60 FPS 工作,加上 GUI 仅用于显示结果,因此您可以进行下采样并将其传递到 40 FPS,而不是尝试显示 50-100 FPS。我不明白你为什么需要这么快。

以上是关于pyqtgraph ImageView 在多线程时冻结的主要内容,如果未能解决你的问题,请参考以下文章

如何在 pyqtgraph ImageView 中启用抗锯齿功能?

如何显示类似于pyqtgraph ImageView的可点击RGB图像?

在 PyQtGraph 和 PySide2 中使用 ImageView 修复文本位置

如何更改 PyQtGraph ImageView 中的鼠标滚轮行为?

加载 hdf5 文件并使用 pyqtgraph 显示数据

在线程内更新 pyqtgraph BarGraphItem