Changes based on PR review recommendations: input format, scaling, and bugfix.

In this new version, all instances of 'spectrum' have been replaced with 'channel'.  The documentation also reflects this change, and the new multichannel kwarg used to indicate multichannel input is named appropriately.

New boolean multichannel kwarg added, which controls if the input has multiple channels or not.  Input 'data' is now array_like for both gray-level and multichannel.  This kwarg is needed mainly because a 3-D array could be either 3 spatial dimensions or a set of different 2-D channels.

New scaling kwarg added (may be removed in future), controlling if data scaling is applied to ALL channels or each channel individually, if multichannel=True. No effect for gray-level data.

Removed np.sqrt(gradients) in _compute_weights_3d(), which was a bug. Tests now pass consistently.

New method for maintaining shape from input to output, where dims = data.shape prior to np.atleast_3d().  A theoretical (70,100,1) array passed should now result in a (70,100,1) shaped output, for example.

Updated and fixed multispectral test script to work with new version.  TODO: Additional test(s) likely needed to cover code branches from new kwargs.
This commit is contained in:
JDWarner
2012-08-29 16:33:56 -05:00
parent 682d0535cd
commit 61320957eb
2 changed files with 78 additions and 45 deletions
@@ -64,22 +64,29 @@ def _make_graph_edges_3d(n_x, n_y, n_z):
return edges
def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1.):
def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1.,
multichannel=False):
# Weight calculation is main difference in multispectral version
# Original gradient**2 replaced with sqrt( sum of gradients**2 )
for i, spectrum in enumerate(data):
if i == 0:
gradients = _compute_gradients_3d(spectrum, depth=depth)**2
else:
gradients += _compute_gradients_3d(spectrum)**2
if not multichannel:
gradients = _compute_gradients_3d( data, depth=depth )**2
else:
for channel in range(data.shape[-1]):
if channel == 0:
gradients = _compute_gradients_3d(data[..., channel],
depth=depth)**2
else:
gradients += _compute_gradients_3d(data[..., channel],
depth=depth)**2
gradients = np.sqrt(gradients)
# gradients = np.sqrt(gradients)
# New final term in beta to give == results in trivial case where
# multiple identical spectra are passed.
# It may be faster and/or more memory efficient do an approximate
# std() combining spectrum.std() together than this 2nd term.
beta /= 10 * np.asarray(data).std() * np.sqrt( len(data) )
# All channels considered together in this standard deviation
beta /= 10 * data.std()
if multichannel:
# New final term in beta to give == results in trivial case where
# multiple identical spectra are passed.
beta /= np.sqrt( data.shape[-1] )
gradients *= beta
weights = np.exp(- gradients)
weights += eps
@@ -161,10 +168,14 @@ def _mask_edges_weights(edges, weights, mask):
return edges, weights
def _build_laplacian(data, mask=None, beta=50, depth=1.):
l_x, l_y, l_z = data[0].shape
def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False):
if not multichannel:
l_x, l_y, l_z = data.shape
else:
l_x, l_y, l_z = data.shape[0], data.shape[1], data.shape[2]
edges = _make_graph_edges_3d(l_x, l_y, l_z)
weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth)
weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth,
multichannel=multichannel)
if mask is not None:
edges, weights = _mask_edges_weights(edges, weights, mask)
lap = _make_laplacian_sparse(edges, weights)
@@ -175,19 +186,21 @@ def _build_laplacian(data, mask=None, beta=50, depth=1.):
#----------- Random walker algorithm --------------------------------
def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
copy=True, return_full_prob=False):
def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
copy=True, multichannel=False, scaling='all',
return_full_prob=False):
"""
Multispectral random walker algorithm for segmentation from markers.
Multichannel random walker algorithm for segmentation from markers.
Parameters
----------
data : array_like OR iterable of arrays
Image to be segmented in phases. Non-multispectral `data` can be
two- or three-dimensional; multispectral data is provided as an
iterable of like-sized 2D or 3D arrays. Data spacing is assumed
isotropic unless depth kwarg is used.
data : array_like
Image to be segmented in phases. Gray-level`data` can be two- or
three-dimensional; multichannel data can be three- or four-
dimensional (requires multichannel=True) with the highest
dimension denoting channels. Data spacing is assumed isotropic
unless depth keyword argument is used.
labels : array of ints, of same shape as `data`
Array of seed markers labeled with different positive integers
@@ -236,6 +249,21 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
the result of the segmentation. Use copy=False if you want to
save on memory.
multichannel : bool, default False
If True, input data is parsed as multichannel data (see 'data' above
for proper input format in this case)
scaling : string, default 'all'
Controls input scaling if multichannel=True (otherwise no effect).
- 'all' (default): Data from all channels is combined when scaling
input data to the range [0,1] as type np.float64. Recommended
option for RGB(A) inputs.
- 'separate': Each channel is scaled individually, separate from the
others, to the range [0,1]. Select this if the channels are very
different, for example if one were x-ray CT and another MRI data.
return_full_prob : bool, default False
If True, the probability that a pixel belongs to each of the labels
will be returned, instead of only the most likely label.
@@ -320,17 +348,22 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
"""
# Parse input data
if isinstance( data, np.ndarray ):
# Wrap into single-element list
data = [ np.atleast_3d( img_as_float(data) ) ]
else:
if not multichannel:
# We work with 3-D arrays of floats
newdata = []
for spectrum in data:
newdata.append( np.atleast_3d( img_as_float(spectrum) ) )
del data
data = newdata
del newdata
dims = data.shape
data = np.atleast_3d( img_as_float(data) )
else:
dims = data[..., 0].shape
data = np.atleast_3d( data ) # Should never be needed
if scaling.lower().strip() == 'all':
data = img_as_float( data )
else:
newdata = np.zeros(data.shape, dtype=np.float64)
for channel in range( data.shape[-1] ):
newdata[..., channel] = img_as_float( data[..., channel] )
del data
data = newdata
del newdata
if copy:
labels = np.copy(labels)
@@ -349,9 +382,10 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
labels = np.atleast_3d(labels)
if np.any(labels < 0):
lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta,
depth=depth)
depth=depth, multichannel=multichannel)
else:
lap_sparse = _build_laplacian(data, beta=beta, depth=depth)
lap_sparse = _build_laplacian(data, beta=beta, depth=depth,
multichannel=multichannel)
lap_sparse, B = _buildAB(lap_sparse, labels)
# We solve the linear system
# lap_sparse X = B
@@ -366,7 +400,7 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
"""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,
X = _solve_cg(lap_sparse, B, tol=tol,
return_full_prob=return_full_prob)
else:
X = _solve_cg_mg(lap_sparse, B, tol=tol,
@@ -375,19 +409,17 @@ def random_walker(data, labels, beta=130, depth=1., mode='bf', tol=1.e-3,
X = _solve_bf(lap_sparse, B,
return_full_prob=return_full_prob)
# Clean up results
for spectrum in data:
spectrum = spectrum.squeeze()
if return_full_prob:
labels = labels.astype(np.float)
X = np.array([_clean_labels_ar(Xline, labels,
copy=True).reshape(data[0].shape) for Xline in X])
copy=True).reshape(dims) for Xline in X])
for i in range(1, int(labels.max()) + 1):
mask_i = np.squeeze(labels == i)
X[i - 1, mask_i] = 1
X[np.setdiff1d(np.arange(0, labels.max(), dtype=np.int),
[i - 1]), mask_i] = 0
else:
X = _clean_labels_ar(X + 1, labels).reshape(data[0].shape)
X = _clean_labels_ar(X + 1, labels).reshape(dims)
return X
@@ -143,11 +143,12 @@ def test_multispectral():
n = 30
lx, ly, lz = n, n, n
data, labels = make_3d_syntheticdata( lx, ly, lz )
data = [data, data] # Result should be identical
multi_labels = random_walker(data, labels, mode='cg')
single_labels = random_walker(data[0], labels, mode='cg')
assert (multi_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all()
assert (single_labels.reshape(data[0].shape)[13:17, 13:17, 13:17] == 2).all()
data.shape += (1,)
data = data.repeat(2, axis=3) # Result should be identical
multi_labels = random_walker(data, labels, mode='cg', multichannel=True)
single_labels = random_walker(data[:,:,:,0], labels, mode='cg')
assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all()
assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all()
return data, multi_labels, single_labels, labels
if __name__ == '__main__':