class RectangleInteractor(QObject): epsilon = 5 showverts = True mySignal = pyqtSignal(str) modSignal = pyqtSignal(str) def __init__(self,ax,corner,width,height=None,angle=0.): super().__init__() from matplotlib.patches import Rectangle 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 = 'Square' height = width else: self.type = 'Rectangle' self.ax = ax self.angle = angle/180.*np.pi self.width = width self.height = height self.rect = Rectangle(corner,width,height,edgecolor='Lime',facecolor='none',angle=angle,fill=False,animated=True) self.ax.add_patch(self.rect) self.canvas = self.rect.figure.canvas x,y = self.compute_markers() self.line = Line2D(x, y, marker='o', linestyle=None, linewidth=0., markerfacecolor='g', animated=True) self.ax.add_line(self.line) self.cid = self.rect.add_callback(self.rectangle_changed) self._ind = None # the active point self.connect() self.aperture = self.rect self.press = None self.lock = None def compute_markers(self): theta0 = self.rect.angle / 180.*np.pi w0 = self.rect.get_width() h0 = self.rect.get_height() x0,y0 = self.rect.get_xy() c, s = np.cos(-theta0), np.sin(-theta0) R = np.matrix('{} {}; {} {}'.format(c, s, -s, c)) x = [0.5*w0, w0, 0.5*w0] y = [0.5*h0, 0.5*h0, h0] self.xy = [] x_ = [] y_ = [] for dx,dy in zip(x,y): (dx_,dy_), = np.array(np.dot(R,np.array([dx,dy]))) self.xy.append((dx_+x0,dy_+y0)) x_.append(dx_+x0) y_.append(dy_+y0) return x_,y_ 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.rect.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.rect) self.ax.draw_artist(self.line) def rectangle_changed(self, rect): '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, rect) 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.rect.get_xy() w0, h0 = self.rect.get_width(), self.rect.get_height() theta0 = self.rect.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.rect = None #self.line = None self.mySignal.emit('rectangle 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.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_rectangle() # Redraw rectangle and points self.canvas.restore_region(self.background) self.ax.draw_artist(self.rect) self.ax.draw_artist(self.line) self.canvas.update() self.canvas.flush_events() # Notify callback self.modSignal.emit('rectangle modified') def update_rectangle(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.rect.set_xy((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': xc,yc = self.xy0[0] # center is conserved in the markers dtheta = np.arctan2(ypress+dy-yc,xpress+dx-xc)-np.arctan2(ypress-yc,xpress-xc) 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 == 'Square': h_ = w_ else: h_ = h0 elif self._ind == 2: h_ = h0+2*dy_ if (h0+2*dy_) > 0 else h0 if self.type == 'Square': w_ = h_ else: w_ = w0 # update rectangle self.rect.set_width(w_) self.rect.set_height(h_) self.rect.angle = theta_ # update markers self.updateMarkers() def updateMarkers(self): # update points x,y = self.compute_markers() self.line.set_data(x,y)
class PixelInteractor(QObject): epsilon = 10 showverts = True mySignal = pyqtSignal(str) modSignal = pyqtSignal(str) def __init__(self,ax,corner,width,angle=0.): super().__init__() from matplotlib.patches import Rectangle 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 self.type = 'Pixel' height = width self.ax = ax self.angle = angle self.width = width self.height = width # print('corner is ', corner) self.rect = Rectangle(corner,width,height,edgecolor='Lime',facecolor='none',angle=angle,fill=False,animated=True) self.ax.add_patch(self.rect) self.canvas = self.rect.figure.canvas x,y = self.compute_markers() self.line = Line2D(x, y, marker='s', linestyle=None, linewidth=0., markerfacecolor='g', animated=True) self.ax.add_line(self.line) self.cid = self.rect.add_callback(self.rectangle_changed) self._ind = None # the active point self.connect() self.aperture = self.rect self.press = None self.lock = None def compute_markers(self): # theta0 = self.rect.angle / 180.*np.pi w0 = self.rect.get_width() # h0 = self.rect.get_height() x0,y0 = self.rect.get_xy() angle0 = self.rect.angle x = [x0+w0/np.sqrt(2.)*np.sin((45.-angle0)*np.pi/180.)] y = [y0+w0/np.sqrt(2.)*np.cos((45.-angle0)*np.pi/180.)] self.xy = [(x,y)] return x, y 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.rect.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.rect) self.ax.draw_artist(self.line) def rectangle_changed(self, rect): '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, rect) self.line.set_visible(vis) def get_ind_under_point(self, event): 'get the index of the point if within epsilon tolerance' x, y = self.xy[0] d = np.hypot(x - event.xdata, y - event.ydata) if d >= self.epsilon: ind = None else: ind = 0 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.rect.get_xy() w0, h0 = self.rect.get_width(), self.rect.get_height() theta0 = self.rect.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.mySignal.emit('rectangle 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.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_rectangle() # Redraw rectangle and points self.canvas.restore_region(self.background) self.ax.draw_artist(self.rect) self.ax.draw_artist(self.line) self.canvas.update() self.canvas.flush_events() # alternative (slower) # self.canvas.draw_idle() # Notify callback self.modSignal.emit('rectangle modified') def update_rectangle(self): x0, y0, w0, h0, theta0, xpress, ypress = self.press dx, dy = self.dx, self.dy if self.lock == "pressed": self.lock = "move" 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.rect.set_xy((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)) self.updateMarkers() def updateMarkers(self): # update points x,y = self.compute_markers() self.line.set_data(x,y)