Example #1
0
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)