I'm displaying an image using imshow inside a tkinter GUI and I have added a NavigationToolbar2TkAgg (python3.5). Very similarly to this question (check it out for visuals), I want to change the formatting of the coordinates and z values (in particular, all my z values are between 0-10000, so I want to write them out instead of using scientific notation).
For x and y this was pretty easy to do by changing the format_coord handle, but I can't seem to find anything to change the final bit [xxx]. I have tried format_cursor_data, but doesn't seem to be it either.
Anyone know a solution?
It seems that the [z] value is only shown for imshow, not for regular plots.
Here goes a minimal code sample to reproduce the problem
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg,NavigationToolbar2TkAgg
top = tk.Tk()
fig = plt.figure()
plt.imshow(np.array([[0,1],[1,2]]))
ax = fig.gca()
ax.format_coord = lambda x,y: "x:%4u, y:%4u" % (x,y)
ax.format_cursor_data = lambda z: "Hello" % (z) # does nothing
canvas = FigureCanvasTkAgg(fig,master=top)
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
canvas.show()
toolbar = NavigationToolbar2TkAgg(canvas, top)
toolbar.update()
top.mainloop()
In the mouse_move() method of Matplotlib's NavigationToolbar2 class, the cursor data and its formatting are obtained from the topmost Artist in the Axes instance, rather than from the Axes instance itself, like for the coordinates. So what you should do is:
fig = Figure()
ax = fig.add_subplot(111)
img = np.array([[0,10000],[10000,20000]])
imgplot = ax.imshow(img, interpolation='none')
# Modify the coordinates format of the Axes instance
ax.format_coord = lambda x,y: "x:{0:>4}, y:{0:>4}".format(int(x), int(y))
# Modify the cursor data format of the Artist created by imshow()
imgplot.format_cursor_data = lambda z: "Hello: {}".format(z)
Related
I am trying to integrate matplotlib into tkinter, this is doable with FigureCanvasTkAgg. However, in my case I want to remove the y axis ticks and then make the graph cover the entire available area (i.e. remove the padding). Removing the y axis was not difficult but I cannot find any information on removing the padding.
If I found a solution (for example: Remove padding from matplotlib plotting) it included using plt.savefig, but saving the image wouldn't help me. I guess I could save the image and display it that way although that would feel pretty hacky. Is there a better way?
My code so far:
import customtkinter
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib
import numpy as np
root = customtkinter.CTk()
root.geometry('600x500')
# figure
figure = plt.Figure()
ax = figure.add_subplot(111)
ax.yaxis.set_visible(False)
t = np.arange(0, 3, .01)
ax.plot(t, 2 * np.sin(2 * np.pi * t))
# add widget
canvas = FigureCanvasTkAgg(figure, master=root)
canvas.draw()
canvas.get_tk_widget().pack(fill='both', expand=True)
root.mainloop()
I have scoured the web for creating a live matplotlib scatter plot within a tkinter window but have found copious amounts of varying information making it confusing to decipher.As an example, I sometimes see some people use matplotlib.pyplot and I sometimes see some people use matplotlib.figure. I have no idea what the real differences between these two modules are.
I have created the example code below which I thought should simply create a matplotlib scatter plot inside the tkinter root window when I click the "Graph It" button. It does nothing when I click it though. The ultimate goal is to have a scatter plot within tkinter that updates whenever new data is read from a sensor but I'm starting simple. It should also be noted this is my first exposure to matplotlib so it may be something trivial I'm overlooking. Any help is appreciated.
#Python 3.7.9#
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,NavigationToolbar2Tk)
import numpy as np
figure = Figure(figsize = (5,5), dpi = 100)
ax = figure.add_subplot(111)
def plot():
x = np.random.rand(1,10)
y = np.random.rand(1,10)
ax.scatter(x,y)
root = tk.Tk()
canvas = FigureCanvasTkAgg(figure, root)
canvas.draw()
canvas.get_tk_widget().pack(pady = 10)
toolbar = NavigationToolbar2Tk(canvas,root)
toolbar.update()
canvas.get_tk_widget().pack()
button = tk.Button(root, text = "Graph It", command = plot)
button.pack()
root.mainloop()
The problem lies in that you do not "update" the plot.
With the minimal modification to your code, you just have to arrange to redraw the figure.
For example, you can use your wrapper canvas object (or the figure.canvas) to update the figure's canvas in each data update step as follows:
import tkinter as tk
import numpy as np
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure
figure = Figure(figsize=(5, 5), dpi=100)
ax = figure.add_subplot(111)
def plot():
x = np.random.rand(1, 10)
y = np.random.rand(1, 10)
ax.scatter(x, y)
canvas.draw()
root = tk.Tk()
canvas = FigureCanvasTkAgg(figure, root)
canvas.draw()
canvas.get_tk_widget().pack(pady=10)
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack()
button = tk.Button(root, text="Graph It", command=plot)
button.pack()
root.mainloop()
Output window:
However, in this case you add/create a new scatter plot in each update step. If you wish to extend the original data and update according to the updated data, you can find different methods in this post to update a single scatter plot.
I am trying to use matplotlib LassoSelector to select some points from a scatter plot and produce a separate figure for selected points only. When I try to use another matplotlib widget on the second plot it doesn't work but there is no error or warning message. Below is a minimal example with LassoSelector and SpanSelector used.
I tried other widgets too; the Button widget displays the button but the action on the button press is not performed.
import numpy as np
from matplotlib.pyplot import *
from matplotlib.widgets import SpanSelector, LassoSelector
from matplotlib.path import Path
def onselect(verts):
global xys,data
#get indexes of selected points
path = Path(verts)
xysn = xys.get_offsets()
ind = np.nonzero([path.contains_point(xy) for xy in xysn])[0]
#plot the second figure
fig=figure(2)
ax=fig.add_subplot(111)
ax.hist(data[:,0][ind],10)
#this should be executed when SpanSelector is used
def action(min,max):
print min,max
#try to do SpanSelector (this fails)
span=SpanSelector(ax,action,'horizontal')
show()
#initialize a figure
fig=figure(1)
ax=fig.add_subplot(111)
#create data
data=np.array([[1,6], [4,8],[0,4],[4,2],[9,6],[10,8],[2,2],[5,5],[0,4],[4,5]])
#plot data
xys=ax.scatter(data[:,0],data[:,1])
#select point by drawing a path around them
lasso = LassoSelector(ax, onselect=onselect)
show()
matplotlib widgets are event driven, so wait for user input. The problem with you code is you are trying to create a new figure with a new event handler SpanSelector. I'm not sure if you can add new events as a result of previous ones and with SpanSelector commented out, I get the following error,
QCoreApplication::exec: The event loop is already running
So the new event, LassoSelector is not registered and user input is not picked up (and the new figure doesn't appear). It is better to create all figures and register all possible events at the beginning of the code. The following should be closer to what you want to do,
import numpy as np
from matplotlib.pyplot import *
from matplotlib.widgets import SpanSelector, LassoSelector
from matplotlib.path import Path
#this should be executed when LassoSelector is used
def onselect(verts):
global xys,data
#get indexes of selected points
path = Path(verts)
xysn = xys.get_offsets()
ind = np.nonzero([path.contains_point(xy) for xy in xysn])[0]
#Clear and update bar chart
h, b = np.histogram(data[:,0][ind],10)
for rect, bars in zip(rects, h):
rect.set_height(bars)
ax2.bar(mb, h, align='center')
draw()
#this should be executed when SpanSelector is used
def action(min,max):
print min,max
#initialize figures
fig1=figure(1)
ax1=fig1.add_subplot(111)
fig2=figure(2)
ax2=fig2.add_subplot(111)
#create data
data=np.array([[1,6],[4,8],[0,4],[4,2],[9,6],[10,8],[2,2],[5,5],[0,4],[4,5]])
#plot data
xys=ax1.scatter(data[:,0],data[:,1])
#Plot initial histogram of all data
h, b = np.histogram(data[:,0],10)
mb = [0.5*(b[i]+b[i+1]) for i in range(b.shape[0]-1)]
rects = ax2.bar(mb, h, align='center')
#Register lasso selector
lasso = LassoSelector(ax1, onselect=onselect)
#Register SpanSelector
span=SpanSelector(ax2,action,'horizontal')
show()
Note, in order to update bar charts, it's a little more tricky than plots so I used this answer here Dynamically updating a bar plot in matplotlib
For some reason, the histogram figure 2 only updates when you click on it. I would consider using a single figure with two axes for this which may be easier to work with,
fig, ax = subplots(2,1)
ax1 = ax[0]; ax2 = ax[1]
I would like to use the ipython notebook widgets to add some degree of interactivity to inline matplotlib plots.
In general the plot can be quite heavy and I want to only update a specific element of the plot. I understand that widgets have a throttling feature built-in that helps to don't flood the kernel, but when the plot takes let say 30s I don't want to wait so long just to update a line.
By reading the example notebooks I was able to create a basic example in which I add a cross cursor (driven by 2 sliders) to a mpl axis.
The problem is that the figure is displayed twice. Here is the code (cell 1):
fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
... figure displayed ..., cell 2 (edit: thanks Thomas K for the improvement):
vline = ax.axvline(1)
hline = ax.axhline(0.5)
def set_cursor(x, y):
vline.set_xdata((x, x))
hline.set_ydata((y, y))
display(fig)
and finally (cell 3):
interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))
shows again the figure with the widgets.
So the question is:
how can I inhibit the first figure display?
is that the right way to do it or is there a better approach?
EDIT
I found an ipython config knob that, according to this notebook, allows inhibiting the figure display
%config InlineBackend.close_figures = False
While the example notebook works, I can't figure out how to use this option by itself (without the context manager class provided in the linked example) to hide a figure display.
EDIT 2
I found some documentation of the InlineBackend.close_figures configurable.
EDIT 3
Triggered by #shadanan answer, I want to clarify that my purpose is to add a cursor to an existing figure without redrawing the plot from scratch at each cursor movement. Merging the 3 cells in a single cell:
fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
vline = ax.axvline(1)
hline = ax.axhline(0.5)
def set_cursor(x, y):
vline.set_xdata((x, x))
hline.set_ydata((y, y))
display(fig)
interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))
it "should" work but it doesn't. The first time the cell is executed it shows 2 figures. After widget interaction only 1 figure is displayed. This is the "strange behavior" that requires a workaround like the one shown in #shadanan answer. Can an ipython dev comment on this? Is it a bug?
The solution turns out to be really simple. To avoid showing the first figure we just need to add a close() call before the interact call.
Recalling the example of the question, a cell like this will correctly show a single interactive figure (instead of two):
fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
plt.close(fig)
vline = ax.axvline(1)
hline = ax.axhline(0.5)
def set_cursor(x, y):
vline.set_xdata((x, x))
hline.set_ydata((y, y))
display(fig)
interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01))
A cleaner approach is defining the function add_cursor (in a separate cell or script):
def add_cursor(fig, ax):
plt.close(fig)
vline = ax.axvline(1, color='k')
hline = ax.axhline(0.5, color='k')
def set_cursor(x, y):
vline.set_xdata((x, x))
hline.set_ydata((y, y))
display(fig)
interact(set_cursor, x=ax.get_xlim(), y=ax.get_ylim())
and then call it whenever we want to add an interactive cursor:
fig, ax = plt.subplots()
ax.plot([3,1,2,4,0,5,3,2,0,2,4])
add_cursor(fig, ax)
I have a hacky workaround that will only display one figure. The problem seems to be that there are two points in the code that generate a figure and really, we only want the second one, but we can't get away with inhibiting the first. The workaround is to use the first one for the first execution and the second one for all subsequent ones. Here's some code that works by switching between the two depending on the initialized flag:
%matplotlib inline
import matplotlib.pyplot as plt
from IPython.html.widgets import interact, interactive, fixed
from IPython.html import widgets
from IPython.display import clear_output, display, HTML
class InteractiveCursor(object):
initialized = False
fig = None
ax = None
vline = None
hline = None
def initialize(self):
self.fig, self.ax = plt.subplots()
self.ax.plot([3,1,2,4,0,5,3,2,0,2,4])
self.vline = self.ax.axvline(1)
self.hline = self.ax.axhline(0.5)
def set_cursor(self, x, y):
if not self.initialized:
self.initialize()
self.vline.set_xdata((x, x))
self.hline.set_ydata((y, y))
if self.initialized:
display(self.fig)
self.initialized = True
ic = InteractiveCursor()
def set_cursor(x, y):
ic.set_cursor(x, y)
interact(set_cursor, x=(1, 9, 0.01), y=(0, 5, 0.01));
My opinion is that this should be considered a bug. I tried it with the object oriented interface and it has the same problem.
You can do this in a very strait forward way using the new(ish) notebook backend
%matplotlib notebook
import matplotlib.pyplot as plt
from IPython.html.widgets import interactive
fig, ax = plt.subplots()
ax.plot(range(5))
vline = ax.axvline(1, color='k')
hline = ax.axhline(0.5, color='k')
def set_cursor(x, y):
vline.set_xdata((x, x))
hline.set_ydata((y, y))
ax.figure.canvas.draw_idle()
and in a separate cell:
interactive(set_cursor, x=ax.get_xlim(), y=ax.get_ylim())
This will still re-draw the entire figure every time you move the cursor because notebook does not currently support blitting (which is being worked on https://github.com/matplotlib/matplotlib/pull/4290 )
I want to plot a sequence of .png images in matplotlib. The goal is to plot them rapidly to simulate the effect of a movie, but I have additional reasons for wanting to avoid actually creating an .avi file or saving matplotlib figures and then viewing them in sequence outside of Python.
I'm specifically trying to view the image files in sequence inside a for-loop in Python. Assuming I have imported matplotlib correctly, and I have my own functions 'new_image()' and 'new_rect()', here's some example code that fails to work because of the blocking effect of the show() function's call to the GUI mainloop:
for index in index_list:
img = new_image(index)
rect = new_rect(index)
plt.imshow(img)
plt.gca().add_patch(rect)
plt.show()
#I also tried pausing briefly and then closing, but this doesn't
#get executed due to the GUI mainloop from show()
time.sleep(0.25)
plt.close()
The above code works to show only the first image, but then the program just hangs and waits for me to manually close the resultant figure window. Once I do close it, the program then just hangs and doesn't re-plot with the new image data. What should I be doing? Also note that I have tried replacing the plt.show() command with a plt.draw() command, and then adding the plt.show() outside of the for-loop. This doesn't display anything and just hangs.
Based on http://matplotlib.sourceforge.net/examples/animation/simple_anim_tkagg.html:
import time
import numpy as np
import matplotlib
matplotlib.use('TkAgg') # do this before importing pylab
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
def animate():
tstart = time.time() # for profiling
data=np.random.randn(10,10)
im=plt.imshow(data)
for i in np.arange(1,200):
data=np.random.randn(10,10)
im.set_data(data)
fig.canvas.draw() # redraw the canvas
print 'FPS:' , 200/(time.time()-tstart)
win = fig.canvas.manager.window
fig.canvas.manager.window.after(100, animate)
plt.show()
plt.imshow can accept a float array, uint8 array, or a PIL image.
So if you have a directory of PNG files, you could open them as PIL images and animate them like this:
import matplotlib
matplotlib.use('TkAgg') # do this before importing pylab
import matplotlib.pyplot as plt
import Image
import glob
fig = plt.figure()
ax = fig.add_subplot(111)
def animate():
filenames=sorted(glob.glob('*.png'))
im=plt.imshow(Image.open(filenames[0]))
for filename in filenames[1:]:
image=Image.open(filename)
im.set_data(image)
fig.canvas.draw()
win = fig.canvas.manager.window
fig.canvas.manager.window.after(100, animate)
plt.show()
The best way I have found for this was with the command pylab.ion() after you import pylab.
Here is a script that does use show(), but which displays the different plots each time pylab.draw() is called, and which leaves the plot windows showing indefinitely. It uses simple input logic to decide when to close the figures (because using show() means pylab won't process clicks on the windows x button), but that should be simple to add to your gui as another button or as a text field.
import numpy as np
import pylab
pylab.ion()
def get_fig(fig_num, some_data, some_labels):
fig = pylab.figure(fig_num,figsize=(8,8),frameon=False)
ax = fig.add_subplot(111)
ax.set_ylim([0.1,0.8]); ax.set_xlim([0.1, 0.8]);
ax.set_title("Quarterly Stapler Thefts")
ax.pie(some_data, labels=some_labels, autopct='%1.1f%%', shadow=True);
return fig
my_labels = ("You", "Me", "Some guy", "Bob")
# To ensure first plot is always made.
do_plot = 1; num_plots = 0;
while do_plot:
num_plots = num_plots + 1;
data = np.random.rand(1,4).tolist()[0]
fig = get_fig(num_plots,data,my_labels)
fig.canvas.draw()
pylab.draw()
print "Close any of the previous plots? If yes, enter its number, otherwise enter 0..."
close_plot = raw_input()
if int(close_plot) > 0:
pylab.close(int(close_plot))
print "Create another random plot? 1 for yes; 0 for no."
do_plot = raw_input();
# Don't allow plots to go over 10.
if num_plots > 10:
do_plot = 0
pylab.show()
By modifying the basic logic here, I can have it close windows and plot images consecutively to simulate playing a movie, or I can maintain keyboard control over how it steps through the movie.
Note: This has worked for me across platforms and seems strictly superior to the window canvas manager approach above, and doesn't require the 'TkAgg' option.
I have implemented a handy script that just suits your need. Try it out here
Below is a example that show images together with its bounding box:
import os
import glob
from scipy.misc import imread
from matplotlib.pyplot import Rectangle
video_dir = 'YOUR-VIDEO-DIRECTORY'
img_files = glob.glob(os.path.join(video_dir, '*.jpg'))
box_files = glob.glob(os.path.join(video_dir, '*.txt'))
def redraw_fn(f, axes):
img = imread(img_files[f])
box = bbread(box_files[f]) # Define your own bounding box reading utility
x, y, w, h = box
if not redraw_fn.initialized:
im = axes.imshow(img, animated=True)
bb = Rectangle((x, y), w, h,
fill=False, # remove background
edgecolor="red")
axes.add_patch(bb)
redraw_fn.im = im
redraw_fn.bb = bb
redraw_fn.initialized = True
else:
redraw_fn.im.set_array(img)
redraw_fn.bb.set_xy((x, y))
redraw_fn.bb.set_width(w)
redraw_fn.bb.set_height(h)
redraw_fn.initialized = False
videofig(len(img_files), redraw_fn, play_fps=30)