def clear_cache(self):
     """ clears the cache. This method should be called if any of the
     parameters of the model are changed """
     self._Pinv_cache = DictFiniteCapacity(capacity=self.max_cache_count)
 def clear_cache(self):
     """ clears the cache. This method should be called if any of the
     parameters of the model are changed """
     self._Pinv_cache = DictFiniteCapacity(capacity=self.max_cache_count)
class ActiveContour(object):
    """ class that manages an algorithm for using active contours for edge
    detection [http://en.wikipedia.org/wiki/Active_contour_model]
    
    This implementation is inspired by the following articles:
        http://www.pagines.ma1.upc.edu/~toni/files/SnakesAivru86c.pdf
        http://www.cb.uu.se/~cris/blog/index.php/archives/217
    """
    
    max_iterations = 50  #< maximal number of iterations
    max_cache_count = 20 #< maximal number of cache entries
    residual_tolerance = 1 #< stop iteration when reaching this residual value
    
    
    def __init__(self, blur_radius=10, alpha=0, beta=1e2, gamma=0.001,
                 closed_loop=False):
        """ initializes the active contour model
        blur_radius sets the length scale of the attraction to features.
            As a drawback, this is also the largest feature size that can be
            resolved by the contour.
        alpha is the line tension of the contour (high alpha leads to shorter
            contours)
        beta is the stiffness of the contour (high beta leads to straighter
            contours)
        gamma is the time scale of the convergence (high gamma might lead to 
            overshoot)
        closed_loop indicates whether the contour is a closed loop
        """
        
        self.blur_radius = blur_radius
        self.alpha = alpha  #< line tension
        self.beta = beta    #< stiffness 
        self.gamma = gamma  #< convergence rate
        self.closed_loop = closed_loop
        
        self.clear_cache()  #< also initializes the cache
        self.fx = self.fy = None
        self.info = {}


    def clear_cache(self):
        """ clears the cache. This method should be called if any of the
        parameters of the model are changed """
        self._Pinv_cache = DictFiniteCapacity(capacity=self.max_cache_count)

        
    def get_evolution_matrix(self, N, ds):
        """ calculates the evolution matrix """
        # scale parameters
        alpha = self.alpha/ds**2 # tension ~1/ds^2
        beta = self.beta/ds**4   # stiffness ~ 1/ds^4
        
        # calculate matrix entries
        a = self.gamma*(2*alpha + 6*beta) + 1
        b = self.gamma*(-alpha - 4*beta)
        c = self.gamma*beta
        
        if self.closed_loop:
            # matrix for closed loop
            P = (
                np.diag(np.zeros(N) + a) +
                np.diag(np.zeros(N-1) + b, 1) + np.diag(   [b], -N+1) +
                np.diag(np.zeros(N-1) + b,-1) + np.diag(   [b],  N-1) +
                np.diag(np.zeros(N-2) + c, 2) + np.diag([c, c], -N+2) +
                np.diag(np.zeros(N-2) + c,-2) + np.diag([c, c],  N-2)
            )
            
        else:
            # matrix for open end with vanishing derivatives
            P = (
                np.diag(np.zeros(N) + a) +
                np.diag(np.zeros(N-1) + b, 1) +
                np.diag(np.zeros(N-1) + b,-1) +
                np.diag(np.zeros(N-2) + c, 2) +
                np.diag(np.zeros(N-2) + c,-2)
            )
            P[0, 1] = P[-1, -2] = 2*b
            P[0, 2] = P[-1, -3] = 2*c
            P[0, 2] = P[-1, -3] = 2*c
            P[1, 1] = P[-2, -2] = a + c

        # create inverse matrix for iteration                
        return np.linalg.inv(P)
        
    
    def set_potential(self, potential):
        """ sets the potential and calculates the associated derivatives """
        # get image gradient
        if self.blur_radius > 0:
            potential = cv2.GaussianBlur(potential, (0, 0), self.blur_radius)
        self.fx = cv2.Sobel(potential, cv2.CV_64F, 1, 0, ksize=5)
        self.fy = cv2.Sobel(potential, cv2.CV_64F, 0, 1, ksize=5)
    
        
    def find_contour(self, curve, anchor_x=None, anchor_y=None):
        """ adapts the contour given by points to the potential image
        anchor_x can be a list of indices for those points whose x-coordinate
            should be kept fixed.
        anchor_y is the respective argument for the y-coordinate
        """
        if self.fx is None:
            raise RuntimeError('Potential must be set before the contour can '
                               'be adapted.')

        # curve must be equidistant for this implementation to work
        curve = np.asarray(curve)    
        points = curves.make_curve_equidistant(curve)
        
        # check for marginal small cases
        if len(points) <= 2:
            return points
        
        def _get_anchors(indices, coord):
            """ helper function for determining the anchor points """
            if indices is None or len(indices) == 0:
                return tuple(), tuple()
            # get points where the coordinate `coord` has to be kept fixed
            ps = curve[indices, :] 
            # find the points closest to the anchor points
            dist = spatial.distance.cdist(points, ps)
            return np.argmin(dist, axis=0), ps[:, coord] 
        
        # determine anchor_points if requested
        if anchor_x is not None or anchor_y is not None:
            has_anchors = True
            x_idx, x_vals = _get_anchors(anchor_x, 0)
            y_idx, y_vals = _get_anchors(anchor_y, 1)
        else:
            has_anchors = False

        # determine point spacing if it is not given
        ds = curves.curve_length(points)/(len(points) - 1)
            
        # try loading the evolution matrix from the cache            
        cache_key = (len(points), ds)
        Pinv = self._Pinv_cache.get(cache_key, None)
        if Pinv is None:
            # add new item to cache
            Pinv = self.get_evolution_matrix(len(points), ds)
            self._Pinv_cache[cache_key] = Pinv
    
        # restrict control points to shape of the potential
        points[:, 0] = np.clip(points[:, 0], 0, self.fx.shape[1] - 2)
        points[:, 1] = np.clip(points[:, 1], 0, self.fx.shape[0] - 2)

        # create intermediate array
        points_initial = points.copy()
        ps = points.copy()
    
        for k in xrange(self.max_iterations):
            # calculate external force
            fex = image.subpixels(self.fx, points)
            fey = image.subpixels(self.fy, points)
            
            # move control points
            ps[:, 0] = np.dot(Pinv, points[:, 0] + self.gamma*fex)
            ps[:, 1] = np.dot(Pinv, points[:, 1] + self.gamma*fey)
            
            # enforce the position of the anchor points
            if has_anchors:
                ps[x_idx, 0] = x_vals
                ps[y_idx, 1] = y_vals
            
            # check the distance that we evolved
            residual = np.abs(ps - points).sum()

            # restrict control points to shape of the potential
            points[:, 0] = np.clip(ps[:, 0], 0, self.fx.shape[1] - 2)
            points[:, 1] = np.clip(ps[:, 1], 0, self.fx.shape[0] - 2)

            if residual < self.residual_tolerance * self.gamma:
                break
            
        # collect additional information
        self.info['iteration_count'] = k + 1
        self.info['total_variation'] = np.abs(points_initial - points).sum()
    
        return points
