Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,38 @@
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
from .boundaries import find_boundaries, mark_boundaries
from ._clear_border import clear_border
from ._join import join_segmentations, relabel_sequential
from ._watershed import watershed
from ._chan_vese import chan_vese
from .morphsnakes import (morphological_geodesic_active_contour,
morphological_chan_vese, inverse_gaussian_gradient,
circle_level_set,
disk_level_set, checkerboard_level_set)
from ..morphology import flood, flood_fill
__all__ = ['random_walker',
'active_contour',
'felzenszwalb',
'slic',
'quickshift',
'find_boundaries',
'mark_boundaries',
'clear_border',
'join_segmentations',
'relabel_sequential',
'watershed',
'chan_vese',
'morphological_geodesic_active_contour',
'morphological_chan_vese',
'inverse_gaussian_gradient',
'circle_level_set',
'disk_level_set',
'checkerboard_level_set',
'flood',
'flood_fill',
]

View file

@ -0,0 +1,338 @@
import numpy as np
from scipy.ndimage import distance_transform_edt as distance
def _cv_curvature(phi):
"""Returns the 'curvature' of a level set 'phi'.
"""
P = np.pad(phi, 1, mode='edge')
fy = (P[2:, 1:-1] - P[:-2, 1:-1]) / 2.0
fx = (P[1:-1, 2:] - P[1:-1, :-2]) / 2.0
fyy = P[2:, 1:-1] + P[:-2, 1:-1] - 2*phi
fxx = P[1:-1, 2:] + P[1:-1, :-2] - 2*phi
fxy = .25 * (P[2:, 2:] + P[:-2, :-2] - P[:-2, 2:] - P[2:, :-2])
grad2 = fx**2 + fy**2
K = ((fxx*fy**2 - 2*fxy*fx*fy + fyy*fx**2) /
(grad2*np.sqrt(grad2) + 1e-8))
return K
def _cv_calculate_variation(image, phi, mu, lambda1, lambda2, dt):
"""Returns the variation of level set 'phi' based on algorithm parameters.
"""
eta = 1e-16
P = np.pad(phi, 1, mode='edge')
phixp = P[1:-1, 2:] - P[1:-1, 1:-1]
phixn = P[1:-1, 1:-1] - P[1:-1, :-2]
phix0 = (P[1:-1, 2:] - P[1:-1, :-2]) / 2.0
phiyp = P[2:, 1:-1] - P[1:-1, 1:-1]
phiyn = P[1:-1, 1:-1] - P[:-2, 1:-1]
phiy0 = (P[2:, 1:-1] - P[:-2, 1:-1]) / 2.0
C1 = 1. / np.sqrt(eta + phixp**2 + phiy0**2)
C2 = 1. / np.sqrt(eta + phixn**2 + phiy0**2)
C3 = 1. / np.sqrt(eta + phix0**2 + phiyp**2)
C4 = 1. / np.sqrt(eta + phix0**2 + phiyn**2)
K = (P[1:-1, 2:] * C1 + P[1:-1, :-2] * C2 +
P[2:, 1:-1] * C3 + P[:-2, 1:-1] * C4)
Hphi = 1 * (phi > 0)
(c1, c2) = _cv_calculate_averages(image, Hphi)
difference_from_average_term = (- lambda1 * (image-c1)**2 +
lambda2 * (image-c2)**2)
new_phi = (phi + (dt*_cv_delta(phi)) *
(mu*K + difference_from_average_term))
return new_phi / (1 + mu * dt * _cv_delta(phi) * (C1+C2+C3+C4))
def _cv_heavyside(x, eps=1.):
"""Returns the result of a regularised heavyside function of the
input value(s).
"""
return 0.5 * (1. + (2./np.pi) * np.arctan(x/eps))
def _cv_delta(x, eps=1.):
"""Returns the result of a regularised dirac function of the
input value(s).
"""
return eps / (eps**2 + x**2)
def _cv_calculate_averages(image, Hphi):
"""Returns the average values 'inside' and 'outside'.
"""
H = Hphi
Hinv = 1. - H
Hsum = np.sum(H)
Hinvsum = np.sum(Hinv)
avg_inside = np.sum(image * H)
avg_oustide = np.sum(image * Hinv)
if Hsum != 0:
avg_inside /= Hsum
if Hinvsum != 0:
avg_oustide /= Hinvsum
return (avg_inside, avg_oustide)
def _cv_difference_from_average_term(image, Hphi, lambda_pos, lambda_neg):
"""Returns the 'energy' contribution due to the difference from
the average value within a region at each point.
"""
(c1, c2) = _cv_calculate_averages(image, Hphi)
Hinv = 1. - Hphi
return (lambda_pos * (image-c1)**2 * Hphi +
lambda_neg * (image-c2)**2 * Hinv)
def _cv_edge_length_term(phi, mu):
"""Returns the 'energy' contribution due to the length of the
edge between regions at each point, multiplied by a factor 'mu'.
"""
toret = _cv_curvature(phi)
return mu * toret
def _cv_energy(image, phi, mu, lambda1, lambda2):
"""Returns the total 'energy' of the current level set function.
"""
H = _cv_heavyside(phi)
avgenergy = _cv_difference_from_average_term(image, H, lambda1, lambda2)
lenenergy = _cv_edge_length_term(phi, mu)
return np.sum(avgenergy) + np.sum(lenenergy)
def _cv_reset_level_set(phi):
"""This is a placeholder function as resetting the level set is not
strictly necessary, and has not been done for this implementation.
"""
return phi
def _cv_checkerboard(image_size, square_size):
"""Generates a checkerboard level set function.
According to Pascal Getreuer, such a level set function has fast convergence.
"""
yv = np.arange(image_size[0]).reshape(image_size[0], 1)
xv = np.arange(image_size[1])
return (np.sin(np.pi/square_size*yv) *
np.sin(np.pi/square_size*xv))
def _cv_large_disk(image_size):
"""Generates a disk level set function.
The disk covers the whole image along its smallest dimension.
"""
res = np.ones(image_size)
centerY = int((image_size[0]-1) / 2)
centerX = int((image_size[1]-1) / 2)
res[centerY, centerX] = 0.
radius = float(min(centerX, centerY))
return (radius-distance(res)) / radius
def _cv_small_disk(image_size):
"""Generates a disk level set function.
The disk covers half of the image along its smallest dimension.
"""
res = np.ones(image_size)
centerY = int((image_size[0]-1) / 2)
centerX = int((image_size[1]-1) / 2)
res[centerY, centerX] = 0.
radius = float(min(centerX, centerY)) / 2.0
return (radius-distance(res)) / (radius*3)
def _cv_init_level_set(init_level_set, image_shape):
"""Generates an initial level set function conditional on input arguments.
"""
if type(init_level_set) == str:
if init_level_set == 'checkerboard':
res = _cv_checkerboard(image_shape, 5)
elif init_level_set == 'disk':
res = _cv_large_disk(image_shape)
elif init_level_set == 'small disk':
res = _cv_small_disk(image_shape)
else:
raise ValueError("Incorrect name for starting level set preset.")
else:
res = init_level_set
return res
def chan_vese(image, mu=0.25, lambda1=1.0, lambda2=1.0, tol=1e-3, max_iter=500,
dt=0.5, init_level_set='checkerboard',
extended_output=False):
"""Chan-Vese segmentation algorithm.
Active contour model by evolving a level set. Can be used to
segment objects without clearly defined boundaries.
Parameters
----------
image : (M, N) ndarray
Grayscale image to be segmented.
mu : float, optional
'edge length' weight parameter. Higher `mu` values will
produce a 'round' edge, while values closer to zero will
detect smaller objects.
lambda1 : float, optional
'difference from average' weight parameter for the output
region with value 'True'. If it is lower than `lambda2`, this
region will have a larger range of values than the other.
lambda2 : float, optional
'difference from average' weight parameter for the output
region with value 'False'. If it is lower than `lambda1`, this
region will have a larger range of values than the other.
tol : float, positive, optional
Level set variation tolerance between iterations. If the
L2 norm difference between the level sets of successive
iterations normalized by the area of the image is below this
value, the algorithm will assume that the solution was
reached.
max_iter : uint, optional
Maximum number of iterations allowed before the algorithm
interrupts itself.
dt : float, optional
A multiplication factor applied at calculations for each step,
serves to accelerate the algorithm. While higher values may
speed up the algorithm, they may also lead to convergence
problems.
init_level_set : str or (M, N) ndarray, optional
Defines the starting level set used by the algorithm.
If a string is inputted, a level set that matches the image
size will automatically be generated. Alternatively, it is
possible to define a custom level set, which should be an
array of float values, with the same shape as 'image'.
Accepted string values are as follows.
'checkerboard'
the starting level set is defined as
sin(x/5*pi)*sin(y/5*pi), where x and y are pixel
coordinates. This level set has fast convergence, but may
fail to detect implicit edges.
'disk'
the starting level set is defined as the opposite
of the distance from the center of the image minus half of
the minimum value between image width and image height.
This is somewhat slower, but is more likely to properly
detect implicit edges.
'small disk'
the starting level set is defined as the
opposite of the distance from the center of the image
minus a quarter of the minimum value between image width
and image height.
extended_output : bool, optional
If set to True, the return value will be a tuple containing
the three return values (see below). If set to False which
is the default value, only the 'segmentation' array will be
returned.
Returns
-------
segmentation : (M, N) ndarray, bool
Segmentation produced by the algorithm.
phi : (M, N) ndarray of floats
Final level set computed by the algorithm.
energies : list of floats
Shows the evolution of the 'energy' for each step of the
algorithm. This should allow to check whether the algorithm
converged.
Notes
-----
The Chan-Vese Algorithm is designed to segment objects without
clearly defined boundaries. This algorithm is based on level sets
that are evolved iteratively to minimize an energy, which is
defined by weighted values corresponding to the sum of differences
intensity from the average value outside the segmented region, the
sum of differences from the average value inside the segmented
region, and a term which is dependent on the length of the
boundary of the segmented region.
This algorithm was first proposed by Tony Chan and Luminita Vese,
in a publication entitled "An Active Contour Model Without Edges"
[1]_.
This implementation of the algorithm is somewhat simplified in the
sense that the area factor 'nu' described in the original paper is
not implemented, and is only suitable for grayscale images.
Typical values for `lambda1` and `lambda2` are 1. If the
'background' is very different from the segmented object in terms
of distribution (for example, a uniform black image with figures
of varying intensity), then these values should be different from
each other.
Typical values for mu are between 0 and 1, though higher values
can be used when dealing with shapes with very ill-defined
contours.
The 'energy' which this algorithm tries to minimize is defined
as the sum of the differences from the average within the region
squared and weighed by the 'lambda' factors to which is added the
length of the contour multiplied by the 'mu' factor.
Supports 2D grayscale images only, and does not implement the area
term described in the original article.
References
----------
.. [1] An Active Contour Model without Edges, Tony Chan and
Luminita Vese, Scale-Space Theories in Computer Vision,
1999, :DOI:`10.1007/3-540-48236-9_13`
.. [2] Chan-Vese Segmentation, Pascal Getreuer Image Processing On
Line, 2 (2012), pp. 214-224,
:DOI:`10.5201/ipol.2012.g-cv`
.. [3] The Chan-Vese Algorithm - Project Report, Rami Cohen, 2011
:arXiv:`1107.2782`
"""
if len(image.shape) != 2:
raise ValueError("Input image should be a 2D array.")
phi = _cv_init_level_set(init_level_set, image.shape)
if type(phi) != np.ndarray or phi.shape != image.shape:
raise ValueError("The dimensions of initial level set do not "
"match the dimensions of image.")
image = image - np.min(image)
if np.max(image) != 0:
image = image / np.max(image)
i = 0
old_energy = _cv_energy(image, phi, mu, lambda1, lambda2)
energies = []
phivar = tol + 1
segmentation = phi > 0
while(phivar > tol and i < max_iter):
# Save old level set values
oldphi = phi
# Calculate new level set
phi = _cv_calculate_variation(image, phi, mu, lambda1, lambda2, dt)
phi = _cv_reset_level_set(phi)
phivar = np.sqrt(((phi-oldphi)**2).mean())
# Extract energy and compare to previous level set and
# segmentation to see if continuing is necessary
segmentation = phi > 0
new_energy = _cv_energy(image, phi, mu, lambda1, lambda2)
# Save old energy values
energies.append(old_energy)
old_energy = new_energy
i += 1
if extended_output:
return (segmentation, phi, energies)
else:
return segmentation

View file

@ -0,0 +1,106 @@
import numpy as np
from ..measure import label
def clear_border(labels, buffer_size=0, bgval=0, in_place=False, mask=None):
"""Clear objects connected to the label image border.
Parameters
----------
labels : (M[, N[, ..., P]]) array of int or bool
Imaging data labels.
buffer_size : int, optional
The width of the border examined. By default, only objects
that touch the outside of the image are removed.
bgval : float or int, optional
Cleared objects are set to this value.
in_place : bool, optional
Whether or not to manipulate the labels array in-place.
mask : ndarray of bool, same shape as `image`, optional.
Image data mask. Objects in labels image overlapping with
False pixels of mask will be removed. If defined, the
argument buffer_size will be ignored.
Returns
-------
out : (M[, N[, ..., P]]) array
Imaging data labels with cleared borders
Examples
--------
>>> import numpy as np
>>> from skimage.segmentation import clear_border
>>> labels = np.array([[0, 0, 0, 0, 0, 0, 0, 1, 0],
... [1, 1, 0, 0, 1, 0, 0, 1, 0],
... [1, 1, 0, 1, 0, 1, 0, 0, 0],
... [0, 0, 0, 1, 1, 1, 1, 0, 0],
... [0, 1, 1, 1, 1, 1, 1, 1, 0],
... [0, 0, 0, 0, 0, 0, 0, 0, 0]])
>>> clear_border(labels)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]])
>>> mask = np.array([[0, 0, 1, 1, 1, 1, 1, 1, 1],
... [0, 0, 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]]).astype(np.bool)
>>> clear_border(labels, mask=mask)
array([[0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 1, 0],
[0, 0, 0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]])
"""
image = labels
if any((buffer_size >= s for s in image.shape)) and mask is None:
# ignore buffer_size if mask
raise ValueError("buffer size may not be greater than image size")
if mask is not None:
err_msg = "image and mask should have the same shape but are {} and {}"
assert image.shape == mask.shape, \
err_msg.format(image.shape, mask.shape)
if mask.dtype != np.bool_:
raise TypeError("mask should be of type bool.")
borders = ~mask
else:
# create borders with buffer_size
borders = np.zeros_like(image, dtype=np.bool_)
ext = buffer_size + 1
slstart = slice(ext)
slend = slice(-ext, None)
slices = [slice(s) for s in image.shape]
for d in range(image.ndim):
slicedim = list(slices)
slicedim[d] = slstart
borders[tuple(slicedim)] = True
slicedim[d] = slend
borders[tuple(slicedim)] = True
# Re-label, in case we are dealing with a binary image
# and to get consistent labeling
labels = label(image, background=0)
number = np.max(labels) + 1
# determine all objects that are connected to borders
borders_indices = np.unique(labels[borders])
indices = np.arange(number + 1)
# mask all label indices that are connected to borders
label_mask = np.in1d(indices, borders_indices)
# create mask for pixels to clear
mask = label_mask[labels.ravel()].reshape(labels.shape)
if not in_place:
image = image.copy()
# clear border pixels
image[mask] = bgval
return image

View file

@ -0,0 +1,64 @@
import numpy as np
from ._felzenszwalb_cy import _felzenszwalb_cython
def felzenszwalb(image, scale=1, sigma=0.8, min_size=20, multichannel=True):
"""Computes Felsenszwalb's efficient graph based image segmentation.
Produces an oversegmentation of a multichannel (i.e. RGB) image
using a fast, minimum spanning tree based clustering on the image grid.
The parameter ``scale`` sets an observation level. Higher scale means
less and larger segments. ``sigma`` is the diameter of a Gaussian kernel,
used for smoothing the image prior to segmentation.
The number of produced segments as well as their size can only be
controlled indirectly through ``scale``. Segment size within an image can
vary greatly depending on local contrast.
For RGB images, the algorithm uses the euclidean distance between pixels in
color space.
Parameters
----------
image : (width, height, 3) or (width, height) ndarray
Input image.
scale : float
Free parameter. Higher means larger clusters.
sigma : float
Width (standard deviation) of Gaussian kernel used in preprocessing.
min_size : int
Minimum component size. Enforced using postprocessing.
multichannel : bool, optional (default: True)
Whether the last axis of the image is to be interpreted as multiple
channels. A value of False, for a 3D image, is not currently supported.
Returns
-------
segment_mask : (width, height) ndarray
Integer mask indicating segment labels.
References
----------
.. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and
Huttenlocher, D.P. International Journal of Computer Vision, 2004
Notes
-----
The `k` parameter used in the original paper renamed to `scale` here.
Examples
--------
>>> from skimage.segmentation import felzenszwalb
>>> from skimage.data import coffee
>>> img = coffee()
>>> segments = felzenszwalb(img, scale=3.0, sigma=0.95, min_size=5)
"""
if not multichannel and image.ndim > 2:
raise ValueError("This algorithm works only on single or "
"multi-channel 2d images. ")
image = np.atleast_3d(image)
return _felzenszwalb_cython(image, scale=scale, sigma=sigma,
min_size=min_size)

View file

