Issue
I am using Python, PyQt5, QThread.
I have a GUI, which displays pictures and changes them with a "Continue" Button.
What I want to reach: In another tab I have a Button "Start".
When the "start" button is pressed, let's say 8 pictures should be preprocessed (changed in size an saved in a dict). With every click on "continue", a picture should be shown another new picture should be preprocessed and the first picture (QPixmap object) should be deleted.
So first run would be: picture 1-8 preprocessed picture 1 displayed
Second run: picture 9 preprocessed picture 2 displayed
To not freeze the GUI, I will need a Thread here. My idea is to stop this thread after 8 pictures at the first run and then after every preprocessed picture but to never destroy the process.
How would you realize this thread pause?
The main problem with this will be if someone clicks too fast on "continue" (example: picture 9 is still preprocessing but its tried to already start preprocessing of picture 10, but i just want to add picture 10 to the pictures to be preprocessed and not to stop something...)
Can you help me with this?
Solution
Generally speaking, you don't usually need to "pause" the thread, but to properly queue the processing based on the needed preload count. The thread will then "pause" itself, but only because it's waiting for new objects to process.
Assuming that the processing is always sequential and in one direction (no going back to the previous image), a possible solution is to always queue the n + preload-count
item every time a new image is shown.
from queue import Queue
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Loader(QThread):
imageReady = pyqtSignal(int, QImage)
def __init__(self):
super().__init__()
self.queue = Queue()
def run(self):
self.keepRunning = True
while self.keepRunning:
index, path = self.queue.get()
image = QImage(path)
# simulate long processing
self.msleep(image.width() + image.height())
result = image.scaled(600, 600, Qt.KeepAspectRatio)
self.imageReady.emit(index, result)
def process(self, index, path):
self.queue.put((index, path))
def stop(self):
self.keepRunning = False
self.wait()
class LinearImageProcess(QWidget):
cacheSize = 10
currentIndex = -1
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.startButton = QPushButton('Start')
self.continueButton = QPushButton('Continue', enabled=False)
self.view = QLabel()
self.view.setAlignment(Qt.AlignCenter)
self.view.setFixedSize(600, 600)
layout.addWidget(self.startButton)
layout.addWidget(self.continueButton)
layout.addWidget(self.view)
self.fileList = tuple(enumerate(
QDir(QDir.tempPath()).entryList(['*.jpg', '*.png'])))
self.cache = []
self.loader = Loader()
self.loader.start()
self.loader.imageReady.connect(self.imageReady)
self.startButton.clicked.connect(self.start)
self.continueButton.clicked.connect(self.showNext)
def start(self):
self.startButton.setEnabled(False)
self.currentIndex = 0
for i, file in self.fileList[:self.cacheSize]:
self.loader.process(i, file)
def showNext(self):
if self.cache:
self.view.setPixmap(self.cache.pop(0))
else:
self.continueButton.setEnabled(False)
nextIndex = self.currentIndex + 1
if nextIndex < len(self.fileList):
self.currentIndex = nextIndex
nextProcess = self.currentIndex + self.cacheSize
if nextProcess < len(self.fileList):
self.loader.process(*self.fileList[nextProcess])
def imageReady(self, index, image):
pm = QPixmap.fromImage(image)
if self.currentIndex == index:
self.view.setPixmap(pm)
self.continueButton.setEnabled(self.currentIndex < len(self.fileList))
else:
self.cache.append(pm)
self.continueButton.setEnabled(True)
if self.continueButton.isEnabled():
self.continueButton.setFocus()
app = QApplication([])
w = LinearImageProcess()
w.show()
app.exec()
If you, instead, want to provide browsing/processing in both directions, things get a bit tricky: the problem is that the thread queue might need to change while it's processing because the user has changed browsing direction (or even skipped to another index).
A possible solution is to implement a further queue for the UI that depends on the current index. This results in the UI thread requesting a new process only when a new image has been received from the thread, and then queue the next process request only based on the current index. A benefit of this approach is that it would obviously work even for the simpler "linear" direction used above.
class NonLinearImageProcess(QWidget):
cacheSize = 20
_currentIndex = -1
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.startButton = QPushButton('Start')
self.browseWidget = QWidget(enabled=False)
browseLayout = QHBoxLayout(self.browseWidget)
browseLayout.setContentsMargins(0, 0, 0, 0)
style = self.style()
self.beginButton = QToolButton(
icon=style.standardIcon(style.SP_MediaSkipBackward))
self.backButton = QToolButton(
icon=style.standardIcon(style.SP_ArrowLeft))
self.imageSpin = QSpinBox(keyboardTracking=False)
self.forwardButton = QToolButton(
icon=style.standardIcon(style.SP_ArrowRight))
self.endButton = QToolButton(
icon=style.standardIcon(style.SP_MediaSkipForward))
browseLayout.addWidget(self.beginButton)
browseLayout.addWidget(self.backButton)
browseLayout.addWidget(self.imageSpin)
browseLayout.addWidget(self.forwardButton)
browseLayout.addWidget(self.endButton)
self.view = QLabel()
self.view.setAlignment(Qt.AlignCenter)
self.view.setFixedSize(600, 600)
layout.addWidget(self.startButton)
layout.addWidget(self.browseWidget)
layout.addWidget(self.view, alignment=Qt.AlignCenter)
self.fileList = tuple(enumerate(
QDir(QDir.tempPath()).entryList(['*.jpg', '*.png'])))
self.imageSpin.setRange(1, len(self.fileList))
self.imageSpin.setSuffix(' of {}'.format(len(self.fileList)))
self.cache = {}
self.loadQueue = []
self.loader = Loader()
self.loader.start()
self.loader.imageReady.connect(self.imageReady)
self.goToTimer = QTimer(self, singleShot=True,
interval=100, timeout=self.goTo)
self.startButton.clicked.connect(self.start)
self.beginButton.clicked.connect(self.goToStart)
self.backButton.clicked.connect(self.goToPrevious)
self.forwardButton.clicked.connect(self.goToNext)
self.endButton.clicked.connect(self.goToEnd)
# the lambda is required since valueChanged sends an integer argument
# that would be used as interval for start()
self.imageSpin.valueChanged.connect(lambda: self.goToTimer.start())
self.setMaximumSize(self.sizeHint())
@pyqtProperty(int)
def currentIndex(self):
return self._currentIndex
@currentIndex.setter
def currentIndex(self, index):
if index < 0:
index = 0
maxIndex = len(self.fileList) - 1
if index > maxIndex:
index = maxIndex
if self._currentIndex == index:
return
self._currentIndex = index
canGoBack = index > 0
self.beginButton.setEnabled(canGoBack)
self.backButton.setEnabled(canGoBack)
canGoForward = index < maxIndex
self.forwardButton.setEnabled(canGoForward)
self.endButton.setEnabled(canGoForward)
def start(self):
self.startButton.setEnabled(False)
if not self.fileList:
return
self.currentIndex = 0
self.loader.process(*self.fileList[0])
for i, file in self.fileList[1:self.cacheSize // 2]:
self.loadQueue.append(i)
def goTo(self, index=None):
if index is None:
index = self.imageSpin.value() - 1
oldIndex = self.currentIndex
self.currentIndex = index
if oldIndex == self.currentIndex:
return
pm = self.cache.get(index)
if isinstance(pm, QPixmap):
self.view.setPixmap(self.cache[index])
self.browseWidget.setEnabled(True)
self.imageSpin.setFocus()
with QSignalBlocker(self.imageSpin):
self.imageSpin.setValue(index + 1)
else:
self.browseWidget.setEnabled(False)
partCacheSize = self.cacheSize // 2
start = max(0, index - partCacheSize)
end = min(index + partCacheSize + 1, len(self.fileList))
for i in range(start):
if isinstance(self.cache.get(i), QPixmap):
self.cache.pop(i)
for i in range(end, len(self.fileList)):
if isinstance(self.cache.get(i), QPixmap):
self.cache.pop(i)
self.loadQueue.clear()
# try to equally process images from both "edges" of the queue: i.e. if
# the current index is 30, queue indexes 31, 29, 32, 28, etc.
for pair in zip_longest(range(index - 1, start - 1, -1), range(index, end)):
for i in pair:
if (i is not None and i not in self.cache):
self.loadQueue.append(i)
self.processNextInQueue()
def processNextInQueue(self):
if self.loadQueue:
nextIndex = self.loadQueue.pop(0)
self.loader.process(*self.fileList[nextIndex])
def goToStart(self):
self.goTo(0)
def goToPrevious(self):
self.goTo(self.currentIndex - 1)
def goToNext(self):
self.goTo(self.currentIndex + 1)
def goToEnd(self):
self.goTo(len(self.fileList) - 1)
def imageReady(self, index, image):
pm = QPixmap.fromImage(image)
self.cache[index] = pm
if self.currentIndex == index:
self.view.setPixmap(pm)
self.browseWidget.setEnabled(True)
self.imageSpin.setFocus()
with QSignalBlocker(self.imageSpin):
self.imageSpin.setValue(index + 1)
if index in self.loadQueue:
self.loadQueue.remove(index)
self.processNextInQueue()
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.