0

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.

Appearance

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) in closeEvent(). 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() in cleanup_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 a Pool(), but it throws me an Can'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()                       
    
Community
  • 1
  • 1
JD Forster
  • 175
  • 10
  • We have an open PR into mpl that will make the color mapping faster. I also suggest finding a way to rate-limit the updates from the slider – tacaswell Jan 22 '16 at 03:30
  • Thanks! Based on your hint upon limiting the update rate I improved the code and got rid of some issues, such as the different blur states and some of my workarounds. What I did was not limiting the updates from the slider, but preventing the slider from starting new jobs all the time. The code is now working quite okay for me unless the plotting can't be made any faster at that moment. Unfortunately `restore_region()` still seems to have no effect. After all it would be nice if you could post the link to the open pull request (I had to google that abbreviation. ;-)) – JD Forster Jan 22 '16 at 17:02

0 Answers0