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

Posted

技术标签:

【中文标题】Python3 + Pillow + QT5:当我调整包含图像的标签大小时崩溃【英文标题】:Python3 + Pillow + QT5: Crash when I resize a label containing an image 【发布时间】:2016-11-01 08:29:00 【问题描述】:

当我将图像加载到已经可见的窗口中的 QLabel 中时,我收到一个消息框:“Python 已停止工作”。选择调试显示:Python.exe中出现未处理的Win32异常。

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

这是精简后的代码:

#!/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\00000229.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 窗口大小将会改变吗?从这里诊断问题的任何建议......


更新:

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

由此我得出结论,存在对象生命周期问题。 QT 和/或 Pillow 在幕后某处保留指向内部缓冲区的指针,而不是复制或“窃取”缓冲区。当包含缓冲区的对象被删除时,指针变得无效并且“Bad Things Happen[TM]”

现在要确定谁在偷懒……

【问题讨论】:

我打算建议您需要保留对基础图像数据的引用 - 但我自己实际上无法重现该问题。但无论如何,这类垃圾收集问题是众所周知的,所以我认为这里没有什么不寻常的事情。 <humor> 如果我想要对象生命周期问题,我会用 c++ 而不是 Python 进行编程——哎呀。 `' 感谢您查看问题。 【参考方案1】:

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

update 函数中的行从

pixmap = QPixmap.fromImage(qimage)

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

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

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

[这个“指向缓冲区的指针”只是我的猜测,但最重要的是程序不再崩溃。]

【讨论】:

我在缩放 QPixmap 对象时遇到了完全相同的问题,我按照指示解决了这个问题,即通过复制:pixmap = QPixmap("tempPic.png")pixmap = pixmap.copy().scaled([...]) # before: pixmap.copy().scaled([...])【参考方案2】:

万一其他人偶然发现:

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

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

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 的其余部分的情况下处理和显示图像是可能的。

编辑:崩溃又回来了,忽略上述内容。正在解决另一个问题。

【讨论】:

以上是关于Python3 + Pillow + QT5:当我调整包含图像的标签大小时崩溃的主要内容,如果未能解决你的问题,请参考以下文章

Python3 Pillow 获取一条线上的所有像素

Python3 - pillow的基本用法(第三天)

苹果电脑python3安装pillow模块

python3用pillow生成验证码,tornado中输出图片

使用 Python3 和 QT5 检测机械按钮按下

python3之成像库pillow