Following up on this Question and the solution provided by tcaswell I tried to adopt the code for imshow()
to generate a non-freezing window with a slider for image processing, such as gaussian blur filter. (I plotted two images on top of each other, because I want to display a partly transparent mask at a later stage.)
I hope some of you might find this useful, although I could still use some help.
EDIT: You can find the current state in section THIRD CODE below. I am keeping the old versions for other users who would like to dig into the details.
I derived two different working codes, each having some (minor) issues and I would really appreciate some advice.
First code:
- As long as the
QSlider
is dragged around the thread is running. However, you can not simply click the slider bar. Any suggestion? - The image axes are not properly plotted, i.e. they disappear again. Why?
- The plot updating is not what I would call fast, although it is faster than calling
imshow()
everytime. How can I speed this up even more? - The window is still frozen for the very short time during which the plot is updated. (The window dragging while the loop is running is stuttering.) Can this be improved?
To not run into
QThread: Destroyed while thread is still running
I have put a time.sleep(1) incloseEvent()
. I know this is really bad, but how can I avoid it without a new flag?import time, sys from PyQt4 import QtCore from PyQt4 import QtGui from scipy import misc from scipy import ndimage from matplotlib.figure import Figure import numpy as np from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas class ApplicationWindow(QtGui.QMainWindow): get_data = QtCore.pyqtSignal() close_request = QtCore.pyqtSignal() def __init__(self, parent = None): QtGui.QMainWindow.__init__(self, parent) self.thread = QtCore.QThread(parent=self) self.worker = Worker(parent=None) self.worker.moveToThread(self.thread) self.create_main_frame() self.close_request.connect(self.thread.quit) self.startButton.clicked.connect(self.start_calculation) self.stopButton.clicked.connect(self.stop_calculation) self.worker.started.connect(self.thread.start) self.worker.new_pixel_array.connect(self.update_figure) self.slider.sliderPressed.connect(self.start_calculation) self.slider.valueChanged.connect(self.slider_value_changed) self.slider.sliderReleased.connect(self.stop_calculation) self.get_data.connect(self.worker.get_data) self.thread.start() def create_main_frame(self): self.main_frame = QtGui.QWidget() self.dpi = 100 self.width = 5 self.height = 5 self.fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) self.axes = self.fig.add_subplot(111) self.axes.axis((0,512,0,512)) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.main_frame) self.canvas.updateGeometry() self.canvas.draw() self.background = None self.background = self.canvas.copy_from_bbox(self.axes.bbox) self.im1 = self.axes.imshow(misc.ascent(), cmap='bone', interpolation='lanczos', extent=[0,512,0,512], aspect=(1), animated=True) self.im2 = self.axes.imshow(misc.lena(), cmap='afmhot', interpolation='lanczos', extent=[0,265,0,256], aspect=(1), animated=True) self.startButton = QtGui.QPushButton(self.tr("Keep Calculating")) self.stopButton = QtGui.QPushButton(self.tr("Stop Calculation")) self.slider = QtGui.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(0, 100) self.slider.setValue(50) self.slider.setTracking(True) self.slider.setTickPosition(QtGui.QSlider.TicksBothSides) layout = QtGui.QGridLayout() layout.addWidget(self.canvas, 0, 0) layout.addWidget(self.slider, 1, 0) layout.addWidget(self.startButton, 2, 0) layout.addWidget(self.stopButton, 3, 0) self.main_frame.setLayout(layout) self.setCentralWidget(self.main_frame) self.setWindowTitle(self.tr("Gaussian Filter - Slider not clickable")) def slider_value_changed(self): #self.worker.blockSignals(False) self.worker.slider = self.slider.value() def start_calculation(self): self.worker.exiting = False self.worker.slider = self.slider.value() self.startButton.setEnabled(False) self.stopButton.setEnabled(True) self.get_data.emit() def stop_calculation(self): self.worker.exiting = True self.startButton.setEnabled(True) self.stopButton.setEnabled(False) self.cleanup_UI() def update_figure(self, im1_data,im2_data): self.canvas.restore_region(self.background) self.im1.set_array(im1_data) self.im2.set_array(im2_data) self.axes.draw_artist(self.im1) self.axes.draw_artist(self.im2) self.canvas.blit(self.axes.bbox) def cleanup_UI(self): self.background = None self.canvas.draw() def closeEvent(self, event): self.stop_calculation() self.close_request.emit() time.sleep(1) ## ugly workaround to prevent window from closing before thread is closed. (calculation takes time) How can this be avoided without additional flag? event.accept() class Worker(QtCore.QObject): new_pixel_array = QtCore.pyqtSignal(np.ndarray,np.ndarray) started = QtCore.pyqtSignal() def __init__(self, parent = None): QtCore.QObject.__init__(self, parent) self.exiting = True self.slider = 0 @QtCore.pyqtSlot() def get_data(self): while self.exiting == False: self.started.emit() im1_data = self.gauss(misc.ascent(),self.slider) im2_data = self.gauss(misc.lena(),self.slider) self.new_pixel_array.emit(im1_data, im2_data) print 'Slider Value: ', self.slider def gauss(self,im,radius): gaussed = ndimage.gaussian_filter(im, radius) return gaussed def main(): app = QtGui.QApplication(sys.argv) form = ApplicationWindow() form.show() app.exec_() if __name__ == "__main__": main()
Second code:
- You can now also click the slider bar.
- Background (axes) reconstruction is still not working. Of course calling
self.canvas.draw()
incleanup_UI()
fixes this somehow. When the slider bar is clicked, the calculation is performed once, but if the slider is dragged around and released, the calculation is performed twice at the same value. Why? I tried to catch this with
blockSignals
but then sometimes (when the slider is dragged around really fast and released) the second image in the plot is not updated properly. You recognize it by two different amounts of blur.import sys from PyQt4 import QtCore from PyQt4 import QtGui from scipy import misc from scipy import ndimage from matplotlib.figure import Figure import numpy as np from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas class ApplicationWindow(QtGui.QMainWindow): get_data = QtCore.pyqtSignal() def __init__(self, parent = None): QtGui.QMainWindow.__init__(self, parent) self.thread = QtCore.QThread(parent=self) self.worker = Worker(parent=None) self.worker.moveToThread(self.thread) self.create_main_frame() self.startButton.clicked.connect(self.start_calculation) self.worker.new_pixel_array.connect(self.update_figure) self.worker.done.connect(self.stop_calculation) self.slider.sliderPressed.connect(self.start_calculation) self.slider.valueChanged.connect(self.slider_value_changed) self.slider.actionTriggered.connect(self.start_calculation) self.get_data.connect(self.worker.get_data) self.thread.start() def create_main_frame(self): self.main_frame = QtGui.QWidget() self.dpi = 100 self.width = 5 self.height = 5 self.fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) self.axes = self.fig.add_subplot(111) self.axes.axis((0,512,0,512)) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.main_frame) self.canvas.updateGeometry() self.canvas.draw() self.background = None self.background = self.canvas.copy_from_bbox(self.axes.bbox) self.im1 = self.axes.imshow(misc.ascent(), cmap='bone', interpolation='lanczos', extent=[0,512,0,512], aspect=(1), animated=True) self.im2 = self.axes.imshow(misc.lena(), cmap='afmhot', interpolation='lanczos', extent=[0,265,0,256], aspect=(1), animated=True) self.startButton = QtGui.QPushButton(self.tr("Do a Calculation")) self.slider = QtGui.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(0, 100) self.slider.setValue(50) self.slider.setTracking(True) self.slider.setTickPosition(QtGui.QSlider.TicksBothSides) layout = QtGui.QGridLayout() layout.addWidget(self.canvas, 0, 0) layout.addWidget(self.slider, 1, 0) layout.addWidget(self.startButton, 2, 0) self.main_frame.setLayout(layout) self.setCentralWidget(self.main_frame) self.setWindowTitle(self.tr("Gaussian Filter")) def slider_value_changed(self): #self.worker.blockSignals(False) self.worker.slider = self.slider.value() def start_calculation(self): self.slider_value_changed() self.worker.exiting = False self.startButton.setEnabled(False) self.get_data.emit() def stop_calculation(self): self.worker.exiting = True self.startButton.setEnabled(True) self.cleanup_UI() def update_figure(self, im1_data,im2_data): self.im1.set_array(im1_data) self.im2.set_array(im2_data) self.axes.draw_artist(self.im1) self.axes.draw_artist(self.im2) self.canvas.blit(self.axes.bbox) def cleanup_UI(self): self.canvas.restore_region(self.background) #self.canvas.draw() #self.worker.blockSignals(True) class Worker(QtCore.QObject): new_pixel_array = QtCore.pyqtSignal(np.ndarray,np.ndarray) done = QtCore.pyqtSignal() def __init__(self, parent = None): QtCore.QObject.__init__(self, parent) self.exiting = True self.slider = 0 @QtCore.pyqtSlot() def get_data(self): if self.exiting == False: im1_data = self.gauss(misc.ascent(),self.slider) im2_data = self.gauss(misc.lena(),self.slider) self.new_pixel_array.emit(im1_data,im2_data) print 'Calculation performed, Slider Value: ', self.slider self.done.emit() else: None def gauss(self,im,radius): gaussed = ndimage.gaussian_filter(im, radius) return gaussed def main(): app = QtGui.QApplication(sys.argv) form = ApplicationWindow() form.show() app.exec_() if __name__ == "__main__": main()
EDIT: Third Code (Major issues resolved and update rate limited)
- The slider is now only starting a new thread when the calculation of the previous one has finished. That was acheived by
disconnect
. - The Plotting is still slow, (the blur function too).
restore_region
still seems to have no effect at all.- I have now put the calculation of both images into threads and return the result via a
Queue()
. If you see some possibility for improvements, plese let me know. I once tried to switch to the
multiprocessing
module and put the calculation inside aPool()
, but it throws me anCan't pickle...
error. As I am totally new to multiprocessing, I would very much like to learn more about it.import sys from PyQt4 import QtCore from PyQt4 import QtGui from scipy import misc from scipy import ndimage from matplotlib.figure import Figure import numpy as np from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas from threading import Thread from Queue import Queue class ApplicationWindow(QtGui.QMainWindow): get_data = QtCore.pyqtSignal() def __init__(self, parent = None): QtGui.QMainWindow.__init__(self, parent) self.thread = QtCore.QThread(parent=self) self.worker = Worker(parent=None) self.worker.moveToThread(self.thread) self.create_main_frame() self.startButton.clicked.connect(self.start_calculation) self.stopButton.clicked.connect(self.stop_calculation) self.worker.started.connect(self.thread.start) self.worker.new_pixel_array.connect(self.update_figure) self.slider.actionTriggered.connect(self.start_calculation) self.slider.valueChanged.connect(self.slider_value_changed) self.worker.done.connect(self.stop_calculation) self.get_data.connect(self.worker.get_data) self.thread.start() def create_main_frame(self): self.main_frame = QtGui.QWidget() self.dpi = 100 self.width = 5 self.height = 5 self.fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) self.axes = self.fig.add_subplot(111) self.axes.axis((0,512,0,512)) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.main_frame) self.canvas.updateGeometry() self.background = None self.canvas.draw() self.background = self.canvas.copy_from_bbox(self.axes.bbox) self.im1 = self.axes.imshow(misc.ascent(), cmap='bone', interpolation='lanczos', extent=[0,512,0,512], aspect=(1), animated=True) self.im2 = self.axes.imshow(misc.lena(), cmap='afmhot', interpolation='lanczos', extent=[0,265,0,256], aspect=(1), animated=True) self.startButton = QtGui.QPushButton(self.tr("Start Calculation")) self.stopButton = QtGui.QPushButton(self.tr("Stop Calculation")) self.slider = QtGui.QSlider(QtCore.Qt.Horizontal) self.slider.setRange(0, 100) self.slider.setValue(50) self.slider.setTracking(True) self.slider.setTickPosition(QtGui.QSlider.TicksBothSides) layout = QtGui.QGridLayout() layout.addWidget(self.canvas, 0, 0) layout.addWidget(self.slider, 1, 0) layout.addWidget(self.startButton, 2, 0) layout.addWidget(self.stopButton, 3, 0) self.main_frame.setLayout(layout) self.setCentralWidget(self.main_frame) self.setWindowTitle(self.tr("Gaussian Filter")) def slider_value_changed(self): self.worker.slider = self.slider.value() def start_calculation(self): if self.worker.exiting: self.slider.actionTriggered.disconnect(self.start_calculation) self.worker.slider = self.slider.value() self.startButton.setEnabled(False) self.stopButton.setEnabled(True) self.get_data.emit() self.worker.exiting = False def stop_calculation(self): if not self.worker.exiting: self.slider.actionTriggered.connect(self.start_calculation) self.worker.exiting = True self.startButton.setEnabled(True) self.stopButton.setEnabled(False) self.cleanup_UI() def update_figure(self, im1_data,im2_data): #self.canvas.restore_region(self.background) self.im1.set_array(im1_data) self.im2.set_array(im2_data) self.axes.draw_artist(self.im1) self.axes.draw_artist(self.im2) self.canvas.blit(self.axes.bbox) def cleanup_UI(self): self.background = None self.canvas.draw() class Worker(QtCore.QObject): new_pixel_array = QtCore.pyqtSignal(np.ndarray,np.ndarray) started = QtCore.pyqtSignal() done = QtCore.pyqtSignal() def __init__(self, parent = None): QtCore.QObject.__init__(self, parent) self.exiting = True self.slider = 0 @QtCore.pyqtSlot() def get_data(self): while self.exiting == False: self.started.emit() queue1 = Queue() queue2 = Queue() im1T = Thread(target=self.gauss, args=(misc.ascent(),queue1)) im2T = Thread(target=self.gauss, args=(misc.lena(),queue2)) slider_val = self.slider im1T.start() im2T.start() im1T.join() im2T.join() im1_data = queue1.get() im2_data = queue2.get() self.new_pixel_array.emit(im1_data, im2_data) if slider_val == self.slider: self.done.emit() print 'Slider Value: ', self.slider break def gauss(self,im,output_queue): gaussed = ndimage.gaussian_filter(im,self.slider) output_queue.put(gaussed) def main(): app = QtGui.QApplication(sys.argv) form = ApplicationWindow() form.show() app.exec_() if __name__ == "__main__": main()