mirror of
https://github.com/wassname/scikit-image.git
synced 2026-08-07 11:28:14 +08:00
Merge pull request #1691 from juliusbierk/active-contour-model
New feature: Active contour model
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
====================
|
||||
Active Contour Model
|
||||
====================
|
||||
|
||||
The active contour model is a method to fit open or closed splines to lines or
|
||||
edges in an image. It works by minimising an energy that is in part defined by
|
||||
the image and part by the spline's shape: length and smoothness. The
|
||||
minimization is done implicitly in the shape energy and explicitly in the
|
||||
image energy.
|
||||
|
||||
In the following two examples the active contour model is used (1) to segment
|
||||
the face of a person from the rest of an image by fitting a closed curve
|
||||
to the edges of the face and (2) to find the darkest curve between two fixed
|
||||
points while obeying smoothness considerations. Typically it is a good idea to
|
||||
smooth images a bit before analyzing, as done in the following examples.
|
||||
|
||||
.. [1] *Snakes: Active contour models*. Kass, M.; Witkin, A.; Terzopoulos, D.
|
||||
International Journal of Computer Vision 1 (4): 321 (1988).
|
||||
|
||||
We initialize a circle around the astronaut's face and use the default boundary
|
||||
condition ``bc='periodic'`` to fit a closed curve. The default parameters
|
||||
``w_line=0, w_edge=1`` will make the curve search towards edges, such as the
|
||||
boundaries of the face.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from skimage.color import rgb2gray
|
||||
from skimage import data
|
||||
from skimage.filters import gaussian_filter
|
||||
from skimage.segmentation import active_contour
|
||||
|
||||
# Test scipy version, since active contour is only possible
|
||||
# with recent scipy version
|
||||
import scipy
|
||||
scipy_version = list(map(int, scipy.__version__.split('.')))
|
||||
new_scipy = scipy_version[0] > 0 or \
|
||||
(scipy_version[0] == 0 and scipy_version[1] >= 14)
|
||||
|
||||
img = data.astronaut()
|
||||
img = rgb2gray(img)
|
||||
|
||||
s = np.linspace(0, 2*np.pi, 400)
|
||||
x = 220 + 100*np.cos(s)
|
||||
y = 100 + 100*np.sin(s)
|
||||
init = np.array([x, y]).T
|
||||
|
||||
if not new_scipy:
|
||||
print('You are using an old version of scipy. '
|
||||
'Active contours is implemented for scipy versions '
|
||||
'0.14.0 and above.')
|
||||
|
||||
if new_scipy:
|
||||
snake = active_contour(gaussian_filter(img, 3),
|
||||
init, alpha=0.015, beta=10, gamma=0.001)
|
||||
|
||||
fig = plt.figure(figsize=(7, 7))
|
||||
ax = fig.add_subplot(111)
|
||||
plt.gray()
|
||||
ax.imshow(img)
|
||||
ax.plot(init[:, 0], init[:, 1], '--r')
|
||||
ax.plot(snake[:, 0], snake[:, 1], '-b')
|
||||
ax.set_xticks([]), ax.set_yticks([])
|
||||
ax.axis([0, img.shape[1], img.shape[0], 0])
|
||||
|
||||
"""
|
||||
.. image:: PLOT2RST.current_figure
|
||||
|
||||
Here we initialize a straight line between two points, `(5, 136)` and
|
||||
`(424, 50)`, and require that the spline has its end points there by giving
|
||||
the boundary condition `bc='fixed'`. We furthermore make the algorithm search
|
||||
for dark lines by giving a negative `w_line` value.
|
||||
"""
|
||||
|
||||
img = data.text()
|
||||
|
||||
x = np.linspace(5, 424, 100)
|
||||
y = np.linspace(136, 50, 100)
|
||||
init = np.array([x, y]).T
|
||||
|
||||
if new_scipy:
|
||||
snake = active_contour(gaussian_filter(img, 1), init, bc='fixed',
|
||||
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
|
||||
|
||||
fig = plt.figure(figsize=(9, 5))
|
||||
ax = fig.add_subplot(111)
|
||||
plt.gray()
|
||||
ax.imshow(img)
|
||||
ax.plot(init[:, 0], init[:, 1], '--r')
|
||||
ax.plot(snake[:, 0], snake[:, 1], '-b')
|
||||
ax.set_xticks([]), ax.set_yticks([])
|
||||
ax.axis([0, img.shape[1], img.shape[0], 0])
|
||||
|
||||
plt.show()
|
||||
|
||||
"""
|
||||
.. image:: PLOT2RST.current_figure
|
||||
"""
|
||||
@@ -1,4 +1,5 @@
|
||||
from .random_walker_segmentation import random_walker
|
||||
from .active_contour_model import active_contour
|
||||
from ._felzenszwalb import felzenszwalb
|
||||
from .slic_superpixels import slic
|
||||
from ._quickshift import quickshift
|
||||
@@ -8,6 +9,7 @@ from ._join import join_segmentations, relabel_from_one, relabel_sequential
|
||||
|
||||
|
||||
__all__ = ['random_walker',
|
||||
'active_contour',
|
||||
'felzenszwalb',
|
||||
'slic',
|
||||
'quickshift',
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import numpy as np
|
||||
from skimage import img_as_float
|
||||
import scipy
|
||||
import scipy.linalg
|
||||
from scipy.interpolate import RectBivariateSpline, interp2d
|
||||
from skimage.filters import sobel
|
||||
|
||||
|
||||
def active_contour(image, snake, alpha=0.01, beta=0.1,
|
||||
w_line=0, w_edge=1, gamma=0.01,
|
||||
bc='periodic', max_px_move=1.0,
|
||||
max_iterations=2500, convergence=0.1):
|
||||
"""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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : (N, M) or (N, M, 3) ndarray
|
||||
Input image.
|
||||
snake : (N, 2) ndarray
|
||||
Initialisation coordinates of snake. For periodic snakes, it should
|
||||
not include duplicate endpoints.
|
||||
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 to
|
||||
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 : {'periodic', 'free', 'fixed'}, optional
|
||||
Boundary conditions for worm. '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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from skimage.draw import circle_perimeter
|
||||
>>> from skimage.filters import gaussian_filter
|
||||
|
||||
Create and smooth image:
|
||||
|
||||
>>> img = np.zeros((100, 100))
|
||||
>>> rr, cc = circle_perimeter(35, 45, 25)
|
||||
>>> img[rr, cc] = 1
|
||||
>>> img = gaussian_filter(img, 2)
|
||||
|
||||
Initiliaze spline:
|
||||
|
||||
>>> s = np.linspace(0, 2*np.pi,100)
|
||||
>>> init = 50*np.array([np.cos(s), np.sin(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
|
||||
|
||||
"""
|
||||
scipy_version = list(map(int, scipy.__version__.split('.')))
|
||||
new_scipy = scipy_version[0] > 0 or \
|
||||
(scipy_version[0] == 0 and scipy_version[1] >= 14)
|
||||
if not new_scipy:
|
||||
raise NotImplementedError('You are using an old version of scipy. '
|
||||
'Active contours is implemented for scipy versions '
|
||||
'0.14.0 and above.')
|
||||
|
||||
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 bc 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)]
|
||||
for i in range(3 if RGB else 1):
|
||||
edge[i][0, :] = edge[i][1, :]
|
||||
edge[i][-1, :] = edge[i][-2, :]
|
||||
edge[i][:, 0] = edge[i][:, 1]
|
||||
edge[i][:, -1] = edge[i][:, -2]
|
||||
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:
|
||||
if new_scipy:
|
||||
intp = RectBivariateSpline(np.arange(img.shape[1]),
|
||||
np.arange(img.shape[0]),
|
||||
img.T, kx=2, ky=2, s=0)
|
||||
else:
|
||||
intp = np.vectorize(interp2d(np.arange(img.shape[1]),
|
||||
np.arange(img.shape[0]), img, kind='cubic',
|
||||
copy=False, bounds_error=False, fill_value=0))
|
||||
|
||||
x, y = snake[:, 0].copy(), snake[:, 1].copy()
|
||||
xsave = np.empty((convergence_order, len(x)))
|
||||
ysave = np.empty((convergence_order, len(x)))
|
||||
|
||||
# Build snake shape matrix for Euler equation
|
||||
n = len(x)
|
||||
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 bc.startswith('fixed'):
|
||||
A[0, :] = 0
|
||||
A[1, :] = 0
|
||||
A[1, :3] = [1, -2, 1]
|
||||
sfixed = True
|
||||
efixed = False
|
||||
if bc.endswith('fixed'):
|
||||
A[-1, :] = 0
|
||||
A[-2, :] = 0
|
||||
A[-2, -3:] = [1, -2, 1]
|
||||
efixed = True
|
||||
sfree = False
|
||||
if bc.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 bc.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 = scipy.linalg.inv(A+gamma*np.eye(n))
|
||||
|
||||
# Explicit time stepping for image energy minimization:
|
||||
for i in range(max_iterations):
|
||||
if new_scipy:
|
||||
fx = intp(x, y, dx=1, grid=False)
|
||||
fy = intp(x, y, dy=1, grid=False)
|
||||
else:
|
||||
fx = intp(x, y, dx=1)
|
||||
fy = intp(x, y, dy=1)
|
||||
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 = np.dot(inv, gamma*x + fx)
|
||||
yn = np.dot(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.array([x, y]).T
|
||||
@@ -0,0 +1,116 @@
|
||||
import numpy as np
|
||||
from skimage import data
|
||||
from skimage.color import rgb2gray
|
||||
from skimage.filters import gaussian_filter
|
||||
from skimage.segmentation import active_contour
|
||||
from numpy.testing import assert_equal, assert_allclose, assert_raises
|
||||
from numpy.testing.decorators import skipif
|
||||
import scipy
|
||||
|
||||
scipy_version = list(map(int, scipy.__version__.split('.')))
|
||||
new_scipy = scipy_version[0] > 0 or \
|
||||
(scipy_version[0] == 0 and scipy_version[1] >= 14)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_periodic_reference():
|
||||
img = data.astronaut()
|
||||
img = rgb2gray(img)
|
||||
s = np.linspace(0, 2*np.pi, 400)
|
||||
x = 220 + 100*np.cos(s)
|
||||
y = 100 + 100*np.sin(s)
|
||||
init = np.array([x, y]).T
|
||||
snake = active_contour(gaussian_filter(img, 3), init,
|
||||
alpha=0.015, beta=10, w_line=0, w_edge=1, gamma=0.001)
|
||||
refx = [299, 298, 298, 298, 298, 297, 297, 296, 296, 295]
|
||||
refy = [98, 99, 100, 101, 102, 103, 104, 105, 106, 108]
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_fixed_reference():
|
||||
img = data.text()
|
||||
x = np.linspace(5, 424, 100)
|
||||
y = np.linspace(136, 50, 100)
|
||||
init = np.array([x, y]).T
|
||||
snake = active_contour(gaussian_filter(img, 1), init, bc='fixed',
|
||||
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
|
||||
refx = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42]
|
||||
refy = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125]
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_free_reference():
|
||||
img = data.text()
|
||||
x = np.linspace(5, 424, 100)
|
||||
y = np.linspace(70, 40, 100)
|
||||
init = np.array([x, y]).T
|
||||
snake = active_contour(gaussian_filter(img, 3), init, bc='free',
|
||||
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
|
||||
refx = [10, 13, 16, 19, 23, 26, 29, 32, 36, 39]
|
||||
refy = [76, 76, 75, 74, 73, 72, 71, 70, 69, 69]
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_RGB():
|
||||
img = gaussian_filter(data.text(), 1)
|
||||
imgR = np.zeros((img.shape[0], img.shape[1], 3))
|
||||
imgG = np.zeros((img.shape[0], img.shape[1], 3))
|
||||
imgRGB = np.zeros((img.shape[0], img.shape[1], 3))
|
||||
imgR[:, :, 0] = img
|
||||
imgG[:, :, 1] = img
|
||||
imgRGB[:, :, :] = img[:, :, None]
|
||||
x = np.linspace(5, 424, 100)
|
||||
y = np.linspace(136, 50, 100)
|
||||
init = np.array([x, y]).T
|
||||
snake = active_contour(imgR, init, bc='fixed',
|
||||
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
|
||||
refx = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42]
|
||||
refy = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125]
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
snake = active_contour(imgG, init, bc='fixed',
|
||||
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1)
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
snake = active_contour(imgRGB, init, bc='fixed',
|
||||
alpha=0.1, beta=1.0, w_line=-5/3., w_edge=0, gamma=0.1)
|
||||
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refx)
|
||||
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refy)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_end_points():
|
||||
img = data.astronaut()
|
||||
img = rgb2gray(img)
|
||||
s = np.linspace(0, 2*np.pi, 400)
|
||||
x = 220 + 100*np.cos(s)
|
||||
y = 100 + 100*np.sin(s)
|
||||
init = np.array([x, y]).T
|
||||
snake = active_contour(gaussian_filter(img, 3), init,
|
||||
bc='periodic', alpha=0.015, beta=10, w_line=0, w_edge=1,
|
||||
gamma=0.001, max_iterations=100)
|
||||
assert np.sum(np.abs(snake[0, :]-snake[-1, :])) < 2
|
||||
snake = active_contour(gaussian_filter(img, 3), init,
|
||||
bc='free', alpha=0.015, beta=10, w_line=0, w_edge=1,
|
||||
gamma=0.001, max_iterations=100)
|
||||
assert np.sum(np.abs(snake[0, :]-snake[-1, :])) > 2
|
||||
snake = active_contour(gaussian_filter(img, 3), init,
|
||||
bc='fixed', alpha=0.015, beta=10, w_line=0, w_edge=1,
|
||||
gamma=0.001, max_iterations=100)
|
||||
assert_allclose(snake[0, :], [x[0], y[0]], atol=1e-5)
|
||||
|
||||
@skipif(not new_scipy)
|
||||
def test_bad_input():
|
||||
img = np.zeros((10, 10))
|
||||
x = np.linspace(5, 424, 100)
|
||||
y = np.linspace(136, 50, 100)
|
||||
init = np.array([x, y]).T
|
||||
assert_raises(ValueError, active_contour, img, init,
|
||||
bc='wrong')
|
||||
assert_raises(ValueError, active_contour, img, init,
|
||||
max_iterations=-15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
np.testing.run_module_suite()
|
||||
Reference in New Issue
Block a user