245 lines
8.5 KiB
Python
245 lines
8.5 KiB
Python
from warnings import warn
|
|
import numpy as np
|
|
from scipy.interpolate import RectBivariateSpline
|
|
from ..util import img_as_float
|
|
from ..filters import sobel
|
|
|
|
|
|
def active_contour(image, snake, alpha=0.01, beta=0.1,
|
|
w_line=0, w_edge=1, gamma=0.01,
|
|
bc=None, max_px_move=1.0,
|
|
max_iterations=2500, convergence=0.1,
|
|
*,
|
|
boundary_condition='periodic',
|
|
coordinates=None):
|
|
"""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 : (N, M) or (N, M, 3) ndarray
|
|
Input image.
|
|
snake : (N, 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.
|
|
bc : deprecated; use ``boundary_condition``
|
|
DEPRECATED. See ``boundary_condition`` below.
|
|
max_px_move : float, optional
|
|
Maximum pixel distance to move per iteration.
|
|
max_iterations : int, optional
|
|
Maximum iterations to optimize snake shape.
|
|
convergence : float, optional
|
|
Convergence criteria.
|
|
boundary_condition : string, 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.
|
|
coordinates : {'rc' or 'xy'}, optional
|
|
Whether to use rc or xy coordinates. The 'xy' option (current default)
|
|
will be removed in version 0.18.
|
|
|
|
Returns
|
|
-------
|
|
snake : (N, 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, 2)
|
|
|
|
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, coordinates='rc') # doctest: +SKIP
|
|
>>> dist = np.sqrt((45-snake[:, 0])**2 + (35-snake[:, 1])**2) # doctest: +SKIP
|
|
>>> int(np.mean(dist)) # doctest: +SKIP
|
|
25
|
|
|
|
"""
|
|
if bc is not None:
|
|
message = ('The keyword argument `bc` to `active_contour` has been '
|
|
'renamed. Use `boundary_condition=` instead. `bc` will be '
|
|
'removed in scikit-image v0.18.')
|
|
warn(message, stacklevel=2)
|
|
boundary_condition = bc
|
|
if coordinates is None:
|
|
message = ('The coordinates used by `active_contour` will change '
|
|
'from xy coordinates (transposed from image dimensions) to '
|
|
'rc coordinates in scikit-image 0.18. Set '
|
|
"`coordinates='rc'` to silence this warning. "
|
|
"`coordinates='xy'` will restore the old behavior until "
|
|
'0.18, but will stop working thereafter.')
|
|
warn(message, category=FutureWarning, stacklevel=2)
|
|
coordinates = 'xy'
|
|
snake_xy = snake
|
|
if coordinates == 'rc':
|
|
snake_xy = snake[:, ::-1]
|
|
max_iterations = int(max_iterations)
|
|
if max_iterations <= 0:
|
|
raise ValueError("max_iterations 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)
|
|
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)
|
|
|
|
x, y = snake_xy[:, 0].astype(np.float), snake_xy[:, 1].astype(np.float)
|
|
n = len(x)
|
|
xsave = np.empty((convergence_order, n))
|
|
ysave = np.empty((convergence_order, n))
|
|
|
|
# Build snake shape matrix for Euler equation
|
|
a = np.roll(np.eye(n), -1, axis=0) + \
|
|
np.roll(np.eye(n), -1, axis=1) - \
|
|
2*np.eye(n) # second order derivative, central difference
|
|
b = np.roll(np.eye(n), -2, axis=0) + \
|
|
np.roll(np.eye(n), -2, axis=1) - \
|
|
4*np.roll(np.eye(n), -1, axis=0) - \
|
|
4*np.roll(np.eye(n), -1, axis=1) + \
|
|
6*np.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*np.eye(n))
|
|
|
|
# Explicit time stepping for image energy minimization:
|
|
for i in range(max_iterations):
|
|
fx = intp(x, y, dx=1, grid=False)
|
|
fy = intp(x, y, dy=1, grid=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
|
|
|
|
if coordinates == 'xy':
|
|
return np.stack([x, y], axis=1)
|
|
else:
|
|
return np.stack([y, x], axis=1)
|