Issue
I made a widget for loading lots of images in a deferred fashion by using threads. When using Python 2 (with PySide
), the scrolling is super smooth with all the threads running. On Python 3 (with PySide2
), it freezes every time you attempt to scroll.
I narrowed it down to the QtGui.QImage
call within the thread.
class ImageLoader(QtCore.QRunnable):
def __init__(self, item):
self.item = item
super(ImageLoader, self).__init__()
def run(self):
QtGui.QImage(self.item.path) # shortened to the min reproducible example
Does anyone know why it would only be causing issues on newer versions of Qt, and perhaps how I can fix the issue?
Here's the full script, trimmed down as much as I was able to. Resizing the window or scrolling will be smooth in PySide
, and in PySide2
it will freeze until all the threads have finished.
PATH_TO_LARGE_FILE = 'C:/large_image.png' # Pick something >1MB to really show the slowness
import sys
from Qt import QtCore, QtGui, QtWidgets
class GridView(QtWidgets.QListView):
def __init__(self):
super(GridView, self).__init__()
self.setViewMode(QtWidgets.QListView.IconMode)
self.setModel(GridModel())
class GridModel(QtGui.QStandardItemModel):
def __init__(self):
super(GridModel, self).__init__()
self.threadPool = QtCore.QThreadPool()
def data(self, index, role=QtCore.Qt.UserRole):
# Load image
if role == QtCore.Qt.DecorationRole:
item = self.itemFromIndex(index)
worker = ImageLoader(item)
self.threadPool.start(worker)
return None
# Set size of icons
elif role == QtCore.Qt.SizeHintRole:
return QtCore.QSize(64, 89)
return super(GridModel, self).data(index, role)
class ImageLoader(QtCore.QRunnable):
def __init__(self, item):
self.item = item
super(ImageLoader, self).__init__()
def run(self):
QtGui.QImage(self.item.path)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = GridView()
for i in range(1000):
item = QtGui.QStandardItem('test {}'.format(i))
item.path = PATH_TO_LARGE_FILE
widget.model().appendRow(item)
widget.show()
app.setActiveWindow(widget)
app.exec_()
Edit (requested in comments): Link to full script
Edit (March 2023): I've found the issue appears to be specific to PySide. With the latest releases, PySide6
has the issue whereas PyQt6
does not. I've reported the bug and it's now been fixed in PySide6 6.6.0
Solution
Turns out it's a bug with PySide
where QImage
was missing the allow-thread
flag. It is now fixed and from what I can see, due to be released in PySide6 6.5.1
.
Since I'm stuck with older versions for the foreseeable future, I found a workaround. By reading the raw bytes and sending the image format, then it's able to display the majority of images just as fast.
Here's the code I used to make it work:
class ImageLoader(QtCore.QRunnable):
...
def run(self):
...
supportedImageFormats = [f.data().decode() for f in QtGui.QImageReader.supportedImageFormats()]
imagePath = self.item.path()
imageExt = os.path.splitext(imagePath)[1][1:].lower()
# Original way
if PySide2.__version__ > '6.5.0':
img = QtGui.QImage(imagePath)
self.loadedImage.emit(img)
# Workaround
elif imageExt in supportedImageFormats:
reader = QtGui.QImageReader(imagePath)
with open(self.item.path(), 'rb') as f:
self.loadedBytes.emit(f.read(), reader.format())
else:
self.loadedImage.emit(None)
class GridModel(QtGui.QStandardItemModel):
...
def setItemPixmapFromBytes(self, item, imgBytes, format):
"""Load an image from bytes."""
image = QtGui.QImage()
image.loadFromData(imgBytes, aformat=format)
self.setItemPixmap(item, image)
def setItemPixmap(self, item, pixmap):
if isinstance(pixmap, QtGui.QImage):
pixmap = QtGui.QPixmap.fromImage(pixmap)
...
The drawback is certain image formats may not work - in my case I found a bunch of TGA files were not loading. I found a post from 2017 showing an alternative way to load them, then converted to Python with the help of AI. To implement this, under the ImageLoader.run
method, you would check the extension, and use self.loadedImage.emit(loadTga(imagePath))
if a TGA, otherwise proceed with sending the self.loadedBytes
signal.
def loadTga(filePath):
img = QtGui.QImage()
if not img.load(filePath):
# open the file
fsPicture = open(filePath, 'rb')
if not fsPicture.is_open():
img = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
img.fill(QtCore.Qt.red)
return img
# some variables
vui8Pixels = []
ui32BpP = 0
ui32Width = 0
ui32Height = 0
# read in the header
ui8x18Header = [0]*19
fsPicture.readinto(ui8x18Header)
#get variables
ui32IDLength = ui8x18Header[0]
ui32PicType = ui8x18Header[2]
ui32PaletteLength = ui8x18Header[6] * 0x100 + ui8x18Header[5]
ui32Width = ui8x18Header[13] * 0x100 + ui8x18Header[12]
ui32Height = ui8x18Header[15] * 0x100 + ui8x18Header[14]
ui32BpP = ui8x18Header[16]
# calculate some more information
ui32Size = ui32Width * ui32Height * ui32BpP // 8
bCompressed = ui32PicType == 9 or ui32PicType == 10
vui8Pixels.resize(ui32Size)
# jump to the data block
fsPicture.seek(ui32IDLength + ui32PaletteLength, 1)
if ui32PicType == 2 and (ui32BpP == 24 or ui32BpP == 32):
fsPicture.readinto(vui8Pixels)
# else if compressed 24 or 32 bit
elif ui32PicType == 10 and (ui32BpP == 24 or ui32BpP == 32): # compressed
tempChunkHeader = 0
tempData = [0]*5
tempByteIndex = 0
while True:
fsPicture.readinto(tempChunkHeader)
if tempChunkHeader >> 7: # repeat count
# just use the first 7 bits
tempChunkHeader = (tempChunkHeader << 1) >> 1
fsPicture.readinto(tempData)
for i in range(0, tempChunkHeader+1):
vui8Pixels[tempByteIndex] = tempData[0]
vui8Pixels[tempByteIndex+1] = tempData[1]
vui8Pixels[tempByteIndex+2] = tempData[2]
if ui32BpP == 32:
vui8Pixels[tempByteIndex+3] = tempData[3]
tempByteIndex += 4
else: # data count
# just use the first 7 bits
tempChunkHeader = (tempChunkHeader << 1) >> 1
for i in range(0, tempChunkHeader+1):
fsPicture.readinto(tempData)
vui8Pixels[tempByteIndex] = tempData[0]
vui8Pixels[tempByteIndex+1] = tempData[1]
vui8Pixels[tempByteIndex+2] = tempData[2]
if ui32BpP == 32:
vui8Pixels[tempByteIndex+3] = tempData[3]
tempByteIndex += 4
if tempByteIndex >= ui32Size:
break
# not useable format
else:
fsPicture.close()
img = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
img.fill(QtCore.Qt.red)
return img
fsPicture.close()
img = QtGui.QImage(ui32Width, ui32Height, QtGui.QImage.Format_RGB888)
pixelSize = 4 if ui32BpP == 32 else 3
for x in range(ui32Width):
for y in range(ui32Height):
valr = vui8Pixels[y * ui32Width * pixelSize + x * pixelSize + 2]
valg = vui8Pixels[y * ui32Width * pixelSize + x * pixelSize + 1]
valb = vui8Pixels[y * ui32Width * pixelSize + x * pixelSize]
value = QtGui.QColor(valr, valg, valb)
img.setPixelColor(x, y, value)
img = img.mirrored()
return img
With all of this implemented, on the test texture library I'm using it on, both Python 2 and my workaround load in 17 seconds. The bugged way in Python 3 takes 34 seconds.
Answered By - Peter
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.