Python3 + Pillow + QT5:调整包含图像的标签大小时崩溃

Python3 + Pillow + QT5: Crash when I resize a label containing an image

我收到一个消息框:"Python has stopped working" 当我将图像加载到已经可见的 window 中的 QLabel 中时。选择调试显示:Python.exe.

中发生未处理的 Win32 异常

如果我在显示 window 之前将图像加载到标签中,它会正确显示。

这是精简代码:

#!/usr/bin/etc python
import sys
import os
import stat
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PIL import *
from PIL.ImageQt import *

def update(label):
    filename = r"C:\Users\me\Pictures\images[=11=]000229.jpg"
    im1 = Image.open(filename)
    print ("Read ({},{})".format(im1.width, im1.height))
    im2 = im1.rotate(90, expand=True)
    print("Rotate ({},{})".format(im2.width, im2.height))

    im2.thumbnail((1200,1200))
    print("Thumbnail({},{})".format(im2.width, im2.height))

    qimage = ImageQt(im2)
    pixmap = QPixmap.fromImage(qimage)
    label.setPixmap(pixmap)


app = QApplication(sys.argv)

desktop = QDesktopWidget()
deskGeometry = desktop.availableGeometry()
print("desktop ({},{})".format(deskGeometry.width(), deskGeometry.height()))

window = QFrame()
# If you let QT pick the sizes itself, it picks dumb ones, then complains
# 'cause they are dumb
window.setMinimumSize(300, 200)
window.setMaximumSize(deskGeometry.width(), deskGeometry.height())

label = QLabel()
#call update here: no crash

caption = QLabel()
caption.setText("Hello world")

box = QVBoxLayout()
box.addWidget(label)
box.addWidget(caption)
#call update here, no crash
window.setLayout(box)
#call update here, no crash

window.show()
#this call to update results in a crash
update(label)

#window.updateGeometry()
print("App: exec")
app.exec_()

输出:

desktop (3623,2160)
Read (1515,1051)
Rotate (1051,1515)
Thumbnail(832,1200)
App: exec

我是否需要做任何特别的事情来告诉 QT window 大小将会改变?从这里诊断问题的任何建议...


更新:

如果我复制更新函数的主体并将其粘贴到更新调用的位置,它不会再崩溃 -- 它会按预期工作。

由此我得出结论,存在对象生命周期问题。在幕后的某个地方,QT and/or Pillow 保留了一个指向内部缓冲区的指针,而不是制作副本或 "stealing" 缓冲区。当包含缓冲区的对象被删除时,指针变为无效并且 "Bad Things Happen[TM]"

现在确定谁在偷懒...

我根据更新中提到的观察发现了一个解决方案,这似乎是一个对象生命周期问题。

update 函数中的行更改为

pixmap = QPixmap.fromImage(qimage)

pixmap = QPixmap.fromImage(qimage).copy()

强制复制像素图。这个副本显然有自己的数据缓冲区,而不是从图像中借用缓冲区。

标签然后保留对像素图的引用——确保缓冲区的生命周期。 'bug' 似乎是 QPixmap.fromImage 捕获指向图像中数据的指针,但不保留对图像的引用,因此如果图像被垃圾收集(这可能是因为它很大对象,标签(和像素图)有一个指向未分配内存的指针。

[这 'pointer to the buffer' 纯粹是我的猜测,但最重要的是程序不再崩溃。]

以防其他人偶然发现:

我遇到了一个非常相似的问题,并且能够使用插槽和信号(基于 this great article)解决它,因为 .copy() 解决方案对我不起作用。我的解决方案是将 QPixmap 和 QLabel.setPixmap 的创建分成不同的函数。当创建 QPixmap 的函数完成时,它会发出一个信号,触发将像素图设置为标签的函数。

例如,如果您想使用线程:

class WorkerSignals(QObject):
    ready = pyqtSignal()

class Worker(QRunnable):
    def __init__(self, some_function, *args, **kwargs):
        super(Worker, self).__init__()
        self.some_function = some_function
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        self.kwargs['ready'] = self.signals.ready

    @pyqtSlot()
    def run(self):
        self.some_function(*self.args, **self.kwargs)

def make_pixmap(qimage, ready, **kwargs):
    pixmap = QPixmap.fromImage(qimage) # qimage is some QImage
    ready.emit()

def set_pixmap(pixmap):
    label.setPixmap(pixmap) # 'label' is some QLabel

threadpool = QThreadPool

worker = Worker(make_pixmap, image)
worker.signals.ready.connect(set_pixmap)

threadpool.start(worker)

显然在这种情况下不需要使用线程,但这只是为了表明如果您想要处理和显示图像而不挂起 GUI 的其余部分是可能的。

编辑:崩溃又回来了,忽略上面的内容。正在处理另一个修复程序。