@ -0,0 +1,155 @@
import numpy as np
from .._shared.utils import deprecated
from ..util._map_array import map_array, ArrayMap
def join_segmentations(s1, s2):
"""Return the join of the two input segmentations.
The join J of S1 and S2 is defined as the segmentation in which two
voxels are in the same segment if and only if they are in the same
segment in *both* S1 and S2.
Parameters
----------
s1, s2 : numpy arrays
s1 and s2 are label fields of the same shape.
Returns
-------
j : numpy array
The join segmentation of s1 and s2.
Examples
--------
>>> from skimage.segmentation import join_segmentations
>>> s1 = np.array([[0, 0, 1, 1],
... [0, 2, 1, 1],
... [2, 2, 2, 1]])
>>> s2 = np.array([[0, 1, 1, 0],
... [0, 1, 1, 0],
... [0, 1, 1, 1]])
>>> join_segmentations(s1, s2)
array([[0, 1, 3, 2],
[0, 5, 3, 2],
[4, 5, 5, 3]])
"""
if s1.shape != s2.shape:
raise ValueError("Cannot join segmentations of different shape. " +
"s1.shape: %s, s2.shape: %s" % (s1.shape, s2.shape))
s1 = relabel_sequential(s1)[0]
s2 = relabel_sequential(s2)[0]
j = (s2.max() + 1) * s1 + s2
j = relabel_sequential(j)[0]
return j
def relabel_sequential(label_field, offset=1):
"""Relabel arbitrary labels to {`offset`, ... `offset` + number_of_labels}.
This function also returns the forward map (mapping the original labels to
the reduced labels) and the inverse map (mapping the reduced labels back
to the original ones).
Parameters
----------
label_field : numpy array of int, arbitrary shape
An array of labels, which must be non-negative integers.
offset : int, optional
The return labels will start at `offset`, which should be
strictly positive.
Returns
-------
relabeled : numpy array of int, same shape as `label_field`
The input label field with labels mapped to
{offset, ..., number_of_labels + offset - 1}.
The data type will be the same as `label_field`, except when
offset + number_of_labels causes overflow of the current data type.
forward_map : ArrayMap
The map from the original label space to the returned label
space. Can be used to re-apply the same mapping. See examples
for usage. The output data type will be the same as `relabeled`.
inverse_map : ArrayMap
The map from the new label space to the original space. This
can be used to reconstruct the original label field from the
relabeled one. The output data type will be the same as `label_field`.
Notes
-----
The label 0 is assumed to denote the background and is never remapped.
The forward map can be extremely big for some inputs, since its
length is given by the maximum of the label field. However, in most
situations, ``label_field.max()`` is much smaller than
``label_field.size``, and in these cases the forward map is
guaranteed to be smaller than either the input or output images.
Examples
--------
>>> from skimage.segmentation import relabel_sequential
>>> label_field = np.array([1, 1, 5, 5, 8, 99, 42])
>>> relab, fw, inv = relabel_sequential(label_field)
>>> relab
array([1, 1, 2, 2, 3, 5, 4])
>>> print(fw)
ArrayMap:
1 1
5 2
8 3
42 4
99 5
>>> np.array(fw)
array([0, 1, 0, 0, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5])
>>> np.array(inv)
array([ 0, 1, 5, 8, 42, 99])
>>> (fw[label_field] == relab).all()
True
>>> (inv[relab] == label_field).all()
True
>>> relab, fw, inv = relabel_sequential(label_field, offset=5)
>>> relab
array([5, 5, 6, 6, 7, 9, 8])
"""
if offset <= 0:
raise ValueError("Offset must be strictly positive.")
if np.min(label_field) < 0:
raise ValueError("Cannot relabel array that contains negative values.")
offset = int(offset)
in_vals = np.unique(label_field)
if in_vals[0] == 0:
# always map 0 to 0
out_vals = np.concatenate(
[[0], np.arange(offset, offset+len(in_vals)-1)]
)
else:
out_vals = np.arange(offset, offset+len(in_vals))
input_type = label_field.dtype
# Some logic to determine the output type:
# - we don't want to return a smaller output type than the input type,
# ie if we get uint32 as labels input, don't return a uint8 array.
# - but, in some cases, using the input type could result in overflow. The
# input type could be a signed integer (e.g. int32) but
# `np.min_scalar_type` will always return an unsigned type. We check for
# that by casting the largest output value to the input type. If it is
# unchanged, we use the input type, else we use the unsigned minimum
# required type
required_type = np.min_scalar_type(out_vals[-1])
if input_type.itemsize < required_type.itemsize:
output_type = required_type
else:
if input_type.type(out_vals[-1]) == out_vals[-1]:
output_type = input_type
else:
output_type = required_type
out_array = np.empty(label_field.shape, dtype=output_type)
out_vals = out_vals.astype(output_type)
map_array(label_field, in_vals, out_vals, out=out_array)
fw_map = ArrayMap(in_vals, out_vals)
inv_map = ArrayMap(out_vals, in_vals)
return out_array, fw_map, inv_map

View file

@ -0,0 +1,74 @@
import numpy as np
import scipy.ndimage as ndi
from ..util import img_as_float
from ..color import rgb2lab
from ._quickshift_cy import _quickshift_cython
def quickshift(image, ratio=1.0, kernel_size=5, max_dist=10,
return_tree=False, sigma=0, convert2lab=True, random_seed=42):
"""Segments image using quickshift clustering in Color-(x,y) space.
Produces an oversegmentation of the image using the quickshift mode-seeking
algorithm.
Parameters
----------
image : (width, height, channels) ndarray
Input image.
ratio : float, optional, between 0 and 1
Balances color-space proximity and image-space proximity.
Higher values give more weight to color-space.
kernel_size : float, optional
Width of Gaussian kernel used in smoothing the
sample density. Higher means fewer clusters.
max_dist : float, optional
Cut-off point for data distances.
Higher means fewer clusters.
return_tree : bool, optional
Whether to return the full segmentation hierarchy tree and distances.
sigma : float, optional
Width for Gaussian smoothing as preprocessing. Zero means no smoothing.
convert2lab : bool, optional
Whether the input should be converted to Lab colorspace prior to
segmentation. For this purpose, the input is assumed to be RGB.
random_seed : int, optional
Random seed used for breaking ties.
Returns
-------
segment_mask : (width, height) ndarray
Integer mask indicating segment labels.
Notes
-----
The authors advocate to convert the image to Lab color space prior to
segmentation, though this is not strictly necessary. For this to work, the
image must be given in RGB format.
References
----------
.. [1] Quick shift and kernel methods for mode seeking,
Vedaldi, A. and Soatto, S.
European Conference on Computer Vision, 2008
"""
image = img_as_float(np.atleast_3d(image))
if convert2lab:
if image.shape[2] != 3:
ValueError("Only RGB images can be converted to Lab space.")
image = rgb2lab(image)
if kernel_size < 1:
raise ValueError("`kernel_size` should be >= 1.")
image = ndi.gaussian_filter(image, [sigma, sigma, 0])
image = np.ascontiguousarray(image * ratio)
segment_mask = _quickshift_cython(
image, kernel_size=kernel_size, max_dist=max_dist,
return_tree=return_tree, random_seed=random_seed)
return segment_mask

View file

@ -0,0 +1,227 @@
"""watershed.py - watershed algorithm
This module implements a watershed algorithm that apportions pixels into
marked basins. The algorithm uses a priority queue to hold the pixels
with the metric for the priority queue being pixel value, then the time
of entry into the queue - this settles ties in favor of the closest marker.
Some ideas taken from
Soille, "Automated Basin Delineation from Digital Elevation Models Using
Mathematical Morphology", Signal Processing 20 (1990) 171-182.
The most important insight in the paper is that entry time onto the queue
solves two problems: a pixel should be assigned to the neighbor with the
largest gradient or, if there is no gradient, pixels on a plateau should
be split between markers on opposite sides.
Originally part of CellProfiler, code licensed under both GPL and BSD licenses.
Website: http://www.cellprofiler.org
Copyright (c) 2003-2009 Massachusetts Institute of Technology
Copyright (c) 2009-2011 Broad Institute
All rights reserved.
Original author: Lee Kamentsky
"""
import numpy as np
from scipy import ndimage as ndi
from . import _watershed_cy
from ..morphology.extrema import local_minima
from ..morphology._util import (_validate_connectivity,
_offsets_to_raveled_neighbors)
from ..util import crop, regular_seeds
def _validate_inputs(image, markers, mask, connectivity):
"""Ensure that all inputs to watershed have matching shapes and types.
Parameters
----------
image : array
The input image.
markers : int or array of int
The marker image.
mask : array, or None
A boolean mask, True where we want to compute the watershed.
connectivity : int in {1, ..., image.ndim}
The connectivity of the neighborhood of a pixel.
Returns
-------
image, markers, mask : arrays
The validated and formatted arrays. Image will have dtype float64,
markers int32, and mask int8. If ``None`` was given for the mask,
it is a volume of all 1s.
Raises
------
ValueError
If the shapes of the given arrays don't match.
"""
n_pixels = image.size
if mask is None:
# Use a complete `True` mask if none is provided
mask = np.ones(image.shape, bool)
else:
mask = np.asanyarray(mask, dtype=bool)
n_pixels = np.sum(mask)
if mask.shape != image.shape:
message = ("`mask` (shape {}) must have same shape as "
"`image` (shape {})".format(mask.shape, image.shape))
raise ValueError(message)
if markers is None:
markers_bool = local_minima(image, connectivity=connectivity) * mask
markers = ndi.label(markers_bool)[0]
elif not isinstance(markers, (np.ndarray, list, tuple)):
# not array-like, assume int
# given int, assume that number of markers *within mask*.
markers = regular_seeds(image.shape,
int(markers / (n_pixels / image.size)))
markers *= mask
else:
markers = np.asanyarray(markers) * mask
if markers.shape != image.shape:
message = ("`markers` (shape {}) must have same shape as "
"`image` (shape {})".format(markers.shape, image.shape))
raise ValueError(message)
return (image.astype(np.float64),
markers.astype(np.int32),
mask.astype(np.int8))
def watershed(image, markers=None, connectivity=1, offset=None, mask=None,
compactness=0, watershed_line=False):
"""Find watershed basins in `image` flooded from given `markers`.
Parameters
----------
image : ndarray (2-D, 3-D, ...) of integers
Data array where the lowest value points are labeled first.
markers : int, or ndarray of int, same shape as `image`, optional
The desired number of markers, or an array marking the basins with the
values to be assigned in the label matrix. Zero means not a marker. If
``None`` (no markers given), the local minima of the image are used as
markers.
connectivity : ndarray, optional
An array with the same number of dimensions as `image` whose
non-zero elements indicate neighbors for connection.
Following the scipy convention, default is a one-connected array of
the dimension of the image.
offset : array_like of shape image.ndim, optional
offset of the connectivity (one offset per dimension)
mask : ndarray of bools or 0s and 1s, optional
Array of same shape as `image`. Only points at which mask == True
will be labeled.
compactness : float, optional
Use compact watershed [3]_ with given compactness parameter.
Higher values result in more regularly-shaped watershed basins.
watershed_line : bool, optional
If watershed_line is True, a one-pixel wide line separates the regions
obtained by the watershed algorithm. The line has the label 0.
Returns
-------
out : ndarray
A labeled matrix of the same type and shape as markers
See also
--------
skimage.segmentation.random_walker: random walker segmentation
A segmentation algorithm based on anisotropic diffusion, usually
slower than the watershed but with good results on noisy data and
boundaries with holes.
Notes
-----
This function implements a watershed algorithm [1]_ [2]_ that apportions
pixels into marked basins. The algorithm uses a priority queue to hold
the pixels with the metric for the priority queue being pixel value, then
the time of entry into the queue - this settles ties in favor of the
closest marker.
Some ideas taken from
Soille, "Automated Basin Delineation from Digital Elevation Models Using
Mathematical Morphology", Signal Processing 20 (1990) 171-182
The most important insight in the paper is that entry time onto the queue
solves two problems: a pixel should be assigned to the neighbor with the
largest gradient or, if there is no gradient, pixels on a plateau should
be split between markers on opposite sides.
This implementation converts all arguments to specific, lowest common
denominator types, then passes these to a C algorithm.
Markers can be determined manually, or automatically using for example
the local minima of the gradient of the image, or the local maxima of the
distance function to the background for separating overlapping objects
(see example).
References
----------
.. [1] https://en.wikipedia.org/wiki/Watershed_%28image_processing%29
.. [2] http://cmm.ensmp.fr/~beucher/wtshed.html
.. [3] Peer Neubert & Peter Protzel (2014). Compact Watershed and
Preemptive SLIC: On Improving Trade-offs of Superpixel Segmentation
Algorithms. ICPR 2014, pp 996-1001. :DOI:`10.1109/ICPR.2014.181`
https://www.tu-chemnitz.de/etit/proaut/publications/cws_pSLIC_ICPR.pdf
Examples
--------
The watershed algorithm is useful to separate overlapping objects.
We first generate an initial image with two overlapping circles:
>>> x, y = np.indices((80, 80))
>>> x1, y1, x2, y2 = 28, 28, 44, 52
>>> r1, r2 = 16, 20
>>> mask_circle1 = (x - x1)**2 + (y - y1)**2 < r1**2
>>> mask_circle2 = (x - x2)**2 + (y - y2)**2 < r2**2
>>> image = np.logical_or(mask_circle1, mask_circle2)
Next, we want to separate the two circles. We generate markers at the
maxima of the distance to the background:
>>> from scipy import ndimage as ndi
>>> distance = ndi.distance_transform_edt(image)
>>> from skimage.feature import peak_local_max
>>> local_maxi = peak_local_max(distance, labels=image,
... footprint=np.ones((3, 3)),
... indices=False)
>>> markers = ndi.label(local_maxi)[0]
Finally, we run the watershed on the image and markers:
>>> labels = watershed(-distance, markers, mask=image)
The algorithm works also for 3-D images, and can be used for example to
separate overlapping spheres.
"""
image, markers, mask = _validate_inputs(image, markers, mask, connectivity)
connectivity, offset = _validate_connectivity(image.ndim, connectivity,
offset)
# pad the image, markers, and mask so that we can use the mask to
# keep from running off the edges
pad_width = [(p, p) for p in offset]
image = np.pad(image, pad_width, mode='constant')
mask = np.pad(mask, pad_width, mode='constant').ravel()
output = np.pad(markers, pad_width, mode='constant')
flat_neighborhood = _offsets_to_raveled_neighbors(
image.shape, connectivity, center=offset)
marker_locations = np.flatnonzero(output)
image_strides = np.array(image.strides, dtype=np.intp) // image.itemsize
_watershed_cy.watershed_raveled(image.ravel(),
marker_locations, flat_neighborhood,
mask, image_strides, compactness,
output.ravel(),
watershed_line)
output = crop(output, pad_width, copy=True)
return output

View file

@ -0,0 +1,245 @@
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)

View file

@ -0,0 +1,230 @@
import numpy as np
from scipy import ndimage as ndi
from ..morphology import dilation, erosion, square
from ..util import img_as_float, view_as_windows
from ..color import gray2rgb
def _find_boundaries_subpixel(label_img):
"""See ``find_boundaries(..., mode='subpixel')``.
Notes
-----
This function puts in an empty row and column between each *actual*
row and column of the image, for a corresponding shape of ``2s - 1``
for every image dimension of size ``s``. These "interstitial" rows
and columns are filled as ``True`` if they separate two labels in
`label_img`, ``False`` otherwise.
I used ``view_as_windows`` to get the neighborhood of each pixel.
Then I check whether there are two labels or more in that
neighborhood.
"""
ndim = label_img.ndim
max_label = np.iinfo(label_img.dtype).max
label_img_expanded = np.zeros([(2 * s - 1) for s in label_img.shape],
label_img.dtype)
pixels = (slice(None, None, 2), ) * ndim
label_img_expanded[pixels] = label_img
edges = np.ones(label_img_expanded.shape, dtype=bool)
edges[pixels] = False
label_img_expanded[edges] = max_label
windows = view_as_windows(np.pad(label_img_expanded, 1,
mode='constant', constant_values=0),
(3,) * ndim)
boundaries = np.zeros_like(edges)
for index in np.ndindex(label_img_expanded.shape):
if edges[index]:
values = np.unique(windows[index].ravel())
if len(values) > 2: # single value and max_label
boundaries[index] = True
return boundaries
def find_boundaries(label_img, connectivity=1, mode='thick', background=0):
"""Return bool array where boundaries between labeled regions are True.
Parameters
----------
label_img : array of int or bool
An array in which different regions are labeled with either different
integers or boolean values.
connectivity : int in {1, ..., `label_img.ndim`}, optional
A pixel is considered a boundary pixel if any of its neighbors
has a different label. `connectivity` controls which pixels are
considered neighbors. A connectivity of 1 (default) means
pixels sharing an edge (in 2D) or a face (in 3D) will be
considered neighbors. A connectivity of `label_img.ndim` means
pixels sharing a corner will be considered neighbors.
mode : string in {'thick', 'inner', 'outer', 'subpixel'}
How to mark the boundaries:
- thick: any pixel not completely surrounded by pixels of the
same label (defined by `connectivity`) is marked as a boundary.
This results in boundaries that are 2 pixels thick.
- inner: outline the pixels *just inside* of objects, leaving
background pixels untouched.
- outer: outline pixels in the background around object
boundaries. When two objects touch, their boundary is also
marked.
- subpixel: return a doubled image, with pixels *between* the
original pixels marked as boundary where appropriate.
background : int, optional
For modes 'inner' and 'outer', a definition of a background
label is required. See `mode` for descriptions of these two.
Returns
-------
boundaries : array of bool, same shape as `label_img`
A bool image where ``True`` represents a boundary pixel. For
`mode` equal to 'subpixel', ``boundaries.shape[i]`` is equal
to ``2 * label_img.shape[i] - 1`` for all ``i`` (a pixel is
inserted in between all other pairs of pixels).
Examples
--------
>>> labels = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0],
... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0],
... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0],
... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0],
... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0],
... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
>>> find_boundaries(labels, mode='thick').astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 1, 1, 0],
[0, 1, 1, 0, 1, 1, 0, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> find_boundaries(labels, mode='inner').astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 1, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> find_boundaries(labels, mode='outer').astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 1, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 1, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 1, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 1, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> labels_small = labels[::2, ::3]
>>> labels_small
array([[0, 0, 0, 0],
[0, 0, 5, 0],
[0, 1, 5, 0],
[0, 0, 5, 0],
[0, 0, 0, 0]], dtype=uint8)
>>> find_boundaries(labels_small, mode='subpixel').astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 0, 1, 0],
[0, 1, 1, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 1, 0, 1, 0],
[0, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> bool_image = np.array([[False, False, False, False, False],
... [False, False, False, False, False],
... [False, False, True, True, True],
... [False, False, True, True, True],
... [False, False, True, True, True]], dtype=np.bool)
>>> find_boundaries(bool_image)
array([[False, False, False, False, False],
[False, False, True, True, True],
[False, True, True, True, True],
[False, True, True, False, False],
[False, True, True, False, False]])
"""
if label_img.dtype == 'bool':
label_img = label_img.astype(np.uint8)
ndim = label_img.ndim
selem = ndi.generate_binary_structure(ndim, connectivity)
if mode != 'subpixel':
boundaries = dilation(label_img, selem) != erosion(label_img, selem)
if mode == 'inner':
foreground_image = (label_img != background)
boundaries &= foreground_image
elif mode == 'outer':
max_label = np.iinfo(label_img.dtype).max
background_image = (label_img == background)
selem = ndi.generate_binary_structure(ndim, ndim)
inverted_background = np.array(label_img, copy=True)
inverted_background[background_image] = max_label
adjacent_objects = ((dilation(label_img, selem) !=
erosion(inverted_background, selem)) &
~background_image)
boundaries &= (background_image | adjacent_objects)
return boundaries
else:
boundaries = _find_boundaries_subpixel(label_img)
return boundaries
def mark_boundaries(image, label_img, color=(1, 1, 0),
outline_color=None, mode='outer', background_label=0):
"""Return image with boundaries between labeled regions highlighted.
Parameters
----------
image : (M, N[, 3]) array
Grayscale or RGB image.
label_img : (M, N) array of int
Label array where regions are marked by different integer values.
color : length-3 sequence, optional
RGB color of boundaries in the output image.
outline_color : length-3 sequence, optional
RGB color surrounding boundaries in the output image. If None, no
outline is drawn.
mode : string in {'thick', 'inner', 'outer', 'subpixel'}, optional
The mode for finding boundaries.
background_label : int, optional
Which label to consider background (this is only useful for
modes ``inner`` and ``outer``).
Returns
-------
marked : (M, N, 3) array of float
An image in which the boundaries between labels are
superimposed on the original image.
See Also
--------
find_boundaries
"""
marked = img_as_float(image, force_copy=True)
if marked.ndim == 2:
marked = gray2rgb(marked)
if mode == 'subpixel':
# Here, we want to interpose an extra line of pixels between
# each original line - except for the last axis which holds
# the RGB information. ``ndi.zoom`` then performs the (cubic)
# interpolation, filling in the values of the interposed pixels
marked = ndi.zoom(marked, [2 - 1/s for s in marked.shape[:-1]] + [1],
mode='reflect')
boundaries = find_boundaries(label_img, mode=mode,
background=background_label)
if outline_color is not None:
outlines = dilation(boundaries, square(3))
marked[outlines] = outline_color
marked[boundaries] = color
return marked

