I have a program which shows an image (fig 1). When the image is clicked it shows the colour in the image that was clicked in a separate Matplotlib window (fig 2). Fig 2 has some buttons that call different functions when they are clicked.
My problem is that the functions that are meant to be called in fig 2 are being called when fig 1 is clicked.
The code looks like this:
def show_fig1(img):
# Plot the image
plt.figure(1)
ax = plt.gca()
fig = plt.gcf()
implot = ax.imshow(img)
# Detect a click on the image
cid = fig.canvas.mpl_connect('button_press_event', on_pixel_click)
plt.show(block=True)
# Called when fig1 is clicked
def on_pixel_click(event):
if event.xdata != None and event.ydata != None:
# Do some computation here that gets the image for fig2
img = get_fig2_img()
show_fig2(img, event)
def show_fig2(img, event):
plt.figure(2)
plt.imshow(img)
# Specify coordinates of the button
ax = plt.axes([0.0, 0.0, 0.2, 0.1])
# Add the button
button = Button(ax, 'button')
# Detect a click on the button
button.on_clicked(test())
plt.show(block=True)
def test():
print "Button clicked"
So test() is called instantly when on_pixel_click() is called even though theoretically it should wait until the button is clicked because of the button.on_clicked() command.
Any help?
Thanks in advance :)
On this line:
button.on_clicked(test())
You are telling Python to execute your test function, rather than just passing a reference to it. Remove the brackets and it should sort it:
button.on_clicked(test)
Related
I would like to interactively change a matplotlib.animation argument depending on the value provided by a GUI.
Example:
I prepared an example code which I show below, where I am trying to change the interval argument of animation based on a value provided by the user through a spinBox created with tkinter.
Problem:
In order to be able to update its argument, I want to call my animation into the call back function called by the spinbox. But if I do that, I get the following error message " UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation."
If I call my animation into the main code, then I won't be able to interactively change its arguments
Question:
How can I change an animation argument interactively, i.e. based on a value which the user can set in a tkinter widget?
Example code:
import tkinter as tk
from random import randint
import matplotlib as plt
import matplotlib.animation as animation
import matplotlib.backends.backend_tkagg as tkagg
#Creating an instance of the Tk class
win = tk.Tk()
#Creating an instance of the figure class
fig = plt.figure.Figure()
#Create a Canvas containing fig into win
aCanvas =tkagg.FigureCanvasTkAgg(fig, master=win)
#Making the canvas a tkinter widget
aFigureWidget=aCanvas.get_tk_widget()
#Showing the figure into win as if it was a normal tkinter widget
aFigureWidget.grid(row=0, column=0)
#Defining the animation
ax = fig.add_subplot(xlim=(0, 1), ylim=(0, 1))
(line,) = ax.plot([],[], '-')
CumulativeX, CumulativeY = [], []
# Providing the input data for the plot for each animation step
def update(i):
CumulativeX.append(randint(0, 10) / 10)
CumulativeY.append(randint(0, 10) / 10)
return line.set_data(CumulativeX, CumulativeY)
spinBoxValue=1000
#When the button is pushed, get the value
def button():
spinBoxValue=aSpinbox.get()
#Running the animation
ani=animation.FuncAnimation(fig, update, interval=spinBoxValue, repeat=True)
#Creating an instance of the Spinbox class
aSpinbox = tk.Spinbox(master=win,from_=0, to=1000, command=button)
#Placing the button
aSpinbox .grid(row=2, column=0)
#run the GUI
win.mainloop()
We have to redraw the animation using fig.canvas.draw() when the animation is created inside the function button:
def button():
global spinBoxValue, CumulativeX, CumulativeY, ani
spinBoxValue = aSpinbox.get()
CumulativeX, CumulativeY = [], [] # This is optional
# To stop the background animation
ani.event_source.stop()
# Unlink/delete the reference to the previous animation
# del ani
ani=animation.FuncAnimation(fig, update, interval=int(spinBoxValue) * 1000, repeat=False)
fig.canvas.draw()
In the code provided, it was drawing the lines too fast when it was recreating animation using the value from aSpinbox.get(), so I changed the input to integer to draw the animation at a slower rate using interval=int(spinBoxvalue) * 1000 inside the button function.
On deleting the animation
Since we have to stop the background animation and also run the newly generated animation when the button is pressed, and because an animation must be stored in a variable as long as it runs, we will have to refer to the previous and the latest animation by the same variable name.
We can delete the animation stored in the global variable ani, using del ani after ani.event_source.stop(), which would lose the reference to the animation stored in memory before the button was pressed, but we can't really free the memory address where the reference by ani was made (I am guessing this would be true as long as we are using default garbage collection method in Python).
EDIT
Jumping to a new animation will not update/remove any variables created on the axes here - we will have to take care of it explicitly. To update variables only once after pressing the button, first create those variables in the global scope of code, and delete them inside button function and recreate/define them before/after using fig.canvas.draw:
# Defined in global scope
text = ax.text(0.7, 0.5, "text")
def button():
global spinBoxValue, CumulativeX, CumulativeY, ani, text
spinBoxValue = int(aSpinbox.get())
# To stop the background animation
ani.event_source.stop()
CumulativeX, CumulativeY = [], []
# Unlink/delete the reference to the previous animation
# del ani
text.remove()
text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")
ani=animation.FuncAnimation(fig, update, interval=spinBoxValue*1000, repeat=False)
fig.canvas.draw()
The same logic can be applied to use update function to redraw text after every button press or after every frame while using the function button provided at the very top:
text = ax.text(0.7, 0.5, "text")
# Providing the input data for the plot for each animation step
def update(i):
global text
text.remove()
# Update text after button press
# "text" is drawn at (0.7 * 1000/10, 0.5) when button = 0
text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")
# Comment previous line and uncomment next line to redraw text at every frame
# text = ax.text(0.7 * i/10 , 0.5, "text")
CumulativeX.append(randint(0, 10) / 10)
CumulativeY.append(randint(0, 10) / 10)
print(CumulativeX)
return line.set_data(CumulativeX, CumulativeY)
I have tried to create an interactive matplotlib plot using some functions. I want to group the functions into one class (I am still new to this, took help from someone else's code)
import matplotlib.pyplot as plt
def draw_line(startx,starty):
ax = plt.gca()
xy = plt.ginput(1)
x = [startx,xy[0][0]]
y = [starty,xy[0][1]]
line = ax.plot(x,y, picker=True , pickradius = 5 , color = "blue")
ax.figure.canvas.draw()
def onclick(event):
"""
This implements click functionality. If it's a double click do something,
else ignore.
Once in the double click block, if its a left click, wait for a further
click and draw a line between the double click co-ordinates and that click
(using ginput(1) - the 1 means wait for one mouse input - a higher number
is used to get multiple clicks to define a polyline)
"""
ax = plt.gca()
if event.dblclick:
if event.button == 1:
# Draw line
draw_line(event.xdata,event.ydata) # here you click on the plot
else:
pass # Do nothing
if event.button == 1:
pass
def onpick(event):
ax = plt.gca()
"""
Handles the pick event - if an object has been picked, store a
reference to it. We do this by simply adding a reference to it
named 'stored_pick' to the axes object. Note that in python we
can dynamically add an attribute variable (stored_pick) to an
existing object - even one that is produced by a library as in this
case
"""
this_artist = event.artist # the picked object is available as event.artist
ax.picked_object = this_artist
def on_key(event):
"""
Function to be bound to the key press event
If the key pressed is delete and there is a picked object,
remove that object from the canvas
"""
if event.key == u'delete':
ax = plt.gca()
if ax.picked_object:
ax.picked_object.remove()
ax.picked_object = None
ax.figure.canvas.draw()
def applyplt():
fig = plt.gcf()
ax = plt.gca()
cidonclic = fig.canvas.mpl_connect('button_press_event', onclick)
cidonpic = fig.canvas.mpl_connect('pick_event', onpick)
cidonkey = fig.canvas.mpl_connect('key_press_event', on_key)
"""
Basic Plot to test the function.
"""
fig1 = plt.figure(figsize = (10,10))
gs = fig1.add_gridspec(10,10)
ax101 = fig1.add_subplot(gs[:,:])
ax101.set_ylim(0,10)
ax101.set_xlim(0,10)
applyplt()
plt.show()
I want to group these event functions in one class name(object) (e.g.: class Drawer(object))
If any other optimization can be done, please suggest that too. Thanks!
Here are the functions grouped into a class:
import matplotlib.pyplot as plt
class Interactivity:
def __init__(self, fig = None):
self.fig = plt.gcf() if fig is None else fig
self.ax = self.fig.gca()
self.connections = ()
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
def connect(self):
""" Install the event handlers for the plot. """
self.connections = (
self.fig.canvas.mpl_connect('button_press_event', self.onclick),
self.fig.canvas.mpl_connect('pick_event', self.onpick),
self.fig.canvas.mpl_connect('key_press_event', self.on_key),
)
def disconnect(self):
""" Uninstall the event handlers for the plot. """
for connection in self.connections:
self.fig.canvas.mpl_disconnect(connection)
def draw_line(self, startx, starty):
xy = plt.ginput(1)
x = [startx, xy[0][0]]
y = [starty, xy[0][1]]
self.ax.plot(x, y, picker=True, pickradius=5, color='blue')
self.ax.figure.canvas.draw()
def onclick(self, event):
"""
This implements click functionality. If it's a double click do
something, else ignore.
Once in the double click block, if its a left click, wait for a further
click and draw a line between the double click co-ordinates and that
click (using ginput(1) - the 1 means wait for one mouse input - a
higher number is used to get multiple clicks to define a polyline)
"""
print('onclick')
if event.dblclick:
if event.button == 1:
self.draw_line(event.xdata, event.ydata)
def onpick(self, event):
"""
Handles the pick event - if an object has been picked, store a
reference to it. We do this by simply adding a reference to it
named 'picked_object' to the axes object.
"""
print('onpick')
this_artist = event.artist
# the picked object is available as event.artist
self.ax.picked_object = this_artist
def on_key(self, event):
"""
Function to be bound to the key press event
If the key pressed is delete and there is a picked object,
remove that object from the canvas
"""
print('onkey: ', event.key)
if event.key == 'delete' and self.ax.picked_object:
self.ax.picked_object.remove()
self.ax.picked_object = None
self.ax.figure.canvas.draw()
Usage:
# Basic plot to test the functionality
fig = plt.figure(figsize = (10,10))
gs = fig.add_gridspec(10,10)
ax101 = fig.add_subplot(gs[:,:])
ax101.set_ylim(0,10)
ax101.set_xlim(0,10)
with Interactivity():
plt.show()
As you can see, it can be used as a context handler to install and uninstall the handlers automatically, or you can create an instance of it, and then call its connect method to install the handlers manually (and then later optionally call the disconnect method to uninstall the handlers).
Through the program interface button or mouse click to make the program pause, click the button or mouse again to allow the program to continue from the place where it was last stopped, the focus is to pause how to achieve.The time from the last pause again is uncertain.My codes are following:
import matplotlib.pyplot as plt
class ab():
def __init__(self,x,y):
self.x=x
self.y=y
a=ab(2,4)
b=ab(2,6)
c=ab(2,8)
d=ab(6,10)
f=ab(6,13)
e=ab(6,15)
task=[]
task.append(a)
task.append(b)
task.append(c)
task.append(d)
task.append(e)
task.append(f)
for i in task:
while i.x<=30:
print(i.x)
plt.plot(i.x,i.y,'o')
plt.pause(0.1)
i.x=i.x+2
i.y=i.y+2
plt.show()
You can combine matplotlib's animation module with matplotlib's event connections. The code below should do more or less what you want:
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# I'm replacing your class with two lists for simplicity
x = [2,2,2,6,6,6]
y = [4,6,8,10,13,15]
# Set up the figure
fig, ax = plt.subplots()
point, = ax.plot(x[0],y[0],'o')
# Make sure all your points will be shown on the axes
ax.set_xlim(0,15)
ax.set_ylim(0,15)
# This function will be used to modify the position of your point
def update(i):
point.set_xdata(x[i])
point.set_ydata(y[i])
return point
# This function will toggle pause on mouse click events
def on_click(event):
if anim.running:
anim.event_source.stop()
else:
anim.event_source.start()
anim.running ^= True
npoints = len(x)
# This creates the animation
anim = FuncAnimation(fig, update, npoints, interval=100)
anim.running=True
# Here we tell matplotlib to call on_click if the mouse is pressed
cid = fig.canvas.mpl_connect('button_press_event', on_click)
# Finally, show the figure
plt.show()
I have a separate plot thread with matplotlib and multiprocesses. Now if I interactively zoom in to the window, autoscale_view() does not work anymore (fixed with using autoscale()). But the "home" button in the Toolbox is still not working: It seems to call autoscale_view() and does not show the updated view but the old view (at point when zoomed in). Example code:
import matplotlib
matplotlib.use("qt4agg")
from matplotlib import pyplot as plt
import multiprocessing
def reset_view():
plt.get
xdata = []
ydata = []
temp = 0
test_ax = plt.gca()
test_line, = plt.plot(xdata, ydata)
plt.show(block=False)
for i in range(10):
temp = temp+1
xdata.append(temp)
ydata.append(temp)
test_line.set_data(xdata, ydata)
test_ax.relim()
test_ax.autoscale(tight= False)
plt.show(block=False)
plt.pause(2)
plot_thread = multiprocessing.Process(target = reset_view, args = ())
reset_view()
if __name__ == '__main__':
plot_thread.start()
Try zooming in during plotting and pressing the Home Button after. Is there a way to either make the home button use autoscale() instead of autoscale_view() or reset & update the toolbar history, so that it doesn't jump back to old views?
P.s.: "Home"-button = reset original view
I finally was able to figure it out by trial & error. There is a function to update the toolbar. This updates the toolbar's history and sets the home() function to the new view. Solution:
figure = plt.gcf() #Get current figure
toolbar = figure.canvas.toolbar #Get the toolbar handler
toolbar.update() #Update the toolbar memory
plt.show(block = False) #Show changes
I am trying to plot a polygon of user clicks and render them over a matplotlib canvas:
def helperClick(self, clickEvent):
self.lastXClick = clickEvent.x
self.lastYClick = clickEvent.y
self.lastButtonClick = clickEvent.button
def measurePoly(self):
self.lastButtonClick = None
cid = self.ui.canvas2.mpl_connect('button_press_event', self.helperClick)
#Exit render loop on right click
while self.lastButtonClick != 3:
print('waiting')
if self.lastButtonClick == 1:
#Eventually render polygon on click of 1
print('clicked')
self.ui.canvas2.mpl_disconnect(cid)
#do more stuff with polygon data
I am just trying to "wait" for user clicks, do something on a user click, then continue down the function on a left-click. However, my infinite loop freezes up python and crashes. I know this is a bad way to do this (clearly as it doesn't work :P) but I am not sure how to properly do this.
Thanks,
tylerthemiler
It sounds like you're trying to manually run the "mainloop" in your own code?
The whole point of using callback functions is that you let the gui toolkit run its own mainloop (in this case, it's entered when you call show).
Here's a very simple example of something along the lines of what you're trying to do.
It adds verticies when you left-click on the (initially blank) plot, and then draws the corresponding polygon when you right click. Nothing is drawn until you right-click (It's not too hard to draw the polygon while you're adding points by left-clicking, but doing it efficiently in matplotlib is a bit verbose).
import matplotlib.pyplot as plt
class Plot(object):
def __init__(self):
self.poly = []
self.fig, self.ax = plt.subplots()
self.ax.axis([0, 10, 0, 10])
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
plt.show()
def on_click(self, event):
if event.button == 1:
self.poly.append((event.xdata, event.ydata))
elif event.button == 3:
self.draw_poly()
def draw_poly(self):
self.ax.fill(*zip(*self.poly))
self.poly = []
self.fig.canvas.draw()
Plot()