Python: how to get coordinates on mouse click using matplotlib.canvas - python

I am writing a class to process images. In that class, I want to define a method that can allow me to return coordinates of the mouse clicks. I can get the coordinates as an attribute but if I call the method to return the coordinates, I get an empty tuple
Here is the code:
import cv2
import matplotlib.pyplot as plt
class TestClass():
def __init__(self):
self.fname = 'image.jpg'
self.img = cv2.imread(self.fname)
self.point = ()
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
return self.point
def __onclick__(self,click):
self.point = (click.xdata,click.ydata)
return self.point

Your code works for me, as long as I insert plt.show() after mpl_connect in getCoord:
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
plt.show()
return self.point

Related

Annotate point on axes with automatic tick formatting in matplotlib?

Consider this example code, derived from Matplotlib Indicate Point on X and Y Axis :
import numpy as np
import matplotlib.pyplot as plt
class PointMarker():
def __init__(self, ax, point, **kwargs):
self.ax = ax
self.point = point
if "line" in kwargs:
self.c = kwargs.get("line").get_color()
else:
self.c = kwargs.get("color", "b")
self.ls=kwargs.get("linestyle", ':')
self.vline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.hline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.draw()
def draw(self):
xmin = ax.get_xlim()[0]
ymin = ax.get_ylim()[0]
self.vline.set_data([self.point[0], self.point[0]], [ymin,self.point[1]])
self.hline.set_data([xmin, self.point[0]], [self.point[1], self.point[1]])
class PointMarkers():
pointmarkers = []
def add(self,ax, point, **kwargs ):
pm = PointMarker(ax, point, **kwargs)
self.pointmarkers.append(pm)
def update(self, event=None):
for pm in self.pointmarkers:
pm.draw()
x = np.arange(1,17)
y = np.log(x)
ax = plt.subplot(111)
line = plt.plot(x,y)
# register the markers
p = PointMarkers()
p.add(ax,[x[5],y[5]], line=line[0])
# connect event listener
cid = plt.gcf().canvas.mpl_connect("draw_event", p.update)
plt.grid(True)
plt.show()
What I would like to do, is to keep the automatic tick label formatting on the axis - and insert text labels that would annotate the point; so something like this (where I've manually added the annotating text labels):
Basically, if the graph is zoomed in, and the ticks/tick labels change, I would like the annotation labels to also be present (if they are still in view, of course) ...
I would be OK with either placing the annotation labels below abscissa/to left of ordinate (as drawn above) - or, with replacing the automatic tick labels with the annotation labels, where they overlap (so in above example, the "6" tick label of the abscissa would be removed and replaced with "my_point_X").
How can an annotation like this be implemented?
We set it up with reference to the official references.
The off-axis position was set manually. I have little experience with this task so there may be a better way to do it.
import numpy as np
import matplotlib.pyplot as plt
class PointMarker():
def __init__(self, ax, point, **kwargs):
self.ax = ax
self.point = point
if "line" in kwargs:
self.c = kwargs.get("line").get_color()
else:
self.c = kwargs.get("color", "b")
self.ls=kwargs.get("linestyle", ':')
self.vline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.hline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.draw()
def draw(self):
xmin = ax.get_xlim()[0]
ymin = ax.get_ylim()[0]
self.vline.set_data([self.point[0], self.point[0]], [ymin,self.point[1]])
self.hline.set_data([xmin, self.point[0]], [self.point[1], self.point[1]])
class PointMarkers():
pointmarkers = []
def add(self,ax, point, **kwargs ):
pm = PointMarker(ax, point, **kwargs)
self.pointmarkers.append(pm)
def update(self, event=None):
for pm in self.pointmarkers:
pm.draw()
x = np.arange(1,17)
y = np.log(x)
fig = plt.figure(figsize=(4,3),dpi=144) # update
ax = fig.add_subplot(111) # update
# ax = plt.subplot(111)
line = plt.plot(x,y)
# register the markers
p = PointMarkers()
p.add(ax,[x[5],y[5]], line=line[0])
# update start
# x_points = x[5]/x.max()
# y_points = y[5]/y.max()
ax.annotate('my_point_Y', xy=(0.3, 1.75), xycoords='data', color='r', fontsize=9)
ax.annotate('my_point_X', xy=(5.0, -0.1), xycoords='data', color='r', fontsize=9)
# update end
# connect event listener
cid = plt.gcf().canvas.mpl_connect("draw_event", p.update)
plt.grid(True)
plt.show()

Getting coordinates of the closest data point on matplotlib plot

I am using matplotlib with NavigationToolbar2QT. The toolbar is showing the position of the cursor. But I would like that the cursor snaps to the nearest data point (when close enough) or simply show the coordinate of nearest data point. Can that be somehow arranged?
If you are working with large sets of points, I advice you to use CKDtrees:
import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial
points = np.column_stack([np.random.rand(50), np.random.rand(50)])
fig, ax = plt.subplots()
coll = ax.scatter(points[:,0], points[:,1])
ckdtree = scipy.spatial.cKDTree(points)
I refactored kpie's answer here little bit. Once ckdtree is created, you can identify closest points instantly and various kind of information about them with a little effort:
def closest_point_distance(ckdtree, x, y):
#returns distance to closest point
return ckdtree.query([x, y])[0]
def closest_point_id(ckdtree, x, y):
#returns index of closest point
return ckdtree.query([x, y])[1]
def closest_point_coords(ckdtree, x, y):
# returns coordinates of closest point
return ckdtree.data[closest_point_id(ckdtree, x, y)]
# ckdtree.data is the same as points
Interactive display of cursor position.
If you want coordinates of the closest point to be displayed on Navigation Toolbar:
def val_shower(ckdtree):
#formatter of coordinates displayed on Navigation Bar
return lambda x, y: '[x = {}, y = {}]'.format(*closest_point_coords(ckdtree, x, y))
plt.gca().format_coord = val_shower(ckdtree)
plt.show()
Using events.
If you want another kind of interactivity, you can use events:
def onclick(event):
if event.inaxes is not None:
print(closest_point_coords(ckdtree, event.xdata, event.ydata))
fig.canvas.mpl_connect('motion_notify_event', onclick)
plt.show()
You could subclass NavigationToolbar2QT and override the mouse_move handler. The xdata and ydata attributes contain the current mouse position in plot coordinates. You can snap that to the closest data point before passing the event to the base class mouse_move handler.
Full example, with highlighting of the closest point in the plot as a bonus:
import sys
import numpy as np
from matplotlib.backends.qt_compat import QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT
from matplotlib.figure import Figure
class Snapper:
"""Snaps to data points"""
def __init__(self, data, callback):
self.data = data
self.callback = callback
def snap(self, x, y):
pos = np.array([x, y])
distances = np.linalg.norm(self.data - pos, axis=1)
dataidx = np.argmin(distances)
datapos = self.data[dataidx,:]
self.callback(datapos[0], datapos[1])
return datapos
class SnappingNavigationToolbar(NavigationToolbar2QT):
"""Navigation toolbar with data snapping"""
def __init__(self, canvas, parent, coordinates=True):
super().__init__(canvas, parent, coordinates)
self.snapper = None
def set_snapper(self, snapper):
self.snapper = snapper
def mouse_move(self, event):
if self.snapper and event.xdata and event.ydata:
event.xdata, event.ydata = self.snapper.snap(event.xdata, event.ydata)
super().mouse_move(event)
class Highlighter:
def __init__(self, ax):
self.ax = ax
self.marker = None
self.markerpos = None
def draw(self, x, y):
"""draws a marker at plot position (x,y)"""
if (x, y) != self.markerpos:
if self.marker:
self.marker.remove()
del self.marker
self.marker = self.ax.scatter(x, y, color='yellow')
self.markerpos = (x, y)
self.ax.figure.canvas.draw()
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
canvas = FigureCanvas(Figure(figsize=(5,3)))
layout.addWidget(canvas)
toolbar = SnappingNavigationToolbar(canvas, self)
self.addToolBar(toolbar)
data = np.random.randn(100, 2)
ax = canvas.figure.subplots()
ax.scatter(data[:,0], data[:,1])
self.highlighter = Highlighter(ax)
snapper = Snapper(data, self.highlighter.draw)
toolbar.set_snapper(snapper)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
The following code will print the coordinates of the dot closest to the mouse when you click.
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(19680801)
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
fig,ax = plt.subplots()
plt.scatter(x, y)
points = list(zip(x,y))
def distance(a,b):
return(sum([(k[0]-k[1])**2 for k in zip(a,b)])**0.5)
def onclick(event):
dists = [distance([event.xdata, event.ydata],k) for k in points]
print(points[dists.index(min(dists))])
fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
Another possibility is to use the picking support axes already have. See this section in the event handling docs.
Jim

Matplotlib Animation does not update within class

I'm writing a simple class to plot a sensor value in real-time; however, the animation does not run within the class.
I've tried to return the animation object to have an instance outside of the class but this does not work.
To my understanding, this is the same issue as raised in GitHub #1656
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from random import random
class Animate:
def __init__(self, sensor):
# Create figure for plotting
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
self.xs = []
self.ys = []
self.ylabel = sensor
self.readings = 20
# This function is called periodically from FuncAnimation
def _update(self, i, xs, ys):
# Get sensor value
value = random()
# Add x and y to lists
self.xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
self.ys.append(value)
# Limit x and y lists to 20 items
self.xs = self.xs[-self.readings:]
self.ys = self.ys[-self.readings:]
# Draw x and y lists
self.ax.clear()
self.ax.plot(xs, ys)
# Format plot
plt.xticks(rotation=45, ha='right')
plt.subplots_adjust(bottom=0.30)
plt.title(self.ylabel + ' over Time')
plt.ylabel(self.ylabel)
def start(self):
print('Starting')
# Set up plot to call animate() function periodically
self.anim = animation.FuncAnimation(self.fig, self._update, fargs=(self.xs, self.ys), interval=200)
plt.show();
rand = Animate('Torque')
rand.start();
your variables xs and ys are already named self.xs and self.ys, which are accessible in the class namespace; you do not need to pass them to self.update
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from random import random
class Animate:
def __init__(self, sensor):
# Create figure for plotting
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
self.xs = []
self.ys = []
self.ylabel = sensor
self.readings = 20
# This function is called periodically from FuncAnimation
def _update(self, i):
# Read temperature (Celsius) from TMP102
temp_c = random()
# Add x and y to lists
self.xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
self.ys.append(temp_c)
# Limit x and y lists to 20 items
self.xs = self.xs[-self.readings:]
self.ys = self.ys[-self.readings:]
# Draw x and y lists
self.ax.clear()
self.ax.plot(self.xs, self.ys)
# Format plot
plt.xticks(rotation=45, ha='right')
plt.subplots_adjust(bottom=0.30)
plt.title(self.ylabel + ' over Time')
plt.ylabel(self.ylabel)
def start(self):
print('Starting')
# Set up plot to call animate() function periodically
self.anim = animation.FuncAnimation(self.fig, self._update, interval=200)
plt.show()
rand = Animate('Torque')
rand.start()

Point picker event_handler drawing line and displaying coordinates in matplotlib

I have the following class that draws a vertical line through the y-axis, so that when I click on it a horizontal line gets drawn at the location.
My goal is to get the y-coordinate to actually print right at the y-axis where the horizontal line gets drawn. For testing, I am trying to print a title with the y-ccordinate, but it is not working as expected.
What I am trying to really accomplish is to make the y-axis on a bar plot clickable so that the user can select a point on the y-axis, and then a bunch of "stuff" happens (including the drawing of a horizontal line). I really see no other way to make this happen, other than drawing a plottable vertical line through the y-axis to make it pickable. This seems rather kludgey, but I have not had any success with any other methods.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot((0, 0), (-6000, 10000), 'k-', linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
fig.canvas.mpl_connect('button_press_event', self.onclick)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axhline(y=y)
print(y)
ax.axvspan(0, 0, facecolor='y', alpha=0.5, picker=10)
self.fig.canvas.draw()
def onclick(event):
self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata))
print(event.xdata, event.ydata)
if __name__ == '__main__':
plt.ion()
p = PointPicker()
plt.show()
Assuming there is no ther way to achieve my end result, all is well with this method, except I cannot for the life of me get the title to print (using the self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata)).
You can use the 'button_press_event' to connect to a method, which verifies that the click has happened close enough to the yaxis spine and then draws a horizontal line using the clicked coordinate.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self, ax, clicklim=0.05):
self.fig=ax.figure
self.ax = ax
self.clicklim = clicklim
self.horizontal_line = ax.axhline(y=.5, color='y', alpha=0.5)
self.text = ax.text(0,0.5, "")
print self.horizontal_line
self.fig.canvas.mpl_connect('button_press_event', self.onclick)
def onclick(self, event):
if event.inaxes == self.ax:
x = event.xdata
y = event.ydata
xlim0, xlim1 = ax.get_xlim()
if x <= xlim0+(xlim1-xlim0)*self.clicklim:
self.horizontal_line.set_ydata(y)
self.text.set_text(str(y))
self.text.set_position((xlim0, y))
self.fig.canvas.draw()
if __name__ == '__main__':
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar([0,2,3,5],[4,5,1,3], color="#dddddd")
p = PointPicker(ax)
plt.show()