View file

@ -0,0 +1,484 @@
import warnings
from itertools import cycle
import numpy as np
from scipy import ndimage as ndi
from .._shared.utils import check_nD
__all__ = ['morphological_chan_vese',
'morphological_geodesic_active_contour',
'inverse_gaussian_gradient',
'circle_level_set',
'disk_level_set',
'checkerboard_level_set'
]
class _fcycle(object):
def __init__(self, iterable):
"""Call functions from the iterable each time it is called."""
self.funcs = cycle(iterable)
def __call__(self, *args, **kwargs):
f = next(self.funcs)
return f(*args, **kwargs)
# SI and IS operators for 2D and 3D.
_P2 = [np.eye(3),
np.array([[0, 1, 0]] * 3),
np.flipud(np.eye(3)),
np.rot90([[0, 1, 0]] * 3)]
_P3 = [np.zeros((3, 3, 3)) for i in range(9)]
_P3[0][:, :, 1] = 1
_P3[1][:, 1, :] = 1
_P3[2][1, :, :] = 1
_P3[3][:, [0, 1, 2], [0, 1, 2]] = 1
_P3[4][:, [0, 1, 2], [2, 1, 0]] = 1
_P3[5][[0, 1, 2], :, [0, 1, 2]] = 1
_P3[6][[0, 1, 2], :, [2, 1, 0]] = 1
_P3[7][[0, 1, 2], [0, 1, 2], :] = 1
_P3[8][[0, 1, 2], [2, 1, 0], :] = 1
def sup_inf(u):
"""SI operator."""
if np.ndim(u) == 2:
P = _P2
elif np.ndim(u) == 3:
P = _P3
else:
raise ValueError("u has an invalid number of dimensions "
"(should be 2 or 3)")
erosions = []
for P_i in P:
erosions.append(ndi.binary_erosion(u, P_i))
return np.array(erosions, dtype=np.int8).max(0)
def inf_sup(u):
"""IS operator."""
if np.ndim(u) == 2:
P = _P2
elif np.ndim(u) == 3:
P = _P3
else:
raise ValueError("u has an invalid number of dimensions "
"(should be 2 or 3)")
dilations = []
for P_i in P:
dilations.append(ndi.binary_dilation(u, P_i))
return np.array(dilations, dtype=np.int8).min(0)
_curvop = _fcycle([lambda u: sup_inf(inf_sup(u)), # SIoIS
lambda u: inf_sup(sup_inf(u))]) # ISoSI
def _check_input(image, init_level_set):
"""Check that shapes of `image` and `init_level_set` match."""
check_nD(image, [2, 3])
if len(image.shape) != len(init_level_set.shape):
raise ValueError("The dimensions of the initial level set do not "
"match the dimensions of the image.")
def _init_level_set(init_level_set, image_shape):
"""Auxiliary function for initializing level sets with a string.
If `init_level_set` is not a string, it is returned as is.
"""
if isinstance(init_level_set, str):
if init_level_set == 'checkerboard':
res = checkerboard_level_set(image_shape)
# TODO: remove me in 0.19.0
elif init_level_set == 'circle':
res = circle_level_set(image_shape)
elif init_level_set == 'disk':
res = disk_level_set(image_shape)
else:
raise ValueError("`init_level_set` not in "
"['checkerboard', 'circle', 'disk']")
else:
res = init_level_set
return res
def circle_level_set(image_shape, center=None, radius=None):
"""Create a circle level set with binary values.
Parameters
----------
image_shape : tuple of positive integers
Shape of the image
center : tuple of positive integers, optional
Coordinates of the center of the circle given in (row, column). If not
given, it defaults to the center of the image.
radius : float, optional
Radius of the circle. If not given, it is set to the 75% of the
smallest image dimension.
Returns
-------
out : array with shape `image_shape`
Binary level set of the circle with the given `radius` and `center`.
Warns
-----
Deprecated:
.. versionadded:: 0.17
This function is deprecated and will be removed in scikit-image 0.19.
Please use the function named ``disk_level_set`` instead.
See also
--------
checkerboard_level_set
"""
warnings.warn("circle_level_set is deprecated in favor of "
"disk_level_set."
"circle_level_set will be removed in version 0.19",
FutureWarning, stacklevel=2)
return disk_level_set(image_shape, center=center, radius=radius)
def disk_level_set(image_shape, *, center=None, radius=None):
"""Create a disk level set with binary values.
Parameters
----------
image_shape : tuple of positive integers
Shape of the image
center : tuple of positive integers, optional
Coordinates of the center of the disk given in (row, column). If not
given, it defaults to the center of the image.
radius : float, optional
Radius of the disk. If not given, it is set to the 75% of the
smallest image dimension.
Returns
-------
out : array with shape `image_shape`
Binary level set of the disk with the given `radius` and `center`.
See also
--------
checkerboard_level_set
"""
if center is None:
center = tuple(i // 2 for i in image_shape)
if radius is None:
radius = min(image_shape) * 3.0 / 8.0
grid = np.mgrid[[slice(i) for i in image_shape]]
grid = (grid.T - center).T
phi = radius - np.sqrt(np.sum((grid)**2, 0))
res = np.int8(phi > 0)
return res
def checkerboard_level_set(image_shape, square_size=5):
"""Create a checkerboard level set with binary values.
Parameters
----------
image_shape : tuple of positive integers
Shape of the image.
square_size : int, optional
Size of the squares of the checkerboard. It defaults to 5.
Returns
-------
out : array with shape `image_shape`
Binary level set of the checkerboard.
See also
--------
circle_level_set
"""
grid = np.mgrid[[slice(i) for i in image_shape]]
grid = (grid // square_size)
# Alternate 0/1 for even/odd numbers.
grid = grid & 1
checkerboard = np.bitwise_xor.reduce(grid, axis=0)
res = np.int8(checkerboard)
return res
def inverse_gaussian_gradient(image, alpha=100.0, sigma=5.0):
"""Inverse of gradient magnitude.
Compute the magnitude of the gradients in the image and then inverts the
result in the range [0, 1]. Flat areas are assigned values close to 1,
while areas close to borders are assigned values close to 0.
This function or a similar one defined by the user should be applied over
the image as a preprocessing step before calling
`morphological_geodesic_active_contour`.
Parameters
----------
image : (M, N) or (L, M, N) array
Grayscale image or volume.
alpha : float, optional
Controls the steepness of the inversion. A larger value will make the
transition between the flat areas and border areas steeper in the
resulting array.
sigma : float, optional
Standard deviation of the Gaussian filter applied over the image.
Returns
-------
gimage : (M, N) or (L, M, N) array
Preprocessed image (or volume) suitable for
`morphological_geodesic_active_contour`.
"""
gradnorm = ndi.gaussian_gradient_magnitude(image, sigma, mode='nearest')
return 1.0 / np.sqrt(1.0 + alpha * gradnorm)
def morphological_chan_vese(image, iterations, init_level_set='checkerboard',
smoothing=1, lambda1=1, lambda2=1,
iter_callback=lambda x: None):
"""Morphological Active Contours without Edges (MorphACWE)
Active contours without edges implemented with morphological operators. It
can be used to segment objects in images and volumes without well defined
borders. It is required that the inside of the object looks different on
average than the outside (i.e., the inner area of the object should be
darker or lighter than the outer area on average).
Parameters
----------
image : (M, N) or (L, M, N) array
Grayscale image or volume to be segmented.
iterations : uint
Number of iterations to run
init_level_set : str, (M, N) array, or (L, M, N) array
Initial level set. If an array is given, it will be binarized and used
as the initial level set. If a string is given, it defines the method
to generate a reasonable initial level set with the shape of the
`image`. Accepted values are 'checkerboard' and 'circle'. See the
documentation of `checkerboard_level_set` and `circle_level_set`
respectively for details about how these level sets are created.
smoothing : uint, optional
Number of times the smoothing operator is applied per iteration.
Reasonable values are around 1-4. Larger values lead to smoother
segmentations.
lambda1 : float, optional
Weight parameter for the outer region. If `lambda1` is larger than
`lambda2`, the outer region will contain a larger range of values than
the inner region.
lambda2 : float, optional
Weight parameter for the inner region. If `lambda2` is larger than
`lambda1`, the inner region will contain a larger range of values than
the outer region.
iter_callback : function, optional
If given, this function is called once per iteration with the current
level set as the only argument. This is useful for debugging or for
plotting intermediate results during the evolution.
Returns
-------
out : (M, N) or (L, M, N) array
Final segmentation (i.e., the final level set)
See also
--------
circle_level_set, checkerboard_level_set
Notes
-----
This is a version of the Chan-Vese algorithm that uses morphological
operators instead of solving a partial differential equation (PDE) for the
evolution of the contour. The set of morphological operators used in this
algorithm are proved to be infinitesimally equivalent to the Chan-Vese PDE
(see [1]_). However, morphological operators are do not suffer from the
numerical stability issues typically found in PDEs (it is not necessary to
find the right time step for the evolution), and are computationally
faster.
The algorithm and its theoretical derivation are described in [1]_.
References
----------
.. [1] A Morphological Approach to Curvature-based Evolution of Curves and
Surfaces, Pablo Márquez-Neila, Luis Baumela, Luis Álvarez. In IEEE
Transactions on Pattern Analysis and Machine Intelligence (PAMI),
2014, :DOI:`10.1109/TPAMI.2013.106`
"""
init_level_set = _init_level_set(init_level_set, image.shape)
_check_input(image, init_level_set)
u = np.int8(init_level_set > 0)
iter_callback(u)
for _ in range(iterations):
# inside = u > 0
# outside = u <= 0
c0 = (image * (1 - u)).sum() / float((1 - u).sum() + 1e-8)
c1 = (image * u).sum() / float(u.sum() + 1e-8)
# Image attachment
du = np.gradient(u)
abs_du = np.abs(du).sum(0)
aux = abs_du * (lambda1 * (image - c1)**2 - lambda2 * (image - c0)**2)
u[aux < 0] = 1
u[aux > 0] = 0
# Smoothing
for _ in range(smoothing):
u = _curvop(u)
iter_callback(u)
return u
def morphological_geodesic_active_contour(gimage, iterations,
init_level_set='circle', smoothing=1,
threshold='auto', balloon=0,
iter_callback=lambda x: None):
"""Morphological Geodesic Active Contours (MorphGAC).
Geodesic active contours implemented with morphological operators. It can
be used to segment objects with visible but noisy, cluttered, broken
borders.
Parameters
----------
gimage : (M, N) or (L, M, N) array
Preprocessed image or volume to be segmented. This is very rarely the
original image. Instead, this is usually a preprocessed version of the
original image that enhances and highlights the borders (or other
structures) of the object to segment.
`morphological_geodesic_active_contour` will try to stop the contour
evolution in areas where `gimage` is small. See
`morphsnakes.inverse_gaussian_gradient` as an example function to
perform this preprocessing. Note that the quality of
`morphological_geodesic_active_contour` might greatly depend on this
preprocessing.
iterations : uint
Number of iterations to run.
init_level_set : str, (M, N) array, or (L, M, N) array
Initial level set. If an array is given, it will be binarized and used
as the initial level set. If a string is given, it defines the method
to generate a reasonable initial level set with the shape of the
`image`. Accepted values are 'checkerboard' and 'circle'. See the
documentation of `checkerboard_level_set` and `circle_level_set`
respectively for details about how these level sets are created.
smoothing : uint, optional
Number of times the smoothing operator is applied per iteration.
Reasonable values are around 1-4. Larger values lead to smoother
segmentations.
threshold : float, optional
Areas of the image with a value smaller than this threshold will be
considered borders. The evolution of the contour will stop in this
areas.
balloon : float, optional
Balloon force to guide the contour in non-informative areas of the
image, i.e., areas where the gradient of the image is too small to push
the contour towards a border. A negative value will shrink the contour,
while a positive value will expand the contour in these areas. Setting
this to zero will disable the balloon force.
iter_callback : function, optional
If given, this function is called once per iteration with the current
level set as the only argument. This is useful for debugging or for
plotting intermediate results during the evolution.
Returns
-------
out : (M, N) or (L, M, N) array
Final segmentation (i.e., the final level set)
See also
--------
inverse_gaussian_gradient, circle_level_set, checkerboard_level_set
Notes
-----
This is a version of the Geodesic Active Contours (GAC) algorithm that uses
morphological operators instead of solving partial differential equations
(PDEs) for the evolution of the contour. The set of morphological operators
used in this algorithm are proved to be infinitesimally equivalent to the
GAC PDEs (see [1]_). However, morphological operators are do not suffer
from the numerical stability issues typically found in PDEs (e.g., it is
not necessary to find the right time step for the evolution), and are
computationally faster.
The algorithm and its theoretical derivation are described in [1]_.
References
----------
.. [1] A Morphological Approach to Curvature-based Evolution of Curves and
Surfaces, Pablo Márquez-Neila, Luis Baumela, Luis Álvarez. In IEEE
Transactions on Pattern Analysis and Machine Intelligence (PAMI),
2014, :DOI:`10.1109/TPAMI.2013.106`
"""
image = gimage
init_level_set = _init_level_set(init_level_set, image.shape)
_check_input(image, init_level_set)
if threshold == 'auto':
threshold = np.percentile(image, 40)
structure = np.ones((3,) * len(image.shape), dtype=np.int8)
dimage = np.gradient(image)
# threshold_mask = image > threshold
if balloon != 0:
threshold_mask_balloon = image > threshold / np.abs(balloon)
u = np.int8(init_level_set > 0)
iter_callback(u)
for _ in range(iterations):
# Balloon
if balloon > 0:
aux = ndi.binary_dilation(u, structure)
elif balloon < 0:
aux = ndi.binary_erosion(u, structure)
if balloon != 0:
u[threshold_mask_balloon] = aux[threshold_mask_balloon]
# Image attachment
aux = np.zeros_like(image)
du = np.gradient(u)
for el1, el2 in zip(dimage, du):
aux += el1 * el2
u[aux > 0] = 1
u[aux < 0] = 0
# Smoothing
for _ in range(smoothing):
u = _curvop(u)
iter_callback(u)
return u

View file

@ -0,0 +1,505 @@
"""
Random walker segmentation algorithm
from *Random walks for image segmentation*, Leo Grady, IEEE Trans
Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83.
Installing pyamg and using the 'cg_mg' mode of random_walker improves
significantly the performance.
"""
import numpy as np
from scipy import sparse, ndimage as ndi
from .._shared.utils import warn
# executive summary for next code block: try to import umfpack from
# scipy, but make sure not to raise a fuss if it fails since it's only
# needed to speed up a few cases.
# See discussions at:
# https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ
# https://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992
try:
from scipy.sparse.linalg.dsolve import umfpack
old_del = umfpack.UmfpackContext.__del__
def new_del(self):
try:
old_del(self)
except AttributeError:
pass
umfpack.UmfpackContext.__del__ = new_del
UmfpackContext = umfpack.UmfpackContext()
except ImportError:
UmfpackContext = None
try:
from pyamg import ruge_stuben_solver
amg_loaded = True
except ImportError:
amg_loaded = False
from ..util import img_as_float
from scipy.sparse.linalg import cg, spsolve
import scipy
from distutils.version import LooseVersion as Version
import functools
if Version(scipy.__version__) >= Version('1.1'):
cg = functools.partial(cg, atol=0)
def _make_graph_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
Returns
-------
edges : (2, N) ndarray
with the total number of edges::
N = n_x * n_y * (nz - 1) +
n_x * (n_y - 1) * nz +
(n_x - 1) * n_y * nz
Graph edges with each column describing a node-id pair.
"""
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(data, spacing, beta, eps, multichannel):
# Weight calculation is main difference in multispectral version
# Original gradient**2 replaced with sum of gradients ** 2
gradients = np.concatenate(
[np.diff(data[..., 0], axis=ax).ravel() / spacing[ax]
for ax in [2, 1, 0] if data.shape[ax] > 1], axis=0) ** 2
for channel in range(1, data.shape[-1]):
gradients += np.concatenate(
[np.diff(data[..., channel], axis=ax).ravel() / spacing[ax]
for ax in [2, 1, 0] if data.shape[ax] > 1], axis=0) ** 2
# All channels considered together in this standard deviation
scale_factor = -beta / (10 * data.std())
if multichannel:
# New final term in beta to give == results in trivial case where
# multiple identical spectra are passed.
scale_factor /= np.sqrt(data.shape[-1])
weights = np.exp(scale_factor * gradients)
weights += eps
return -weights
def _build_laplacian(data, spacing, mask, beta, multichannel):
l_x, l_y, l_z = data.shape[:3]
edges = _make_graph_edges_3d(l_x, l_y, l_z)
weights = _compute_weights_3d(data, spacing, beta=beta, eps=1.e-10,
multichannel=multichannel)
if mask is not None:
# Remove edges of the graph connected to masked nodes, as well
# as corresponding weights of the edges.
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]
# Reassign edges labels to 0, 1, ... edges_number - 1
_, inv_idx = np.unique(edges, return_inverse=True)
edges = inv_idx.reshape(edges.shape)
# Build the sparse linear system
pixel_nb = edges.shape[1]
i_indices = edges.ravel()
j_indices = edges[::-1].ravel()
data = np.hstack((weights, weights))
lap = sparse.coo_matrix((data, (i_indices, j_indices)),
shape=(pixel_nb, pixel_nb))
lap.setdiag(-np.ravel(lap.sum(axis=0)))
return lap.tocsr()
def _build_linear_system(data, spacing, labels, nlabels, mask,
beta, multichannel):
"""
Build the matrix A and rhs B of the linear system to solve.
A and B are two block of the laplacian of the image graph.
"""
if mask is None:
labels = labels.ravel()
else:
labels = labels[mask]
indices = np.arange(labels.size)
seeds_mask = labels > 0
unlabeled_indices = indices[~seeds_mask]
seeds_indices = indices[seeds_mask]
lap_sparse = _build_laplacian(data, spacing, mask=mask,
beta=beta, multichannel=multichannel)
rows = lap_sparse[unlabeled_indices, :]
lap_sparse = rows[:, unlabeled_indices]
B = -rows[:, seeds_indices]
seeds = labels[seeds_mask]
seeds_mask = sparse.csc_matrix(np.hstack(
[np.atleast_2d(seeds == lab).T for lab in range(1, nlabels + 1)]))
rhs = B.dot(seeds_mask)
return lap_sparse, rhs
def _solve_linear_system(lap_sparse, B, tol, mode):
if mode is None:
mode = 'cg_j'
if mode == 'cg_mg' and not amg_loaded:
warn('"cg_mg" not available, it requires pyamg to be installed. '
'The "cg_j" mode will be used instead.',
stacklevel=2)
mode = 'cg_j'
if mode == 'bf':
X = spsolve(lap_sparse, B.toarray()).T
else:
maxiter = None
if mode == 'cg':
if UmfpackContext is None:
warn('"cg" mode may be slow because UMFPACK is not available. '
'Consider building Scipy with UMFPACK or use a '
'preconditioned version of CG ("cg_j" or "cg_mg" modes).',
stacklevel=2)
M = None
elif mode == 'cg_j':
M = sparse.diags(1.0 / lap_sparse.diagonal())
else:
# mode == 'cg_mg'
lap_sparse = lap_sparse.tocsr()
ml = ruge_stuben_solver(lap_sparse)
M = ml.aspreconditioner(cycle='V')
maxiter = 30
cg_out = [
cg(lap_sparse, B[:, i].toarray(), tol=tol, M=M, maxiter=maxiter)
for i in range(B.shape[1])]
if np.any([info > 0 for _, info in cg_out]):
warn("Conjugate gradient convergence to tolerance not achieved. "
"Consider decreasing beta to improve system conditionning.",
stacklevel=2)
X = np.asarray([x for x, _ in cg_out])
return X
def _preprocess(labels):
label_values, inv_idx = np.unique(labels, return_inverse=True)
if not (label_values == 0).any():
warn('Random walker only segments unlabeled areas, where '
'labels == 0. No zero valued areas in labels were '
'found. Returning provided labels.',
stacklevel=2)
return labels, None, None, None, None
# If some labeled pixels are isolated inside pruned zones, prune them
# as well and keep the labels for the final output
null_mask = labels == 0
pos_mask = labels > 0
mask = labels >= 0
fill = ndi.binary_propagation(null_mask, mask=mask)
isolated = np.logical_and(pos_mask, np.logical_not(fill))
pos_mask[isolated] = False
# If the array has pruned zones, be sure that no isolated pixels
# exist between pruned zones (they could not be determined)
if label_values[0] < 0 or np.any(isolated):
isolated = np.logical_and(
np.logical_not(ndi.binary_propagation(pos_mask, mask=mask)),
null_mask)
labels[isolated] = -1
if np.all(isolated[null_mask]):
warn('All unlabeled pixels are isolated, they could not be '
'determined by the random walker algorithm.',
stacklevel=2)
return labels, None, None, None, None
mask[isolated] = False
mask = np.atleast_3d(mask)
else:
mask = None
# Reorder label values to have consecutive integers (no gaps)
zero_idx = np.searchsorted(label_values, 0)
labels = np.atleast_3d(inv_idx.reshape(labels.shape) - zero_idx)
nlabels = label_values[zero_idx + 1:].shape[0]
inds_isolated_seeds = np.nonzero(isolated)
isolated_values = labels[inds_isolated_seeds]
return labels, nlabels, mask, inds_isolated_seeds, isolated_values
def random_walker(data, labels, beta=130, mode='cg_j', tol=1.e-3, copy=True,
multichannel=False, return_full_prob=False, spacing=None):
"""Random walker algorithm for segmentation from markers.
Random walker algorithm is implemented for gray-level or multichannel
images.
Parameters
----------
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 (multichannel=True) with the highest dimension denoting
channels. Data spacing is assumed isotropic unless the `spacing`
keyword argument is used.
labels : array of ints, of same shape as `data` without channels dimension
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). If labels are not
consecutive integers, the labels array will be transformed so that
labels are consecutive. In the multichannel case, `labels` should have
the same shape as a single channel of `data`, i.e. without the final
dimension denoting channels.
beta : float, optional
Penalization coefficient for the random walker motion
(the greater `beta`, the more difficult the diffusion).
mode : string, available options {'cg', 'cg_j', 'cg_mg', 'bf'}
Mode for solving the linear system in the random walker algorithm.
- 'bf' (brute force): an LU factorization of the Laplacian is
computed. This is fast for small images (<1024x1024), but very slow
and memory-intensive for large images (e.g., 3-D volumes).
- '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_j' (conjugate gradient with Jacobi preconditionner): the
Jacobi preconditionner is applyed during the Conjugate
gradient method iterations. This may accelerate the
convergence of the 'cg' method.
- '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 is installed.
tol : float, optional
tolerance to achieve when solving the linear system using
the conjugate gradient based modes ('cg', 'cg_j' and 'cg_mg').
copy : bool, optional
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.
multichannel : bool, optional
If True, input data is parsed as multichannel data (see 'data' above
for proper input format in this case).
return_full_prob : bool, optional
If True, the probability that a pixel belongs to each of the
labels will be returned, instead of only the most likely
label.
spacing : iterable of floats, optional
Spacing between voxels in each spatial dimension. If `None`, then
the spacing between pixels/voxels in each dimension is assumed 1.
Returns
-------
output : ndarray
* If `return_full_prob` is False, array of ints of same shape
and data type as `labels`, in which each pixel has been
labeled according to the marker that reached the pixel first
by anisotropic diffusion.
* If `return_full_prob` is True, array of floats of shape
`(nlabels, labels.shape)`. `output[label_nb, i, j]` is the
probability that label `label_nb` reaches the pixel `(i, j)`
first.
See also
--------
skimage.morphology.watershed: watershed segmentation
A segmentation algorithm based on mathematical morphology
and "flooding" of regions from markers.
Notes
-----
Multichannel inputs are scaled with all channel data combined. Ensure all
channels are separately normalized prior to running this algorithm.
The `spacing` argument is specifically for anisotropic datasets, where
data points are spaced differently in one or more spatial dimensions.
Anisotropic data is commonly encountered in medical imaging.
The algorithm was first proposed in [1]_.
The algorithm solves the diffusion equation at infinite times for
sources placed on markers of each phase in turn. A pixel is labeled with
the phase that has the greatest probability to diffuse first to the pixel.
The diffusion equation is solved by minimizing x.T L x for each phase,
where L is the Laplacian of the weighted graph of the image, and x is
the probability that a marker of the given phase arrives first at a pixel
by diffusion (x=1 on markers of the phase, x=0 on the other markers, and
the other coefficients are looked for). Each pixel is attributed the label
for which it has a maximal value of x. The Laplacian L of the image
is defined as:
- L_ii = d_i, the number of neighbors of pixel i (the degree of i)
- L_ij = -w_ij if i and j are adjacent pixels
The weight w_ij is a decreasing function of the norm of the local gradient.
This ensures that diffusion is easier between pixels of similar values.
When the Laplacian is decomposed into blocks of marked and unmarked
pixels::
L = M B.T
B A
with first indices corresponding to marked pixels, and then to unmarked
pixels, minimizing x.T L x for one phase amount to solving::
A x = - B x_m
where x_m = 1 on markers of the given phase, and 0 on other markers.
This linear system is solved in the algorithm using a direct method for
small images, and an iterative method for larger images.
References
----------
.. [1] Leo Grady, Random walks for image segmentation, IEEE Trans Pattern
Anal Mach Intell. 2006 Nov;28(11):1768-83.
:DOI:`10.1109/TPAMI.2006.233`.
Examples
--------
>>> np.random.seed(0)
>>> a = np.zeros((10, 10)) + 0.2 * np.random.rand(10, 10)
>>> a[5:8, 5:8] += 1
>>> b = np.zeros_like(a, dtype=np.int32)
>>> 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]], dtype=int32)
"""
# Parse input data
if mode not in ('cg_mg', 'cg', 'bf', 'cg_j', None):
raise ValueError(
"{mode} is not a valid mode. Valid modes are 'cg_mg',"
" 'cg', 'cg_j', 'bf' and None".format(mode=mode))
# Spacing kwarg checks
if spacing is None:
spacing = np.ones(3)
elif len(spacing) == labels.ndim:
if len(spacing) == 2:
# Need a dummy spacing for singleton 3rd dim
spacing = np.r_[spacing, 1.]
spacing = np.asarray(spacing)
else:
raise ValueError('Input argument `spacing` incorrect, should be an '
'iterable with one number per spatial dimension.')
# This algorithm expects 4-D arrays of floats, where the first three
# dimensions are spatial and the final denotes channels. 2-D images have
# a singleton placeholder dimension added for the third spatial dimension,
# and single channel images likewise have a singleton added for channels.
# The following block ensures valid input and coerces it to the correct
# form.
if not multichannel:
if data.ndim not in (2, 3):
raise ValueError('For non-multichannel input, data must be of '
'dimension 2 or 3.')
if data.shape != labels.shape:
raise ValueError('Incompatible data and labels shapes.')
data = np.atleast_3d(img_as_float(data))[..., np.newaxis]
else:
if data.ndim not in (3, 4):
raise ValueError('For multichannel input, data must have 3 or 4 '
'dimensions.')
if data.shape[:-1] != labels.shape:
raise ValueError('Incompatible data and labels shapes.')
data = img_as_float(data)
if data.ndim == 3: # 2D multispectral, needs singleton in 3rd axis
data = data[:, :, np.newaxis, :]
labels_shape = labels.shape
labels_dtype = labels.dtype
if copy:
labels = np.copy(labels)
(labels, nlabels, mask,
inds_isolated_seeds, isolated_values) = _preprocess(labels)
if isolated_values is None:
# No non isolated zero valued areas in labels were
# found. Returning provided labels.
if return_full_prob:
# Return the concatenation of the masks of each unique label
return np.concatenate([np.atleast_3d(labels == lab)
for lab in np.unique(labels) if lab > 0],
axis=-1)
return labels
# Build the linear system (lap_sparse, B)
lap_sparse, B = _build_linear_system(data, spacing, labels, nlabels, mask,
beta, multichannel)
# 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.
X = _solve_linear_system(lap_sparse, B, tol, mode)
# Build the output according to return_full_prob value
# Put back labels of isolated seeds
labels[inds_isolated_seeds] = isolated_values
labels = labels.reshape(labels_shape)
if return_full_prob:
mask = labels == 0
out = np.zeros((nlabels,) + labels_shape)
for lab, (label_prob, prob) in enumerate(zip(out, X), start=1):
label_prob[mask] = prob
label_prob[labels == lab] = 1
else:
X = np.argmax(X, axis=0) + 1
out = labels.astype(labels_dtype)
out[labels == 0] = X
return out

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python
import os
from skimage._build import cython
base_path = os.path.abspath(os.path.dirname(__file__))
def configuration(parent_package='', top_path=None):
from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs
config = Configuration('segmentation', parent_package, top_path)
cython(['_watershed_cy.pyx',
'_felzenszwalb_cy.pyx',
'_quickshift_cy.pyx',
'_slic.pyx',
], working_path=base_path)
config.add_extension('_watershed_cy', sources=['_watershed_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_felzenszwalb_cy', sources=['_felzenszwalb_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_quickshift_cy', sources=['_quickshift_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_slic', sources=['_slic.c'],
include_dirs=[get_numpy_include_dirs()])
return config
if __name__ == '__main__':
from numpy.distutils.core import setup
setup(maintainer='scikit-image Developers',
maintainer_email='scikit-image@python.org',
description='Segmentation Algorithms',
url='https://github.com/scikit-image/scikit-image',
license='SciPy License (BSD Style)',
**(configuration(top_path='').todict())
)

View file

@ -0,0 +1,307 @@
import warnings
import functools
import collections as coll
import numpy as np
from scipy import ndimage as ndi
from scipy.spatial.distance import pdist, squareform
from scipy.cluster.vq import kmeans2
from numpy import random
from ._slic import (_slic_cython, _enforce_label_connectivity_cython)
from ..util import img_as_float, regular_grid
from ..color import rgb2lab
def _get_mask_centroids(mask, n_centroids):
"""Find regularly spaced centroids on a mask.
Parameters
----------
mask : 3D ndarray
The mask within which the centroids must be positioned.
n_centroids : int
The number of centroids to be returned.
Returns
-------
centroids : 2D ndarray
The coordinates of the centroids with shape (n_centroids, 3).
steps : 1D ndarray
The approximate distance between two seeds in all dimensions.
"""
# Get tight ROI around the mask to optimize
coord = np.array(np.nonzero(mask), dtype=float).T
# Fix random seed to ensure repeatability
rnd = random.RandomState(123)
idx = np.sort(rnd.choice(np.arange(len(coord), dtype=int),
min(n_centroids, len(coord)),
replace=False))
centroids, _ = kmeans2(coord, coord[idx])
# Compute the minimum distance of each centroid to the others
dist = squareform(pdist(centroids))
np.fill_diagonal(dist, np.inf)
closest_pts = dist.argmin(-1)
steps = abs(centroids - centroids[closest_pts, :]).mean(0)
return centroids, steps
def _get_grid_centroids(image, n_centroids):
"""Find regularly spaced centroids on the image.
Parameters
----------
image : 2D, 3D or 4D ndarray
Input image, which can be 2D or 3D, and grayscale or
multichannel.
n_centroids : int
The (approximate) number of centroids to be returned.
Returns
-------
centroids : 2D ndarray
The coordinates of the centroids with shape (~n_centroids, 3).
steps : 1D ndarray
The approximate distance between two seeds in all dimensions.
"""
d, h, w = image.shape[:3]
grid_z, grid_y, grid_x = np.mgrid[:d, :h, :w]
slices = regular_grid(image.shape[:3], n_centroids)
centroids_z = grid_z[slices].ravel()[..., np.newaxis]
centroids_y = grid_y[slices].ravel()[..., np.newaxis]
centroids_x = grid_x[slices].ravel()[..., np.newaxis]
centroids = np.concatenate([centroids_z, centroids_y, centroids_x],
axis=-1)
steps = np.asarray([float(s.step) if s.step is not None else 1.0
for s in slices])
return centroids, steps
def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0,
spacing=None, multichannel=True, convert2lab=None,
enforce_connectivity=True, min_size_factor=0.5, max_size_factor=3,
slic_zero=False, start_label=None, mask=None):
"""Segments image using k-means clustering in Color-(x,y,z) space.
Parameters
----------
image : 2D, 3D or 4D ndarray
Input image, which can be 2D or 3D, and grayscale or multichannel
(see `multichannel` parameter).
n_segments : int, optional
The (approximate) number of labels in the segmented output image.
compactness : float, optional
Balances color proximity and space proximity. Higher values give
more weight to space proximity, making superpixel shapes more
square/cubic. In SLICO mode, this is the initial compactness.
This parameter depends strongly on image contrast and on the
shapes of objects in the image. We recommend exploring possible
values on a log scale, e.g., 0.01, 0.1, 1, 10, 100, before
refining around a chosen value.
max_iter : int, optional
Maximum number of iterations of k-means.
sigma : float or (3,) array-like of floats, optional
Width of Gaussian smoothing kernel for pre-processing for each
dimension of the image. The same sigma is applied to each dimension in
case of a scalar value. Zero means no smoothing.
Note, that `sigma` is automatically scaled if it is scalar and a
manual voxel spacing is provided (see Notes section).
spacing : (3,) array-like of floats, optional
The voxel spacing along each image dimension. By default, `slic`
assumes uniform spacing (same voxel resolution along z, y and x).
This parameter controls the weights of the distances along z, y,
and x during k-means clustering.
multichannel : bool, optional
Whether the last axis of the image is to be interpreted as multiple
channels or another spatial dimension.
convert2lab : bool, optional
Whether the input should be converted to Lab colorspace prior to
segmentation. The input image *must* be RGB. Highly recommended.
This option defaults to ``True`` when ``multichannel=True`` *and*
``image.shape[-1] == 3``.
enforce_connectivity : bool, optional
Whether the generated segments are connected or not
min_size_factor : float, optional
Proportion of the minimum segment size to be removed with respect
to the supposed segment size ```depth*width*height/n_segments```
max_size_factor : float, optional
Proportion of the maximum connected segment size. A value of 3 works
in most of the cases.
slic_zero : bool, optional
Run SLIC-zero, the zero-parameter mode of SLIC. [2]_
start_label: int, optional
The labels' index start. Should be 0 or 1.
mask : 2D ndarray, optional
If provided, superpixels are computed only where mask is True,
and seed points are homogeneously distributed over the mask
using a K-means clustering strategy.
Returns
-------
labels : 2D or 3D array
Integer mask indicating segment labels.
Raises
------
ValueError
If ``convert2lab`` is set to ``True`` but the last array
dimension is not of length 3.
ValueError
If ``start_label`` is not 0 or 1.
Notes
-----
* If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to
segmentation.
* If `sigma` is scalar and `spacing` is provided, the kernel width is
divided along each dimension by the spacing. For example, if ``sigma=1``
and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This
ensures sensible smoothing for anisotropic images.
* The image is rescaled to be in [0, 1] prior to processing.
* Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To
interpret them as 3D with the last dimension having length 3, use
`multichannel=False`.
* `start_label` is introduced to handle the issue [4]_. The labels
indexing starting at 0 will be deprecated in future versions. If
`mask` is not `None` labels indexing starts at 1 and masked area
is set to 0.
References
----------
.. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi,
Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to
State-of-the-art Superpixel Methods, TPAMI, May 2012.
:DOI:`10.1109/TPAMI.2012.120`
.. [2] https://www.epfl.ch/labs/ivrl/research/slic-superpixels/#SLICO
.. [3] Irving, Benjamin. "maskSLIC: regional superpixel generation with
application to local pathology characterisation in medical images.",
2016, :arXiv:`1606.09518`
.. [4] https://github.com/scikit-image/scikit-image/issues/3722
Examples
--------
>>> from skimage.segmentation import slic
>>> from skimage.data import astronaut
>>> img = astronaut()
>>> segments = slic(img, n_segments=100, compactness=10)
Increasing the compactness parameter yields more square regions:
>>> segments = slic(img, n_segments=100, compactness=20)
"""
image = img_as_float(image)
use_mask = mask is not None
dtype = image.dtype
is_2d = False
if image.ndim == 2:
# 2D grayscale image
image = image[np.newaxis, ..., np.newaxis]
is_2d = True
elif image.ndim == 3 and multichannel:
# Make 2D multichannel image 3D with depth = 1
image = image[np.newaxis, ...]
is_2d = True
elif image.ndim == 3 and not multichannel:
# Add channel as single last dimension
image = image[..., np.newaxis]
if multichannel and (convert2lab or convert2lab is None):
if image.shape[-1] != 3 and convert2lab:
raise ValueError("Lab colorspace conversion requires a RGB image.")
elif image.shape[-1] == 3:
image = rgb2lab(image)
if start_label is None:
if use_mask:
start_label = 1
else:
warnings.warn("skimage.measure.label's indexing starts from 0. " +
"In future version it will start from 1. " +
"To disable this warning, explicitely " +
"set the `start_label` parameter to 1.",
FutureWarning, stacklevel=2)
start_label = 0
if start_label not in [0, 1]:
raise ValueError("start_label should be 0 or 1.")
# initialize cluster centroids for desired number of segments
update_centroids = False
if use_mask:
mask = np.ascontiguousarray(mask, dtype=np.bool).view('uint8')
if mask.ndim == 2:
mask = np.ascontiguousarray(mask[np.newaxis, ...])
if mask.shape != image.shape[:3]:
raise ValueError("image and mask should have the same shape.")
centroids, steps = _get_mask_centroids(mask, n_segments)
update_centroids = True
else:
centroids, steps = _get_grid_centroids(image, n_segments)
if spacing is None:
spacing = np.ones(3, dtype=dtype)
elif isinstance(spacing, (list, tuple)):
spacing = np.ascontiguousarray(spacing, dtype=dtype)
if not isinstance(sigma, coll.Iterable):
sigma = np.array([sigma, sigma, sigma], dtype=dtype)
sigma /= spacing.astype(dtype)
elif isinstance(sigma, (list, tuple)):
sigma = np.array(sigma, dtype=dtype)
if (sigma > 0).any():
# add zero smoothing for multichannel dimension
sigma = list(sigma) + [0]
image = ndi.gaussian_filter(image, sigma)
n_centroids = centroids.shape[0]
segments = np.ascontiguousarray(np.concatenate(
[centroids, np.zeros((n_centroids, image.shape[3]))],
axis=-1), dtype=dtype)
# Scaling of ratio in the same way as in the SLIC paper so the
# values have the same meaning
step = max(steps)
ratio = 1.0 / compactness
image = np.ascontiguousarray(image * ratio, dtype=dtype)
if update_centroids:
# Step 2 of the algorithm [3]_
_slic_cython(image, mask, segments, step, max_iter, spacing,
slic_zero, ignore_color=True,
start_label=start_label)
labels = _slic_cython(image, mask, segments, step, max_iter,
spacing, slic_zero, ignore_color=False,
start_label=start_label)
if enforce_connectivity:
if use_mask:
segment_size = mask.sum() / n_centroids
else:
segment_size = np.prod(image.shape[:3]) / n_centroids
min_size = int(min_size_factor * segment_size)
max_size = int(max_size_factor * segment_size)
labels = _enforce_label_connectivity_cython(
labels, min_size, max_size, start_label=start_label)
if is_2d:
labels = labels[0]
return labels

View file

@ -0,0 +1,9 @@
from ..._shared.testing import setup_test, teardown_test
def setup():
setup_test()
def teardown():
teardown_test()

View file

@ -0,0 +1,146 @@
import numpy as np
from skimage import data
from skimage.color import rgb2gray
from skimage.filters import gaussian
from skimage.segmentation import active_contour
from skimage._shared import testing
from skimage._shared.testing import assert_equal, assert_allclose
from skimage._shared._warnings import expected_warnings
def test_periodic_reference():
img = data.astronaut()
img = rgb2gray(img)
s = np.linspace(0, 2*np.pi, 400)
r = 100 + 100*np.sin(s)
c = 220 + 100*np.cos(s)
init = np.array([r, c]).T
snake = active_contour(gaussian(img, 3), init, alpha=0.015, beta=10,
w_line=0, w_edge=1, gamma=0.001, coordinates='rc')
refr = [98, 99, 100, 101, 102, 103, 104, 105, 106, 108]
refc = [299, 298, 298, 298, 298, 297, 297, 296, 296, 295]
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
def test_fixed_reference():
img = data.text()
r = np.linspace(136, 50, 100)
c = np.linspace(5, 424, 100)
init = np.array([r, c]).T
snake = active_contour(gaussian(img, 1), init, boundary_condition='fixed',
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1,
coordinates='rc')
refr = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125]
refc = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42]
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
def test_free_reference():
img = data.text()
r = np.linspace(70, 40, 100)
c = np.linspace(5, 424, 100)
init = np.array([r, c]).T
snake = active_contour(gaussian(img, 3), init, boundary_condition='free',
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1,
coordinates='rc')
refr = [76, 76, 75, 74, 73, 72, 71, 70, 69, 69]
refc = [10, 13, 16, 19, 23, 26, 29, 32, 36, 39]
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
def test_RGB():
img = gaussian(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]
r = np.linspace(136, 50, 100)
c = np.linspace(5, 424, 100)
init = np.array([r, c]).T
snake = active_contour(imgR, init, boundary_condition='fixed',
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1,
coordinates='rc')
refr = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125]
refc = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42]
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
snake = active_contour(imgG, init, boundary_condition='fixed',
alpha=0.1, beta=1.0, w_line=-5, w_edge=0, gamma=0.1,
coordinates='rc')
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
snake = active_contour(imgRGB, init, boundary_condition='fixed',
alpha=0.1, beta=1.0, w_line=-5/3., w_edge=0,
gamma=0.1, coordinates='rc')
assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr)
assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc)
def test_end_points():
img = data.astronaut()
img = rgb2gray(img)
s = np.linspace(0, 2*np.pi, 400)
r = 100 + 100*np.sin(s)
c = 220 + 100*np.cos(s)
init = np.array([r, c]).T
snake = active_contour(gaussian(img, 3), init,
boundary_condition='periodic', alpha=0.015, beta=10,
w_line=0, w_edge=1, gamma=0.001, max_iterations=100,
coordinates='rc')
assert np.sum(np.abs(snake[0, :]-snake[-1, :])) < 2
snake = active_contour(gaussian(img, 3), init,
boundary_condition='free', alpha=0.015, beta=10,
w_line=0, w_edge=1, gamma=0.001, max_iterations=100,
coordinates='rc')
assert np.sum(np.abs(snake[0, :]-snake[-1, :])) > 2
snake = active_contour(gaussian(img, 3), init,
boundary_condition='fixed', alpha=0.015, beta=10,
w_line=0, w_edge=1, gamma=0.001, max_iterations=100,
coordinates='rc')
assert_allclose(snake[0, :], [r[0], c[0]], atol=1e-5)
def test_bad_input():
img = np.zeros((10, 10))
r = np.linspace(136, 50, 100)
c = np.linspace(5, 424, 100)
init = np.array([r, c]).T
with testing.raises(ValueError):
active_contour(img, init, boundary_condition='wrong',
coordinates='rc')
with testing.raises(ValueError):
active_contour(img, init, max_iterations=-15,
coordinates='rc')
def test_bc_deprecation():
with expected_warnings(['boundary_condition']):
img = rgb2gray(data.astronaut())
s = np.linspace(0, 2*np.pi, 400)
r = 100 + 100*np.sin(s)
c = 220 + 100*np.cos(s)
init = np.array([r, c]).T
snake = active_contour(gaussian(img, 3), init,
bc='periodic', alpha=0.015, beta=10,
w_line=0, w_edge=1, gamma=0.001,
max_iterations=100, coordinates='rc')
def test_xy_coord_warning():
# this should raise ValueError after 0.18.
with expected_warnings(['xy coordinates']):
img = rgb2gray(data.astronaut())
s = np.linspace(0, 2*np.pi, 400)
x = 100 + 100*np.sin(s)
y = 220 + 100*np.cos(s)
init = np.array([x, y]).T
snake = active_contour(gaussian(img, 3), init,
boundary_condition='periodic', alpha=0.015,
beta=10, w_line=0, w_edge=1, gamma=0.001,
max_iterations=100)

View file

@ -0,0 +1,120 @@
import numpy as np
from skimage.segmentation import find_boundaries, mark_boundaries
from skimage._shared.testing import assert_array_equal, assert_allclose
white = (1, 1, 1)
def test_find_boundaries():
image = np.zeros((10, 10), dtype=np.uint8)
image[2:7, 2:7] = 1
ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
result = find_boundaries(image)
assert_array_equal(result, ref)
def test_find_boundaries_bool():
image = np.zeros((5, 5), dtype=np.bool)
image[2:5, 2:5] = True
ref = np.array([[False, False, False, False, False],
[False, False, True, True, True],
[False, True, True, True, True],
[False, True, True, False, False],
[False, True, True, False, False]], dtype=np.bool)
result = find_boundaries(image)
assert_array_equal(result, ref)
def test_mark_boundaries():
image = np.zeros((10, 10))
label_image = np.zeros((10, 10), dtype=np.uint8)
label_image[2:7, 2:7] = 1
ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
marked = mark_boundaries(image, label_image, color=white, mode='thick')
result = np.mean(marked, axis=-1)
assert_array_equal(result, ref)
ref = np.array([[0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
[2, 2, 1, 1, 1, 1, 1, 2, 2, 0],
[2, 1, 1, 1, 1, 1, 1, 1, 2, 0],
[2, 1, 1, 2, 2, 2, 1, 1, 2, 0],
[2, 1, 1, 2, 0, 2, 1, 1, 2, 0],
[2, 1, 1, 2, 2, 2, 1, 1, 2, 0],
[2, 1, 1, 1, 1, 1, 1, 1, 2, 0],
[2, 2, 1, 1, 1, 1, 1, 2, 2, 0],
[0, 2, 2, 2, 2, 2, 2, 2, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
marked = mark_boundaries(image, label_image, color=white,
outline_color=(2, 2, 2), mode='thick')
result = np.mean(marked, axis=-1)
assert_array_equal(result, ref)
def test_mark_boundaries_bool():
image = np.zeros((10, 10), dtype=np.bool)
label_image = np.zeros((10, 10), dtype=np.uint8)
label_image[2:7, 2:7] = 1
ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
marked = mark_boundaries(image, label_image, color=white, mode='thick')
result = np.mean(marked, axis=-1)
assert_array_equal(result, ref)
def test_mark_boundaries_subpixel():
labels = np.array([[0, 0, 0, 0],
[0, 0, 5, 0],
[0, 1, 5, 0],
[0, 0, 5, 0],
[0, 0, 0, 0]], dtype=np.uint8)
np.random.seed(0)
image = np.round(np.random.rand(*labels.shape), 2)
marked = mark_boundaries(image, labels, color=white, mode='subpixel')
marked_proj = np.round(np.mean(marked, axis=-1), 2)
ref_result = np.array(
[[ 0.55, 0.63, 0.72, 0.69, 0.6 , 0.55, 0.54],
[ 0.45, 0.58, 0.72, 1. , 1. , 1. , 0.69],
[ 0.42, 0.54, 0.65, 1. , 0.44, 1. , 0.89],
[ 0.69, 1. , 1. , 1. , 0.69, 1. , 0.83],
[ 0.96, 1. , 0.38, 1. , 0.79, 1. , 0.53],
[ 0.89, 1. , 1. , 1. , 0.38, 1. , 0.16],
[ 0.57, 0.78, 0.93, 1. , 0.07, 1. , 0.09],
[ 0.2 , 0.52, 0.92, 1. , 1. , 1. , 0.54],
[ 0.02, 0.35, 0.83, 0.9 , 0.78, 0.81, 0.87]])
assert_allclose(marked_proj, ref_result, atol=0.01)

View file

@ -0,0 +1,90 @@
import numpy as np
from skimage.segmentation import chan_vese
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
def test_chan_vese_flat_level_set():
# because the algorithm evolves the level set around the
# zero-level, it the level-set has no zero level, the algorithm
# will not produce results in theory. However, since a continuous
# approximation of the delta function is used, the algorithm
# still affects the entirety of the level-set. Therefore with
# infinite time, the segmentation will still converge.
img = np.zeros((10, 10))
img[3:6, 3:6] = np.ones((3, 3))
ls = np.full((10, 10), 1000)
result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set=ls)
assert_array_equal(result.astype(np.float), np.ones((10, 10)))
result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set=-ls)
assert_array_equal(result.astype(np.float), np.zeros((10, 10)))
def test_chan_vese_small_disk_level_set():
img = np.zeros((10, 10))
img[3:6, 3:6] = np.ones((3, 3))
result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set="small disk")
assert_array_equal(result.astype(np.float), img)
def test_chan_vese_simple_shape():
img = np.zeros((10, 10))
img[3:6, 3:6] = np.ones((3, 3))
result = chan_vese(img, mu=0.0, tol=1e-8).astype(np.float)
assert_array_equal(result, img)
def test_chan_vese_extended_output():
img = np.zeros((10, 10))
img[3:6, 3:6] = np.ones((3, 3))
result = chan_vese(img, mu=0.0, tol=1e-8, extended_output=True)
assert_array_equal(len(result), 3)
def test_chan_vese_remove_noise():
ref = np.zeros((10, 10))
ref[1:6, 1:6] = np.array([[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0]])
img = ref.copy()
img[8, 3] = 1
result = chan_vese(img, mu=0.3, tol=1e-3, max_iter=100, dt=10,
init_level_set="disk").astype(np.float)
assert_array_equal(result, ref)
def test_chan_vese_incorrect_image_type():
img = np.zeros((10, 10, 3))
ls = np.zeros((10, 9))
with testing.raises(ValueError):
chan_vese(img, mu=0.0, init_level_set=ls)
def test_chan_vese_gap_closing():
ref = np.zeros((20, 20))
ref[8:15, :] = np.ones((7, 20))
img = ref.copy()
img[:, 6] = np.zeros((20))
result = chan_vese(img, mu=0.7, tol=1e-3, max_iter=1000, dt=1000,
init_level_set="disk").astype(np.float)
assert_array_equal(result, ref)
def test_chan_vese_incorrect_level_set():
img = np.zeros((10, 10))
ls = np.zeros((10, 9))
with testing.raises(ValueError):
chan_vese(img, mu=0.0, init_level_set=ls)
with testing.raises(ValueError):
chan_vese(img, mu=0.0, init_level_set="a")
def test_chan_vese_blank_image():
img = np.zeros((10, 10))
level_set = np.random.rand(10, 10)
ref = level_set > 0
result = chan_vese(img, mu=0.0, tol=0.0, init_level_set=level_set)
assert_array_equal(result, ref)

View file

@ -0,0 +1,175 @@
import numpy as np
from skimage.segmentation import clear_border
from skimage._shared.testing import assert_array_equal, assert_
def test_clear_border():
image = np.array(
[[0, 0, 0, 0, 0, 0, 0, 1, 0],
[1, 1, 0, 0, 1, 0, 0, 1, 0],
[1, 1, 0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]])
# test default case
result = clear_border(image.copy())
ref = image.copy()
ref[1:3, 0:2] = 0
ref[0:2, -2] = 0
assert_array_equal(result, ref)
# test buffer
result = clear_border(image.copy(), 1)
assert_array_equal(result, np.zeros(result.shape))
# test background value
result = clear_border(image.copy(), buffer_size=1, bgval=2)
assert_array_equal(result, 2 * np.ones_like(image))
# test mask
mask = np.array([[0, 0, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 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]]).astype(np.bool)
result = clear_border(image.copy(), mask=mask)
ref = image.copy()
ref[1:3, 0:2] = 0
assert_array_equal(result, ref)
def test_clear_border_3d():
image = np.array([
[[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 0]],
[[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 0, 1, 0],
[0, 0, 0, 0]],
[[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]],
])
# test default case
result = clear_border(image.copy())
ref = image.copy()
ref[0, 3, 0] = 0
assert_array_equal(result, ref)
# test buffer
result = clear_border(image.copy(), 1)
assert_array_equal(result, np.zeros(result.shape))
# test background value
result = clear_border(image.copy(), buffer_size=1, bgval=2)
assert_array_equal(result, 2 * np.ones_like(image))
def test_clear_border_non_binary():
image = np.array([[1, 2, 3, 1, 2],
[3, 3, 5, 4, 2],
[3, 4, 5, 4, 2],
[3, 3, 2, 1, 2]])
result = clear_border(image)
expected = np.array([[0, 0, 0, 0, 0],
[0, 0, 5, 4, 0],
[0, 4, 5, 4, 0],
[0, 0, 0, 0, 0]])
assert_array_equal(result, expected)
assert_(not np.all(image == result))
def test_clear_border_non_binary_3d():
image3d = np.array(
[[[1, 2, 3, 1, 2],
[3, 3, 3, 4, 2],
[3, 4, 3, 4, 2],
[3, 3, 2, 1, 2]],
[[1, 2, 3, 1, 2],
[3, 3, 5, 4, 2],
[3, 4, 5, 4, 2],
[3, 3, 2, 1, 2]],
[[1, 2, 3, 1, 2],
[3, 3, 3, 4, 2],
[3, 4, 3, 4, 2],
[3, 3, 2, 1, 2]],
])
result = clear_border(image3d)
expected = np.array(
[[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 5, 0, 0],
[0, 0, 5, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
])
assert_array_equal(result, expected)
assert_(not np.all(image3d == result))
def test_clear_border_non_binary_inplace():
image = np.array([[1, 2, 3, 1, 2],
[3, 3, 5, 4, 2],
[3, 4, 5, 4, 2],
[3, 3, 2, 1, 2]])
result = clear_border(image, in_place=True)
expected = np.array([[0, 0, 0, 0, 0],
[0, 0, 5, 4, 0],
[0, 4, 5, 4, 0],
[0, 0, 0, 0, 0]])
assert_array_equal(result, expected)
assert_array_equal(image, result)
def test_clear_border_non_binary_inplace_3d():
image3d = np.array(
[[[1, 2, 3, 1, 2],
[3, 3, 3, 4, 2],
[3, 4, 3, 4, 2],
[3, 3, 2, 1, 2]],
[[1, 2, 3, 1, 2],
[3, 3, 5, 4, 2],
[3, 4, 5, 4, 2],
[3, 3, 2, 1, 2]],
[[1, 2, 3, 1, 2],
[3, 3, 3, 4, 2],
[3, 4, 3, 4, 2],
[3, 3, 2, 1, 2]],
])
result = clear_border(image3d, in_place=True)
expected = np.array(
[[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 5, 0, 0],
[0, 0, 5, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
])
assert_array_equal(result, expected)
assert_array_equal(image3d, result)

View file

@ -0,0 +1,82 @@
import numpy as np
from skimage import data
from skimage.segmentation import felzenszwalb
from skimage._shared import testing
from skimage._shared.testing import (assert_greater, test_parallel,
assert_equal, assert_array_equal,
assert_warns, assert_no_warnings)
@test_parallel()
def test_grey():
# very weak tests.
img = np.zeros((20, 21))
img[:10, 10:] = 0.2
img[10:, :10] = 0.4
img[10:, 10:] = 0.6
seg = felzenszwalb(img, sigma=0)
# we expect 4 segments:
assert_equal(len(np.unique(seg)), 4)
# that mostly respect the 4 regions:
for i in range(4):
hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0]
assert_greater(hist[i], 40)
def test_minsize():
# single-channel:
img = data.coins()[20:168, 0:128]
for min_size in np.arange(10, 100, 10):
segments = felzenszwalb(img, min_size=min_size, sigma=3)
counts = np.bincount(segments.ravel())
# actually want to test greater or equal.
assert_greater(counts.min() + 1, min_size)
# multi-channel:
coffee = data.coffee()[::4, ::4]
for min_size in np.arange(10, 100, 10):
segments = felzenszwalb(coffee, min_size=min_size, sigma=3)
counts = np.bincount(segments.ravel())
# actually want to test greater or equal.
assert_greater(counts.min() + 1, min_size)
def test_3D():
grey_img = np.zeros((10, 10))
rgb_img = np.zeros((10, 10, 3))
three_d_img = np.zeros((10, 10, 10))
with assert_no_warnings():
felzenszwalb(grey_img, multichannel=True)
felzenszwalb(grey_img, multichannel=False)
felzenszwalb(rgb_img, multichannel=True)
with assert_warns(RuntimeWarning):
felzenszwalb(three_d_img, multichannel=True)
with testing.raises(ValueError):
felzenszwalb(rgb_img, multichannel=False)
felzenszwalb(three_d_img, multichannel=False)
def test_color():
# very weak tests.
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
seg = felzenszwalb(img, sigma=0)
# we expect 4 segments:
assert_equal(len(np.unique(seg)), 4)
assert_array_equal(seg[:10, :10], 0)
assert_array_equal(seg[10:, :10], 2)
assert_array_equal(seg[:10, 10:], 1)
assert_array_equal(seg[10:, 10:], 3)
def test_merging():
# test region merging in the post-processing step
img = np.array([[0, 0.3], [0.7, 1]])
# With scale=0, only the post-processing is performed.
seg = felzenszwalb(img, scale=0, sigma=0, min_size=2)
# we expect 2 segments:
assert_equal(len(np.unique(seg)), 2)
assert_array_equal(seg[0, :], 0)
assert_array_equal(seg[1, :], 1)

View file

@ -0,0 +1,211 @@
import numpy as np
from skimage.segmentation import join_segmentations, relabel_sequential
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
import pytest
def test_join_segmentations():
s1 = np.array([[0, 0, 1, 1],
[0, 2, 1, 1],
[2, 2, 2, 1]])
s2 = np.array([[0, 1, 1, 0],
[0, 1, 1, 0],
[0, 1, 1, 1]])
# test correct join
# NOTE: technically, equality to j_ref is not required, only that there
# is a one-to-one mapping between j and j_ref. I don't know of an easy way
# to check this (i.e. not as error-prone as the function being tested)
j = join_segmentations(s1, s2)
j_ref = np.array([[0, 1, 3, 2],
[0, 5, 3, 2],
[4, 5, 5, 3]])
assert_array_equal(j, j_ref)
# test correct exception when arrays are different shapes
s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]])
with testing.raises(ValueError):
join_segmentations(s1, s3)
def _check_maps(ar, ar_relab, fw, inv):
assert_array_equal(fw[ar], ar_relab)
assert_array_equal(inv[ar_relab], ar)
def test_relabel_sequential_offset1():
ar = np.array([1, 1, 5, 5, 8, 99, 42])
ar_relab, fw, inv = relabel_sequential(ar)
_check_maps(ar, ar_relab, fw, inv)
ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4])
assert_array_equal(ar_relab, ar_relab_ref)
fw_ref = np.zeros(100, int)
fw_ref[1] = 1
fw_ref[5] = 2
fw_ref[8] = 3
fw_ref[42] = 4
fw_ref[99] = 5
assert_array_equal(fw, fw_ref)
inv_ref = np.array([0, 1, 5, 8, 42, 99])
assert_array_equal(inv, inv_ref)
def test_relabel_sequential_offset5():
ar = np.array([1, 1, 5, 5, 8, 99, 42])
ar_relab, fw, inv = relabel_sequential(ar, offset=5)
_check_maps(ar, ar_relab, fw, inv)
ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8])
assert_array_equal(ar_relab, ar_relab_ref)
fw_ref = np.zeros(100, int)
fw_ref[1] = 5
fw_ref[5] = 6
fw_ref[8] = 7
fw_ref[42] = 8
fw_ref[99] = 9
assert_array_equal(fw, fw_ref)
inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99])
assert_array_equal(inv, inv_ref)
def test_relabel_sequential_offset5_with0():
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0])
ar_relab, fw, inv = relabel_sequential(ar, offset=5)
_check_maps(ar, ar_relab, fw, inv)
ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0])
assert_array_equal(ar_relab, ar_relab_ref)
fw_ref = np.zeros(100, int)
fw_ref[1] = 5
fw_ref[5] = 6
fw_ref[8] = 7
fw_ref[42] = 8
fw_ref[99] = 9
assert_array_equal(fw, fw_ref)
inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99])
assert_array_equal(inv, inv_ref)
def test_relabel_sequential_dtype():
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.uint8)
ar_relab, fw, inv = relabel_sequential(ar, offset=5)
_check_maps(ar.astype(int), ar_relab, fw, inv)
ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0])
assert_array_equal(ar_relab, ar_relab_ref)
fw_ref = np.zeros(100, int)
fw_ref[1] = 5
fw_ref[5] = 6
fw_ref[8] = 7
fw_ref[42] = 8
fw_ref[99] = 9
assert_array_equal(fw, fw_ref)
inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99])
assert_array_equal(inv, inv_ref)
def test_relabel_sequential_signed_overflow():
imax = np.iinfo(np.int32).max
labels = np.array([0, 1, 99, 42, 42], dtype=np.int32)
output, fw, inv = relabel_sequential(labels, offset=imax)
reference = np.array([0, imax, imax + 2, imax + 1, imax + 1],
dtype=np.uint32)
assert_array_equal(output, reference)
assert output.dtype == reference.dtype
def test_very_large_labels():
imax = np.iinfo(np.int64).max
labels = np.array([0, 1, imax, 42, 42], dtype=np.int64)
output, fw, inv = relabel_sequential(labels, offset=imax)
assert np.max(output) == imax + 2
@pytest.mark.parametrize('dtype', (np.byte, np.short, np.intc, np.int_,
np.longlong, np.ubyte, np.ushort,
np.uintc, np.uint, np.ulonglong))
@pytest.mark.parametrize('data_already_sequential', (False, True))
def test_relabel_sequential_int_dtype_stability(data_already_sequential,
dtype):
if data_already_sequential:
ar = np.array([1, 3, 0, 2, 5, 4], dtype=dtype)
else:
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=dtype)
assert all(a.dtype == dtype for a in relabel_sequential(ar))
def test_relabel_sequential_int_dtype_overflow():
ar = np.array([1, 3, 0, 2, 5, 4], dtype=np.uint8)
offset = 254
ar_relab, fw, inv = relabel_sequential(ar, offset=offset)
_check_maps(ar, ar_relab, fw, inv)
assert all(a.dtype == np.uint16 for a in (ar_relab, fw))
assert inv.dtype == ar.dtype
ar_relab_ref = np.where(ar > 0, ar.astype(np.int) + offset - 1, 0)
assert_array_equal(ar_relab, ar_relab_ref)
def test_relabel_sequential_negative_values():
ar = np.array([1, 1, 5, -5, 8, 99, 42, 0])
with pytest.raises(ValueError):
relabel_sequential(ar)
@pytest.mark.parametrize('offset', (0, -3))
@pytest.mark.parametrize('data_already_sequential', (False, True))
def test_relabel_sequential_nonpositive_offset(data_already_sequential,
offset):
if data_already_sequential:
ar = np.array([1, 3, 0, 2, 5, 4])
else:
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0])
with pytest.raises(ValueError):
relabel_sequential(ar, offset=offset)
@pytest.mark.parametrize('offset', (1, 5))
@pytest.mark.parametrize('with0', (False, True))
@pytest.mark.parametrize('input_starts_at_offset', (False, True))
def test_relabel_sequential_already_sequential(offset, with0,
input_starts_at_offset):
if with0:
ar = np.array([1, 3, 0, 2, 5, 4])
else:
ar = np.array([1, 3, 2, 5, 4])
if input_starts_at_offset:
ar[ar > 0] += offset - 1
ar_relab, fw, inv = relabel_sequential(ar, offset=offset)
_check_maps(ar, ar_relab, fw, inv)
if input_starts_at_offset:
ar_relab_ref = ar
else:
ar_relab_ref = np.where(ar > 0, ar + offset - 1, 0)
assert_array_equal(ar_relab, ar_relab_ref)
def test_incorrect_input_dtype():
labels = np.array([0, 2, 2, 1, 1, 8], dtype=float)
with testing.raises(TypeError):
_ = relabel_sequential(labels)
def test_arraymap_call():
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp)
relabeled, fw, inv = relabel_sequential(ar)
testing.assert_array_equal(relabeled, fw(ar))
testing.assert_array_equal(ar, inv(relabeled))
def test_arraymap_len():
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp)
relabeled, fw, inv = relabel_sequential(ar)
assert len(fw) == 100
assert len(fw) == len(np.array(fw))
assert len(inv) == 6
assert len(inv) == len(np.array(inv))
def test_arraymap_set():
ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp)
relabeled, fw, inv = relabel_sequential(ar)
fw[72] = 6
assert fw[72] == 6

View file

@ -0,0 +1,149 @@
import numpy as np
from skimage.segmentation import (morphological_chan_vese,
morphological_geodesic_active_contour,
inverse_gaussian_gradient,
circle_level_set,
disk_level_set)
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
from skimage._shared._warnings import expected_warnings
def gaussian_blob():
coords = np.mgrid[-5:6, -5:6]
sqrdistances = (coords ** 2).sum(0)
return np.exp(-sqrdistances / 10)
def test_morphsnakes_incorrect_image_shape():
img = np.zeros((10, 10, 3))
ls = np.zeros((10, 9))
with testing.raises(ValueError):
morphological_chan_vese(img, iterations=1, init_level_set=ls)
with testing.raises(ValueError):
morphological_geodesic_active_contour(img, iterations=1,
init_level_set=ls)
def test_morphsnakes_incorrect_ndim():
img = np.zeros((4, 4, 4, 4))
ls = np.zeros((4, 4, 4, 4))
with testing.raises(ValueError):
morphological_chan_vese(img, iterations=1, init_level_set=ls)
with testing.raises(ValueError):
morphological_geodesic_active_contour(img, iterations=1,
init_level_set=ls)
def test_morphsnakes_black():
img = np.zeros((11, 11))
ls = disk_level_set(img.shape, center=(5, 5), radius=3)
ref_zeros = np.zeros(img.shape, dtype=np.int8)
ref_ones = np.ones(img.shape, dtype=np.int8)
acwe_ls = morphological_chan_vese(img, iterations=6, init_level_set=ls)
assert_array_equal(acwe_ls, ref_zeros)
gac_ls = morphological_geodesic_active_contour(img, iterations=6,
init_level_set=ls)
assert_array_equal(gac_ls, ref_zeros)
gac_ls2 = morphological_geodesic_active_contour(img, iterations=6,
init_level_set=ls,
balloon=1, threshold=-1,
smoothing=0)
assert_array_equal(gac_ls2, ref_ones)
assert acwe_ls.dtype == gac_ls.dtype == gac_ls2.dtype == np.int8
def test_morphsnakes_simple_shape_chan_vese():
img = gaussian_blob()
ls1 = disk_level_set(img.shape, center=(5, 5), radius=3)
ls2 = disk_level_set(img.shape, center=(5, 5), radius=6)
acwe_ls1 = morphological_chan_vese(img, iterations=10, init_level_set=ls1)
acwe_ls2 = morphological_chan_vese(img, iterations=10, init_level_set=ls2)
assert_array_equal(acwe_ls1, acwe_ls2)
assert acwe_ls1.dtype == acwe_ls2.dtype == np.int8
def test_morphsnakes_simple_shape_geodesic_active_contour():
img = np.float_(disk_level_set((11, 11), center=(5, 5), radius=3.5))
gimg = inverse_gaussian_gradient(img, alpha=10.0, sigma=1.0)
ls = disk_level_set(img.shape, center=(5, 5), radius=6)
ref = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.int8)
gac_ls = morphological_geodesic_active_contour(gimg, iterations=10,
init_level_set=ls,
balloon=-1)
assert_array_equal(gac_ls, ref)
assert gac_ls.dtype == np.int8
def test_deprecated_circle_level_set():
img = gaussian_blob()
with expected_warnings(['circle_level_set is deprecated']):
ls1 = circle_level_set(img.shape, (5, 5), 3)
def test_init_level_sets():
image = np.zeros((6, 6))
checkerboard_ls = morphological_chan_vese(image, 0, 'checkerboard')
checkerboard_ref = np.array([[0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 0]], dtype=np.int8)
disk_ls = morphological_geodesic_active_contour(image, 0, 'disk')
disk_ref = np.array([[0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 0]], dtype=np.int8)
assert_array_equal(checkerboard_ls, checkerboard_ref)
assert_array_equal(disk_ls, disk_ref)
def test_morphsnakes_3d():
image = np.zeros((7, 7, 7))
evolution = []
def callback(x):
evolution.append(x.sum())
ls = morphological_chan_vese(image, 5, 'disk',
iter_callback=callback)
# Check that the initial disk level set is correct
assert evolution[0] == 81
# Check that the final level set is correct
assert ls.sum() == 0
# Check that the contour is shrinking at every iteration
for v1, v2 in zip(evolution[:-1], evolution[1:]):
assert v1 >= v2

View file

@ -0,0 +1,49 @@
import numpy as np
from skimage.segmentation import quickshift
from skimage._shared.testing import (assert_greater, test_parallel,
assert_equal, assert_array_equal)
@test_parallel()
def test_grey():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21))
img[:10, 10:] = 0.2
img[10:, :10] = 0.4
img[10:, 10:] = 0.6
img += 0.1 * rnd.normal(size=img.shape)
seg = quickshift(img, kernel_size=2, max_dist=3, random_seed=0,
convert2lab=False, sigma=0)
# we expect 4 segments:
assert_equal(len(np.unique(seg)), 4)
# that mostly respect the 4 regions:
for i in range(4):
hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0]
assert_greater(hist[i], 20)
def test_color():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
img += 0.01 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = quickshift(img, random_seed=0, max_dist=30, kernel_size=10, sigma=0)
# we expect 4 segments:
assert_equal(len(np.unique(seg)), 4)
assert_array_equal(seg[:10, :10], 1)
assert_array_equal(seg[10:, :10], 2)
assert_array_equal(seg[:10, 10:], 0)
assert_array_equal(seg[10:, 10:], 3)
seg2 = quickshift(img, kernel_size=1, max_dist=2, random_seed=0,
convert2lab=False, sigma=0)
# very oversegmented:
assert_equal(len(np.unique(seg2)), 7)
# still don't cross lines
assert (seg2[9, :] != seg2[10, :]).all()
assert (seg2[:, 9] != seg2[:, 10]).all()

View file

@ -0,0 +1,439 @@
import numpy as np
from skimage.segmentation import random_walker
from skimage.transform import resize
from skimage._shared._warnings import expected_warnings
from skimage._shared import testing
from skimage._shared.testing import xfail, arch32
import scipy
from distutils.version import LooseVersion as Version
# older versions of scipy raise a warning with new NumPy because they use
# numpy.rank() instead of arr.ndim or numpy.linalg.matrix_rank.
SCIPY_RANK_WARNING = r'numpy.linalg.matrix_rank|\A\Z'
PYAMG_MISSING_WARNING = r'pyamg|\A\Z'
PYAMG_OR_SCIPY_WARNING = SCIPY_RANK_WARNING + '|' + PYAMG_MISSING_WARNING
if Version(scipy.__version__) < '1.3':
NUMPY_MATRIX_WARNING = 'matrix subclass'
else:
NUMPY_MATRIX_WARNING = None
def make_2d_syntheticdata(lx, ly=None):
if ly is None:
ly = lx
np.random.seed(1234)
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
np.random.seed(1234)
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)
with expected_warnings([NUMPY_MATRIX_WARNING]):
labels_bf = random_walker(data, labels, beta=90, mode='bf')
assert (labels_bf[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
with expected_warnings([NUMPY_MATRIX_WARNING]):
full_prob_bf = random_walker(data, labels, beta=90, mode='bf',
return_full_prob=True)
assert (full_prob_bf[1, 25:45, 40:60] >=
full_prob_bf[0, 25:45, 40:60]).all()
assert data.shape == labels.shape
# Now test with more than two labels
labels[55, 80] = 3
with expected_warnings([NUMPY_MATRIX_WARNING]):
full_prob_bf = random_walker(data, labels, beta=90, mode='bf',
return_full_prob=True)
assert (full_prob_bf[1, 25:45, 40:60] >=
full_prob_bf[0, 25:45, 40:60]).all()
assert len(full_prob_bf) == 3
assert data.shape == labels.shape
def test_2d_cg():
lx = 70
ly = 100
data, labels = make_2d_syntheticdata(lx, ly)
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
labels_cg = random_walker(data, labels, beta=90, mode='cg')
assert (labels_cg[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
full_prob = random_walker(data, labels, beta=90, mode='cg',
return_full_prob=True)
assert (full_prob[1, 25:45, 40:60] >=
full_prob[0, 25:45, 40:60]).all()
assert data.shape == labels.shape
return data, labels_cg
def test_2d_cg_mg():
lx = 70
ly = 100
data, labels = make_2d_syntheticdata(lx, ly)
anticipated_warnings = [
'scipy.sparse.sparsetools|%s' % PYAMG_OR_SCIPY_WARNING,
NUMPY_MATRIX_WARNING]
with expected_warnings(anticipated_warnings):
labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg')
assert (labels_cg_mg[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
with expected_warnings(anticipated_warnings):
full_prob = random_walker(data, labels, beta=90, mode='cg_mg',
return_full_prob=True)
assert (full_prob[1, 25:45, 40:60] >=
full_prob[0, 25:45, 40:60]).all()
assert data.shape == labels.shape
return data, labels_cg_mg
def test_2d_cg_j():
lx = 70
ly = 100
data, labels = make_2d_syntheticdata(lx, ly)
with expected_warnings([NUMPY_MATRIX_WARNING]):
labels_cg = random_walker(data, labels, beta=90, mode='cg_j')
assert (labels_cg[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
with expected_warnings([NUMPY_MATRIX_WARNING]):
full_prob = random_walker(data, labels, beta=90, mode='cg_j',
return_full_prob=True)
assert (full_prob[1, 25:45, 40:60]
>= full_prob[0, 25:45, 40:60]).all()
assert data.shape == labels.shape
def test_types():
lx = 70
ly = 100
data, labels = make_2d_syntheticdata(lx, ly)
data = 255 * (data - data.min()) // (data.max() - data.min())
data = data.astype(np.uint8)
with expected_warnings([PYAMG_OR_SCIPY_WARNING, NUMPY_MATRIX_WARNING]):
labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg')
assert (labels_cg_mg[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
return data, labels_cg_mg
def test_reorder_labels():
lx = 70
ly = 100
data, labels = make_2d_syntheticdata(lx, ly)
labels[labels == 2] = 4
with expected_warnings([NUMPY_MATRIX_WARNING]):
labels_bf = random_walker(data, labels, beta=90, mode='bf')
assert (labels_bf[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
return data, labels_bf
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
with expected_warnings([NUMPY_MATRIX_WARNING]):
labels = random_walker(data, labels, beta=90)
assert (labels.reshape((lx, ly))[25:45, 40:60] == 2).all()
assert data.shape == labels.shape
return data, labels
def test_3d():
n = 30
lx, ly, lz = n, n, n
data, labels = make_3d_syntheticdata(lx, ly, lz)
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
labels = random_walker(data, labels, mode='cg')
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
assert data.shape == labels.shape
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)
with expected_warnings(['"cg" mode|CObject type' + '|'
+ SCIPY_RANK_WARNING, NUMPY_MATRIX_WARNING]):
labels = random_walker(data, labels, mode='cg')
assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all()
assert data.shape == labels.shape
return data, labels, old_labels, after_labels
def test_multispectral_2d():
lx, ly = 70, 100
data, labels = make_2d_syntheticdata(lx, ly)
data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
multi_labels = random_walker(data, labels, mode='cg',
multichannel=True)
assert data[..., 0].shape == labels.shape
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
single_labels = random_walker(data[..., 0], labels, mode='cg')
assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all()
assert data[..., 0].shape == labels.shape
return data, multi_labels, single_labels, labels
def test_multispectral_3d():
n = 30
lx, ly, lz = n, n, n
data, labels = make_3d_syntheticdata(lx, ly, lz)
data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
multi_labels = random_walker(data, labels, mode='cg',
multichannel=True)
assert data[..., 0].shape == labels.shape
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
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()
assert data[..., 0].shape == labels.shape
return data, multi_labels, single_labels, labels
def test_spacing_0():
n = 30
lx, ly, lz = n, n, n
data, _ = make_3d_syntheticdata(lx, ly, lz)
# Rescale `data` along Z axis
data_aniso = np.zeros((n, n, n // 2))
for i, yz in enumerate(data):
data_aniso[i, :, :] = resize(yz, (n, n // 2),
mode='constant',
anti_aliasing=False)
# Generate new labels
small_l = int(lx // 5)
labels_aniso = np.zeros_like(data_aniso)
labels_aniso[lx // 5, ly // 5, lz // 5] = 1
labels_aniso[lx // 2 + small_l // 4,
ly // 2 - small_l // 4,
lz // 4 - small_l // 8] = 2
# Test with `spacing` kwarg
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg',
spacing=(1., 1., 0.5))
assert (labels_aniso[13:17, 13:17, 7:9] == 2).all()
@xfail(condition=arch32,
reason=('Known test failure on 32-bit platforms. See links for '
'details: '
'https://github.com/scikit-image/scikit-image/issues/3091 '
'https://github.com/scikit-image/scikit-image/issues/3092'))
def test_spacing_1():
n = 30
lx, ly, lz = n, n, n
data, _ = make_3d_syntheticdata(lx, ly, lz)
# Rescale `data` along Y axis
# `resize` is not yet 3D capable, so this must be done by looping in 2D.
data_aniso = np.zeros((n, n * 2, n))
for i, yz in enumerate(data):
data_aniso[i, :, :] = resize(yz, (n * 2, n),
mode='constant',
anti_aliasing=False)
# Generate new labels
small_l = int(lx // 5)
labels_aniso = np.zeros_like(data_aniso)
labels_aniso[lx // 5, ly // 5, lz // 5] = 1
labels_aniso[lx // 2 + small_l // 4,
ly - small_l // 2,
lz // 2 - small_l // 4] = 2
# Test with `spacing` kwarg
# First, anisotropic along Y
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg',
spacing=(1., 2., 1.))
assert (labels_aniso[13:17, 26:34, 13:17] == 2).all()
# Rescale `data` along X axis
# `resize` is not yet 3D capable, so this must be done by looping in 2D.
data_aniso = np.zeros((n, n * 2, n))
for i in range(data.shape[1]):
data_aniso[i, :, :] = resize(data[:, 1, :], (n * 2, n),
mode='constant',
anti_aliasing=False)
# Generate new labels
small_l = int(lx // 5)
labels_aniso2 = np.zeros_like(data_aniso)
labels_aniso2[lx // 5, ly // 5, lz // 5] = 1
labels_aniso2[lx - small_l // 2,
ly // 2 + small_l // 4,
lz // 2 - small_l // 4] = 2
# Anisotropic along X
with expected_warnings(['"cg" mode' + '|' + SCIPY_RANK_WARNING,
NUMPY_MATRIX_WARNING]):
labels_aniso2 = random_walker(data_aniso,
labels_aniso2,
mode='cg', spacing=(2., 1., 1.))
assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all()
def test_trivial_cases():
# When all voxels are labeled
img = np.ones((10, 10))
labels = np.ones((10, 10))
with expected_warnings(["Returning provided labels"]):
pass_through = random_walker(img, labels)
np.testing.assert_array_equal(pass_through, labels)
# When all voxels are labeled AND return_full_prob is True
labels[:, :5] = 3
expected = np.concatenate(((labels == 1)[..., np.newaxis],
(labels == 3)[..., np.newaxis]), axis=2)
with expected_warnings(["Returning provided labels"]):
test = random_walker(img, labels, return_full_prob=True)
np.testing.assert_array_equal(test, expected)
# Unlabeled voxels not connected to seed, so nothing can be done
img = np.full((10, 10), False)
object_A = np.array([(6,7), (6,8), (7,7), (7,8)])
object_B = np.array([(3,1), (4,1), (2,2), (3,2), (4,2), (2,3), (3,3)])
for x, y in np.vstack((object_A, object_B)):
img[y][x] = True
markers = np.zeros((10, 10), dtype=np.int8)
for x, y in object_B:
markers[y][x] = 1
markers[img == 0] = -1
with expected_warnings(["All unlabeled pixels are isolated"]):
output_labels = random_walker(img, markers)
assert np.all(output_labels[markers == 1] == 1)
# Here 0-labeled pixels could not be determined (no connexion to seed)
assert np.all(output_labels[markers == 0] == -1)
with expected_warnings(["All unlabeled pixels are isolated"]):
test = random_walker(img, markers, return_full_prob=True)
def test_length2_spacing():
# If this passes without raising an exception (warnings OK), the new
# spacing code is working properly.
np.random.seed(42)
img = np.ones((10, 10)) + 0.2 * np.random.normal(size=(10, 10))
labels = np.zeros((10, 10), dtype=np.uint8)
labels[2, 4] = 1
labels[6, 8] = 4
with expected_warnings([NUMPY_MATRIX_WARNING]):
random_walker(img, labels, spacing=(1., 2.))
def test_bad_inputs():
# Too few dimensions
img = np.ones(10)
labels = np.arange(10)
with testing.raises(ValueError):
random_walker(img, labels)
with testing.raises(ValueError):
random_walker(img, labels, multichannel=True)
# Too many dimensions
np.random.seed(42)
img = np.random.normal(size=(3, 3, 3, 3, 3))
labels = np.arange(3 ** 5).reshape(img.shape)
with testing.raises(ValueError):
random_walker(img, labels)
with testing.raises(ValueError):
random_walker(img, labels, multichannel=True)
# Spacing incorrect length
img = np.random.normal(size=(10, 10))
labels = np.zeros((10, 10))
labels[2, 4] = 2
labels[6, 8] = 5
with testing.raises(ValueError):
random_walker(img, labels, spacing=(1,))
# Invalid mode
img = np.random.normal(size=(10, 10))
labels = np.zeros((10, 10))
with testing.raises(ValueError):
random_walker(img, labels, mode='bad')
def test_isolated_seeds():
np.random.seed(0)
a = np.random.random((7, 7))
mask = - np.ones(a.shape)
# This pixel is an isolated seed
mask[1, 1] = 1
# Unlabeled pixels
mask[3:, 3:] = 0
# Seeds connected to unlabeled pixels
mask[4, 4] = 2
mask[6, 6] = 1
# Test that no error is raised, and that labels of isolated seeds are OK
with expected_warnings([NUMPY_MATRIX_WARNING]):
res = random_walker(a, mask)
assert res[1, 1] == 1
with expected_warnings([NUMPY_MATRIX_WARNING]):
res = random_walker(a, mask, return_full_prob=True)
assert res[0, 1, 1] == 1
assert res[1, 1, 1] == 0

View file

@ -0,0 +1,474 @@
from itertools import product
import pytest
import numpy as np
from skimage.segmentation import slic
from skimage._shared import testing
from skimage._shared.testing import test_parallel, assert_equal
@test_parallel()
def test_color_2d():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
img += 0.01 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, n_segments=4, sigma=0, enforce_connectivity=False,
start_label=0)
# we expect 4 segments
assert_equal(len(np.unique(seg)), 4)
assert_equal(seg.shape, img.shape[:-1])
assert_equal(seg[:10, :10], 0)
assert_equal(seg[10:, :10], 2)
assert_equal(seg[:10, 10:], 1)
assert_equal(seg[10:, 10:], 3)
def test_multichannel_2d():
rnd = np.random.RandomState(0)
img = np.zeros((20, 20, 8))
img[:10, :10, 0:2] = 1
img[:10, 10:, 2:4] = 1
img[10:, :10, 4:6] = 1
img[10:, 10:, 6:8] = 1
img += 0.01 * rnd.normal(size=img.shape)
img = np.clip(img, 0, 1, out=img)
seg = slic(img, n_segments=4, enforce_connectivity=False, start_label=0)
# we expect 4 segments
assert_equal(len(np.unique(seg)), 4)
assert_equal(seg.shape, img.shape[:-1])
assert_equal(seg[:10, :10], 0)
assert_equal(seg[10:, :10], 2)
assert_equal(seg[:10, 10:], 1)
assert_equal(seg[10:, 10:], 3)
def test_gray_2d():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21))
img[:10, :10] = 0.33
img[10:, :10] = 0.67
img[10:, 10:] = 1.00
img += 0.0033 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, sigma=0, n_segments=4, compactness=1,
multichannel=False, convert2lab=False, start_label=0)
assert_equal(len(np.unique(seg)), 4)
assert_equal(seg.shape, img.shape)
assert_equal(seg[:10, :10], 0)
assert_equal(seg[10:, :10], 2)
assert_equal(seg[:10, 10:], 1)
assert_equal(seg[10:, 10:], 3)
def test_color_3d():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 22, 3))
slices = []
for dim_size in img.shape[:-1]:
midpoint = dim_size // 2
slices.append((slice(None, midpoint), slice(midpoint, None)))
slices = list(product(*slices))
colors = list(product(*(([0, 1],) * 3)))
for s, c in zip(slices, colors):
img[s] = c
img += 0.01 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, sigma=0, n_segments=8, start_label=0)
assert_equal(len(np.unique(seg)), 8)
for s, c in zip(slices, range(8)):
assert_equal(seg[s], c)
def test_gray_3d():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 22))
slices = []
for dim_size in img.shape:
midpoint = dim_size // 2
slices.append((slice(None, midpoint), slice(midpoint, None)))
slices = list(product(*slices))
shades = np.arange(0, 1.000001, 1.0 / 7)
for s, sh in zip(slices, shades):
img[s] = sh
img += 0.001 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, sigma=0, n_segments=8, compactness=1,
multichannel=False, convert2lab=False, start_label=0)
assert_equal(len(np.unique(seg)), 8)
for s, c in zip(slices, range(8)):
assert_equal(seg[s], c)
def test_list_sigma():
rnd = np.random.RandomState(0)
img = np.array([[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1]], np.float)
img += 0.1 * rnd.normal(size=img.shape)
result_sigma = np.array([[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1]], np.int)
seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1],
multichannel=False, start_label=0)
assert_equal(seg_sigma, result_sigma)
def test_spacing():
rnd = np.random.RandomState(0)
img = np.array([[1, 1, 1, 0, 0],
[1, 1, 0, 0, 0]], np.float)
result_non_spaced = np.array([[0, 0, 0, 1, 1],
[0, 0, 1, 1, 1]], np.int)
result_spaced = np.array([[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1]], np.int)
img += 0.1 * rnd.normal(size=img.shape)
seg_non_spaced = slic(img, n_segments=2, sigma=0, multichannel=False,
compactness=1.0, start_label=0)
seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 500, 1],
compactness=1.0, multichannel=False, start_label=0)
assert_equal(seg_non_spaced, result_non_spaced)
assert_equal(seg_spaced, result_spaced)
def test_invalid_lab_conversion():
img = np.array([[1, 1, 1, 0, 0],
[1, 1, 0, 0, 0]], np.float) + 1
with testing.raises(ValueError):
slic(img, multichannel=True, convert2lab=True, start_label=0)
def test_enforce_connectivity():
img = np.array([[0, 0, 0, 1, 1, 1],
[1, 0, 0, 1, 1, 0],
[0, 0, 0, 1, 1, 0]], np.float)
segments_connected = slic(img, 2, compactness=0.0001,
enforce_connectivity=True,
convert2lab=False, start_label=0)
segments_disconnected = slic(img, 2, compactness=0.0001,
enforce_connectivity=False,
convert2lab=False, start_label=0)
# Make sure nothing fatal occurs (e.g. buffer overflow) at low values of
# max_size_factor
segments_connected_low_max = slic(img, 2, compactness=0.0001,
enforce_connectivity=True,
convert2lab=False,
max_size_factor=0.8,
start_label=0)
result_connected = np.array([[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1]], np.float)
result_disconnected = np.array([[0, 0, 0, 1, 1, 1],
[1, 0, 0, 1, 1, 0],
[0, 0, 0, 1, 1, 0]], np.float)
assert_equal(segments_connected, result_connected)
assert_equal(segments_disconnected, result_disconnected)
assert_equal(segments_connected_low_max, result_connected)
def test_slic_zero():
# Same as test_color_2d but with slic_zero=True
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
img += 0.01 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, n_segments=4, sigma=0, slic_zero=True, start_label=0)
# we expect 4 segments
assert_equal(len(np.unique(seg)), 4)
assert_equal(seg.shape, img.shape[:-1])
assert_equal(seg[:10, :10], 0)
assert_equal(seg[10:, :10], 2)
assert_equal(seg[:10, 10:], 1)
assert_equal(seg[10:, 10:], 3)
def test_more_segments_than_pixels():
rnd = np.random.RandomState(0)
img = np.zeros((20, 21))
img[:10, :10] = 0.33
img[10:, :10] = 0.67
img[10:, 10:] = 1.00
img += 0.0033 * rnd.normal(size=img.shape)
img[img > 1] = 1
img[img < 0] = 0
seg = slic(img, sigma=0, n_segments=500, compactness=1,
multichannel=False, convert2lab=False, start_label=0)
assert np.all(seg.ravel() == np.arange(seg.size))
def test_color_2d_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((20, 21))
msk[2:-2, 2:-2] = 1
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
img += 0.01 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, n_segments=4, sigma=0, enforce_connectivity=False,
mask=msk)
# we expect 4 segments + masked area
assert_equal(len(np.unique(seg)), 5)
assert_equal(seg.shape, img.shape[:-1])
# segments
assert_equal(seg[2:10, 2:10], 1)
assert_equal(seg[10:-2, 2:10], 4)
assert_equal(seg[2:10, 10:-2], 2)
assert_equal(seg[10:-2, 10:-2], 3)
# non masked area
assert_equal(seg[:2, :], 0)
assert_equal(seg[-2:, :], 0)
assert_equal(seg[:, :2], 0)
assert_equal(seg[:, -2:], 0)
def test_multichannel_2d_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((20, 20))
msk[2:-2, 2:-2] = 1
img = np.zeros((20, 20, 8))
img[:10, :10, 0:2] = 1
img[:10, 10:, 2:4] = 1
img[10:, :10, 4:6] = 1
img[10:, 10:, 6:8] = 1
img += 0.01 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, n_segments=4, enforce_connectivity=False,
mask=msk)
# we expect 4 segments + masked area
assert_equal(len(np.unique(seg)), 5)
assert_equal(seg.shape, img.shape[:-1])
# segments
assert_equal(seg[2:10, 2:10], 2)
assert_equal(seg[2:10, 10:-2], 1)
assert_equal(seg[10:-2, 2:10], 4)
assert_equal(seg[10:-2, 10:-2], 3)
# non masked area
assert_equal(seg[:2, :], 0)
assert_equal(seg[-2:, :], 0)
assert_equal(seg[:, :2], 0)
assert_equal(seg[:, -2:], 0)
def test_gray_2d_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((20, 21))
msk[2:-2, 2:-2] = 1
img = np.zeros((20, 21))
img[:10, :10] = 0.33
img[10:, :10] = 0.67
img[10:, 10:] = 1.00
img += 0.0033 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, sigma=0, n_segments=4, compactness=1,
multichannel=False, convert2lab=False, mask=msk)
assert_equal(len(np.unique(seg)), 5)
assert_equal(seg.shape, img.shape)
# segments
assert_equal(seg[2:10, 2:10], 1)
assert_equal(seg[2:10, 10:-2], 2)
assert_equal(seg[10:-2, 2:10], 3)
assert_equal(seg[10:-2, 10:-2], 4)
# non masked area
assert_equal(seg[:2, :], 0)
assert_equal(seg[-2:, :], 0)
assert_equal(seg[:, :2], 0)
assert_equal(seg[:, -2:], 0)
def test_list_sigma_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((2, 6))
msk[:, 1:-1] = 1
img = np.array([[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1]], np.float)
img += 0.1 * rnd.normal(size=img.shape)
result_sigma = np.array([[0, 1, 1, 2, 2, 0],
[0, 1, 1, 2, 2, 0]], np.int)
seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1],
multichannel=False, mask=msk)
assert_equal(seg_sigma, result_sigma)
def test_spacing_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((2, 5))
msk[:, 1:-1] = 1
img = np.array([[1, 1, 1, 0, 0],
[1, 1, 0, 0, 0]], np.float)
result_non_spaced = np.array([[0, 1, 1, 2, 0],
[0, 1, 2, 2, 0]], np.int)
result_spaced = np.array([[0, 1, 1, 1, 0],
[0, 2, 2, 2, 0]], np.int)
img += 0.1 * rnd.normal(size=img.shape)
seg_non_spaced = slic(img, n_segments=2, sigma=0, multichannel=False,
compactness=1.0, mask=msk)
seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 50, 1],
compactness=1.0, multichannel=False, mask=msk)
assert_equal(seg_non_spaced, result_non_spaced)
assert_equal(seg_spaced, result_spaced)
def test_enforce_connectivity_mask():
msk = np.zeros((3, 6))
msk[:, 1:-1] = 1
img = np.array([[0, 0, 0, 1, 1, 1],
[1, 0, 0, 1, 1, 0],
[0, 0, 0, 1, 1, 0]], np.float)
segments_connected = slic(img, 2, compactness=0.0001,
enforce_connectivity=True,
convert2lab=False, mask=msk)
segments_disconnected = slic(img, 2, compactness=0.0001,
enforce_connectivity=False,
convert2lab=False, mask=msk)
# Make sure nothing fatal occurs (e.g. buffer overflow) at low values of
# max_size_factor
segments_connected_low_max = slic(img, 2, compactness=0.0001,
enforce_connectivity=True,
convert2lab=False,
max_size_factor=0.8, mask=msk)
result_connected = np.array([[0, 1, 1, 2, 2, 0],
[0, 1, 1, 2, 2, 0],
[0, 1, 1, 2, 2, 0]], np.float)
result_disconnected = np.array([[0, 1, 1, 2, 2, 0],
[0, 1, 1, 2, 2, 0],
[0, 1, 1, 2, 2, 0]], np.float)
assert_equal(segments_connected, result_connected)
assert_equal(segments_disconnected, result_disconnected)
assert_equal(segments_connected_low_max, result_connected)
def test_slic_zero_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((20, 21))
msk[2:-2, 2:-2] = 1
img = np.zeros((20, 21, 3))
img[:10, :10, 0] = 1
img[10:, :10, 1] = 1
img[10:, 10:, 2] = 1
img += 0.01 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, n_segments=4, sigma=0, slic_zero=True,
mask=msk)
# we expect 4 segments + masked area
assert_equal(len(np.unique(seg)), 5)
assert_equal(seg.shape, img.shape[:-1])
# segments
assert_equal(seg[2:10, 2:10], 1)
assert_equal(seg[2:10, 10:-2], 2)
assert_equal(seg[10:-2, 2:10], 3)
assert_equal(seg[10:-2, 10:-2], 4)
# non masked area
assert_equal(seg[:2, :], 0)
assert_equal(seg[-2:, :], 0)
assert_equal(seg[:, :2], 0)
assert_equal(seg[:, -2:], 0)
def test_more_segments_than_pixels_mask():
rnd = np.random.RandomState(0)
msk = np.zeros((20, 21))
msk[2:-2, 2:-2] = 1
img = np.zeros((20, 21))
img[:10, :10] = 0.33
img[10:, :10] = 0.67
img[10:, 10:] = 1.00
img += 0.0033 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, sigma=0, n_segments=500, compactness=1,
multichannel=False, convert2lab=False, mask=msk)
expected = np.arange(seg[2:-2, 2:-2].size) + 1
assert np.all(seg[2:-2, 2:-2].ravel() == expected)
def test_color_3d_mask():
msk = np.zeros((20, 21, 22))
msk[2:-2, 2:-2, 2:-2] = 1
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 22, 3))
slices = []
for dim_size in msk.shape:
midpoint = dim_size // 2
slices.append((slice(None, midpoint), slice(midpoint, None)))
slices = list(product(*slices))
colors = list(product(*(([0, 1],) * 3)))
for s, c in zip(slices, colors):
img[s] = c
img += 0.01 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, sigma=0, n_segments=8, mask=msk)
# we expect 8 segments + masked area
assert_equal(len(np.unique(seg)), 9)
for s, c in zip(slices, range(1, 9)):
assert_equal(seg[s][2:-2, 2:-2, 2:-2], c)
def test_gray_3d_mask():
msk = np.zeros((20, 21, 22))
msk[2:-2, 2:-2, 2:-2] = 1
rnd = np.random.RandomState(0)
img = np.zeros((20, 21, 22))
slices = []
for dim_size in img.shape:
midpoint = dim_size // 2
slices.append((slice(None, midpoint), slice(midpoint, None)))
slices = list(product(*slices))
shades = np.linspace(0, 1, 8)
for s, sh in zip(slices, shades):
img[s] = sh
img += 0.001 * rnd.normal(size=img.shape)
np.clip(img, 0, 1, out=img)
seg = slic(img, sigma=0, n_segments=8, multichannel=False,
convert2lab=False, mask=msk)
# we expect 8 segments + masked area
assert_equal(len(np.unique(seg)), 9)
for s, c in zip(slices, range(1, 9)):
assert_equal(seg[s][2:-2, 2:-2, 2:-2], c)
@pytest.mark.parametrize("dtype", ['float32', 'float64', 'uint8', 'int'])
def test_dtype_support(dtype):
img = np.random.rand(28, 28).astype(dtype)
# Simply run the function to assert that it runs without error
slic(img, start_label=1)

View file

@ -0,0 +1,498 @@
"""test_watershed.py - tests the watershed function
Originally part of CellProfiler, code licensed under both GPL and BSD licenses.
Website: http://www.cellprofiler.org
Copyright (c) 2003-2009 Massachusetts Institute of Technology
Copyright (c) 2009-2011 Broad Institute
All rights reserved.
Original author: Lee Kamentsky
"""
#Portions of this test were taken from scipy's watershed test in test_ndimage.py
#
# Copyright (C) 2003-2005 Peter J. Verveer
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# 3. The name of the author may not be used to endorse or promote
# products derived from this software without specific prior
# written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import math
import unittest
import pytest
import numpy as np
from scipy import ndimage as ndi
from .._watershed import watershed
from skimage.measure import label
eps = 1e-12
blob = np.array([[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 204, 204, 204, 204, 204, 204, 255, 255, 255, 255, 255],
[255, 255, 255, 204, 204, 183, 153, 153, 153, 153, 183, 204, 204, 255, 255, 255],
[255, 255, 204, 183, 153, 141, 111, 103, 103, 111, 141, 153, 183, 204, 255, 255],
[255, 255, 204, 153, 111, 94, 72, 52, 52, 72, 94, 111, 153, 204, 255, 255],
[255, 255, 204, 153, 111, 72, 39, 1, 1, 39, 72, 111, 153, 204, 255, 255],
[255, 255, 204, 183, 141, 111, 72, 39, 39, 72, 111, 141, 183, 204, 255, 255],
[255, 255, 255, 204, 183, 141, 111, 72, 72, 111, 141, 183, 204, 255, 255, 255],
[255, 255, 255, 255, 204, 183, 141, 94, 94, 141, 183, 204, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 204, 153, 103, 103, 153, 204, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 204, 183, 141, 94, 94, 141, 183, 204, 255, 255, 255, 255],
[255, 255, 255, 204, 183, 141, 111, 72, 72, 111, 141, 183, 204, 255, 255, 255],
[255, 255, 204, 183, 141, 111, 72, 39, 39, 72, 111, 141, 183, 204, 255, 255],
[255, 255, 204, 153, 111, 72, 39, 1, 1, 39, 72, 111, 153, 204, 255, 255],
[255, 255, 204, 153, 111, 94, 72, 52, 52, 72, 94, 111, 153, 204, 255, 255],
[255, 255, 204, 183, 153, 141, 111, 103, 103, 111, 141, 153, 183, 204, 255, 255],
[255, 255, 255, 204, 204, 183, 153, 153, 153, 153, 183, 204, 204, 255, 255, 255],
[255, 255, 255, 255, 255, 204, 204, 204, 204, 204, 204, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]])
def diff(a, b):
if not isinstance(a, np.ndarray):
a = np.asarray(a)
if not isinstance(b, np.ndarray):
b = np.asarray(b)
if (0 in a.shape) and (0 in b.shape):
return 0.0
b[a == 0] = 0
if (a.dtype in [np.complex64, np.complex128] or
b.dtype in [np.complex64, np.complex128]):
a = np.asarray(a, np.complex128)
b = np.asarray(b, np.complex128)
t = ((a.real - b.real)**2).sum() + ((a.imag - b.imag)**2).sum()
else:
a = np.asarray(a)
a = a.astype(np.float64)
b = np.asarray(b)
b = b.astype(np.float64)
t = ((a - b)**2).sum()
return math.sqrt(t)
class TestWatershed(unittest.TestCase):
eight = np.ones((3, 3), bool)
def test_watershed01(self):
"watershed 1"
data = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[ -1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0]],
np.int8)
out = watershed(data, markers, self.eight)
expected = np.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, -1],
[-1, -1, -1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1, -1, -1]])
error = diff(expected, out)
assert error < eps
def test_watershed02(self):
"watershed 2"
data = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[-1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.int8)
out = watershed(data, markers)
error = diff([[-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, -1],
[-1, -1, 1, 1, 1, -1, -1],
[-1, -1, -1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1, -1, -1]], out)
self.assertTrue(error < eps)
def test_watershed03(self):
"watershed 3"
data = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 2, 0, 3, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, -1]], np.int8)
out = watershed(data, markers)
error = diff([[-1, -1, -1, -1, -1, -1, -1],
[-1, 0, 2, 0, 3, 0, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 0, 2, 0, 3, 0, -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]], out)
self.assertTrue(error < eps)
def test_watershed04(self):
"watershed 4"
data = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 2, 0, 3, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, -1]], np.int8)
out = watershed(data, markers, self.eight)
error = diff([[-1, -1, -1, -1, -1, -1, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -1],
[-1, 2, 2, 0, 3, 3, -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]], out)
self.assertTrue(error < eps)
def test_watershed05(self):
"watershed 5"
data = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 3, 0, 2, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, -1]], np.int8)
out = watershed(data, markers, self.eight)
error = diff([[-1, -1, -1, -1, -1, -1, -1],
[-1, 3, 3, 0, 2, 2, -1],
[-1, 3, 3, 0, 2, 2, -1],
[-1, 3, 3, 0, 2, 2, -1],
[-1, 3, 3, 0, 2, 2, -1],
[-1, 3, 3, 0, 2, 2, -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]], out)
self.assertTrue(error < eps)
def test_watershed06(self):
"watershed 6"
data = np.array([[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], np.uint8)
markers = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]], np.int8)
out = watershed(data, markers, self.eight)
error = diff([[-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, -1],
[-1, -1, -1, -1, -1, -1, -1]], out)
self.assertTrue(error < eps)
def test_watershed07(self):
"A regression test of a competitive case that failed"
data = blob
mask = (data != 255)
markers = np.zeros(data.shape, int)
markers[6, 7] = 1
markers[14, 7] = 2
out = watershed(data, markers, self.eight, mask=mask)
#
# The two objects should be the same size, except possibly for the
# border region
#
size1 = np.sum(out == 1)
size2 = np.sum(out == 2)
self.assertTrue(abs(size1 - size2) <= 6)
def test_watershed08(self):
"The border pixels + an edge are all the same value"
data = blob.copy()
data[10, 7:9] = 141
mask = (data != 255)
markers = np.zeros(data.shape, int)
markers[6, 7] = 1
markers[14, 7] = 2
out = watershed(data, markers, self.eight, mask=mask)
#
# The two objects should be the same size, except possibly for the
# border region
#
size1 = np.sum(out == 1)
size2 = np.sum(out == 2)
self.assertTrue(abs(size1 - size2) <= 6)
def test_watershed09(self):
"""Test on an image of reasonable size
This is here both for timing (does it take forever?) and to
ensure that the memory constraints are reasonable
"""
image = np.zeros((1000, 1000))
coords = np.random.uniform(0, 1000, (100, 2)).astype(int)
markers = np.zeros((1000, 1000), int)
idx = 1
for x, y in coords:
image[x, y] = 1
markers[x, y] = idx
idx += 1
image = ndi.gaussian_filter(image, 4)
watershed(image, markers, self.eight)
ndi.watershed_ift(image.astype(np.uint16), markers, self.eight)
def test_watershed10(self):
"watershed 10"
data = np.array([[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]], np.uint8)
markers = np.array([[1, 0, 0, 2],
[0, 0, 0, 0],
[0, 0, 0, 0],
[3, 0, 0, 4]], np.int8)
out = watershed(data, markers, self.eight)
error = diff([[1, 1, 2, 2],
[1, 1, 2, 2],
[3, 3, 4, 4],
[3, 3, 4, 4]], out)
self.assertTrue(error < eps)
def test_watershed11(self):
'''Make sure that all points on this plateau are assigned to closest seed'''
# https://github.com/scikit-image/scikit-image/issues/803
#
# Make sure that no point in a level image is farther away
# from its seed than any other
#
image = np.zeros((21, 21))
markers = np.zeros((21, 21), int)
markers[5, 5] = 1
markers[5, 10] = 2
markers[10, 5] = 3
markers[10, 10] = 4
structure = np.array([[False, True, False],
[True, True, True],
[False, True, False]])
out = watershed(image, markers, structure)
i, j = np.mgrid[0:21, 0:21]
d = np.dstack(
[np.sqrt((i.astype(float)-i0)**2, (j.astype(float)-j0)**2)
for i0, j0 in ((5, 5), (5, 10), (10, 5), (10, 10))])
dmin = np.min(d, 2)
self.assertTrue(np.all(d[i, j, out[i, j]-1] == dmin))
def test_watershed12(self):
"The watershed line"
data = np.array([[203, 255, 203, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153],
[203, 255, 203, 153, 153, 153, 102, 102, 102, 102, 102, 102, 153, 153, 153, 153],
[203, 255, 203, 203, 153, 153, 102, 102, 77, 0, 102, 102, 153, 153, 203, 203],
[203, 255, 255, 203, 153, 153, 153, 102, 102, 102, 102, 153, 153, 203, 203, 255],
[203, 203, 255, 203, 203, 203, 153, 153, 153, 153, 153, 153, 203, 203, 255, 255],
[153, 203, 255, 255, 255, 203, 203, 203, 203, 203, 203, 203, 203, 255, 255, 203],
[153, 203, 203, 203, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 203, 203],
[153, 153, 153, 203, 203, 203, 203, 203, 255, 203, 203, 203, 203, 203, 203, 153],
[102, 102, 153, 153, 153, 153, 203, 203, 255, 203, 203, 255, 203, 153, 153, 153],
[102, 102, 102, 102, 102, 153, 203, 255, 255, 203, 203, 203, 203, 153, 102, 153],
[102, 51, 51, 102, 102, 153, 203, 255, 203, 203, 153, 153, 153, 153, 102, 153],
[ 77, 51, 51, 102, 153, 153, 203, 255, 203, 203, 203, 153, 102, 102, 102, 153],
[ 77, 0, 51, 102, 153, 203, 203, 255, 203, 255, 203, 153, 102, 51, 102, 153],
[ 77, 0, 51, 102, 153, 203, 255, 255, 203, 203, 203, 153, 102, 0, 102, 153],
[102, 0, 51, 102, 153, 203, 255, 203, 203, 153, 153, 153, 102, 102, 102, 153],
[102, 102, 102, 102, 153, 203, 255, 203, 153, 153, 153, 153, 153, 153, 153, 153]])
markerbin = (data==0)
marker = label(markerbin)
ws = watershed(data, marker, connectivity=2, watershed_line=True)
for lab, area in zip(range(4), [34,74,74,74]):
self.assertTrue(np.sum(ws == lab) == area)
def test_compact_watershed():
image = np.zeros((5, 6))
image[:, 3:] = 1
seeds = np.zeros((5, 6), dtype=int)
seeds[2, 0] = 1
seeds[2, 3] = 2
compact = watershed(image, seeds, compactness=0.01)
expected = np.array([[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2]], dtype=int)
np.testing.assert_equal(compact, expected)
normal = watershed(image, seeds)
expected = np.ones(image.shape, dtype=int)
expected[2, 3:] = 2
np.testing.assert_equal(normal, expected)
def test_numeric_seed_watershed():
"""Test that passing just the number of seeds to watershed works."""
image = np.zeros((5, 6))
image[:, 3:] = 1
compact = watershed(image, 2, compactness=0.01)
expected = np.array([[1, 1, 1, 1, 2, 2],
[1, 1, 1, 1, 2, 2],
[1, 1, 1, 1, 2, 2],
[1, 1, 1, 1, 2, 2],
[1, 1, 1, 1, 2, 2]], dtype=np.int32)
np.testing.assert_equal(compact, expected)
def test_incorrect_markers_shape():
with pytest.raises(ValueError):
image = np.ones((5, 6))
markers = np.ones((5, 7))
output = watershed(image, markers)
def test_incorrect_mask_shape():
with pytest.raises(ValueError):
image = np.ones((5, 6))
mask = np.ones((5, 7))
output = watershed(image, markers=4, mask=mask)
def test_markers_in_mask():
data = blob
mask = (data != 255)
out = watershed(data, 25, connectivity=2, mask=mask)
# There should be no markers where the mask is false
assert np.all(out[~mask] == 0)
def test_no_markers():
data = blob
mask = (data != 255)
out = watershed(data, mask=mask)
assert np.max(out) == 2
if __name__ == "__main__":
np.testing.run_module_suite()