class ActiveContour(object):
    """ class that manages an algorithm for using active contours for edge
    detection [http://en.wikipedia.org/wiki/Active_contour_model]
    
    This implementation is inspired by the following articles:
        http://www.pagines.ma1.upc.edu/~toni/files/SnakesAivru86c.pdf
        http://www.cb.uu.se/~cris/blog/index.php/archives/217
    """

    max_iterations = 50  #< maximal number of iterations
    max_cache_count = 20  #< maximal number of cache entries
    residual_tolerance = 1  #< stop iteration when reaching this residual value

    def __init__(self,
                 blur_radius=10,
                 alpha=0,
                 beta=1e2,
                 gamma=0.001,
                 closed_loop=False):
        """ initializes the active contour model
        blur_radius sets the length scale of the attraction to features.
            As a drawback, this is also the largest feature size that can be
            resolved by the contour.
        alpha is the line tension of the contour (high alpha leads to shorter
            contours)
        beta is the stiffness of the contour (high beta leads to straighter
            contours)
        gamma is the time scale of the convergence (high gamma might lead to 
            overshoot)
        closed_loop indicates whether the contour is a closed loop
        """

        self.blur_radius = blur_radius
        self.alpha = alpha  #< line tension
        self.beta = beta  #< stiffness
        self.gamma = gamma  #< convergence rate
        self.closed_loop = closed_loop

        self.clear_cache()  #< also initializes the cache
        self.fx = self.fy = None
        self.info = {}

    def clear_cache(self):
        """ clears the cache. This method should be called if any of the
        parameters of the model are changed """
        self._Pinv_cache = DictFiniteCapacity(capacity=self.max_cache_count)

    def get_evolution_matrix(self, N, ds):
        """ calculates the evolution matrix """
        # scale parameters
        alpha = self.alpha / ds**2  # tension ~1/ds^2
        beta = self.beta / ds**4  # stiffness ~ 1/ds^4

        # calculate matrix entries
        a = self.gamma * (2 * alpha + 6 * beta) + 1
        b = self.gamma * (-alpha - 4 * beta)
        c = self.gamma * beta

        if self.closed_loop:
            # matrix for closed loop
            P = (np.diag(np.zeros(N) + a) + np.diag(np.zeros(N - 1) + b, 1) +
                 np.diag([b], -N + 1) + np.diag(np.zeros(N - 1) + b, -1) +
                 np.diag([b], N - 1) + np.diag(np.zeros(N - 2) + c, 2) +
                 np.diag([c, c], -N + 2) + np.diag(np.zeros(N - 2) + c, -2) +
                 np.diag([c, c], N - 2))

        else:
            # matrix for open end with vanishing derivatives
            P = (np.diag(np.zeros(N) + a) + np.diag(np.zeros(N - 1) + b, 1) +
                 np.diag(np.zeros(N - 1) + b, -1) +
                 np.diag(np.zeros(N - 2) + c, 2) +
                 np.diag(np.zeros(N - 2) + c, -2))
            P[0, 1] = P[-1, -2] = 2 * b
            P[0, 2] = P[-1, -3] = 2 * c
            P[0, 2] = P[-1, -3] = 2 * c
            P[1, 1] = P[-2, -2] = a + c

        # create inverse matrix for iteration
        return np.linalg.inv(P)

    def set_potential(self, potential):
        """ sets the potential and calculates the associated derivatives """
        # get image gradient
        if self.blur_radius > 0:
            potential = cv2.GaussianBlur(potential, (0, 0), self.blur_radius)
        self.fx = cv2.Sobel(potential, cv2.CV_64F, 1, 0, ksize=5)
        self.fy = cv2.Sobel(potential, cv2.CV_64F, 0, 1, ksize=5)

    def find_contour(self, curve, anchor_x=None, anchor_y=None):
        """ adapts the contour given by points to the potential image
        anchor_x can be a list of indices for those points whose x-coordinate
            should be kept fixed.
        anchor_y is the respective argument for the y-coordinate
        """
        if self.fx is None:
            raise RuntimeError('Potential must be set before the contour can '
                               'be adapted.')

        # curve must be equidistant for this implementation to work
        curve = np.asarray(curve)
        points = curves.make_curve_equidistant(curve)

        # check for marginal small cases
        if len(points) <= 2:
            return points

        def _get_anchors(indices, coord):
            """ helper function for determining the anchor points """
            if indices is None or len(indices) == 0:
                return tuple(), tuple()
            # get points where the coordinate `coord` has to be kept fixed
            ps = curve[indices, :]
            # find the points closest to the anchor points
            dist = spatial.distance.cdist(points, ps)
            return np.argmin(dist, axis=0), ps[:, coord]

        # determine anchor_points if requested
        if anchor_x is not None or anchor_y is not None:
            has_anchors = True
            x_idx, x_vals = _get_anchors(anchor_x, 0)
            y_idx, y_vals = _get_anchors(anchor_y, 1)
        else:
            has_anchors = False

        # determine point spacing if it is not given
        ds = curves.curve_length(points) / (len(points) - 1)

        # try loading the evolution matrix from the cache
        cache_key = (len(points), ds)
        Pinv = self._Pinv_cache.get(cache_key, None)
        if not Pinv:
            # add new item to cache
            Pinv = self.get_evolution_matrix(len(points), ds)
            self._Pinv_cache[cache_key] = Pinv

        # restrict control points to shape of the potential
        points[:, 0] = np.clip(points[:, 0], 0, self.fx.shape[1] - 2)
        points[:, 1] = np.clip(points[:, 1], 0, self.fx.shape[0] - 2)

        # create intermediate array
        points_initial = points.copy()
        ps = points.copy()

        for k in xrange(self.max_iterations):
            # calculate external force
            fex = image.subpixels(self.fx, points)
            fey = image.subpixels(self.fy, points)

            # move control points
            ps[:, 0] = np.dot(Pinv, points[:, 0] + self.gamma * fex)
            ps[:, 1] = np.dot(Pinv, points[:, 1] + self.gamma * fey)

            # enforce the position of the anchor points
            if has_anchors:
                ps[x_idx, 0] = x_vals
                ps[y_idx, 1] = y_vals

            # check the distance that we evolved
            residual = np.abs(ps - points).sum()

            # restrict control points to shape of the potential
            points[:, 0] = np.clip(ps[:, 0], 0, self.fx.shape[1] - 2)
            points[:, 1] = np.clip(ps[:, 1], 0, self.fx.shape[0] - 2)

            if residual < self.residual_tolerance * self.gamma:
                break

        # collect additional information
        self.info['iteration_count'] = k + 1
        self.info['total_variation'] = np.abs(points_initial - points).sum()

        return points