在另一个线程中保存大图像

2024-10-03 00:22:33 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个小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,所有这些都会产生相同的结果:将pixmap存储为PNG(实际上应该发生在后台线程中)会阻塞GUI


Tags: topathinimageimportselftargetpng
1条回答
网友
1楼 · 发布于 2024-10-03 00:22:33

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

GUI Thread and Worker Thread

As mentioned, each program has one thread when it is started. This thread is called the "main thread" (also known as the "GUI thread" in Qt applications). The Qt GUI must run in this thread. All widgets and several related classes, for example QPixmap, don't work in secondary threads. A secondary thread is commonly referred to as a "worker thread" because it is used to offload processing work from the main thread.

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

Qt provides four classes for handling image data: QImage, QPixmap, QBitmap and QPicture. QImage is designed and optimized for I/O, and for direct pixel access and manipulation, while QPixmap is designed and optimized for showing images on screen. QBitmap is only a convenience class that inherits QPixmap, ensuring a depth of 1. The isQBitmap() function returns true if a QPixmap object is really a bitmap, otherwise returns false. Finally, the QPicture class is a paint device that records and replays QPainter commands.

因此,解决方案是:

self.imageLabel.pixmap().toImage()

代码:

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_())

相关问题 更多 >