I'm trying to make a program in which I have a main window and a second window. The second window should be opened by checking a Check-Box in the main window and closed by unchecking it.
The following minimal example works already fine (thanks to ImportanceOfBeingErnest !), but I want to spin the arrow (the one, which is already bent when you run the example) by changing the SpinBox in the main window.
Solution: See 5th comment in the first answer!
import sys
from PyQt4 import QtGui, QtCore
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import animation
import numpy as np
class Newsphere(QtGui.QMainWindow):
def __init__(self):
super(Newsphere, self).__init__()
self.mainbox = QtGui.QWidget()
self.mainbox.setLayout(QtGui.QHBoxLayout())
self.setCentralWidget(self.mainbox)
self.spin = QtGui.QSpinBox()
self.spin.setValue(20)
self.spin.setMaximum(100)
self.spin.setMinimum(-100)
self.checkPlot = QtGui.QCheckBox("Check")
self.mainbox.layout().addWidget(self.spin)
self.mainbox.layout().addWidget(self.checkPlot)
self.Plot = None
self.checkPlot.clicked.connect(self.showPlot)
def showPlot(self):
if self.Plot == None:
self.Plot = Plot(self.kinematic())
self.Plot.show()
# register signal for closure
self.Plot.signalClose.connect(self.uncheck)
# register signal for spin value changed
self.spin.valueChanged.connect(self.kinematic)
else:
self.Plot.close()
self.Plot = None
def kinematic(self):
x = self.spin.value() / 100
v = np.matrix([[1.,x,0.],[0.,1.,0.],[0.,0.,1.]])
zero = np.matrix([[0.,0.,0.],[0.,0.,0.],[0.,0.,0.]])
pos = np.hstack([v, zero])
return pos
def uncheck(self):
self.checkPlot.setChecked(False)
self.Plot = None
class Plot(QtGui.QWidget):
signalClose = QtCore.pyqtSignal()
def __init__(self, pos=None):
super(Plot, self).__init__()
self.setLayout(QtGui.QHBoxLayout())
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111,projection = '3d')
self.fig.tight_layout()
self.ax.view_init(40, 225)
''' dashed coordinate system '''
self.ax.plot([0,1], [0,0], [0,0], label='$X_0$', linestyle="dashed", color="red")
self.ax.plot([0,0], [0,-10], [0,0], label='$Y_0$', linestyle="dashed", color="green")
self.ax.plot([0,0], [0,0], [0,1], label='$Z_0$', linestyle="dashed", color="blue")
self.ax.set_xlim3d(-3,3)
self.ax.set_ylim3d(-3,3)
self.ax.set_zlim3d(-3,3)
self.canvas = FigureCanvas(self.fig)
self.layout().addWidget(self.canvas)
self.pos = pos
self.setup_plot()
self.ani = animation.FuncAnimation(self.fig, self.update_plot, init_func=self.setup_plot, blit=True)
def setup_plot(self):
self.ax.legend(loc='best')
self.position = self.ax.quiver(0, 0, 0, 0, 0, 0, pivot="tail", color="black")
return self.position,
def update_plot(self, i):
x_zero = self.pos[:,3]
y_zero = self.pos[:,4]
z_zero = self.pos[:,5]
v_x = self.pos[0,0:3]
v_y = self.pos[1,0:3]
v_z = self.pos[2,0:3]
self.position = self.ax.quiver(-x_zero, -y_zero, z_zero, -v_x[0,:], v_y[0,:], v_z[0,:], pivot="tail", color="black")
self.canvas.draw()
return self.position,
# We need to make sure the animation stops, when the window is closed
def closeEvent(self, event):
self.signalClose.emit()
self.close()
super(Plot, self).closeEvent(event)
def close(self):
self.ani.event_source.stop()
super(Plot, self).close()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Newsphere()
main.show()
sys.exit(app.exec_())
Here is an working example of what I think you are trying to achieve.
The Main Window has a spin box and a check box. Once the checkbox is clicked, a new window with a plot will show up and an animation will start. The current value and some array will be given to the plot window. If you change the spin box value while the animation is running, it will be updated. When the plot window is closed or when the checkbox is unchecked, the animation will stop (and be deleted).
import sys
from PyQt4 import QtGui, QtCore
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import animation
import numpy as np
class Newsphere(QtGui.QMainWindow):
def __init__(self):
super(Newsphere, self).__init__()
self.mainbox = QtGui.QWidget()
self.mainbox.setLayout(QtGui.QHBoxLayout())
self.setCentralWidget(self.mainbox)
self.spin = QtGui.QSpinBox()
self.spin.setValue(5)
self.spin.setMaximum(10)
self.spin.setMinimum(1)
self.checkPlot = QtGui.QCheckBox("Check")
self.mainbox.layout().addWidget(self.spin)
self.mainbox.layout().addWidget(self.checkPlot)
self.Plot = None
self.checkPlot.clicked.connect(self.showPlot)
def showPlot(self):
if self.Plot == None:
self.Plot = Plot(self.kinematic(), self.spin.value())
self.Plot.show()
# register signal for closure
self.Plot.signalClose.connect(self.uncheck)
# register signal for spin value changed
self.spin.valueChanged.connect(self.Plot.update_factor)
else:
self.Plot.close()
self.Plot = None
def kinematic(self):
v = np.array([[1.,2.,3.],[2.,1.,3.],[3.,2.,1.]])
return v
def uncheck(self):
self.checkPlot.setChecked(False)
self.Plot = None
class Plot(QtGui.QWidget):
signalClose = QtCore.pyqtSignal()
def __init__(self, v=None, factor=1):
super(Plot, self).__init__()
self.setLayout(QtGui.QHBoxLayout())
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111,projection = '3d')
self.ax.set_aspect('equal')
self.fig.tight_layout()
self.ax.view_init(40, 225)
self.ax.set_xlim3d(0,3)
self.ax.set_ylim3d(0,3)
self.ax.set_zlim3d(0,4)
self.canvas = FigureCanvas(self.fig)
self.layout().addWidget(self.canvas)
self.pos = v
self.setup_plot()
self.update_factor(factor)
self.ani = animation.FuncAnimation(self.fig, self.update_plot, blit=False)
def setup_plot(self):
xpos, ypos = np.meshgrid(np.arange(self.pos.shape[0]),np.arange(self.pos.shape[1]) )
self.xpos = xpos.flatten('F')
self.ypos = ypos.flatten('F')
self.zpos = np.zeros_like(self.xpos)
self.bar = None
def update_factor(self, factor):
self.factor = factor
self.dx = np.ones_like(self.xpos)*np.min(np.abs(self.factor/10.), 0.1)
self.dy = self.dx.copy()
def update_plot(self, i):
if self.bar != None:
self.bar.remove()
del self.bar
pos = self.pos+np.sin(i/8.)
dz = pos.flatten()
self.bar = self.ax.bar3d(self.xpos, self.ypos, self.zpos, self.dx, self.dy, dz,
color=(1.-self.factor/10.,0,self.factor/10.), zsort='average', linewidth=0)
self.canvas.draw()
# We need to make sure the animation stops, when the window is closed
def closeEvent(self, event):
self.signalClose.emit()
self.close()
super(Plot, self).closeEvent(event)
def close(self):
self.ani.event_source.stop()
super(Plot, self).close()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Newsphere()
main.show()
sys.exit(app.exec_())
Since I wasn't sure about what you want to animate, I changed the plot to a barplot, but you can change it back to whatever you need. Hope that helps.
Related
Hello!
So I got this button Im clicking to plot a figure then I want that figure to update with every mouse buttton push to draw out a cross hair.
But I can't get it to update the figure after the pressing the button but it works great if I just plot outside on the class.
import cross_hair
import numpy as np
import matplotlib.pyplot as plt
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
import sys
class PushButton(QWidget):
def __init__(self):
super(PushButton,self).__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("PushButton")
self.setGeometry(400,400,300,260)
self.closeButton = QPushButton(self)
self.closeButton.setText("Press") #text
self.closeButton.clicked.connect(self.button_pressed)
def button_pressed(self):
#Fuction variables
self.x= np.arange(0, 1, 0.01)
self.y = np.sin(2 * 2 * np.pi * self.x)
#Figure
self.fig, self.ax = plt.subplots()
#title
self.ax.set_title('Snapping cursor')
#Plotting
self.line, = self.ax.plot(self.x, self.y, 'o')
#
snap_cursor = cross_hair.SnappingCursor(self.ax, self.line)
self.fig.canvas.mpl_connect('button_press_event', snap_cursor.on_mouse_click)
plt.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = PushButton()
ex.show()
sys.exit(app.exec_())
cross hair code looks like this (cross_hair.py)
import numpy as np
class SnappingCursor:
def __init__(self, ax, line):
self.ax = ax
self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
self.x, self.y = line.get_data()
self._last_index = None
# text location in axes coords
self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
self.text.set_visible(visible)
return need_redraw
def on_mouse_click(self, event):
if not event.inaxes:
self._last_index = None
need_redraw = self.set_cross_hair_visible(False)
print('you no pressed' )#, event.button, event.xdata, event.ydata)
if need_redraw:
self.ax.figure.canvas.draw()
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
print('you pressed') # , event.button, event.xdata, event.ydata)
index = min(np.searchsorted(self.x, x), len(self.x) - 1)
if index == self._last_index:
return # still on the same data point. Nothing to do.
self._last_index = index
x = self.x[index]
y = self.y[index]
# update the line positions
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
self.ax.figure.canvas.draw()
Solved
So I tried to solve this for some time and now did it with the easiest solution... I think?
added a function update_plot() in the class PushButton with the following
def update_plot(self):
print('Testing')
self.fig.canvas.mpl_connect('button_press_event', self.snap_cursor.on_mouse_click)
And in def button_pressed added
self.update_plot()
plt.show()
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.update_plot)
self.timer.start(100)
In def button_pressed
and ofc also used
from PyQt5 import QtCore
My ui file contains a widget container with a vertical layout named "VL" and a line edit named "Radiance". I created a single bar graph that I want to change as I input values into the line edit. At the moment, it does just that, except it creates a new plot every time. If I use my "remove" function it doesn't make a whole separate plot, but it ruins the layout of the one. I think the problem lies with my "remove" function and where to put it, please help.
I imported QtWidgets, uic, matplot.figure, and necessary backends:
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
uic.loadUi('PyQt_App1.ui', self)
self.setWindowTitle("Window Title")
self.Radiance.textChanged.connect(self.animate)
def animate(self):
self.remove()
r = self.Radiance.text()
if r:
rad = float(r)
positions = [0.25]
fig1 = Figure()
ax1f1 = fig1.add_subplot(111)
ax1f1.set_ylim([0, 100])
ax1f1.set_xlim([0, 0.5])
ax1f1.bar(positions, rad, width=0.2, color="g")
self.addmpl(fig1)
else:
r = 0
rad = float(r)
positions = [0.25]
fig1 = Figure()
ax1f1 = fig1.add_subplot(111)
ax1f1.set_ylim([0, 100])
ax1f1.set_xlim([0, 0.5])
ax1f1.bar(positions, rad, width=0.2, color="g")
self.addmpl(fig1)
def addmpl(self, fig):
self.canvas = FigureCanvas(fig)
self.VL.addWidget(self.canvas)
# self.canvas.setParent(self.Frame)
self.canvas.draw()
def remove(self):
self.VL.removeWidget(self.canvas)
self.canvas.close()
if __name__ == '__main__':
import sys
from PyQt5 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
main = MyWindow()
main.show()
sys.exit(app.exec_())
Instead of creating a new figure every time I would just keep references to the current bar plot and the current axes, and use those to update the figure, e.g.
from PyQt5 import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.pyplot import Figure
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
central = QtWidgets.QWidget(self)
self.VL = QtWidgets.QVBoxLayout(central)
self.Radiance = QtWidgets.QLineEdit(self)
self.VL.addWidget(self.Radiance)
self.canvas = FigureCanvas(Figure())
self.VL.addWidget(self.canvas)
self.ax1f1 = self.canvas.figure.subplots()
self.ax1f1.set_ylim([0, 100])
self.ax1f1.set_xlim([0, 0.5])
self.bar = None
self.setWindowTitle("Window Title")
self.setCentralWidget(central)
self.Radiance.textChanged.connect(self.animate)
def animate(self):
r = self.Radiance.text()
try:
rad = float(r)
except ValueError:
rad = 0
positions = [0.25]
if self.bar:
self.bar.remove()
self.bar = self.ax1f1.bar(positions, rad, width=0.2, color="g")
self.canvas.draw()
if __name__ == '__main__':
import sys
from PyQt5 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
main = MyWindow()
main.show()
sys.exit(app.exec_())
I am trying to write a GUI in Python3 using PyQt4.
For data visualization, I need to isolate a specific point on a curve plotted by the function whole_plot(). To do so, I am currently using a slider that let the GUI user choose a point of interest. When the slider_value is changed, the point is selected and plotted by calling the function point_plot().
Regarding some previous answers, I am now trying to update my graph through matplotlib.animation (cf. post python matplotlib update scatter plot from a function). But for some reasons, I still got the wrong updating, can someone help me figure out what is the problem in my code?
import sys
import numpy as np
from PyQt4 import QtGui
from PyQt4 import QtCore
import matplotlib.pyplot as plt
import matplotlib.animation
#
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
#%%
# some Arbitrary data
nbr_points = 500
my_X_data = np.linspace(-10,10,nbr_points)
my_Y_data = my_X_data**3 + 100*np.cos(my_X_data*5)
class MyWidget(QtGui.QWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.initUI()
def initUI(self):
self.setGeometry(600,300,1000,600)
grid = QtGui.QGridLayout()
self.setLayout(grid)
self.figure_1 = plt.figure(figsize=(15,5))
self.canvas_1 = FigureCanvas(self.figure_1)
self.toolbar = NavigationToolbar(self.canvas_1, self)
grid.addWidget(self.canvas_1, 2,0,1,2)
grid.addWidget(self.toolbar, 0,0,1,2)
# Slider
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
self.slider.setMinimum(0)
self.slider.setMaximum(nbr_points-1)
self.slider.setTickInterval(1)
self.slider.valueChanged.connect(self.point_plot)
# Slider info box
self.label = QtGui.QLabel(self)
grid.addWidget(self.label,4,0)
# +1 / -1 buttons
btn_plus_one = QtGui.QPushButton('+1', self)
btn_plus_one.clicked.connect(self.value_plus_one)
btn_minus_one = QtGui.QPushButton('-1', self)
btn_minus_one.clicked.connect(self.value_minus_one)
hbox = QtGui.QHBoxLayout()
hbox.addWidget(btn_minus_one)
hbox.addWidget(self.slider)
hbox.addWidget(btn_plus_one)
grid.addLayout(hbox, 3,0,1,3)
self.whole_plot()
self.point_plot()
self.show()
def whole_plot(self):
ax1 = self.figure_1.add_subplot(111)
ax1.clear()
ax1.cla()
#
ax1.plot(my_X_data,my_Y_data,'b-')
#
ax1.set_xlim([-10,10])
ax1.set_ylim([-1000,1000])
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
#
self.canvas_1.draw()
def point_plot(self):
ax1 = self.figure_1.add_subplot(111)
#
X_point, Y_point = [],[]
scat = ax1.scatter(X_point,Y_point, s=100,c='k')
def animate(i):
index_slider_value = self.slider.value()
X_point = my_X_data[index_slider_value,]
Y_point = my_Y_data[index_slider_value,]
scat.set_offsets(np.c_[X_point,Y_point])
anim = matplotlib.animation.FuncAnimation(self.figure_1,animate, frames=my_X_data, interval=200, repeat=True)
self.canvas_1.draw()
def value_plus_one(self):
# slider +1
if self.slider.value() < (my_X_data.shape[0]-1):
index_slider_value = self.slider.value() + 1
self.slider.setValue(index_slider_value)
def value_minus_one(self):
# slider -1
if self.slider.value() > 0:
index_slider_value = self.slider.value() - 1
self.slider.setValue(index_slider_value)
def main():
app = QtGui.QApplication(sys.argv)
MyWidget()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You have to learn what is better to reuse than create, in this case you just need to create a scatter and update the data with set_offsets() when the value of the slider changes, so FuncAnimation is not necessary.
import numpy as np
from PyQt4 import QtCore, QtGui
import matplotlib.pyplot as plt
import matplotlib.animation
#
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
#%%
# some Arbitrary data
nbr_points = 500
my_X_data = np.linspace(-10,10,nbr_points)
my_Y_data = my_X_data**3 + 100*np.cos(my_X_data*5)
class MyWidget(QtGui.QWidget):
def __init__(self):
super(MyWidget, self).__init__()
self.initUI()
def initUI(self):
self.setGeometry(600,300,1000,600)
self.figure_1 = plt.figure(figsize=(15,5))
self.canvas = FigureCanvas(self.figure_1)
self.toolbar = NavigationToolbar(self.canvas, self)
# Slider
self.slider = QtGui.QSlider(minimum=0,
maximum= nbr_points-1,
orientation=QtCore.Qt.Horizontal,
tickInterval=1)
self.slider.valueChanged.connect(self.on_valueChanged)
# Slider info box
self.label = QtGui.QLabel()
# +1 / -1 buttons
btn_plus_one = QtGui.QPushButton('+1')
btn_plus_one.clicked.connect(self.value_plus_one)
btn_minus_one = QtGui.QPushButton('-1')
btn_minus_one.clicked.connect(self.value_minus_one)
grid = QtGui.QGridLayout(self)
grid.addWidget(self.canvas, 2, 0, 1, 2)
grid.addWidget(self.toolbar, 0, 0, 1, 2)
grid.addWidget(self.label, 4, 0)
hbox = QtGui.QHBoxLayout()
hbox.addWidget(btn_minus_one)
hbox.addWidget(self.slider)
hbox.addWidget(btn_plus_one)
grid.addLayout(hbox, 3, 0, 1, 3)
self.whole_plot()
def whole_plot(self):
ax = self.figure_1.add_subplot(111)
ax.clear()
ax.plot(my_X_data,my_Y_data,'b-')
ax.set_xlim([-10,10])
ax.set_ylim([-1000,1000])
ax.set_xlabel('X')
ax.set_ylabel('Y')
self.canvas.draw()
X_point, Y_point = [],[]
self.scat = ax.scatter(X_point, Y_point, s=100,c='k')
# set initial
self.on_valueChanged(self.slider.value())
#QtCore.pyqtSlot(int)
def on_valueChanged(self, value):
X_point = my_X_data[value,]
Y_point = my_Y_data[value,]
self.scat.set_offsets(np.c_[X_point,Y_point])
self.canvas.draw()
#QtCore.pyqtSlot()
def value_plus_one(self):
self.slider.setValue(self.slider.value() + 1)
#QtCore.pyqtSlot()
def value_minus_one(self):
self.slider.setValue(self.slider.value() - 1)
def main():
import sys
app = QtGui.QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
On the other hand QSlider will not update the value if it is less than minimum or greater than maximum
is there any way to relate a click on the Canvas but outside of a plot axes to the closest axes of the click? I have a canvas with x number of subplots and I'm trying to find out the closest subplot near the mouse click. Ultimately, this would help me create a zoom-in-rectangle that allows the user to zoom in the area of selected subplots (the navigation toolbar only zoom in one subplot).
#import os
#os.environ['QT_API'] = 'pyside'
from PyQt4 import QtGui, QtCore
import sys
import matplotlib
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import numpy as np
class Canvas(FigureCanvasQTAgg):
def __init__(self, parent=None):
self.figure = Figure()
super(Canvas, self).__init__(self.figure)
self.ax1 = self.figure.add_subplot(1,1,1)
self.figure.subplots_adjust(left = 0.05, bottom = 0.02, right = 0.98, top = 0.99)
self.setMinimumWidth(1000)
self.ax1.plot([1,2,3])
self.draw()
def add_subplot(self, data=[]):
rows = len(self.figure.axes) + 1
for index, axes in enumerate(self.figure.axes, start=1):
axes.change_geometry(rows, 1, index)
ax = self.figure.add_subplot(rows, 1, index+1)
x = np.arange(1000,1)
y = np.arange(1000,1)
ax.step(x,y)
ax.patch.set_facecolor('None')
self.figure.set_figheight(self.figure.get_figheight()*self.figScalingfactor)
def figScaling(self, numSubplot):
self.figScalingfactor = round(1.1729*pow(numSubplot, -0.028),3)
class Window(QtGui.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.showMaximized()
self.widget = QtGui.QWidget()
self.setCentralWidget(self.widget)
self.widget.setLayout(QtGui.QVBoxLayout())
self.widget.layout().setContentsMargins(0,0,0,0)
self.widget.layout().setSpacing(5)
self.canvas = Canvas(self)
self.scroll = QtGui.QScrollArea(self.widget)
self.scroll.setWidget(self.canvas)
self.scroll.setWidgetResizable(False)
self.nav = NavigationToolbar(self.canvas, self.widget)
self.numSubplots = 30
self.canvas.figScaling(self.numSubplots)
for x in range(self.numSubplots):
self.canvas.add_subplot()
self.canvas.adjustSize()
self.canvas.draw_idle()
self.widget.layout().addWidget(self.nav)
self.widget.layout().addWidget(self.scroll)
self.showVline = False
self.hoveringLine = None
self.canvas.mpl_connect("scroll_event", self.scrolling)
self.canvas.mpl_connect("button_press_event", self.onClick)
self.canvas.mpl_connect("motion_notify_event", self.onMove)
def scrolling(self, event):
val = self.scroll.verticalScrollBar().value()
if event.button =="down":
self.scroll.verticalScrollBar().setValue(val+100)
else:
self.scroll.verticalScrollBar().setValue(val-100)
def onClick(self, event):
if event.dblclick and self.showVline == False:
self.background = self.canvas.copy_from_bbox(self.canvas.figure.bbox)
self.showVline = True
self.hoveringLine = self.canvas.ax1.axvline(x=event.xdata, ymin=-1.2*self.numSubplots, ymax=1.2,
lw=2, zorder=0, clip_on=False)
elif event.dblclick and self.showVline:
self.showVline = False
self.hoveringLine = None
self.canvas.ax1.axvline(x=event.xdata, ymin=-1.2*self.numSubplots, ymax=1.2,
lw=2, zorder=0, clip_on=False)
self.canvas.draw()
else:
print(event.xdata)
print(event.ydata)
def onMove(self, event):
if (self.showVline):
self.canvas.restore_region(self.background)
self.hoveringLine.set_xdata(event.xdata)
self.canvas.ax1.draw_artist(self.hoveringLine)
self.canvas.blit(self.canvas.figure.bbox)
def main():
app = QtGui.QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
GUI = Window()
GUI.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
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) 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()