Following a tutorial Poly Editor I’ve created 2 polygons, added to the figure, and then instantiated 2 interactive polygons. I expected that when I adjusted the individual polygon it would work separately. However they were conflicting with each other.
Then I have tried some changes in the original code yet still not produce a successful result. If you’re familiar I would like to have your help. I really appreciate in advance.
Here is my following adjusted code:
"""
===========
Poly Editor
===========
This is an example to show how to build cross-GUI applications using
Matplotlib event handling to interact with objects on the canvas.
"""
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
def dist(x, y):
"""
Return the distance between two points.
"""
d = x - y
return np.sqrt(np.dot(d, d))
def dist_point_to_segment(p, s0, s1):
"""
Get the distance of a point to a segment.
*p*, *s0*, *s1* are *xy* sequences
This algorithm from
http://geomalgorithms.com/a02-_lines.html
"""
v = s1 - s0
w = p - s0
c1 = np.dot(w, v)
if c1 <= 0:
return dist(p, s0)
c2 = np.dot(v, v)
if c2 <= c1:
return dist(p, s1)
b = c1 / c2
pb = s0 + b * v
return dist(p, pb)
class PolygonInteractor:
"""
A polygon editor.
Key-bindings
't' toggle vertex markers on and off. When vertex markers are on,
you can move them, delete them
'd' delete the vertex under point
'i' insert a vertex at point. You must be within epsilon of the
line connecting two existing vertices
"""
showverts = True
epsilon = 5 # max pixel distance to count as a vertex hit
def __init__(self, ax, poly):
if poly.figure is None:
raise RuntimeError('You must first add the polygon to a figure '
'or canvas before defining the interactor')
self.ax = ax
self.poly = poly
x, y = zip(*self.poly.xy)
self.line = Line2D(x, y,
marker='o', markerfacecolor='r',
animated=True)
self.ax.add_line(self.line)
self._ind = None # the active vert
def connect(self):
'connect to all the events we need'
self.cid = self.poly.add_callback(self.poly_changed)
self.ciddraw = self.poly.figure.canvas.mpl_connect('draw_event', self.on_draw)
self.cidpress = self.poly.figure.canvas.mpl_connect('button_press_event', self.on_button_press)
self.cidrelease = self.poly.figure.canvas.mpl_connect('button_release_event', self.on_button_release)
self.cidkeypress = self.poly.figure.canvas.mpl_connect('key_press_event', self.on_key_press)
self.cidmotion = self.poly.figure.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
def on_draw(self, event):
self.background = self.poly.figure.canvas.copy_from_bbox(self.poly.figure.bbox)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
# do not need to blit here, this will fire before the screen is
# updated
def poly_changed(self, poly):
"""This method is called whenever the pathpatch object is called."""
# only copy the artist props to the line (except visibility)
vis = self.line.get_visible()
Artist.update_from(self.line, poly)
self.line.set_visible(vis) # don't use the poly visibility state
def get_ind_under_point(self, event):
"""
Return the index of the point closest to the event position or *None*
if no point is within ``self.epsilon`` to the event position.
"""
# display coords
xy = np.asarray(self.poly.xy)
xyt = self.poly.get_transform().transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.hypot(xt - event.x, yt - event.y)
indseq, = np.nonzero(d == d.min())
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def on_button_press(self, event):
"""Callback for mouse button presses."""
if not self.showverts:
return
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def on_button_release(self, event):
"""Callback for mouse button releases."""
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def on_key_press(self, event):
"""Callback for key presses."""
if not event.inaxes:
return
if event.key == 't':
self.showverts = not self.showverts
self.line.set_visible(self.showverts)
if not self.showverts:
self._ind = None
elif event.key == 'd':
ind = self.get_ind_under_point(event)
if ind is not None:
self.poly.xy = np.delete(self.poly.xy,
ind, axis=0)
self.line.set_data(zip(*self.poly.xy))
elif event.key == 'i':
xys = self.poly.get_transform().transform(self.poly.xy)
p = event.x, event.y # display coords
for i in range(len(xys) - 1):
s0 = xys[i]
s1 = xys[i + 1]
d = dist_point_to_segment(p, s0, s1)
if d <= self.epsilon:
self.poly.xy = np.insert(
self.poly.xy, i+1,
[event.xdata, event.ydata],
axis=0)
self.line.set_data(zip(*self.poly.xy))
break
if self.line.stale:
self.poly.figure.canvas.draw_idle()
def on_mouse_move(self, event):
"""Callback for mouse movements."""
if not self.showverts:
return
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
return
x, y = event.xdata, event.ydata
self.poly.xy[self._ind] = x, y
if self._ind == 0:
self.poly.xy[-1] = x, y
elif self._ind == len(self.poly.xy) - 1:
self.poly.xy[0] = x, y
self.line.set_data(zip(*self.poly.xy))
self.poly.figure.canvas.restore_region(self.background)
self.poly.figure.draw_artist(self.poly)
self.poly.figure.draw_artist(self.line)
self.poly.figure.canvas.blit(self.poly.figure.bbox)
def disconnect(self):
'disconnect all the stored connection ids'
self.poly.figure.canvas.mpl_disconnect(self.ciddraw)
self.poly.figure.canvas.mpl_disconnect(self.cidpress)
self.poly.figure.canvas.mpl_disconnect(self.cidrelease)
self.poly.figure.canvas.mpl_disconnect(self.cidmotion)
self.poly.figure.canvas.mpl_disconnect(self.cidkeypress)
if __name__ == '__main__':
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
import matplotlib
# Fixing random state for reproducibility
np.random.seed(19680801)
fig, ax = plt.subplots()
N, LP = (4, 3)
poly_4_pts = Polygon(np.random.rand(N, 2), True, fill=None, animated=True)
poly_2_pts = Polygon(np.random.rand(LP, 2), True, fill=None, animated=True)
ax.add_patch(poly_4_pts)
fig.patches.extend([poly_2_pts])
ax.add_patch(poly_2_pts)
poly2 = PolygonInteractor(ax, poly_2_pts)
poly2.connect()
poly1 = PolygonInteractor(ax, poly_4_pts)
poly1.connect()
ax.set_title('Click and drag a point to move it')
ax.set_xlim((-2, 2))
ax.set_ylim((-2, 2))
plt.show()
Advertisement
Answer
I think that the problem is that you don’t restore the animated artists when restoring backgronds. See tutorial on blitting here. I could get it working adding some modifications to restore all the animated object added to the figure, since also the contour lines are animated you should declare them in the main and not in the class then restore all animated inside the class. Here’s my code and result:
"""
===========
Poly Editor
===========
This is an example to show how to build cross-GUI applications using
Matplotlib event handling to interact with objects on the canvas.
"""
import numpy as np
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
def dist(x, y):
"""
Return the distance between two points.
"""
d = x - y
return np.sqrt(np.dot(d, d))
def dist_point_to_segment(p, s0, s1):
"""
Get the distance of a point to a segment.
*p*, *s0*, *s1* are *xy* sequences
This algorithm from
http://geomalgorithms.com/a02-_lines.html
"""
v = s1 - s0
w = p - s0
c1 = np.dot(w, v)
if c1 <= 0:
return dist(p, s0)
c2 = np.dot(v, v)
if c2 <= c1:
return dist(p, s1)
b = c1 / c2
pb = s0 + b * v
return dist(p, pb)
class PolygonInteractor:
"""
A polygon editor.
Key-bindings
't' toggle vertex markers on and off. When vertex markers are on,
you can move them, delete them
'd' delete the vertex under point
'i' insert a vertex at point. You must be within epsilon of the
line connecting two existing vertices
"""
showverts = True
epsilon = 5 # max pixel distance to count as a vertex hit
def __init__(self, ax, poly, contour, animated_artists=()):
if poly.figure is None:
raise RuntimeError('You must first add the polygon to a figure '
'or canvas before defining the interactor')
self.ax = ax
canvas = poly.figure.canvas
self.poly = poly
self.line = contour
self._ind = None # the active vert
canvas.mpl_connect('draw_event', self.on_draw)
canvas.mpl_connect('button_press_event', self.on_button_press)
canvas.mpl_connect('key_press_event', self.on_key_press)
canvas.mpl_connect('button_release_event', self.on_button_release)
canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
self.canvas = canvas
self._artists = []
for a in animated_artists:
self.add_artist(a)
#self.add_artist(self.line)
def add_artist(self, art):
if art.figure != self.canvas.figure:
raise RuntimeError
art.set_animated(True)
self._artists.append(art)
def on_draw(self, event):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
self._draw_animated()
self.canvas.blit(self.ax.bbox)
# do not need to blit here, this will fire before the screen is
# updated
def _draw_animated(self):
"""Draw all of the animated artists."""
fig = self.canvas.figure
for a in self._artists:
fig.draw_artist(a)
def get_ind_under_point(self, event):
"""
Return the index of the point closest to the event position or *None*
if no point is within ``self.epsilon`` to the event position.
"""
# display coords
xy = np.asarray(self.poly.xy)
xyt = self.poly.get_transform().transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.hypot(xt - event.x, yt - event.y)
indseq, = np.nonzero(d == d.min())
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def on_button_press(self, event):
"""Callback for mouse button presses."""
if not self.showverts:
return
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def on_button_release(self, event):
"""Callback for mouse button releases."""
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def on_key_press(self, event):
"""Callback for key presses."""
if not event.inaxes:
return
if event.key == 't':
self.showverts = not self.showverts
self.line.set_visible(self.showverts)
if not self.showverts:
self._ind = None
elif event.key == 'd':
ind = self.get_ind_under_point(event)
if ind is not None:
self.poly.xy = np.delete(self.poly.xy,
ind, axis=0)
self.line.set_data(zip(*self.poly.xy))
elif event.key == 'i':
xys = self.poly.get_transform().transform(self.poly.xy)
p = event.x, event.y # display coords
for i in range(len(xys) - 1):
s0 = xys[i]
s1 = xys[i + 1]
d = dist_point_to_segment(p, s0, s1)
if d <= self.epsilon:
self.poly.xy = np.insert(
self.poly.xy, i+1,
[event.xdata, event.ydata],
axis=0)
self.line.set_data(zip(*self.poly.xy))
break
if self.line.stale:
self.canvas.draw_idle()
def on_mouse_move(self, event):
"""Callback for mouse movements."""
if not self.showverts:
return
if self._ind is None:
return
if event.inaxes is None:
return
if event.button != 1:
return
x, y = event.xdata, event.ydata
self.poly.xy[self._ind] = x, y
if self._ind == 0:
self.poly.xy[-1] = x, y
elif self._ind == len(self.poly.xy) - 1:
self.poly.xy[0] = x, y
self.line.set_data(zip(*self.poly.xy))
self.canvas.restore_region(self.background)
self._draw_animated()
self.canvas.draw_idle()
# self.ax.draw_artist(self.poly)
# self.ax.draw_artist(self.line)
self.canvas.blit(self.ax.bbox)
if __name__ == '__main__':
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
theta = np.arange(0, 2*np.pi, 0.1)
r = 1.5
xs = r * np.cos(theta)
ys = r * np.sin(theta)
poly = Polygon(np.column_stack([xs, ys]), animated=True)
xc1, yc1 = zip(*poly.xy)
poly_contour = Line2D(xc1, yc1, marker='o', markerfacecolor='r',
animated=True)
poly2 = Polygon(np.column_stack([xs+3, ys+3]), animated=True)
xc2, yc2 = zip(*poly2.xy)
poly_contour2 = Line2D(xc2, yc2, marker='o', markerfacecolor='r',
animated=True)
fig, ax = plt.subplots()
ax.add_patch(poly)
ax.add_line(poly_contour)
ax.add_patch(poly2)
ax.add_line(poly_contour2)
p = PolygonInteractor(ax, poly, poly_contour, [poly, poly2, poly_contour, poly_contour2])
p2 = PolygonInteractor(ax, poly2, poly_contour2, [poly, poly2, poly_contour, poly_contour2])
ax.set_title('Click and drag a point to move it')
ax.set_xlim((-2, 6))
ax.set_ylim((-2, 6))
plt.show()