来自 QByteArray 的 QBuffer 的 QMovie 不显示 GIF



我可以使用某人的帮助来理解为什么PyQt5.QtGui.QMovie不渲染而是崩溃由PyQt5.QtCore.QByteArray制成的PyQt5.QtCore.QBuffer制成的GIF。我已经分支了一个分支,以便在我的应用程序中试用。

PILImage类生成 GIFgenerate_gif简化方法如下所示。其他代码只是整个简化的应用程序。

要求:Python3.6.9

图书馆:PyQt5==5.9.2fbsPillow

import io
import sys
from typing import List
import requests
from PIL import Image
from PyQt5.QtCore import QByteArray, QBuffer, QIODevice, Qt
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QScrollArea, QWidget
from fbs_runtime.application_context.PyQt5 import ApplicationContext, cached_property
CORGI_URLS = [
'https://img.freepik.com/free-vector/cute-pembroke-welsh-corgi-dog-cartoon-icon_42750-632.jpg',
'https://img.freepik.com/free-vector/cute-welsh-corgi-puppy-lying-back-cartoon-icon_42750-352.jpg',
]
GIF_DELAY = 600

class MainWindow(QMainWindow):
def __init__(self, ctx):
super(MainWindow, self).__init__()
self.ctx = ctx  # Store a reference to the context for resources, etc.
self.title = "Giffer"
self.setWindowTitle(self.title)
self.init_ui()
self.showMaximized()
def init_ui(self):
vLayout = QVBoxLayout()
vLayout.setContentsMargins(0, 0, 0, 0)
scroll = QScrollArea()
scroll.setWidgetResizable(1)
widget = QWidget()
scroll.setWidget(widget)
widget.setLayout(vLayout)
self.gif_view = self.setup_gif_view()
vLayout.addWidget(self.gif_view)
self.setCentralWidget(scroll)
def setup_gif_view(self):
label = QLabel()
label.setAlignment(Qt.AlignCenter)
return label
def generate_gif(self):
img: Image
imgs: List[Image]
img, *imgs = self.ctx.corgo_pics
bytesio = io.BytesIO()
img.save(fp=bytesio, format='GIF', append_images=imgs, save_all=True, duration=GIF_DELAY, loop=0)
qbuffer = QBuffer(QByteArray(bytesio.getvalue()))
gif = QMovie()
gif.setDevice(qbuffer)
self.gif_view.setMovie(gif)
gif.start()

class AppContext(ApplicationContext):           # 1. Subclass ApplicationContext
def run(self):                              # 2. Implement run()
self.main_window.show()
self.main_window.generate_gif()
return self.app.exec_()                 # 3. End run() with this line
@cached_property
def main_window(self):
return MainWindow(self)  # Pass context to the window.
@cached_property
def corgo_pics(self) -> List[Image]:
imgs = []
for url in CORGI_URLS:
response = requests.get(url)
imgs.append(Image.open(io.BytesIO(response.content)))
return imgs

if __name__ == '__main__':
appctxt = AppContext()                      # 4. Instantiate the subclass
exit_code = appctxt.run()                   # 5. Invoke run()
sys.exit(exit_code)

(现在在应用程序中,生成 GIF 的方法将其保存到文件中,然后QMovie读取此文件没有问题,但我想避免保存到文件中并从中读取)

<小时 />

编辑

经过一些阅读和调整,我通过将QByteArray(bytesio.getvalue())分配给一个值而不是直接将其传递给QBuffer来对其进行一些更改。我尝试了这个,因为我读到 QBuffer 只引用这个对象。因此,如果没有对该对象的引用,我想 Python 的垃圾收集器会破坏该QByteArray。 这似乎有所帮助,因为程序渲染了 GIF 的第一帧,但随后崩溃了。我想还有一个额外的内存问题。执行程序退出方法后是否收集QBuffer垃圾?

我还添加了gif.setCacheMode(QMovie.CacheAll).

这是简化的generate_gif方法

def generate_gif(self):
img: Image
imgs: List[Image]
img, *imgs = self.ctx.corgo_pics
bytesio = io.BytesIO()
img.save(fp=bytesio, format='GIF', append_images=imgs, save_all=True, duration=GIF_DELAY, loop=0)
qbytearray = QByteArray(bytesio.getvalue())
bytesio.close()
qbuffer = QBuffer(qbytearray)
gif = QMovie()
gif.setDevice(qbuffer)
gif.setCacheMode(QMovie.CacheAll)
print(f'Movie isValid() {gif.isValid()}')
self.gif_view.setMovie(gif)
gif.start()

第一次运行时它崩溃了,我得到这样的响应:

Movie isValid() True
QIODevice::peek (QDesktopWidget, "desktop"): WriteOnly device
QIODevice::read (QDesktopWidget, "desktop"): WriteOnly device
QWidget::paintEngine: Should no longer be called

连续运行时它会崩溃,我看到这个:

Movie isValid() True
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

