Reputation: 1774
I have a problem where I am trying to load a whole lot of png images and subsequently display theme using PyQt. My current workflow is to use a multiprocessor pool to map a function which opens each file with 'rb' values, then reads the bytes of each file into a unified list. Finally the parent process then displays the image by calling the fromImageData
method of a QPixmap object. This method seems to work fine, but is quite slow to redraw a new pixmap each time I switch between images (8K resolution).
I was hoping that it may be faster to instead create a pixmap for each image and cycle through the pixmap rather than recreating the same pixmap with a new image at each step. To do this, I tried to create a pixmap in the multiprocess function, however this is not allowed because there is no parent QApp in the thread.
My question is if there is a proper way to do this? I have alsp thought of doing it with celery/reddis, but i cannot see that having any different ourcome. Is creating a new pixmap for each image and switching them using setPixmap
is even a viable option or are there more appropriate ways to achieve this?
Upvotes: 1
Views: 1767
Reputation: 6430
You should be able to do this using a QThreadPool
and some QRunnable
s which wrap the code that loads the pixmaps. Something like:
from PyQt5 import QtCore, QtGui
class PixmapLoader(QtCore.QRunnable):
def __init__(self, filename):
super().__init__()
self.filename = filename
def run(self):
# Load pixmap at filename
# ...
# then emit in a signal
loaded.emit(pixmap)
loaded = QtCore.pyqtSignal(QtGui.QPixmap)
Then somewhere in the main application, create a thread-pool, run the loading objects, and handle their signals.
pool = QtCore.QThreadPool()
loaders = [PixmapLoader(filename) for filename in filenames]
for loader in loaders:
loader.loaded.connect(handle_new_pixmap)
pool.start(loader)
def handle_new_pixmap(QtGui.QPixmap):
# do stuff with pixmap
I've not tried this, but because Qt is handling the threading, this should be able to take advantage of multiple threads just fine.
EDIT
As explained in the comments, this won't work. I've forgotten that QRunnable
doesn't inherit QObject
, and that QPixmaps
cannot be created outside the main thread. But it's pretty straightforward to instead use an image loader object, move it to one or more background threads, in which it loads a QImage
, and then sends that to the main thread for later use. Here's tested code that will do the bare-bones, loading all PNG files in the current directory.
#!/usr/bin/env python3
import os
from PyQt5.QtCore import pyqtSignal, QObject, QThread
from PyQt5.QtGui import QImage
from PyQt5.QtWidgets import QApplication
class ImageLoader(QObject):
loaded = pyqtSignal(str, QImage)
def __init__(self, filename):
super().__init__()
self.filename = filename
def on_load_signal(self):
img = QImage(self.filename)
self.loaded.emit(self.filename, img)
class LoaderManager(QObject):
request_img_load = pyqtSignal()
def __init__(self):
super().__init__()
self.loaders = list(map(ImageLoader,
filter(lambda f: f.endswith('.png'), os.listdir())))
self.bg_thread = QThread()
for loader in self.loaders:
self.request_img_load.connect(loader.on_load_signal)
loader.loaded.connect(self.handle_img_loaded)
loader.moveToThread(self.bg_thread)
self.bg_thread.start()
def __del__(self):
self.bg_thread.quit()
def load_all(self):
self.request_img_load.emit()
def handle_img_loaded(self, name, img):
print('File {} of size {} loaded'.format(name, img.byteCount()))
if __name__ == '__main__':
app = QApplication([])
manager = LoaderManager()
manager.load_all()
Upvotes: 2