| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import numpy as np
- from scipy.interpolate import RectBivariateSpline
- from .._shared.utils import _supported_float_type
- from ..util import img_as_float
- from ..filters import sobel
- def active_contour(
- image,
- snake,
- alpha=0.01,
- beta=0.1,
- w_line=0.0,
- w_edge=1,
- gamma=0.01,
- max_px_move=1.0,
- max_num_iter=2500,
- convergence=0.1,
- *,
- boundary_condition='periodic',
- ):
- """Active contour model.
- Active contours by fitting snakes to features of images. Supports single
- and multichannel 2D images. Snakes can be periodic (for segmentation) or
- have fixed and/or free ends.
- The output snake has the same length as the input boundary.
- As the number of points is constant, make sure that the initial snake
- has enough points to capture the details of the final contour.
- Parameters
- ----------
- image : (M, N) or (M, N, 3) ndarray
- Input image.
- snake : (K, 2) ndarray
- Initial snake coordinates. For periodic boundary conditions, endpoints
- must not be duplicated.
- alpha : float, optional
- Snake length shape parameter. Higher values makes snake contract
- faster.
- beta : float, optional
- Snake smoothness shape parameter. Higher values makes snake smoother.
- w_line : float, optional
- Controls attraction to brightness. Use negative values to attract
- toward dark regions.
- w_edge : float, optional
- Controls attraction to edges. Use negative values to repel snake from
- edges.
- gamma : float, optional
- Explicit time stepping parameter.
- max_px_move : float, optional
- Maximum pixel distance to move per iteration.
- max_num_iter : int, optional
- Maximum iterations to optimize snake shape.
- convergence : float, optional
- Convergence criteria.
- boundary_condition : str, optional
- Boundary conditions for the contour. Can be one of 'periodic',
- 'free', 'fixed', 'free-fixed', or 'fixed-free'. 'periodic' attaches
- the two ends of the snake, 'fixed' holds the end-points in place,
- and 'free' allows free movement of the ends. 'fixed' and 'free' can
- be combined by parsing 'fixed-free', 'free-fixed'. Parsing
- 'fixed-fixed' or 'free-free' yields same behaviour as 'fixed' and
- 'free', respectively.
- Returns
- -------
- snake : (K, 2) ndarray
- Optimised snake, same shape as input parameter.
- References
- ----------
- .. [1] Kass, M.; Witkin, A.; Terzopoulos, D. "Snakes: Active contour
- models". International Journal of Computer Vision 1 (4): 321
- (1988). :DOI:`10.1007/BF00133570`
- Examples
- --------
- >>> from skimage.draw import circle_perimeter
- >>> from skimage.filters import gaussian
- Create and smooth image:
- >>> img = np.zeros((100, 100))
- >>> rr, cc = circle_perimeter(35, 45, 25)
- >>> img[rr, cc] = 1
- >>> img = gaussian(img, sigma=2, preserve_range=False)
- Initialize spline:
- >>> s = np.linspace(0, 2*np.pi, 100)
- >>> init = 50 * np.array([np.sin(s), np.cos(s)]).T + 50
- Fit spline to image:
- >>> snake = active_contour(img, init, w_edge=0, w_line=1) # doctest: +SKIP
- >>> dist = np.sqrt((45-snake[:, 0])**2 + (35-snake[:, 1])**2) # doctest: +SKIP
- >>> int(np.mean(dist)) # doctest: +SKIP
- 25
- """
- max_num_iter = int(max_num_iter)
- if max_num_iter <= 0:
- raise ValueError("max_num_iter should be >0.")
- convergence_order = 10
- valid_bcs = [
- 'periodic',
- 'free',
- 'fixed',
- 'free-fixed',
- 'fixed-free',
- 'fixed-fixed',
- 'free-free',
- ]
- if boundary_condition not in valid_bcs:
- raise ValueError(
- "Invalid boundary condition.\n"
- + "Should be one of: "
- + ", ".join(valid_bcs)
- + '.'
- )
- img = img_as_float(image)
- float_dtype = _supported_float_type(image.dtype)
- img = img.astype(float_dtype, copy=False)
- RGB = img.ndim == 3
- # Find edges using sobel:
- if w_edge != 0:
- if RGB:
- edge = [sobel(img[:, :, 0]), sobel(img[:, :, 1]), sobel(img[:, :, 2])]
- else:
- edge = [sobel(img)]
- else:
- edge = [0]
- # Superimpose intensity and edge images:
- if RGB:
- img = w_line * np.sum(img, axis=2) + w_edge * sum(edge)
- else:
- img = w_line * img + w_edge * edge[0]
- # Interpolate for smoothness:
- intp = RectBivariateSpline(
- np.arange(img.shape[1]), np.arange(img.shape[0]), img.T, kx=2, ky=2, s=0
- )
- snake_xy = snake[:, ::-1]
- x = snake_xy[:, 0].astype(float_dtype)
- y = snake_xy[:, 1].astype(float_dtype)
- n = len(x)
- xsave = np.empty((convergence_order, n), dtype=float_dtype)
- ysave = np.empty((convergence_order, n), dtype=float_dtype)
- # Build snake shape matrix for Euler equation in double precision
- eye_n = np.eye(n, dtype=float)
- a = (
- np.roll(eye_n, -1, axis=0) + np.roll(eye_n, -1, axis=1) - 2 * eye_n
- ) # second order derivative, central difference
- b = (
- np.roll(eye_n, -2, axis=0)
- + np.roll(eye_n, -2, axis=1)
- - 4 * np.roll(eye_n, -1, axis=0)
- - 4 * np.roll(eye_n, -1, axis=1)
- + 6 * eye_n
- ) # fourth order derivative, central difference
- A = -alpha * a + beta * b
- # Impose boundary conditions different from periodic:
- sfixed = False
- if boundary_condition.startswith('fixed'):
- A[0, :] = 0
- A[1, :] = 0
- A[1, :3] = [1, -2, 1]
- sfixed = True
- efixed = False
- if boundary_condition.endswith('fixed'):
- A[-1, :] = 0
- A[-2, :] = 0
- A[-2, -3:] = [1, -2, 1]
- efixed = True
- sfree = False
- if boundary_condition.startswith('free'):
- A[0, :] = 0
- A[0, :3] = [1, -2, 1]
- A[1, :] = 0
- A[1, :4] = [-1, 3, -3, 1]
- sfree = True
- efree = False
- if boundary_condition.endswith('free'):
- A[-1, :] = 0
- A[-1, -3:] = [1, -2, 1]
- A[-2, :] = 0
- A[-2, -4:] = [-1, 3, -3, 1]
- efree = True
- # Only one inversion is needed for implicit spline energy minimization:
- inv = np.linalg.inv(A + gamma * eye_n)
- # can use float_dtype once we have computed the inverse in double precision
- inv = inv.astype(float_dtype, copy=False)
- # Explicit time stepping for image energy minimization:
- for i in range(max_num_iter):
- # RectBivariateSpline always returns float64, so call astype here
- fx = intp(x, y, dx=1, grid=False).astype(float_dtype, copy=False)
- fy = intp(x, y, dy=1, grid=False).astype(float_dtype, copy=False)
- if sfixed:
- fx[0] = 0
- fy[0] = 0
- if efixed:
- fx[-1] = 0
- fy[-1] = 0
- if sfree:
- fx[0] *= 2
- fy[0] *= 2
- if efree:
- fx[-1] *= 2
- fy[-1] *= 2
- xn = inv @ (gamma * x + fx)
- yn = inv @ (gamma * y + fy)
- # Movements are capped to max_px_move per iteration:
- dx = max_px_move * np.tanh(xn - x)
- dy = max_px_move * np.tanh(yn - y)
- if sfixed:
- dx[0] = 0
- dy[0] = 0
- if efixed:
- dx[-1] = 0
- dy[-1] = 0
- x += dx
- y += dy
- # Convergence criteria needs to compare to a number of previous
- # configurations since oscillations can occur.
- j = i % (convergence_order + 1)
- if j < convergence_order:
- xsave[j, :] = x
- ysave[j, :] = y
- else:
- dist = np.min(
- np.max(np.abs(xsave - x[None, :]) + np.abs(ysave - y[None, :]), 1)
- )
- if dist < convergence:
- break
- return np.stack([y, x], axis=1)
|