【问题标题】:Save big image in another thread在另一个线程中保存大图像
【发布时间】:2020-02-05 17:00:30
【问题描述】:

我有一个小的 GUI,在显示的图像上显示不同的层。在某些时候,我想将带有所有可视化图层的当前图像存储到磁盘,同时继续使用 GUI。图像相当大(存储大约需要 5 秒),所以我想将保存的内容卸载到后台线程中。

我尝试了不同的方法,但都没有奏效。最小的工作示例(仍然需要 PNG 进行测试,抱歉):

import sys
import threading

from PIL import Image
from PIL.ImageQt import ImageQt
from PyQt5.QtCore import QThread, pyqtSignal, QRunnable, QThreadPool
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QSizePolicy, QAction, QToolBar


class StorageQRunnable(QRunnable):

    def __init__(self,
                 pixmap: QPixmap,
                 target_path: str):
        super(StorageQRunnable, self).__init__()

        self.pixmap = pixmap
        self.target_path = target_path

    def run(self):
        print("Starting to write image in QRunnable.")
        self.pixmap.save(self.target_path, "PNG")
        print("Done writing image in QRunnable.")


class StorageQThread(QThread):

    signal = pyqtSignal("PyQt_PyObject")

    def __init__(self,
                 pixmap: QPixmap,
                 target_path: str):
        super(StorageQThread, self).__init__()

        self.pixmap = pixmap
        self.target_path = target_path

    def run(self):
        print("Starting to write image in QThread.")
        self.pixmap.save(self.target_path, "PNG")
        print("Done writing image in QThread.")
        self.signal.emit(0)


class StorageThread(threading.Thread):

    def __init__(self,
                 pixmap: QPixmap,
                 target_path: str):
        super(StorageThread, self).__init__()

        self.pixmap = pixmap
        self.target_path = target_path

    def run(self):

        print("Starting to write image in threading.Thread.")
        self.pixmap.save(self.target_path, "PNG")
        print("Done writing image in threading.Thread.")


class TrialWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(TrialWindow, self).__init__(*args, **kwargs)

        self.imageLabel = QLabel()
        self.imageLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
        self.imageLabel.setScaledContents(True)
        self.setCentralWidget(self.imageLabel)

        toolbar = QToolBar("Controls")
        toolbar.addAction(QAction("Do", self, shortcut="Ctrl+F", triggered=self.continue_in_foreground))
        toolbar.addAction(QAction("To background", self, shortcut="Ctrl+B", triggered=self.to_background))
        toolbar.addAction(QAction("Exit", self, shortcut="Ctrl+Q", triggered=self.close))
        self.addToolBar(toolbar)

        self.bg_task_started = False

    def continue_in_foreground(self):
        print("Doing.")

    def thread_done(self, status: int):
        if status == 0:
            print(":)")

    def to_background(self):
        print("Background.")

        if not self.bg_task_started:

            # print("I Pushing threading.Thread to background.")
            # StorageThread(pixmap=self.imageLabel.pixmap().copy(), target_path="/tmp/target1.png").start()

            # print("II Pushing QThread to background.")
            # self.w2 = StorageQThread(pixmap=self.imageLabel.pixmap().copy(), target_path="/tmp/target1.png")
            # self.w2.signal.connect(self.thread_done)
            # self.w2.start()

            print("III Pushing QRunnable to background.")
            r = StorageQRunnable(pixmap=pixmap.copy(), target_path="/tmp/target1.png")
            QThreadPool.globalInstance().start(r)

            self.bg_task_started = True

    def visualize(self, pxmp: QPixmap):

        self.imageLabel.setPixmap(pxmp)


if __name__ == '__main__':

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

    # load pixmap
    img = Image.open("/tmp/sample.png")
    image = ImageQt(img)
    pixmap = QPixmap.fromImage(image)

    window.show()
    window.visualize(pixmap)

    sys.exit(app.exec_())

简短手册:

  • 开始
  • Ctrl+F 可查看来自 GUI 线程的控制台输出(一次或多次)
  • Ctrl+B 开始在后台线程中存储大 PNG
  • 继续按Ctrl+F,看到图像被存储之前什么都没有发生,所有事件都只在之后处理(GUI不可用)
  • 通过Ctrl+Q退出

