Using the following as reference : Interactive BSpline Fitting I have the following tool to drag the points of the spline in any direction using mouse:
import numpy as np
from scipy.interpolate import interp1d
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
from matplotlib.mlab import dist_point_to_segment
class PolygonInteractor(object):
"""
A polygon editor.
https://matplotlib.org/gallery/event_handling/poly_editor.html
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, visible=False):
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.poly.set_visible(visible)
x, y = zip(*self.poly.xy)
self.line = Line2D(x, y, ls="",
marker='o', markerfacecolor='r',
animated=True)
self.ax.add_line(self.line)
self.cid = self.poly.add_callback(self.poly_changed)
self._ind = None # the active vert
canvas.mpl_connect('draw_event', self.draw_callback)
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('key_press_event', self.key_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
self.canvas = canvas
x,y = self.interpolate()
self.line2 = Line2D(x, y, animated=True)
self.ax.add_line(self.line2)
def interpolate(self):
x, y = self.poly.xy[:].T
i = np.arange(len(x))
interp_i = np.linspace(0, i.max(), 100 * i.max())
xi = interp1d(i, x, kind='cubic')(interp_i)
yi = interp1d(i, y, kind='cubic')(interp_i)
return xi,yi
def draw_callback(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.ax.draw_artist(self.line2)
# 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 polygon 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):
'get the index of the vertex under point if within epsilon tolerance'
# 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 button_press_callback(self, event):
'whenever a mouse button is pressed'
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 button_release_callback(self, event):
'whenever a mouse button is released'
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def key_press_callback(self, event):
'whenever a key is pressed'
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 motion_notify_callback(self, event):
'on mouse movement'
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))
x,y = self.interpolate()
self.line2.set_data(x,y)
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
self.ax.draw_artist(self.line2)
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)
xs = (921, 951, 993, 1035, 1065, 1045, 993, 945)
ys = (1181, 1230, 1243, 1230, 1181, 1130, 1130, 1130)
poly = Polygon(list(zip(xs, ys)), animated=True)
fig, ax = plt.subplots()
ax.add_patch(poly)
p = PolygonInteractor(ax, poly, visible=False)
ax.set_title('Click and drag a point to move it')
ax.set_xlim((800, 1300))
ax.set_ylim((1000, 1300))
plt.show()
I wish to add additional constraints:
1) The movement of points C and G should be limited to only the Y axis, or the vertical direction, ie, the user should only be able to drag the points up or down, and not left or right
2) The movement of points A and E should be limited to only the X axis, or the horizontal direction, ie, the user should only be able to drag the points left and right, and not up or down
Can someone please suggest how to add these constraints to the given points?
EDIT--
When I drag point A left/right by some amount 'd0', then point E needs to be dragged right/left respectively by 'd0'. I tried to constrain the movement of both these points along the horizontal direction only by repeatedly setting the changed y coordinate back to it's original value whenever points are dragged. However it is not being reflected in the code.
The amount of movement 'd0' is calculated with respect to a reference center point having coordinates (993,1181)
I tried to do it as follows, by editing the motion_notify_callback() function:
def motion_notify_callback(self, event):
'on mouse movement'
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))
xpts,ypts,x,y = self.interpolate()
#Note that in xpts and ypts, we are getting the new coordinates of
#the points after dragging
if(xpts[0]<xs[0]):
#when point A is being dragged outwards(towards left)
d0=abs(993-xpts[0])
#movement amount
ypts[0]=ys[0] #setting the y value of dragged point back to
#original value to constrain the movement along horizontal
#direction only. This is however not being reflected
xpts[4] = 993+d0 #moving point E by same amount to the right
ypts[4]=ys[4] #Trying to stop E from moving in Y direction
d0=0
elif(xpts[0]>xs[0]):
#when point A is moved inwards/towards right
d0=abs(993-xpts[0])
ypts[0]=ys[0]
xnew=993+d0
xpts[4] = xnew #moving E inwards/towards left by same amount
ypts[4]=ys[4]
d0=0
self.line2.set_data(x,y)
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
self.ax.draw_artist(self.line2)
self.canvas.blit(self.ax.bbox)
return d0
This can be done by restraining the coordinates of these points in the function interpolate() as follows:
def interpolate(self):
x, y = self.poly.xy[:].T
y[0]=1181
y[4]=1181
x[2]=993
x[6]=993
#print(x,y)
P=np.array([(x[0],y[0]),
(x[1], y[1]),
(x[2], y[2]),
(x[3], y[3]),
(x[4],y[4]),
(x[5],y[5]),
(x[6],y[6]),
(x[7],y[7]),
(x[0], y[0]),
])
#print(P)
tck, u = splprep(P.T, u=None, s=0.0, per=1)
u_new = np.linspace(u.min(), u.max(), 1000)
xi, yi = splev(u_new, tck, der=0)
#print(xi,yi)
#plt.plot(pts[:, 0], pts[:, 1], 'ro')
#plt.plot(x_new, y_new, 'b--')
#plt.show()
L=0
for j in range(0,len(xi)-1):
L=L+np.sqrt((xi[j+1]-xi[j])**2 + (yi[j+1]-yi[j])**2)
#print(L," pixels")
scaled = L * 1.439464535124507
print(scaled, " mm")
cm = scaled / 10
print(cm, " cm")
inches = scaled * 0.0394
print(inches, "inch")
print("\n")
return x,y,xi,yi
The point may be moved on the screen if you drag it but the spline would always be fit along the same coordinates as defined