mirror of
https://github.com/wassname/scikit-image.git
synced 2026-08-12 12:30:16 +08:00
Random walker algo for segmentation + example
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
==========================
|
||||
Random walker segmentation
|
||||
==========================
|
||||
|
||||
The random walker algorithm (*Random walks for image segmentation*, Leo Grady,
|
||||
IEEE Trans Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83) determines the
|
||||
segmentation of an image from a set of markers labeling several phases (2 or
|
||||
more). An anisotropic diffusion equation is solved with tracers initiated
|
||||
at the markers' position. The local diffusivity coefficient is greater if
|
||||
neighboring pixels have similar values, so that diffusion is difficult across
|
||||
high gradients. The label of each unknown pixel is attributed to the label
|
||||
of the known marker that has the highest probability to be reached first
|
||||
during this diffusion process.
|
||||
|
||||
In this example, two phases are clearly visible, but the data are too
|
||||
noisy to perform the segmentation from the histogram only. We determine
|
||||
markers of the two phases from the extreme tails of the histogram of gray
|
||||
values, and use the random walker for the segmentation.
|
||||
"""
|
||||
print __doc__
|
||||
|
||||
import numpy as np
|
||||
from scipy import ndimage
|
||||
from skimage.segmentation import random_walker
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def microstructure(l=256):
|
||||
"""
|
||||
Synthetic binary data: binary microstructure with blobs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
l: int, optional
|
||||
linear size of the returned image
|
||||
"""
|
||||
n = 5
|
||||
x, y = np.ogrid[0:l, 0:l]
|
||||
mask_outer = (x - l / 2) ** 2 + (y - l / 2) ** 2 < (l / 2) ** 2
|
||||
mask = np.zeros((l, l))
|
||||
generator = np.random.RandomState(1)
|
||||
points = l * generator.rand(2, n ** 2)
|
||||
mask[(points[0]).astype(np.int), (points[1]).astype(np.int)] = 1
|
||||
mask = ndimage.gaussian_filter(mask, sigma=l / (4. * n))
|
||||
return (mask > mask.mean()).astype(np.float)
|
||||
|
||||
|
||||
# Generate noisy synthetic data
|
||||
data = microstructure(l=128)
|
||||
data += 0.35 * np.random.randn(*data.shape)
|
||||
markers = np.zeros(data.shape, dtype=np.uint)
|
||||
markers[data < -0.3] = 1
|
||||
markers[data > 1.3] = 2
|
||||
|
||||
# Run random walker algorithm
|
||||
labels = random_walker(data, markers, beta=10, mode='bf')
|
||||
|
||||
# Plot results
|
||||
plt.figure(figsize=(9, 3.5))
|
||||
plt.subplot(131)
|
||||
plt.imshow(data, cmap='gray', interpolation='nearest')
|
||||
plt.axis('off')
|
||||
plt.title('Noisy data')
|
||||
plt.subplot(132)
|
||||
plt.imshow(markers, cmap='hot', interpolation='nearest')
|
||||
plt.axis('off')
|
||||
plt.title('Markers')
|
||||
plt.subplot(133)
|
||||
plt.imshow(labels, cmap='gray', interpolation='nearest')
|
||||
plt.axis('off')
|
||||
plt.title('Segmentation')
|
||||
|
||||
plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0,
|
||||
right=1)
|
||||
plt.show()
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Random walker segmentation algorithm
|
||||
|
||||
from *Random walks for image segmentation*, Leo Grady, IEEE Trans
|
||||
Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83.
|
||||
|
||||
Dependencies:
|
||||
|
||||
* numpy >= 1.4, scipy
|
||||
|
||||
* optional: pyamg
|
||||
|
||||
Installing pyamg and using the 'cg_mg' mode of random_walker improves
|
||||
significantly the performance.
|
||||
"""
|
||||
|
||||
# Author: Emmanuelle Gouillart <emmanuelle.gouillart@normalesup.org>
|
||||
# Copyright (c) 2009-2011, Emmanuelle Gouillart
|
||||
# License: BSD
|
||||
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from scipy import sparse, ndimage
|
||||
try:
|
||||
from scipy.sparse.linalg.dsolve import umfpack
|
||||
u = umfpack.UmfpackContext()
|
||||
except:
|
||||
warnings.warn("""Scipy was built without UMFPACK. Consider rebuilding
|
||||
Scipy with UMFPACK, this will greatly speed up the random walker
|
||||
functions. You may also install pyamg and run the random walker function
|
||||
in cg_mg mode (see the docstrings)
|
||||
""")
|
||||
try:
|
||||
from pyamg import ruge_stuben_solver
|
||||
amg_loaded = True
|
||||
except ImportError:
|
||||
amg_loaded = False
|
||||
from scipy.sparse.linalg import cg
|
||||
|
||||
#-----------Laplacian--------------------
|
||||
|
||||
|
||||
def _make_edges_3d(n_x, n_y, n_z):
|
||||
"""
|
||||
Returns a list of edges for a 3D image.
|
||||
|
||||
Parameters
|
||||
===========
|
||||
n_x: integer
|
||||
The size of the grid in the x direction.
|
||||
n_y: integer
|
||||
The size of the grid in the y direction
|
||||
n_z: integer
|
||||
The size of the grid in the z direction
|
||||
"""
|
||||
vertices = np.arange(n_x * n_y * n_z).reshape((n_x, n_y, n_z))
|
||||
edges_deep = np.vstack((vertices[:, :, :-1].ravel(),
|
||||
vertices[:, :, 1:].ravel()))
|
||||
edges_right = np.vstack((vertices[:, :-1].ravel(),
|
||||
vertices[:, 1:].ravel()))
|
||||
edges_down = np.vstack((vertices[:-1].ravel(), vertices[1:].ravel()))
|
||||
edges = np.hstack((edges_deep, edges_right, edges_down))
|
||||
return edges
|
||||
|
||||
|
||||
def _compute_weights_3d(edges, data, beta=130, eps=1.e-6):
|
||||
l_x, l_y, l_z = data.shape
|
||||
gradients = _compute_gradients_3d(data) ** 2
|
||||
beta /= 10 * data.std()
|
||||
gradients *= beta
|
||||
weights = np.exp(- gradients)
|
||||
weights += eps
|
||||
return weights
|
||||
|
||||
|
||||
def _compute_gradients_3d(data):
|
||||
l_x, l_y, l_z = data.shape
|
||||
gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel()
|
||||
gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel()
|
||||
gr_down = np.abs(data[:-1] - data[1:]).ravel()
|
||||
return np.r_[gr_deep, gr_right, gr_down]
|
||||
|
||||
|
||||
def _make_laplacian_sparse(edges, weights):
|
||||
"""
|
||||
Sparse implementation
|
||||
"""
|
||||
pixel_nb = edges.max() + 1
|
||||
diag = np.arange(pixel_nb)
|
||||
i_indices = np.hstack((edges[0], edges[1]))
|
||||
j_indices = np.hstack((edges[1], edges[0]))
|
||||
data = np.hstack((-weights, -weights))
|
||||
lap = sparse.coo_matrix((data, (i_indices, j_indices)),
|
||||
shape=(pixel_nb, pixel_nb))
|
||||
connect = - np.ravel(lap.sum(axis=1))
|
||||
lap = sparse.coo_matrix((np.hstack((data, connect)),
|
||||
(np.hstack((i_indices, diag)), np.hstack((j_indices, diag)))),
|
||||
shape=(pixel_nb, pixel_nb))
|
||||
return lap.tocsr()
|
||||
|
||||
|
||||
def _clean_labels_ar(X, labels):
|
||||
labels = np.ravel(labels)
|
||||
labels[labels == 0] = X
|
||||
return labels
|
||||
|
||||
|
||||
def _buildAB(lap_sparse, labels):
|
||||
"""
|
||||
Build the matrix A and rhs B of the linear system to solve
|
||||
"""
|
||||
l_x, l_y, l_z = labels.shape
|
||||
labels = labels[labels >= 0]
|
||||
indices = np.arange(labels.size)
|
||||
unlabeled_indices = indices[labels == 0]
|
||||
seeds_indices = indices[labels > 0]
|
||||
# The following two lines take most of the time
|
||||
B = lap_sparse[unlabeled_indices][:, seeds_indices]
|
||||
lap_sparse = lap_sparse[unlabeled_indices][:, unlabeled_indices]
|
||||
nlabels = labels.max()
|
||||
rhs = []
|
||||
for lab in range(1, nlabels + 1):
|
||||
mask = (labels[seeds_indices] == lab)
|
||||
fs = sparse.csr_matrix(mask)
|
||||
fs = fs.transpose()
|
||||
rhs.append(B * fs)
|
||||
return lap_sparse, rhs
|
||||
|
||||
|
||||
def _trim_edges_weights(edges, weights, mask):
|
||||
mask0 = np.hstack((mask[:, :, :-1].ravel(), mask[:, :-1].ravel(),
|
||||
mask[:-1].ravel()))
|
||||
mask1 = np.hstack((mask[:, :, 1:].ravel(), mask[:, 1:].ravel(),
|
||||
mask[1:].ravel()))
|
||||
ind_mask = np.logical_and(mask0, mask1)
|
||||
edges, weights = edges[:, ind_mask], weights[ind_mask]
|
||||
maxval = edges.max()
|
||||
order = np.searchsorted(np.unique(edges.ravel()), np.arange(maxval + 1))
|
||||
edges = order[edges]
|
||||
return edges, weights
|
||||
|
||||
|
||||
def _build_laplacian(data, mask=None, beta=50):
|
||||
l_x, l_y, l_z = data.shape
|
||||
edges = _make_edges_3d(l_x, l_y, l_z)
|
||||
weights = _compute_weights_3d(edges, data, beta=beta, eps=1.e-10)
|
||||
if mask is not None:
|
||||
edges, weights = _trim_edges_weights(edges, weights, mask)
|
||||
lap = _make_laplacian_sparse(edges, weights)
|
||||
del edges, weights
|
||||
return lap
|
||||
|
||||
|
||||
#----------- Random walker algorithm --------------------------------
|
||||
|
||||
|
||||
def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True):
|
||||
"""
|
||||
Random walker algorithm for segmentation from markers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
data : array_like
|
||||
Image to be segmented in phases. `data` can be two- or
|
||||
three-dimensional.
|
||||
|
||||
labels : array of ints, of same shape as `data`
|
||||
Array of seed markers labeled with different positive integers
|
||||
for different phases. Zero-labeled pixels are unlabeled pixels.
|
||||
Negative labels correspond to inactive pixels that are not taken
|
||||
into account (they are removed from the graph).
|
||||
|
||||
beta : float
|
||||
Penalization coefficient for the random walker motion
|
||||
(the greater `beta`, the more difficult the diffusion).
|
||||
|
||||
mode : {'bf', 'cg_mg', 'cg'} (default: 'bf')
|
||||
Mode for solving the linear system in the random walker
|
||||
algorithm.
|
||||
|
||||
- 'bf' (brute force, default): an LU factorization of the
|
||||
Laplacian is computed. This is fast for small images (<1024x1024),
|
||||
but very slow (due to the memory cost) and memory-consuming for
|
||||
big images (in 3-D for example).
|
||||
|
||||
- 'cg' (conjugate gradient): the linear system is solved
|
||||
iteratively using the Conjugate Gradient method from
|
||||
scipy.sparse.linalg. This is less memory-consuming than the
|
||||
brute force method for large images, but it is quite slow.
|
||||
|
||||
- 'cg_mg' (conjugate gradient with multigrid preconditioner):
|
||||
a preconditioner is computed using a multigrid solver, then
|
||||
the solution is computed with the Conjugate Gradient method.
|
||||
This mode requires that the pyamg module
|
||||
(http://code.google.com/p/pyamg/) is installed. For images of
|
||||
size > 512x512, this is the recommended (fastest) mode.
|
||||
|
||||
tol : tolerance to achieve when solving the linear system, in
|
||||
cg' and 'cg_mg' modes.
|
||||
|
||||
copy : bool
|
||||
If copy is False, the `labels` array will be overwritten with
|
||||
the result of the segmentation. Use copy=False if you want to
|
||||
save on memory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
output : ndarray of ints
|
||||
Array in which each pixel has been labeled according to the marker
|
||||
that reached the pixel first by anisotropic diffusion.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
The algorithm was first proposed in *Random walks for image
|
||||
segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell.
|
||||
2006 Nov;28(11):1768-83.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> a = np.zeros((10, 10)) + 0.2*np.random.random((10, 10))
|
||||
>>> a[5:8, 5:8] += 1
|
||||
>>> b = np.zeros_like(a)
|
||||
>>> b[3,3] = 1 #Marker for first phase
|
||||
>>> b[6,6] = 2 #Marker for second phase
|
||||
>>> random_walker(a, b)
|
||||
array([[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 2., 2., 2., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
|
||||
[ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
|
||||
|
||||
"""
|
||||
# We work with 3-D arrays
|
||||
data = np.atleast_3d(data)
|
||||
if copy:
|
||||
labels = np.copy(labels)
|
||||
labels = labels.astype(np.int)
|
||||
# If the array has pruned zones, be sure that no isolated pixels
|
||||
# exist between pruned zones (they could not be determined)
|
||||
if np.any(labels < 0):
|
||||
filled = ndimage.binary_propagation(labels > 0, mask=labels >= 0)
|
||||
labels[np.logical_and(np.logical_not(filled), labels == 0)] = -1
|
||||
del filled
|
||||
labels = np.atleast_3d(labels)
|
||||
if np.any(labels < 0):
|
||||
lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta)
|
||||
else:
|
||||
lap_sparse = _build_laplacian(data, beta=beta)
|
||||
lap_sparse, B = _buildAB(lap_sparse, labels)
|
||||
# We solve the linear system
|
||||
# lap_sparse X = B
|
||||
# where X[i, j] is the probability that a marker of label i arrives
|
||||
# first at pixel j by anisotropic diffusion.
|
||||
if mode == 'cg':
|
||||
X = _solve_cg(lap_sparse, B, tol=tol)
|
||||
if mode == 'cg_mg':
|
||||
if not amg_loaded:
|
||||
warnings.warn(
|
||||
"""pyamg (http://code.google.com/p/pyamg/)) is needed to use
|
||||
this mode, but is not installed. The 'cg' mode will be used
|
||||
instead.""")
|
||||
X = _solve_cg(lap_sparse, B, tol=tol)
|
||||
else:
|
||||
X = _solve_cg_mg(lap_sparse, B, tol=tol)
|
||||
if mode == 'bf':
|
||||
X = _solve_bf(lap_sparse, B)
|
||||
X = _clean_labels_ar(X + 1, labels)
|
||||
data = np.squeeze(data)
|
||||
return X.reshape(data.shape)
|
||||
|
||||
|
||||
def _solve_bf(lap_sparse, B):
|
||||
"""
|
||||
solves lap_sparse X_i = B_i for each phase i. An LU decomposition
|
||||
of lap_sparse is computed first. For each pixel, the label i
|
||||
corresponding to the maximal X_i is returned.
|
||||
"""
|
||||
lap_sparse = lap_sparse.tocsc()
|
||||
solver = sparse.linalg.factorized(lap_sparse.astype(np.double))
|
||||
X = np.array([solver(np.array((-B[i]).todense()).ravel())\
|
||||
for i in range(len(B))])
|
||||
X = np.argmax(X, axis=0)
|
||||
return X
|
||||
|
||||
|
||||
def _solve_cg(lap_sparse, B, tol):
|
||||
"""
|
||||
solves lap_sparse X_i = B_i for each phase i, using the conjugate
|
||||
gradient method. For each pixel, the label i corresponding to the
|
||||
maximal X_i is returned.
|
||||
"""
|
||||
lap_sparse = lap_sparse.tocsc()
|
||||
X = []
|
||||
for i in range(len(B)):
|
||||
x0 = cg(lap_sparse, -B[i].todense(), tol=tol)[0]
|
||||
X.append(x0)
|
||||
X = np.array(X)
|
||||
X = np.argmax(X, axis=0)
|
||||
return X
|
||||
|
||||
|
||||
def _solve_cg_mg(lap_sparse, B, tol):
|
||||
"""
|
||||
solves lap_sparse X_i = B_i for each phase i, using the conjugate
|
||||
gradient method with a multigrid preconditioner (ruge-stuben from
|
||||
pyamg). For each pixel, the label i corresponding to the maximal
|
||||
X_i is returned.
|
||||
"""
|
||||
X = []
|
||||
ml = ruge_stuben_solver(lap_sparse)
|
||||
M = ml.aspreconditioner(cycle='V')
|
||||
for i in range(len(B)):
|
||||
x0 = cg(lap_sparse, -B[i].todense(), tol=tol, M=M, maxiter=30)[0]
|
||||
X.append(x0)
|
||||
X = np.array(X)
|
||||
X = np.argmax(X, axis=0)
|
||||
return X
|
||||
@@ -0,0 +1,107 @@
|
||||
import numpy as np
|
||||
from random_walker import random_walker
|
||||
try:
|
||||
import pyamg
|
||||
amg_loaded = True
|
||||
except ImportError:
|
||||
amg_loaded = False
|
||||
|
||||
|
||||
def make_2d_syntheticdata(lx, ly=None):
|
||||
if ly is None:
|
||||
ly = lx
|
||||
data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly)
|
||||
small_l = int(lx / 5)
|
||||
data[lx / 2 - small_l:lx / 2 + small_l,
|
||||
ly / 2 - small_l:ly / 2 + small_l] = 1
|
||||
data[lx / 2 - small_l + 1:lx / 2 + small_l - 1,
|
||||
ly / 2 - small_l + 1:ly / 2 + small_l - 1] = \
|
||||
0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2)
|
||||
data[lx / 2 - small_l, ly / 2 - small_l / 8:ly / 2 + small_l / 8] = 0
|
||||
seeds = np.zeros_like(data)
|
||||
seeds[lx / 5, ly / 5] = 1
|
||||
seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4] = 2
|
||||
return data, seeds
|
||||
|
||||
|
||||
def make_3d_syntheticdata(lx, ly=None, lz=None):
|
||||
if ly is None:
|
||||
ly = lx
|
||||
if lz is None:
|
||||
lz = lx
|
||||
data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz)
|
||||
small_l = int(lx / 5)
|
||||
data[lx / 2 - small_l:lx / 2 + small_l,
|
||||
ly / 2 - small_l:ly / 2 + small_l,
|
||||
lz / 2 - small_l:lz / 2 + small_l] = 1
|
||||
data[lx / 2 - small_l + 1:lx / 2 + small_l - 1,
|
||||
ly / 2 - small_l + 1:ly / 2 + small_l - 1,
|
||||
lz / 2 - small_l + 1:lz / 2 + small_l - 1] = 0
|
||||
# make a hole
|
||||
hole_size = np.max([1, small_l / 8])
|
||||
data[lx / 2 - small_l,
|
||||
ly / 2 - hole_size:ly / 2 + hole_size,\
|
||||
lz / 2 - hole_size:lz / 2 + hole_size] = 0
|
||||
seeds = np.zeros_like(data)
|
||||
seeds[lx / 5, ly / 5, lz / 5] = 1
|
||||
seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4, lz / 2 - small_l / 4] = 2
|
||||
return data, seeds
|
||||
|
||||
|
||||
def test_2d_bf():
|
||||
lx = 70
|
||||
ly = 100
|
||||
data, labels = make_2d_syntheticdata(lx, ly)
|
||||
labels_bf = random_walker(data, labels, beta=90, mode='bf')
|
||||
assert (labels_bf[25:45, 40:60] == 2).all()
|
||||
return data, labels_bf
|
||||
|
||||
|
||||
def test_2d_cg():
|
||||
lx = 70
|
||||
ly = 100
|
||||
data, labels = make_2d_syntheticdata(lx, ly)
|
||||
labels_cg = random_walker(data, labels, beta=90, mode='cg')
|
||||
assert (labels_cg[25:45, 40:60] == 2).all()
|
||||
return data, labels_cg
|
||||
|
||||
|
||||
def test_2d_cg_mg():
|
||||
lx = 70
|
||||
ly = 100
|
||||
data, labels = make_2d_syntheticdata(lx, ly)
|
||||
labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg')
|
||||
assert (labels_cg_mg[25:45, 40:60] == 2).all()
|
||||
return data, labels_cg_mg
|
||||
|
||||
|
||||
def test_2d_inactive():
|
||||
lx = 70
|
||||
ly = 100
|
||||
data, labels = make_2d_syntheticdata(lx, ly)
|
||||
labels[10:20, 10:20] = -1
|
||||
labels[46:50, 33:38] = -2
|
||||
labels = random_walker(data, labels, beta=90)
|
||||
assert (labels.reshape((lx, ly))[25:45, 40:60] == 2).all()
|
||||
return data, labels
|
||||
|
||||
|
||||
def test_3d():
|
||||
n = 30
|
||||
lx, ly, lz = n, n, n
|
||||
data, labels = make_3d_syntheticdata(lx, ly, lz)
|
||||
labels = random_walker(data, labels, mode='cg')
|
||||
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
|
||||
return data, labels
|
||||
|
||||
|
||||
def test_3d_inactive():
|
||||
n = 30
|
||||
lx, ly, lz = n, n, n
|
||||
data, labels = make_3d_syntheticdata(lx, ly, lz)
|
||||
old_labels = np.copy(labels)
|
||||
labels[5:25, 26:29, 26:29] = -1
|
||||
after_labels = np.copy(labels)
|
||||
labels = random_walker(data, labels, mode='cg')
|
||||
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
|
||||
return data, labels, old_labels, after_labels
|
||||
Reference in New Issue
Block a user