似乎即使对 QBuffer 进行育儿也不足以让它保持活动状态,避免垃圾回收,因此程序崩溃,因为缓冲区在被访问时被破坏。

虽然你显然可以通过使其成为标签的属性(或带有setProperty('gif', gif)的Qt动态属性)来使其持久化,但使用setCacheMode(QMovie.CacheAll)足以加载整个GIF并使其持久化,但问题是gif必须在返回之前完全加载。

要实现这一点,加载最后一帧就足够了:

gif.jumpToFrame(gif.frameCount() - 1)
gif.jumpToFrame(0)
gif.start()

如果这还不够(但我看不出它不会的原因),您显然可以加载所有帧:

for f in range(gif.frameCount()):
gif.jumpToFrame(f)
gif.jumpToFrame(0)
gif.start()

注意:

  • 可以直接在 QMovie 构造函数中添加缓冲区,这将执行与gif.setDevice()相同的操作;
  • requests.get阻塞,请考虑使用带有信号的自定义QThread(但请记住,必须在主线程中创建QMovie)或QNetworkAccessManager;

巧妙的@musicamante在评论中解决了这个问题。解决方案是在开始QMovie之前添加gif.jumpToFrame(gif.frameCount() - 1)。它强制QMovie在返回之前加载(和缓存)所有内容。以前方法的问题在于,一旦函数返回,QBuffer就会被销毁。因此,没有什么可以从中读取帧(或者QBuffer引用现在可能指向内存中完全不同的对象)。

最终简化工作generate_gif方法:

def generate_gif(self):
img: Image
imgs: List[Image]
img, *imgs = self.ctx.corgo_pics
bytesio = io.BytesIO()
img.save(fp=bytesio, format='GIF', append_images=imgs, save_all=True, duration=GIF_DELAY, loop=0)
qbytearray = QByteArray(bytesio.getvalue())
bytesio.close()
qbuffer = QBuffer(qbytearray)
gif = QMovie()
gif.setDevice(qbuffer)
gif.setCacheMode(QMovie.CacheAll)
print(f'Movie isValid() {gif.isValid()}')
self.gif_view.setMovie(gif)
gif.jumpToFrame(gif.frameCount() - 1)
gif.start()
<小时 />

编辑

在玩了一会儿代码后,我找到了另一种解决方案。解决方案是将qbufferqbytearray附加到全局变量。QMovie只保存对传递给它的对象的引用,它不会创建对象的副本并将其存储在自身内部。QBuffer对象在我们从函数返回后被销毁generate_gif因为不再有变量引用它。然后QMovie尝试访问内存中不再存在的QBuffer对象。我们可以将QBuffer附加到全局变量。那么在函数返回后,它就不会被垃圾回收。但是,问题仍然存在,因为QBuffer指向(它也只有对对象的引用)指向一个QByteArray,该在函数返回后被销毁。我们可以通过将QByteArray附加到全局变量来解决此问题。这样,它们都不会被垃圾回收,然后可以引用它们。它有效!仔细想想,这似乎很明显。尽管如此,直到现在,我从未在 Python 中处理过类似的问题。我必须说,我通常也不处理pyqt,字节和图片,所以也许这是这些知识将使我受益的领域!

我们也不需要再跳到最后一帧gif.jumpToFrame(gif.frameCount() - 1)

最终简化工作generate_gif方法:

def generate_gif(self):
img: Image
imgs: List[Image]
img, *imgs = self.ctx.corgo_pics
bytesio = io.BytesIO()
img.save(fp=bytesio, format='GIF', append_images=imgs, save_all=True, duration=GIF_DELAY, loop=0)
self.gif_qbytearray = QByteArray(bytesio.getvalue())
bytesio.close()
self.gif_qbuffer = QBuffer(self.gif_qbytearray)
gif = QMovie()
gif.setDevice(self.gif_qbuffer)
gif.setCacheMode(QMovie.CacheAll)
print(f'Movie isValid() {gif.isValid()}')
self.gif_view.setMovie(gif)
gif.start()

在我的原始应用程序中,将qbtearray保存在全局变量中也是有益的,因为它可以选择将 GIF 保存到文件中。因此,字节数组应该是可用的,而不必重新生成它(对于帮助创建 GIF 的应用程序,保存 GIF 应该是最基本的功能之一)。

<小时 />

最后的想法

如果以后不需要访问应用程序中的qbytearray,我相信如果我们不将其附加到全局内存并让它被垃圾回收,我们会节省一些内存,因为我们已经看到QMovie也单独缓存图像帧。然后,我们将依靠gif.jumpToFrame(gif.frameCount() - 1)来缓存图像帧,并且需要一些初始的额外时间来执行此操作。

也可以禁用缓存,但将qbytearrayqbuffer附加到全局变量。这样我们可以节省一些内存,但 GIF 图像帧必须每回合重新渲染一次。这应该需要更长的时间来渲染它们。我相信缓存 GIF 是有意义的,因为它一遍又一遍地显示相同的帧。

最新更新