Connect a matplotlib widget using keyboard event

Hi I am making a small program to select data from a plot. I would like to use the inbuilt matplotlib.widgets but I am misunderstanding the basics of connecting/disconnecting to the figure canvas.
Below is an example of the code I had in mind. If I declare the cursor/span widgets outside the event manager class the widgets are launched properly. However, this is not the case within the Event_Manager class.
But how should I modify the code to launch these widgets using a key event? Also how could I disconect/reconect them, also using key events?
import matplotlib.widgets as widgets
import numpy as np
import matplotlib.pyplot as plt
def Span_Manager(Wlow,Whig):
print Wlow,Whig
return
class Event_Manager:
def __init__(self, fig, ax):
self.fig = fig
self.ax = ax
self.redraw()
def redraw(self):
self.fig.canvas.draw()
def connect(self):
self.fig.canvas.mpl_connect('key_press_event', self.onKeyPress)
def onKeyPress(self, ev):
if ev.key == 'q':
print 'activate span selector'
Span = widgets.SpanSelector(ax, Span_Manager, 'horizontal', useblit=False, rectprops=dict(alpha=1, facecolor='Blue'))
elif ev.key == 'w':
print 'activate cursor'
cursor = widgets.Cursor(ax, useblit=True, color='red', linewidth=2)
self.redraw()
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
x, y = 4*(np.random.rand(2, 100)-.5)
ax.plot(x, y, 'o')
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
mat_wid = Event_Manager(fig, ax)
mat_wid.connect()
# Span = widgets.SpanSelector(ax, Span_Manager, 'horizontal', useblit=False, rectprops=dict(alpha=1, facecolor='Blue'))
# cursor = widgets.Cursor(ax, useblit=True, color='red', linewidth=2)
plt.show()

Categories