随意评论不同的方法,即threading.Thread的用法,QThread的用法或QRunnable的用法,所有结果都相同:将像素图存储为PNG(这实际上应该发生在后台线程中)会阻塞 GUI。

【问题讨论】:

  • 我知道它失败了,因为 GUI 没有响应,而线程应该在后台运行。无论我(尝试)是否在后台线程中运行像素图存储,程序的行为都是相同的; pixmap-storage 调用被阻塞。这是我不想要的。
  • 18MegaPixel, RGB
  • upload.wikimedia.org/wikipedia/commons/9/90/… 这应该可以解决问题,至少在将其存储为 PNG 之后。否则我明天一回来就必须提供一个 png。

标签: python python-3.x multithreading pyqt pyqt5


【解决方案1】:

问题与线程无关,但 QPixmap 不是线程安全的,不应该像the docs 指出的那样从另一个线程进行操作:

GUI线程和工作线程

如前所述,每个程序在启动时都有一个线程。这 线程被称为“主线程”(在 Qt 应用程序)。 Qt GUI 必须在这个线程中运行。 所有小部件和 几个相关的类,例如 QPixmap,在辅助中不起作用 线程。辅助线程通常称为“工作线程” 线程”,因为它用于从主线程卸载处理工作 线程。

相反,您应该使用为 I/O 操作优化的 QImage,正如the docs 指出的那样:

Qt 提供了四个类来处理图像数据:QImage、QPixmap、 QBitmap 和 QPicture。 QImage 专为 I/O 设计和优化,并且 用于直接像素访问和操作,而 QPixmap 设计 并针对在屏幕上显示图像进行了优化。 QBitmap 只是一个 继承 QPixmap 的便利类,确保深度为 1。 如果 QPixmap 对象确实是 位图,否则返回 false。最后,QPicture 类是一个 记录和重放 QPainter 命令的绘图设备。

所以解决办法是:

self.imageLabel.pixmap().<b>toImage()</b>

代码:

import sys
import threading

from PyQt5 import QtCore, QtGui, QtWidgets


class SaveWorker(QtCore.QObject):
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()

    def save(self, image, path):
        threading.Thread(target=self._save, args=(image, path,), daemon=True).start()

    def _save(self, image, path):
        self.started.emit()
        image.save(path, "PNG")
        self.finished.emit()


class TrialWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(TrialWindow, self).__init__(parent)

        self.imageLabel = QtWidgets.QLabel()
        self.imageLabel.setSizePolicy(
            QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored
        )
        self.imageLabel.setScaledContents(True)
        self.setCentralWidget(self.imageLabel)

        toolbar = QtWidgets.QToolBar("Controls")
        toolbar.addAction(
            QtWidgets.QAction(
                "Do", self, shortcut="Ctrl+F", triggered=self.continue_in_foreground
            )
        )
        toolbar.addAction(
            QtWidgets.QAction(
                "To background", self, shortcut="Ctrl+B", triggered=self.to_background
            )
        )
        toolbar.addAction(
            QtWidgets.QAction("Exit", self, shortcut="Ctrl+Q", triggered=self.close)
        )
        self.addToolBar(toolbar)

        self.worker = SaveWorker()
        self.worker.started.connect(self.on_started)
        self.worker.finished.connect(self.on_finished)

    def visualize(self, pxmp: QtGui.QPixmap):
        self.imageLabel.setPixmap(pxmp)

    @QtCore.pyqtSlot()
    def continue_in_foreground(self):
        print("Doing.")

    @QtCore.pyqtSlot()
    def on_started(self):
        print("started")

    @QtCore.pyqtSlot()
    def on_finished(self):
        print("finished")

    @QtCore.pyqtSlot()
    def to_background(self):
        self.worker.save(self.imageLabel.pixmap().toImage(), "/tmp/target1.png")


if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)

    # load pixmap
    pixmap = QtGui.QPixmap("/tmp/sample.png")

    window = TrialWindow()
    window.show()

    window.visualize(pixmap)

    sys.exit(app.exec_())

【讨论】:

  • You.Are.My.Hero.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-23
  • 2016-10-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-06
  • 1970-01-01
相关资源
最近更新 更多