class EllipseInteractor(QObject): epsilon = 5 showverts = True mySignal = pyqtSignal(str) modSignal = pyqtSignal(str) def __init__(self,ax,center,width,height=None,angle=0.): super().__init__() from matplotlib.patches import Ellipse from matplotlib.lines import Line2D # from matplotlib.artist import Artist # To avoid crashing with maximum recursion depth exceeded import sys sys.setrecursionlimit(10000) # 10000 is 10x the default value if height is None: self.type = 'Circle' height = width else: self.type = 'Ellipse' self.ax = ax self.ellipse = Ellipse(center,width,height,edgecolor='Lime',facecolor='none',angle=angle,fill=False,animated=True) self.ax.add_patch(self.ellipse) self.canvas = self.ellipse.figure.canvas # Create a line with center, width, and height points self.center = self.ellipse.center self.angle = self.ellipse.angle*180./np.pi self.width = self.ellipse.width self.height = self.ellipse.height ca = np.cos(self.angle); sa = np.sin(self.angle) x0, y0 = self.center w0 = self.width*0.5 h0 = self.height*0.5 x = [x0, x0+w0*ca, x0-h0*sa] y = [y0, y0+w0*sa, y0+h0*ca] self.xy = [(i,j) for (i,j) in zip(x,y)] self.line = Line2D(x, y, marker='o', linestyle=None, linewidth=0., markerfacecolor='g', animated=True) self.ax.add_line(self.line) self.cid = self.ellipse.add_callback(self.ellipse_changed) self._ind = None # the active point self.connect() self.aperture = self.ellipse self.press = None self.lock = None def connect(self): self.cid_draw = self.canvas.mpl_connect('draw_event', self.draw_callback) self.cid_press = self.canvas.mpl_connect('button_press_event', self.button_press_callback) self.cid_release = self.canvas.mpl_connect('button_release_event', self.button_release_callback) self.cid_motion = self.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback) self.cid_key = self.canvas.mpl_connect('key_press_event', self.key_press_callback) self.canvas.draw_idle() def disconnect(self): self.canvas.mpl_disconnect(self.cid_draw) self.canvas.mpl_disconnect(self.cid_press) self.canvas.mpl_disconnect(self.cid_release) self.canvas.mpl_disconnect(self.cid_motion) self.canvas.mpl_disconnect(self.cid_key) self.ellipse.remove() self.line.remove() self.canvas.draw_idle() self.aperture = None def draw_callback(self, event): self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self.ellipse) self.ax.draw_artist(self.line) def ellipse_changed(self, ellipse): '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, ellipse) self.line.set_visible(vis) def get_ind_under_point(self, event): 'get the index of the point if within epsilon tolerance' x, y = zip(*self.xy) d = np.hypot(x - event.xdata, y - event.ydata) 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) x0, y0 = self.ellipse.center w0, h0 = self.ellipse.width, self.ellipse.height theta0 = self.ellipse.angle/180*np.pi self.press = x0, y0, w0, h0, theta0, event.xdata, event.ydata self.xy0 = self.xy self.lock = "pressed" 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': #self.disconnect() #self.ellipse = None #self.line = None self.mySignal.emit('ellipse deleted') self.canvas.draw_idle() 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 self.press = None self.lock = "released" self.background = None # To get other aperture redrawn self.canvas.draw_idle() def motion_notify_callback(self, event): 'on mouse movement' #if not self.ellipse.contains(event): return if not self.showverts: return if self._ind is None: return if event.inaxes is None: return if event.button != 1: return x0, y0, w0, h0, theta0, xpress, ypress = self.press self.dx = event.xdata - xpress self.dy = event.ydata - ypress self.update_ellipse() # Redraw ellipse and points self.canvas.restore_region(self.background) self.ax.draw_artist(self.ellipse) self.ax.draw_artist(self.line) self.canvas.update() self.canvas.flush_events() # Notify callback self.modSignal.emit('ellipse modified') def update_ellipse(self): x0, y0, w0, h0, theta0, xpress, ypress = self.press dx, dy = self.dx, self.dy if self.lock == "pressed": if self._ind == 0: self.lock = "move" else: self.lock = "resizerotate" elif self.lock == "move": if x0+dx < 0: xn = x0 dx = 0 else: xn = x0+dx if y0+dy < 0: yn = y0 dy = 0 else: yn = y0+dy self.ellipse.center = xn,yn # update line self.xy = [(i+dx,j+dy) for (i,j) in self.xy0] # Redefine line self.line.set_data(zip(*self.xy)) # otherwise rotate and resize elif self.lock == 'resizerotate': dtheta = np.arctan2(ypress+dy-y0,xpress+dx-x0)-np.arctan2(ypress-y0,xpress-x0) theta_ = (theta0+dtheta) * 180./np.pi c, s = np.cos(theta0), np.sin(theta0) R = np.matrix('{} {}; {} {}'.format(c, s, -s, c)) (dx_,dy_), = np.array(np.dot(R,np.array([dx,dy]))) # Avoid to pass through the center if self._ind == 1: w_ = w0+2*dx_ if (w0+2*dx_) > 0 else w0 if self.type == 'Circle': h_ = w_ else: h_ = h0 elif self._ind == 2: h_ = h0+2*dy_ if (h0+2*dy_) > 0 else h0 if self.type == 'Circle': w_ = h_ else: w_ = w0 # update ellipse self.ellipse.width = w_ self.ellipse.height = h_ if self.type == 'Circle': theta_ = 0. self.ellipse.angle = theta_ # update points self.updateMarkers() def updateMarkers(self): # update points theta_ = self.ellipse.angle*np.pi/180. x0,y0 = self.ellipse.center w_ = self.ellipse.width h_ = self.ellipse.height ca = np.cos(theta_); sa = np.sin(theta_) x = [x0, x0+w_*0.5*ca, x0-h_*0.5*sa] y = [y0, y0+w_*0.5*sa, y0+h_*0.5*ca] self.xy = [(i,j) for (i,j) in zip(x,y)] self.line.set_data(x,y)