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,60 @@
from .binary import (binary_erosion, binary_dilation, binary_opening,
binary_closing)
from .grey import (erosion, dilation, opening, closing, white_tophat,
black_tophat)
from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball,
octagon, star)
from ..measure._label import label
from ._skeletonize import skeletonize, medial_axis, thin, skeletonize_3d
from .convex_hull import convex_hull_image, convex_hull_object
from .greyreconstruct import reconstruction
from .misc import remove_small_objects, remove_small_holes
from .extrema import h_minima, h_maxima, local_maxima, local_minima
from ._flood_fill import flood, flood_fill
from .max_tree import (max_tree, area_opening, area_closing,
diameter_opening, diameter_closing,
max_tree_local_maxima)
from ._deprecated import watershed
__all__ = ['binary_erosion',
'binary_dilation',
'binary_opening',
'binary_closing',
'erosion',
'dilation',
'opening',
'closing',
'white_tophat',
'black_tophat',
'square',
'rectangle',
'diamond',
'disk',
'cube',
'octahedron',
'ball',
'octagon',
'star',
'label',
'watershed',
'skeletonize',
'skeletonize_3d',
'thin',
'medial_axis',
'convex_hull_image',
'convex_hull_object',
'reconstruction',
'remove_small_objects',
'remove_small_holes',
'h_minima',
'h_maxima',
'local_maxima',
'local_minima',
'flood',
'flood_fill',
'max_tree',
'area_opening',
'area_closing',
'diameter_opening',
'diameter_closing',
'max_tree_local_maxima']

View file

@ -0,0 +1,111 @@
from .._shared.utils import deprecated
@deprecated('skimage.segmentation.watershed', removed_version='0.19')
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:
>>> import numpy as np
>>> 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) # doctest: +SKIP
The algorithm works also for 3-D images, and can be used for example to
separate overlapping spheres.
"""
from ..segmentation import watershed as _watershed
return _watershed(image, markers, connectivity, offset, mask,
compactness, watershed_line)

View file

@ -0,0 +1,289 @@
"""flood_fill.py - in place flood fill algorithm
This module provides a function to fill all equal (or within tolerance) values
connected to a given seed point with a different value.
"""
import numpy as np
from warnings import warn
from ._util import (_resolve_neighborhood, _set_border_values,
_fast_pad, _offsets_to_raveled_neighbors)
from ._flood_fill_cy import _flood_fill_equal, _flood_fill_tolerance
def flood_fill(image, seed_point, new_value, *, selem=None, connectivity=None,
tolerance=None, in_place=False, inplace=None):
"""Perform flood filling on an image.
Starting at a specific `seed_point`, connected points equal or within
`tolerance` of the seed value are found, then set to `new_value`.
Parameters
----------
image : ndarray
An n-dimensional array.
seed_point : tuple or int
The point in `image` used as the starting point for the flood fill. If
the image is 1D, this point may be given as an integer.
new_value : `image` type
New value to set the entire fill. This must be chosen in agreement
with the dtype of `image`.
selem : ndarray, optional
A structuring element used to determine the neighborhood of each
evaluated pixel. It must contain only 1's and 0's, have the same number
of dimensions as `image`. If not given, all adjacent pixels are
considered as part of the neighborhood (fully connected).
connectivity : int, optional
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is less than or
equal to `connectivity` are considered neighbors. Ignored if `selem` is
not None.
tolerance : float or int, optional
If None (default), adjacent values must be strictly equal to the
value of `image` at `seed_point` to be filled. This is fastest.
If a tolerance is provided, adjacent points with values within plus or
minus tolerance from the seed point are filled (inclusive).
in_place : bool, optional
If True, flood filling is applied to `image` in place. If False, the
flood filled result is returned without modifying the input `image`
(default).
inplace : bool, optional
This parameter is deprecated and will be removed in version 0.19.0
in favor of in_place. If True, flood filling is applied to `image`
inplace. If False, the flood filled result is returned without
modifying the input `image` (default).
Returns
-------
filled : ndarray
An array with the same shape as `image` is returned, with values in
areas connected to and equal (or within tolerance of) the seed point
replaced with `new_value`.
Notes
-----
The conceptual analogy of this operation is the 'paint bucket' tool in many
raster graphics programs.
Examples
--------
>>> from skimage.morphology import flood_fill
>>> image = np.zeros((4, 7), dtype=int)
>>> image[1:3, 1:3] = 1
>>> image[3, 0] = 1
>>> image[1:3, 4:6] = 2
>>> image[3, 6] = 3
>>> image
array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3]])
Fill connected ones with 5, with full connectivity (diagonals included):
>>> flood_fill(image, (1, 1), 5)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 5, 5, 0, 2, 2, 0],
[0, 5, 5, 0, 2, 2, 0],
[5, 0, 0, 0, 0, 0, 3]])
Fill connected ones with 5, excluding diagonal points (connectivity 1):
>>> flood_fill(image, (1, 1), 5, connectivity=1)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 5, 5, 0, 2, 2, 0],
[0, 5, 5, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3]])
Fill with a tolerance:
>>> flood_fill(image, (0, 0), 5, tolerance=1)
array([[5, 5, 5, 5, 5, 5, 5],
[5, 5, 5, 5, 2, 2, 5],
[5, 5, 5, 5, 2, 2, 5],
[5, 5, 5, 5, 5, 5, 3]])
"""
if inplace is not None:
warn('The `inplace` parameter is depreciated and will be removed '
'in version 0.19.0. Use `in_place` instead.',
stacklevel=2,
category=FutureWarning)
in_place = inplace
mask = flood(image, seed_point, selem=selem, connectivity=connectivity,
tolerance=tolerance)
if not in_place:
image = image.copy()
image[mask] = new_value
return image
def flood(image, seed_point, *, selem=None, connectivity=None, tolerance=None):
"""Mask corresponding to a flood fill.
Starting at a specific `seed_point`, connected points equal or within
`tolerance` of the seed value are found.
Parameters
----------
image : ndarray
An n-dimensional array.
seed_point : tuple or int
The point in `image` used as the starting point for the flood fill. If
the image is 1D, this point may be given as an integer.
selem : ndarray, optional
A structuring element used to determine the neighborhood of each
evaluated pixel. It must contain only 1's and 0's, have the same number
of dimensions as `image`. If not given, all adjacent pixels are
considered as part of the neighborhood (fully connected).
connectivity : int, optional
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is larger or
equal to `connectivity` are considered neighbors. Ignored if
`selem` is not None.
tolerance : float or int, optional
If None (default), adjacent values must be strictly equal to the
initial value of `image` at `seed_point`. This is fastest. If a value
is given, a comparison will be done at every point and if within
tolerance of the initial value will also be filled (inclusive).
Returns
-------
mask : ndarray
A Boolean array with the same shape as `image` is returned, with True
values for areas connected to and equal (or within tolerance of) the
seed point. All other values are False.
Notes
-----
The conceptual analogy of this operation is the 'paint bucket' tool in many
raster graphics programs. This function returns just the mask
representing the fill.
If indices are desired rather than masks for memory reasons, the user can
simply run `numpy.nonzero` on the result, save the indices, and discard
this mask.
Examples
--------
>>> from skimage.morphology import flood
>>> image = np.zeros((4, 7), dtype=int)
>>> image[1:3, 1:3] = 1
>>> image[3, 0] = 1
>>> image[1:3, 4:6] = 2
>>> image[3, 6] = 3
>>> image
array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3]])
Fill connected ones with 5, with full connectivity (diagonals included):
>>> mask = flood(image, (1, 1))
>>> image_flooded = image.copy()
>>> image_flooded[mask] = 5
>>> image_flooded
array([[0, 0, 0, 0, 0, 0, 0],
[0, 5, 5, 0, 2, 2, 0],
[0, 5, 5, 0, 2, 2, 0],
[5, 0, 0, 0, 0, 0, 3]])
Fill connected ones with 5, excluding diagonal points (connectivity 1):
>>> mask = flood(image, (1, 1), connectivity=1)
>>> image_flooded = image.copy()
>>> image_flooded[mask] = 5
>>> image_flooded
array([[0, 0, 0, 0, 0, 0, 0],
[0, 5, 5, 0, 2, 2, 0],
[0, 5, 5, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3]])
Fill with a tolerance:
>>> mask = flood(image, (0, 0), tolerance=1)
>>> image_flooded = image.copy()
>>> image_flooded[mask] = 5
>>> image_flooded
array([[5, 5, 5, 5, 5, 5, 5],
[5, 5, 5, 5, 2, 2, 5],
[5, 5, 5, 5, 2, 2, 5],
[5, 5, 5, 5, 5, 5, 3]])
"""
# Correct start point in ravelled image - only copy if non-contiguous
image = np.asarray(image)
if image.flags.f_contiguous is True:
order = 'F'
elif image.flags.c_contiguous is True:
order = 'C'
else:
image = np.ascontiguousarray(image)
order = 'C'
seed_value = image[seed_point]
# Shortcut for rank zero
if 0 in image.shape:
return np.zeros(image.shape, dtype=np.bool)
# Convenience for 1d input
try:
iter(seed_point)
except TypeError:
seed_point = (seed_point,)
selem = _resolve_neighborhood(selem, connectivity, image.ndim)
# Must annotate borders
working_image = _fast_pad(image, image.min(), order=order)
# Stride-aware neighbors - works for both C- and Fortran-contiguity
ravelled_seed_idx = np.ravel_multi_index([i+1 for i in seed_point],
working_image.shape, order=order)
neighbor_offsets = _offsets_to_raveled_neighbors(
working_image.shape, selem, center=((1,) * image.ndim), order=order)
# Use a set of flags; see _flood_fill_cy.pyx for meanings
flags = np.zeros(working_image.shape, dtype=np.uint8, order=order)
_set_border_values(flags, value=2)
try:
if tolerance is not None:
# Check if tolerance could create overflow problems
try:
max_value = np.finfo(working_image.dtype).max
min_value = np.finfo(working_image.dtype).min
except ValueError:
max_value = np.iinfo(working_image.dtype).max
min_value = np.iinfo(working_image.dtype).min
high_tol = min(max_value, seed_value + tolerance)
low_tol = max(min_value, seed_value - tolerance)
_flood_fill_tolerance(working_image.ravel(order),
flags.ravel(order),
neighbor_offsets,
ravelled_seed_idx,
seed_value,
low_tol,
high_tol)
else:
_flood_fill_equal(working_image.ravel(order),
flags.ravel(order),
neighbor_offsets,
ravelled_seed_idx,
seed_value)
except TypeError:
if working_image.dtype == np.float16:
# Provide the user with clearer error message
raise TypeError("dtype of `image` is float16 which is not "
"supported, try upcasting to float32")
else:
raise
# Output what the user requested; view does not create a new copy.
return flags[(slice(1, -1),) * image.ndim].view(np.bool)

View file

@ -0,0 +1,647 @@
"""
Algorithms for computing the skeleton of a binary image
"""
import numpy as np
from ..util import img_as_ubyte, crop
from scipy import ndimage as ndi
from .._shared.utils import check_nD, warn
from ._skeletonize_cy import (_fast_skeletonize, _skeletonize_loop,
_table_lookup_index)
from ._skeletonize_3d_cy import _compute_thin_image
def skeletonize(image, *, method=None):
"""Compute the skeleton of a binary image.
Thinning is used to reduce each connected component in a binary image
to a single-pixel wide skeleton.
Parameters
----------
image : ndarray, 2D or 3D
A binary image containing the objects to be skeletonized. Zeros
represent background, nonzero values are foreground.
method : {'zhang', 'lee'}, optional
Which algorithm to use. Zhang's algorithm [Zha84]_ only works for
2D images, and is the default for 2D. Lee's algorithm [Lee94]_
works for 2D or 3D images and is the default for 3D.
Returns
-------
skeleton : ndarray
The thinned image.
See also
--------
medial_axis
References
----------
.. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models
via 3-D medial surface/axis thinning algorithms.
Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994.
.. [Zha84] A fast parallel algorithm for thinning digital patterns,
T. Y. Zhang and C. Y. Suen, Communications of the ACM,
March 1984, Volume 27, Number 3.
Examples
--------
>>> X, Y = np.ogrid[0:9, 0:9]
>>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(np.uint8)
>>> ellipse
array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
>>> skel = skeletonize(ellipse)
>>> skel.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, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 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]], dtype=uint8)
"""
if image.ndim == 2 and (method is None or method == 'zhang'):
skeleton = skeletonize_2d(image)
elif image.ndim == 3 and method == 'zhang':
raise ValueError('skeletonize method "zhang" only works for 2D '
'images.')
elif image.ndim == 3 or (image.ndim == 2 and method == 'lee'):
skeleton = skeletonize_3d(image)
else:
raise ValueError('skeletonize requires a 2D or 3D image as input, '
'got {}D.'.format(image.ndim))
return skeleton
def skeletonize_2d(image):
"""Return the skeleton of a 2D binary image.
Thinning is used to reduce each connected component in a binary image
to a single-pixel wide skeleton.
Parameters
----------
image : numpy.ndarray
A binary image containing the objects to be skeletonized. '1'
represents foreground, and '0' represents background. It
also accepts arrays of boolean values where True is foreground.
Returns
-------
skeleton : ndarray
A matrix containing the thinned image.
See also
--------
medial_axis
Notes
-----
The algorithm [Zha84]_ works by making successive passes of the image,
removing pixels on object borders. This continues until no
more pixels can be removed. The image is correlated with a
mask that assigns each pixel a number in the range [0...255]
corresponding to each possible pattern of its 8 neighbouring
pixels. A look up table is then used to assign the pixels a
value of 0, 1, 2 or 3, which are selectively removed during
the iterations.
Note that this algorithm will give different results than a
medial axis transform, which is also often referred to as
"skeletonization".
References
----------
.. [Zha84] A fast parallel algorithm for thinning digital patterns,
T. Y. Zhang and C. Y. Suen, Communications of the ACM,
March 1984, Volume 27, Number 3.
Examples
--------
>>> X, Y = np.ogrid[0:9, 0:9]
>>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(np.uint8)
>>> ellipse
array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
>>> skel = skeletonize(ellipse)
>>> skel.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, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 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]], dtype=uint8)
"""
# convert to unsigned int (this should work for boolean values)
image = image.astype(np.uint8)
# check some properties of the input image:
# - 2D
# - binary image with only 0's and 1's
if image.ndim != 2:
raise ValueError('Skeletonize requires a 2D array')
if not np.all(np.in1d(image.flat, (0, 1))):
raise ValueError('Image contains values other than 0 and 1')
return _fast_skeletonize(image)
# --------- Skeletonization and thinning based on Guo and Hall 1989 ---------
def _generate_thin_luts():
"""generate LUTs for thinning algorithm (for reference)"""
def nabe(n):
return np.array([n >> i & 1 for i in range(0, 9)]).astype(np.bool)
def G1(n):
s = 0
bits = nabe(n)
for i in (0, 2, 4, 6):
if not(bits[i]) and (bits[i + 1] or bits[(i + 2) % 8]):
s += 1
return s == 1
g1_lut = np.array([G1(n) for n in range(256)])
def G2(n):
n1, n2 = 0, 0
bits = nabe(n)
for k in (1, 3, 5, 7):
if bits[k] or bits[k - 1]:
n1 += 1
if bits[k] or bits[(k + 1) % 8]:
n2 += 1
return min(n1, n2) in [2, 3]
g2_lut = np.array([G2(n) for n in range(256)])
g12_lut = g1_lut & g2_lut
def G3(n):
bits = nabe(n)
return not((bits[1] or bits[2] or not(bits[7])) and bits[0])
def G3p(n):
bits = nabe(n)
return not((bits[5] or bits[6] or not(bits[3])) and bits[4])
g3_lut = np.array([G3(n) for n in range(256)])
g3p_lut = np.array([G3p(n) for n in range(256)])
g123_lut = g12_lut & g3_lut
g123p_lut = g12_lut & g3p_lut
return g123_lut, g123p_lut
G123_LUT = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 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, 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, 1,
1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0,
0, 1, 1, 0, 0, 1, 0, 0, 0], dtype=np.bool)
G123P_LUT = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0,
0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 1, 0, 1, 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, 1, 0,
1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 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, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1,
0, 1, 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], dtype=np.bool)
def thin(image, max_iter=None):
"""
Perform morphological thinning of a binary image.
Parameters
----------
image : binary (M, N) ndarray
The image to be thinned.
max_iter : int, number of iterations, optional
Regardless of the value of this parameter, the thinned image
is returned immediately if an iteration produces no change.
If this parameter is specified it thus sets an upper bound on
the number of iterations performed.
Returns
-------
out : ndarray of bool
Thinned image.
See also
--------
skeletonize, medial_axis
Notes
-----
This algorithm [1]_ works by making multiple passes over the image,
removing pixels matching a set of criteria designed to thin
connected regions while preserving eight-connected components and
2 x 2 squares [2]_. In each of the two sub-iterations the algorithm
correlates the intermediate skeleton image with a neighborhood mask,
then looks up each neighborhood in a lookup table indicating whether
the central pixel should be deleted in that sub-iteration.
References
----------
.. [1] Z. Guo and R. W. Hall, "Parallel thinning with
two-subiteration algorithms," Comm. ACM, vol. 32, no. 3,
pp. 359-373, 1989. :DOI:`10.1145/62065.62074`
.. [2] Lam, L., Seong-Whan Lee, and Ching Y. Suen, "Thinning
Methodologies-A Comprehensive Survey," IEEE Transactions on
Pattern Analysis and Machine Intelligence, Vol 14, No. 9,
p. 879, 1992. :DOI:`10.1109/34.161346`
Examples
--------
>>> square = np.zeros((7, 7), dtype=np.uint8)
>>> square[1:-1, 2:-2] = 1
>>> square[0, 1] = 1
>>> square
array([[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> skel = thin(square)
>>> skel.astype(np.uint8)
array([[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
# check that image is 2d
check_nD(image, 2)
# convert image to uint8 with values in {0, 1}
skel = np.asanyarray(image, dtype=bool).astype(np.uint8)
# neighborhood mask
mask = np.array([[ 8, 4, 2],
[16, 0, 1],
[32, 64, 128]], dtype=np.uint8)
# iterate until convergence, up to the iteration limit
max_iter = max_iter or np.inf
n_iter = 0
n_pts_old, n_pts_new = np.inf, np.sum(skel)
while n_pts_old != n_pts_new and n_iter < max_iter:
n_pts_old = n_pts_new
# perform the two "subiterations" described in the paper
for lut in [G123_LUT, G123P_LUT]:
# correlate image with neighborhood mask
N = ndi.correlate(skel, mask, mode='constant')
# take deletion decision from this subiteration's LUT
D = np.take(lut, N)
# perform deletion
skel[D] = 0
n_pts_new = np.sum(skel) # count points after thinning
n_iter += 1
return skel.astype(np.bool)
# --------- Skeletonization by medial axis transform --------
_eight_connect = ndi.generate_binary_structure(2, 2)
def medial_axis(image, mask=None, return_distance=False):
"""
Compute the medial axis transform of a binary image
Parameters
----------
image : binary ndarray, shape (M, N)
The image of the shape to be skeletonized.
mask : binary ndarray, shape (M, N), optional
If a mask is given, only those elements in `image` with a true
value in `mask` are used for computing the medial axis.
return_distance : bool, optional
If true, the distance transform is returned as well as the skeleton.
Returns
-------
out : ndarray of bools
Medial axis transform of the image
dist : ndarray of ints, optional
Distance transform of the image (only returned if `return_distance`
is True)
See also
--------
skeletonize
Notes
-----
This algorithm computes the medial axis transform of an image
as the ridges of its distance transform.
The different steps of the algorithm are as follows
* A lookup table is used, that assigns 0 or 1 to each configuration of
the 3x3 binary square, whether the central pixel should be removed
or kept. We want a point to be removed if it has more than one neighbor
and if removing it does not change the number of connected components.
* The distance transform to the background is computed, as well as
the cornerness of the pixel.
* The foreground (value of 1) points are ordered by
the distance transform, then the cornerness.
* A cython function is called to reduce the image to its skeleton. It
processes pixels in the order determined at the previous step, and
removes or maintains a pixel according to the lookup table. Because
of the ordering, it is possible to process all pixels in only one
pass.
Examples
--------
>>> square = np.zeros((7, 7), dtype=np.uint8)
>>> square[1:-1, 2:-2] = 1
>>> square
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> medial_axis(square).astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
global _eight_connect
if mask is None:
masked_image = image.astype(np.bool)
else:
masked_image = image.astype(bool).copy()
masked_image[~mask] = False
#
# Build lookup table - three conditions
# 1. Keep only positive pixels (center_is_foreground array).
# AND
# 2. Keep if removing the pixel results in a different connectivity
# (if the number of connected components is different with and
# without the central pixel)
# OR
# 3. Keep if # pixels in neighbourhood is 2 or less
# Note that table is independent of image
center_is_foreground = (np.arange(512) & 2**4).astype(bool)
table = (center_is_foreground # condition 1.
&
(np.array([ndi.label(_pattern_of(index), _eight_connect)[1] !=
ndi.label(_pattern_of(index & ~ 2**4),
_eight_connect)[1]
for index in range(512)]) # condition 2
|
np.array([np.sum(_pattern_of(index)) < 3 for index in range(512)]))
# condition 3
)
# Build distance transform
distance = ndi.distance_transform_edt(masked_image)
if return_distance:
store_distance = distance.copy()
# Corners
# The processing order along the edge is critical to the shape of the
# resulting skeleton: if you process a corner first, that corner will
# be eroded and the skeleton will miss the arm from that corner. Pixels
# with fewer neighbors are more "cornery" and should be processed last.
# We use a cornerness_table lookup table where the score of a
# configuration is the number of background (0-value) pixels in the
# 3x3 neighbourhood
cornerness_table = np.array([9 - np.sum(_pattern_of(index))
for index in range(512)])
corner_score = _table_lookup(masked_image, cornerness_table)
# Define arrays for inner loop
i, j = np.mgrid[0:image.shape[0], 0:image.shape[1]]
result = masked_image.copy()
distance = distance[result]
i = np.ascontiguousarray(i[result], dtype=np.intp)
j = np.ascontiguousarray(j[result], dtype=np.intp)
result = np.ascontiguousarray(result, np.uint8)
# Determine the order in which pixels are processed.
# We use a random # for tiebreaking. Assign each pixel in the image a
# predictable, random # so that masking doesn't affect arbitrary choices
# of skeletons
#
generator = np.random.RandomState(0)
tiebreaker = generator.permutation(np.arange(masked_image.sum()))
order = np.lexsort((tiebreaker,
corner_score[masked_image],
distance))
order = np.ascontiguousarray(order, dtype=np.int32)
table = np.ascontiguousarray(table, dtype=np.uint8)
# Remove pixels not belonging to the medial axis
_skeletonize_loop(result, i, j, order, table)
result = result.astype(bool)
if mask is not None:
result[~mask] = image[~mask]
if return_distance:
return result, store_distance
else:
return result
def _pattern_of(index):
"""
Return the pattern represented by an index value
Byte decomposition of index
"""
return np.array([[index & 2**0, index & 2**1, index & 2**2],
[index & 2**3, index & 2**4, index & 2**5],
[index & 2**6, index & 2**7, index & 2**8]], bool)
def _table_lookup(image, table):
"""
Perform a morphological transform on an image, directed by its
neighbors
Parameters
----------
image : ndarray
A binary image
table : ndarray
A 512-element table giving the transform of each pixel given
the values of that pixel and its 8-connected neighbors.
border_value : bool
The value of pixels beyond the border of the image.
Returns
-------
result : ndarray of same shape as `image`
Transformed image
Notes
-----
The pixels are numbered like this::
0 1 2
3 4 5
6 7 8
The index at a pixel is the sum of 2**<pixel-number> for pixels
that evaluate to true.
"""
#
# We accumulate into the indexer to get the index into the table
# at each point in the image
#
if image.shape[0] < 3 or image.shape[1] < 3:
image = image.astype(bool)
indexer = np.zeros(image.shape, int)
indexer[1:, 1:] += image[:-1, :-1] * 2**0
indexer[1:, :] += image[:-1, :] * 2**1
indexer[1:, :-1] += image[:-1, 1:] * 2**2
indexer[:, 1:] += image[:, :-1] * 2**3
indexer[:, :] += image[:, :] * 2**4
indexer[:, :-1] += image[:, 1:] * 2**5
indexer[:-1, 1:] += image[1:, :-1] * 2**6
indexer[:-1, :] += image[1:, :] * 2**7
indexer[:-1, :-1] += image[1:, 1:] * 2**8
else:
indexer = _table_lookup_index(np.ascontiguousarray(image, np.uint8))
image = table[indexer]
return image
def skeletonize_3d(image):
"""Compute the skeleton of a binary image.
Thinning is used to reduce each connected component in a binary image
to a single-pixel wide skeleton.
Parameters
----------
image : ndarray, 2D or 3D
A binary image containing the objects to be skeletonized. Zeros
represent background, nonzero values are foreground.
Returns
-------
skeleton : ndarray
The thinned image.
See also
--------
skeletonize, medial_axis
Notes
-----
The method of [Lee94]_ uses an octree data structure to examine a 3x3x3
neighborhood of a pixel. The algorithm proceeds by iteratively sweeping
over the image, and removing pixels at each iteration until the image
stops changing. Each iteration consists of two steps: first, a list of
candidates for removal is assembled; then pixels from this list are
rechecked sequentially, to better preserve connectivity of the image.
The algorithm this function implements is different from the algorithms
used by either `skeletonize` or `medial_axis`, thus for 2D images the
results produced by this function are generally different.
References
----------
.. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models
via 3-D medial surface/axis thinning algorithms.
Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994.
"""
# make sure the image is 3D or 2D
if image.ndim < 2 or image.ndim > 3:
raise ValueError("skeletonize_3d can only handle 2D or 3D images; "
"got image.ndim = %s instead." % image.ndim)
image = np.ascontiguousarray(image)
image = img_as_ubyte(image, force_copy=False)
# make an in image 3D and pad it w/ zeros to simplify dealing w/ boundaries
# NB: careful here to not clobber the original *and* minimize copying
image_o = image
if image.ndim == 2:
image_o = image[np.newaxis, ...]
image_o = np.pad(image_o, pad_width=1, mode='constant')
# normalize to binary
maxval = image_o.max()
image_o[image_o != 0] = 1
# do the computation
image_o = np.asarray(_compute_thin_image(image_o))
# crop it back and restore the original intensity range
image_o = crop(image_o, crop_width=1)
if image.ndim == 2:
image_o = image_o[0]
image_o *= maxval
return image_o

View file

@ -0,0 +1,261 @@
"""Utility functions used in the morphology subpackage."""
import numpy as np
from scipy import ndimage as ndi
def _validate_connectivity(image_dim, connectivity, offset):
"""Convert any valid connectivity to a structuring element and offset.
Parameters
----------
image_dim : int
The number of dimensions of the input image.
connectivity : int, array, or None
The neighborhood connectivity. An integer is interpreted as in
``scipy.ndimage.generate_binary_structure``, as the maximum number
of orthogonal steps to reach a neighbor. An array is directly
interpreted as a structuring element and its shape is validated against
the input image shape. ``None`` is interpreted as a connectivity of 1.
offset : tuple of int, or None
The coordinates of the center of the structuring element.
Returns
-------
c_connectivity : array of bool
The structuring element corresponding to the input `connectivity`.
offset : array of int
The offset corresponding to the center of the structuring element.
Raises
------
ValueError:
If the image dimension and the connectivity or offset dimensions don't
match.
"""
if connectivity is None:
connectivity = 1
if np.isscalar(connectivity):
c_connectivity = ndi.generate_binary_structure(image_dim, connectivity)
else:
c_connectivity = np.array(connectivity, bool)
if c_connectivity.ndim != image_dim:
raise ValueError("Connectivity dimension must be same as image")
if offset is None:
if any([x % 2 == 0 for x in c_connectivity.shape]):
raise ValueError("Connectivity array must have an unambiguous "
"center")
offset = np.array(c_connectivity.shape) // 2
return c_connectivity, offset
def _offsets_to_raveled_neighbors(image_shape, selem, center, order='C'):
"""Compute offsets to a samples neighbors if the image would be raveled.
Parameters
----------
image_shape : tuple
The shape of the image for which the offsets are computed.
selem : ndarray
A structuring element determining the neighborhood expressed as an
n-D array of 1's and 0's.
center : tuple
Tuple of indices to the center of `selem`.
order : {"C", "F"}, optional
Whether the image described by `image_shape` is in row-major (C-style)
or column-major (Fortran-style) order.
Returns
-------
raveled_offsets : ndarray
Linear offsets to a samples neighbors in the raveled image, sorted by
their distance from the center.
Notes
-----
This function will return values even if `image_shape` contains a dimension
length that is smaller than `selem`.
Examples
--------
>>> _offsets_to_raveled_neighbors((4, 5), np.ones((4, 3)), (1, 1))
array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11])
>>> _offsets_to_raveled_neighbors((2, 3, 2), np.ones((3, 3, 3)), (1, 1, 1))
array([ 2, -6, 1, -1, 6, -2, 3, 8, -3, -4, 7, -5, -7, -8, 5, 4, -9,
9])
"""
if not selem.ndim == len(image_shape) == len(center):
raise ValueError(
"number of dimensions in image shape, structuring element and its"
"center index does not match"
)
selem_indices = np.array(np.nonzero(selem)).T
offsets = selem_indices - center
if order == 'F':
offsets = offsets[:, ::-1]
image_shape = image_shape[::-1]
elif order != 'C':
raise ValueError("order must be 'C' or 'F'")
# Scale offsets in each dimension and sum
ravel_factors = image_shape[1:] + (1,)
ravel_factors = np.cumprod(ravel_factors[::-1])[::-1]
raveled_offsets = (offsets * ravel_factors).sum(axis=1)
# Sort by distance
distances = np.abs(offsets).sum(axis=1)
raveled_offsets = raveled_offsets[np.argsort(distances)]
# If any dimension in image_shape is smaller than selem.shape
# duplicates might occur, remove them
if any(x < y for x, y in zip(image_shape, selem.shape)):
# np.unique reorders, which we don't want
_, indices = np.unique(raveled_offsets, return_index=True)
raveled_offsets = raveled_offsets[np.sort(indices)]
# Remove "offset to center"
raveled_offsets = raveled_offsets[1:]
return raveled_offsets
def _resolve_neighborhood(selem, connectivity, ndim):
"""Validate or create structuring element.
Depending on the values of `connectivity` and `selem` this function
either creates a new structuring element (`selem` is None) using
`connectivity` or validates the given structuring element (`selem` is not
None).
Parameters
----------
selem : ndarray
A structuring element used to determine the neighborhood of each
evaluated pixel (``True`` denotes a connected pixel). It must be a
boolean array and have the same number of dimensions as `image`. If
neither `selem` nor `connectivity` are given, all adjacent pixels are
considered as part of the neighborhood.
connectivity : int
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is less than or
equal to `connectivity` are considered neighbors. Ignored if
`selem` is not None.
ndim : int
Number of dimensions `selem` ought to have.
Returns
-------
selem : ndarray
Validated or new structuring element specifying the neighborhood.
Examples
--------
>>> _resolve_neighborhood(None, 1, 2)
array([[False, True, False],
[ True, True, True],
[False, True, False]])
>>> _resolve_neighborhood(None, None, 3).shape
(3, 3, 3)
"""
if selem is None:
if connectivity is None:
connectivity = ndim
selem = ndi.generate_binary_structure(ndim, connectivity)
else:
# Validate custom structured element
selem = np.asarray(selem, dtype=np.bool)
# Must specify neighbors for all dimensions
if selem.ndim != ndim:
raise ValueError(
"number of dimensions in image and structuring element do not"
"match"
)
# Must only specify direct neighbors
if any(s != 3 for s in selem.shape):
raise ValueError("dimension size in structuring element is not 3")
return selem
def _set_border_values(image, value):
"""Set edge values along all axes to a constant value.
Parameters
----------
image : ndarray
The array to modify inplace.
value : scalar
The value to use. Should be compatible with `image`'s dtype.
Examples
--------
>>> image = np.zeros((4, 5), dtype=int)
>>> _set_border_values(image, 1)
>>> image
array([[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]])
"""
for axis in range(image.ndim):
# Index first and last element in each dimension
sl = (slice(None),) * axis + ((0, -1),) + (...,)
image[sl] = value
def _fast_pad(image, value, *, order="C"):
"""Pad an array on all axes by one with a value.
Parameters
----------
image : ndarray
Image to pad.
value : scalar
The value to use. Should be compatible with `image`'s dtype.
order : "C" or "F"
Specify the memory layout of the padded image (C or Fortran style).
Returns
-------
padded_image : ndarray
The new image.
Notes
-----
The output of this function is equivalent to::
np.pad(image, 1, mode="constant", constant_values=value)
Up to versions < 1.17 `numpy.pad` uses concatenation to create padded
arrays while this method needs to only allocate and copy once.
This can result in significant speed gains if `image` has a large number of
dimensions.
Thus this function may be safely removed once that version is the minimum
required by scikit-image.
Examples
--------
>>> _fast_pad(np.zeros((2, 3), dtype=int), 4)
array([[4, 4, 4, 4, 4],
[4, 0, 0, 0, 4],
[4, 0, 0, 0, 4],
[4, 4, 4, 4, 4]])
"""
# Allocate padded image
new_shape = np.array(image.shape) + 2
new_image = np.empty(new_shape, dtype=image.dtype, order=order)
# Copy old image into new space
sl = (slice(1, -1),) * image.ndim
new_image[sl] = image
# and set the edge values
_set_border_values(new_image, value)
return new_image

View file

@ -0,0 +1,146 @@
"""
Binary morphological operations
"""
import numpy as np
from scipy import ndimage as ndi
from .misc import default_selem
# The default_selem decorator provides a diamond structuring element as default
# with the same dimension as the input image and size 3 along each axis.
@default_selem
def binary_erosion(image, selem=None, out=None):
"""Return fast binary morphological erosion of an image.
This function returns the same result as greyscale erosion but performs
faster for binary images.
Morphological erosion sets a pixel at ``(i,j)`` to the minimum over all
pixels in the neighborhood centered at ``(i,j)``. Erosion shrinks bright
regions and enlarges dark regions.
Parameters
----------
image : ndarray
Binary input image.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped structuring element (connectivity=1).
out : ndarray of bool, optional
The array to store the result of the morphology. If None is
passed, a new array will be allocated.
Returns
-------
eroded : ndarray of bool or uint
The result of the morphological erosion taking values in
``[False, True]``.
"""
if out is None:
out = np.empty(image.shape, dtype=np.bool)
ndi.binary_erosion(image, structure=selem, output=out, border_value=True)
return out
@default_selem
def binary_dilation(image, selem=None, out=None):
"""Return fast binary morphological dilation of an image.
This function returns the same result as greyscale dilation but performs
faster for binary images.
Morphological dilation sets a pixel at ``(i,j)`` to the maximum over all
pixels in the neighborhood centered at ``(i,j)``. Dilation enlarges bright
regions and shrinks dark regions.
Parameters
----------
image : ndarray
Binary input image.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped structuring element (connectivity=1).
out : ndarray of bool, optional
The array to store the result of the morphology. If None is
passed, a new array will be allocated.
Returns
-------
dilated : ndarray of bool or uint
The result of the morphological dilation with values in
``[False, True]``.
"""
if out is None:
out = np.empty(image.shape, dtype=np.bool)
ndi.binary_dilation(image, structure=selem, output=out)
return out
@default_selem
def binary_opening(image, selem=None, out=None):
"""Return fast binary morphological opening of an image.
This function returns the same result as greyscale opening but performs
faster for binary images.
The morphological opening on an image is defined as an erosion followed by
a dilation. Opening can remove small bright spots (i.e. "salt") and connect
small dark cracks. This tends to "open" up (dark) gaps between (bright)
features.
Parameters
----------
image : ndarray
Binary input image.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped structuring element (connectivity=1).
out : ndarray of bool, optional
The array to store the result of the morphology. If None
is passed, a new array will be allocated.
Returns
-------
opening : ndarray of bool
The result of the morphological opening.
"""
eroded = binary_erosion(image, selem)
out = binary_dilation(eroded, selem, out=out)
return out
@default_selem
def binary_closing(image, selem=None, out=None):
"""Return fast binary morphological closing of an image.
This function returns the same result as greyscale closing but performs
faster for binary images.
The morphological closing on an image is defined as a dilation followed by
an erosion. Closing can remove small dark spots (i.e. "pepper") and connect
small bright cracks. This tends to "close" up (dark) gaps between (bright)
features.
Parameters
----------
image : ndarray
Binary input image.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped structuring element (connectivity=1).
out : ndarray of bool, optional
The array to store the result of the morphology. If None,
is passed, a new array will be allocated.
Returns
-------
closing : ndarray of bool
The result of the morphological closing.
"""
dilated = binary_dilation(image, selem)
out = binary_erosion(dilated, selem, out=out)
return out

View file

@ -0,0 +1,164 @@
"""Convex Hull."""
from itertools import product
import numpy as np
from scipy.spatial import ConvexHull
from ..measure.pnpoly import grid_points_in_poly
from ._convex_hull import possible_hull
from ..measure._label import label
from ..util import unique_rows
from .._shared.utils import warn
__all__ = ['convex_hull_image', 'convex_hull_object']
def _offsets_diamond(ndim):
offsets = np.zeros((2 * ndim, ndim))
for vertex, (axis, offset) in enumerate(product(range(ndim), (-0.5, 0.5))):
offsets[vertex, axis] = offset
return offsets
def convex_hull_image(image, offset_coordinates=True, tolerance=1e-10):
"""Compute the convex hull image of a binary image.
The convex hull is the set of pixels included in the smallest convex
polygon that surround all white pixels in the input image.
Parameters
----------
image : array
Binary input image. This array is cast to bool before processing.
offset_coordinates : bool, optional
If ``True``, a pixel at coordinate, e.g., (4, 7) will be represented
by coordinates (3.5, 7), (4.5, 7), (4, 6.5), and (4, 7.5). This adds
some "extent" to a pixel when computing the hull.
tolerance : float, optional
Tolerance when determining whether a point is inside the hull. Due
to numerical floating point errors, a tolerance of 0 can result in
some points erroneously being classified as being outside the hull.
Returns
-------
hull : (M, N) array of bool
Binary image with pixels in convex hull set to True.
References
----------
.. [1] https://blogs.mathworks.com/steve/2011/10/04/binary-image-convex-hull-algorithm-notes/
"""
ndim = image.ndim
if np.count_nonzero(image) == 0:
warn("Input image is entirely zero, no valid convex hull. "
"Returning empty image", UserWarning)
return np.zeros(image.shape, dtype=np.bool_)
# In 2D, we do an optimisation by choosing only pixels that are
# the starting or ending pixel of a row or column. This vastly
# limits the number of coordinates to examine for the virtual hull.
if ndim == 2:
coords = possible_hull(np.ascontiguousarray(image, dtype=np.uint8))
else:
coords = np.transpose(np.nonzero(image))
if offset_coordinates:
# when offsetting, we multiply number of vertices by 2 * ndim.
# therefore, we reduce the number of coordinates by using a
# convex hull on the original set, before offsetting.
hull0 = ConvexHull(coords)
coords = hull0.points[hull0.vertices]
# Add a vertex for the middle of each pixel edge
if offset_coordinates:
offsets = _offsets_diamond(image.ndim)
coords = (coords[:, np.newaxis, :] + offsets).reshape(-1, ndim)
# repeated coordinates can *sometimes* cause problems in
# scipy.spatial.ConvexHull, so we remove them.
coords = unique_rows(coords)
# Find the convex hull
hull = ConvexHull(coords)
vertices = hull.points[hull.vertices]
# If 2D, use fast Cython function to locate convex hull pixels
if ndim == 2:
mask = grid_points_in_poly(image.shape, vertices)
else:
gridcoords = np.reshape(np.mgrid[tuple(map(slice, image.shape))],
(ndim, -1))
# A point is in the hull if it satisfies all of the hull's inequalities
coords_in_hull = np.all(hull.equations[:, :ndim].dot(gridcoords) +
hull.equations[:, ndim:] < tolerance, axis=0)
mask = np.reshape(coords_in_hull, image.shape)
return mask
def convex_hull_object(image, neighbors=None, *, connectivity=None):
r"""Compute the convex hull image of individual objects in a binary image.
The convex hull is the set of pixels included in the smallest convex
polygon that surround all white pixels in the input image.
Parameters
----------
image : (M, N) ndarray
Binary input image.
neighbors : {4, 8}, int, optional
Whether to use 4 or 8 adjacent pixels as neighbors.
If ``None``, set to 8. **Deprecated, use** ``connectivity`` **instead.**
connectivity : {1, 2}, int, optional
Determines the neighbors of each pixel. Adjacent elements
within a squared distance of ``connectivity`` from pixel center
are considered neighbors. If ``None``, set to 2::
1-connectivity 2-connectivity
[ ] [ ] [ ] [ ]
| \ | /
[ ]--[x]--[ ] [ ]--[x]--[ ]
| / | \
[ ] [ ] [ ] [ ]
Returns
-------
hull : ndarray of bool
Binary image with pixels inside convex hull set to ``True``.
Notes
-----
This function uses ``skimage.morphology.label`` to define unique objects,
finds the convex hull of each using ``convex_hull_image``, and combines
these regions with logical OR. Be aware the convex hulls of unconnected
objects may overlap in the result. If this is suspected, consider using
convex_hull_image separately on each object or adjust ``connectivity``.
"""
if image.ndim > 2:
raise ValueError("Input must be a 2D image")
if neighbors is None and connectivity is None:
connectivity = 2
elif neighbors is not None:
# Backward-compatibility
if neighbors == 4:
connectivity = 1
elif neighbors == 8:
connectivity = 2
else:
raise ValueError('`neighbors` must be either 4 or 8.')
warn("The argument `neighbors` is deprecated and will be removed in "
"scikit-image 0.18, use `connectivity` instead. "
"For neighbors={neighbors}, use connectivity={connectivity}"
"".format(neighbors=neighbors, connectivity=connectivity),
stacklevel=2)
else:
if connectivity not in (1, 2):
raise ValueError('`connectivity` must be either 1 or 2.')
labeled_im = label(image, connectivity=connectivity, background=0)
convex_obj = np.zeros(image.shape, dtype=bool)
convex_img = np.zeros(image.shape, dtype=bool)
for i in range(1, labeled_im.max() + 1):
convex_obj = convex_hull_image(labeled_im == i)
convex_img = np.logical_or(convex_img, convex_obj)
return convex_img

View file

@ -0,0 +1,536 @@
"""extrema.py - local minima and maxima
This module provides functions to find local maxima and minima of an image.
Here, local maxima (minima) are defined as connected sets of pixels with equal
gray level which is strictly greater (smaller) than the gray level of all
pixels in direct neighborhood of the connected set. In addition, the module
provides the related functions h-maxima and h-minima.
Soille, P. (2003). Morphological Image Analysis: Principles and Applications
(2nd ed.), Chapter 6. Springer-Verlag New York, Inc.
"""
import numpy as np
from ..util import dtype_limits, invert, crop
from .._shared.utils import warn
from . import greyreconstruct, _util
from ._extrema_cy import _local_maxima
def _add_constant_clip(image, const_value):
"""Add constant to the image while handling overflow issues gracefully.
"""
min_dtype, max_dtype = dtype_limits(image, clip_negative=False)
if const_value > (max_dtype - min_dtype):
raise ValueError("The added constant is not compatible"
"with the image data type.")
result = image + const_value
result[image > max_dtype-const_value] = max_dtype
return(result)
def _subtract_constant_clip(image, const_value):
"""Subtract constant from image while handling underflow issues.
"""
min_dtype, max_dtype = dtype_limits(image, clip_negative=False)
if const_value > (max_dtype-min_dtype):
raise ValueError("The subtracted constant is not compatible"
"with the image data type.")
result = image - const_value
result[image < (const_value + min_dtype)] = min_dtype
return(result)
def h_maxima(image, h, selem=None):
"""Determine all maxima of the image with height >= h.
The local maxima are defined as connected sets of pixels with equal
grey level strictly greater than the grey level of all pixels in direct
neighborhood of the set.
A local maximum M of height h is a local maximum for which
there is at least one path joining M with a higher maximum on which the
minimal value is f(M) - h (i.e. the values along the path are not
decreasing by more than h with respect to the maximum's value) and no
path for which the minimal value is greater.
Parameters
----------
image : ndarray
The input image for which the maxima are to be calculated.
h : unsigned integer
The minimal height of all extracted maxima.
selem : ndarray, optional
The neighborhood expressed as an n-D array of 1's and 0's.
Default is the ball of radius 1 according to the maximum norm
(i.e. a 3x3 square for 2D images, a 3x3x3 cube for 3D images, etc.)
Returns
-------
h_max : ndarray
The maxima of height >= h. The resulting image is a binary image, where
pixels belonging to the selected maxima take value 1, the others
take value 0.
See also
--------
skimage.morphology.extrema.h_minima
skimage.morphology.extrema.local_maxima
skimage.morphology.extrema.local_minima
References
----------
.. [1] Soille, P., "Morphological Image Analysis: Principles and
Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883.
Examples
--------
>>> import numpy as np
>>> from skimage.morphology import extrema
We create an image (quadratic function with a maximum in the center and
4 additional constant maxima.
The heights of the maxima are: 1, 21, 41, 61, 81
>>> w = 10
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:4,2:4] = 40; f[2:4,7:9] = 60; f[7:9,2:4] = 80; f[7:9,7:9] = 100
>>> f = f.astype(np.int)
We can calculate all maxima with a height of at least 40:
>>> maxima = extrema.h_maxima(f, 40)
The resulting image will contain 3 local maxima.
"""
# Check for h value that is larger then range of the image. If this
# is True then there are no h-maxima in the image.
if h > np.ptp(image):
return np.zeros(image.shape, dtype=np.uint8)
# Check for floating point h value. For this to work properly
# we need to explicitly convert image to float64.
#
# FIXME: This could give incorrect results if image is int64 and
# has a very high dynamic range. The dtype of image is
# changed to float64, and different integer values could
# become the same float due to rounding.
#
# >>> ii64 = np.iinfo(np.int64)
# >>> a = np.array([ii64.max, ii64.max - 2])
# >>> a[0] == a[1]
# False
# >>> b = a.astype(np.float64)
# >>> b[0] == b[1]
# True
#
if np.issubdtype(type(h), np.floating) and \
np.issubdtype(image.dtype, np.integer):
if ((h % 1) != 0):
warn('possible precision loss converting image to '
'floating point. To silence this warning, '
'ensure image and h have same data type.',
stacklevel=2)
image = image.astype(np.float_)
else:
h = image.dtype.type(h)
if (h == 0):
raise ValueError("h = 0 is ambiguous, use local_maxima() "
"instead?")
if np.issubdtype(image.dtype, np.floating):
# The purpose of the resolution variable is to allow for the
# small rounding errors that inevitably occur when doing
# floating point arithmetic. We want shifted_img to be
# guaranteed to be h less than image. If we only subtract h
# there may be pixels were shifted_img ends up being
# slightly greater than image - h.
#
# The resolution is scaled based on the pixel values in the
# image because floating point precision is relative. A
# very large value of 1.0e10 will have a large precision,
# say +-1.0e4, and a very small value of 1.0e-10 will have
# a very small precision, say +-1.0e-16.
#
resolution = 2 * np.finfo(image.dtype).resolution * np.abs(image)
shifted_img = image - h - resolution
else:
shifted_img = _subtract_constant_clip(image, h)
rec_img = greyreconstruct.reconstruction(shifted_img, image,
method='dilation', selem=selem)
residue_img = image - rec_img
return (residue_img >= h).astype(np.uint8)
def h_minima(image, h, selem=None):
"""Determine all minima of the image with depth >= h.
The local minima are defined as connected sets of pixels with equal
grey level strictly smaller than the grey levels of all pixels in direct
neighborhood of the set.
A local minimum M of depth h is a local minimum for which
there is at least one path joining M with a deeper minimum on which the
maximal value is f(M) + h (i.e. the values along the path are not
increasing by more than h with respect to the minimum's value) and no
path for which the maximal value is smaller.
Parameters
----------
image : ndarray
The input image for which the minima are to be calculated.
h : unsigned integer
The minimal depth of all extracted minima.
selem : ndarray, optional
The neighborhood expressed as an n-D array of 1's and 0's.
Default is the ball of radius 1 according to the maximum norm
(i.e. a 3x3 square for 2D images, a 3x3x3 cube for 3D images, etc.)
Returns
-------
h_min : ndarray
The minima of depth >= h. The resulting image is a binary image, where
pixels belonging to the selected minima take value 1, the other pixels
take value 0.
See also
--------
skimage.morphology.extrema.h_maxima
skimage.morphology.extrema.local_maxima
skimage.morphology.extrema.local_minima
References
----------
.. [1] Soille, P., "Morphological Image Analysis: Principles and
Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883.
Examples
--------
>>> import numpy as np
>>> from skimage.morphology import extrema
We create an image (quadratic function with a minimum in the center and
4 additional constant maxima.
The depth of the minima are: 1, 21, 41, 61, 81
>>> w = 10
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:4,2:4] = 160; f[2:4,7:9] = 140; f[7:9,2:4] = 120; f[7:9,7:9] = 100
>>> f = f.astype(np.int)
We can calculate all minima with a depth of at least 40:
>>> minima = extrema.h_minima(f, 40)
The resulting image will contain 3 local minima.
"""
if h > np.ptp(image):
return np.zeros(image.shape, dtype=np.uint8)
if np.issubdtype(type(h), np.floating) and \
np.issubdtype(image.dtype, np.integer):
if ((h % 1) != 0):
warn('possible precision loss converting image to '
'floating point. To silence this warning, '
'ensure image and h have same data type.',
stacklevel=2)
image = image.astype(np.float_)
else:
h = image.dtype.type(h)
if (h == 0):
raise ValueError("h = 0 is ambiguous, use local_minima() "
"instead?")
if np.issubdtype(image.dtype, np.floating):
resolution = 2 * np.finfo(image.dtype).resolution * np.abs(image)
shifted_img = image + h + resolution
else:
shifted_img = _add_constant_clip(image, h)
rec_img = greyreconstruct.reconstruction(shifted_img, image,
method='erosion', selem=selem)
residue_img = rec_img - image
return (residue_img >= h).astype(np.uint8)
def local_maxima(image, selem=None, connectivity=None, indices=False,
allow_borders=True):
"""Find local maxima of n-dimensional array.
The local maxima are defined as connected sets of pixels with equal gray
level (plateaus) strictly greater than the gray levels of all pixels in the
neighborhood.
Parameters
----------
image : ndarray
An n-dimensional array.
selem : ndarray, optional
A structuring element used to determine the neighborhood of each
evaluated pixel (``True`` denotes a connected pixel). It must be a
boolean array and have the same number of dimensions as `image`. If
neither `selem` nor `connectivity` are given, all adjacent pixels are
considered as part of the neighborhood.
connectivity : int, optional
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is less than or
equal to `connectivity` are considered neighbors. Ignored if
`selem` is not None.
indices : bool, optional
If True, the output will be a tuple of one-dimensional arrays
representing the indices of local maxima in each dimension. If False,
the output will be a boolean array with the same shape as `image`.
allow_borders : bool, optional
If true, plateaus that touch the image border are valid maxima.
Returns
-------
maxima : ndarray or tuple[ndarray]
If `indices` is false, a boolean array with the same shape as `image`
is returned with ``True`` indicating the position of local maxima
(``False`` otherwise). If `indices` is true, a tuple of one-dimensional
arrays containing the coordinates (indices) of all found maxima.
Warns
-----
UserWarning
If `allow_borders` is false and any dimension of the given `image` is
shorter than 3 samples, maxima can't exist and a warning is shown.
See Also
--------
skimage.morphology.local_minima
skimage.morphology.h_maxima
skimage.morphology.h_minima
Notes
-----
This function operates on the following ideas:
1. Make a first pass over the image's last dimension and flag candidates
for local maxima by comparing pixels in only one direction.
If the pixels aren't connected in the last dimension all pixels are
flagged as candidates instead.
For each candidate:
2. Perform a flood-fill to find all connected pixels that have the same
gray value and are part of the plateau.
3. Consider the connected neighborhood of a plateau: if no bordering sample
has a higher gray level, mark the plateau as a definite local maximum.
Examples
--------
>>> from skimage.morphology import local_maxima
>>> image = np.zeros((4, 7), dtype=int)
>>> image[1:3, 1:3] = 1
>>> image[3, 0] = 1
>>> image[1:3, 4:6] = 2
>>> image[3, 6] = 3
>>> image
array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3]])
Find local maxima by comparing to all neighboring pixels (maximal
connectivity):
>>> local_maxima(image)
array([[False, False, False, False, False, False, False],
[False, True, True, False, False, False, False],
[False, True, True, False, False, False, False],
[ True, False, False, False, False, False, True]])
>>> local_maxima(image, indices=True)
(array([1, 1, 2, 2, 3, 3]), array([1, 2, 1, 2, 0, 6]))
Find local maxima without comparing to diagonal pixels (connectivity 1):
>>> local_maxima(image, connectivity=1)
array([[False, False, False, False, False, False, False],
[False, True, True, False, True, True, False],
[False, True, True, False, True, True, False],
[ True, False, False, False, False, False, True]])
and exclude maxima that border the image edge:
>>> local_maxima(image, connectivity=1, allow_borders=False)
array([[False, False, False, False, False, False, False],
[False, True, True, False, True, True, False],
[False, True, True, False, True, True, False],
[False, False, False, False, False, False, False]])
"""
image = np.asarray(image, order="C")
if image.size == 0:
# Return early for empty input
if indices:
# Make sure that output is a tuple of 1 empty array per dimension
return np.nonzero(image)
else:
return np.zeros(image.shape, dtype=np.bool)
if allow_borders:
# Ensure that local maxima are always at least one smaller sample away
# from the image border
image = _util._fast_pad(image, image.min())
# Array of flags used to store the state of each pixel during evaluation.
# See _extrema_cy.pyx for their meaning
flags = np.zeros(image.shape, dtype=np.uint8)
_util._set_border_values(flags, value=3)
if any(s < 3 for s in image.shape):
# Warn and skip if any dimension is smaller than 3
# -> no maxima can exist & structuring element can't be applied
warn(
"maxima can't exist for an image with any dimension smaller 3 "
"if borders aren't allowed",
stacklevel=3
)
else:
selem = _util._resolve_neighborhood(selem, connectivity, image.ndim)
neighbor_offsets = _util._offsets_to_raveled_neighbors(
image.shape, selem, center=((1,) * image.ndim)
)
try:
_local_maxima(image.ravel(), flags.ravel(), neighbor_offsets)
except TypeError:
if image.dtype == np.float16:
# Provide the user with clearer error message
raise TypeError("dtype of `image` is float16 which is not "
"supported, try upcasting to float32")
else:
raise # Otherwise raise original message
if allow_borders:
# Revert padding performed at the beginning of the function
flags = crop(flags, 1)
else:
# No padding was performed but set edge values back to 0
_util._set_border_values(flags, value=0)
if indices:
return np.nonzero(flags)
else:
return flags.view(np.bool)
def local_minima(image, selem=None, connectivity=None, indices=False,
allow_borders=True):
"""Find local minima of n-dimensional array.
The local minima are defined as connected sets of pixels with equal gray
level (plateaus) strictly smaller than the gray levels of all pixels in the
neighborhood.
Parameters
----------
image : ndarray
An n-dimensional array.
selem : ndarray, optional
A structuring element used to determine the neighborhood of each
evaluated pixel (``True`` denotes a connected pixel). It must be a
boolean array and have the same number of dimensions as `image`. If
neither `selem` nor `connectivity` are given, all adjacent pixels are
considered as part of the neighborhood.
connectivity : int, optional
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is less than or
equal to `connectivity` are considered neighbors. Ignored if
`selem` is not None.
indices : bool, optional
If True, the output will be a tuple of one-dimensional arrays
representing the indices of local minima in each dimension. If False,
the output will be a boolean array with the same shape as `image`.
allow_borders : bool, optional
If true, plateaus that touch the image border are valid minima.
Returns
-------
minima : ndarray or tuple[ndarray]
If `indices` is false, a boolean array with the same shape as `image`
is returned with ``True`` indicating the position of local minima
(``False`` otherwise). If `indices` is true, a tuple of one-dimensional
arrays containing the coordinates (indices) of all found minima.
See Also
--------
skimage.morphology.local_maxima
skimage.morphology.h_maxima
skimage.morphology.h_minima
Notes
-----
This function operates on the following ideas:
1. Make a first pass over the image's last dimension and flag candidates
for local minima by comparing pixels in only one direction.
If the pixels aren't connected in the last dimension all pixels are
flagged as candidates instead.
For each candidate:
2. Perform a flood-fill to find all connected pixels that have the same
gray value and are part of the plateau.
3. Consider the connected neighborhood of a plateau: if no bordering sample
has a smaller gray level, mark the plateau as a definite local minimum.
Examples
--------
>>> from skimage.morphology import local_minima
>>> image = np.zeros((4, 7), dtype=int)
>>> image[1:3, 1:3] = -1
>>> image[3, 0] = -1
>>> image[1:3, 4:6] = -2
>>> image[3, 6] = -3
>>> image
array([[ 0, 0, 0, 0, 0, 0, 0],
[ 0, -1, -1, 0, -2, -2, 0],
[ 0, -1, -1, 0, -2, -2, 0],
[-1, 0, 0, 0, 0, 0, -3]])
Find local minima by comparing to all neighboring pixels (maximal
connectivity):
>>> local_minima(image)
array([[False, False, False, False, False, False, False],
[False, True, True, False, False, False, False],
[False, True, True, False, False, False, False],
[ True, False, False, False, False, False, True]])
>>> local_minima(image, indices=True)
(array([1, 1, 2, 2, 3, 3]), array([1, 2, 1, 2, 0, 6]))
Find local minima without comparing to diagonal pixels (connectivity 1):
>>> local_minima(image, connectivity=1)
array([[False, False, False, False, False, False, False],
[False, True, True, False, True, True, False],
[False, True, True, False, True, True, False],
[ True, False, False, False, False, False, True]])
and exclude minima that border the image edge:
>>> local_minima(image, connectivity=1, allow_borders=False)
array([[False, False, False, False, False, False, False],
[False, True, True, False, True, True, False],
[False, True, True, False, True, True, False],
[False, False, False, False, False, False, False]])
"""
return local_maxima(
image=invert(image),
selem=selem,
connectivity=connectivity,
indices=indices,
allow_borders=allow_borders
)

View file

@ -0,0 +1,489 @@
"""
Grayscale morphological operations
"""
import functools
import numpy as np
from scipy import ndimage as ndi
from .misc import default_selem
from ..util import crop
__all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat',
'black_tophat']
def _shift_selem(selem, shift_x, shift_y):
"""Shift the binary image `selem` in the left and/or up.
This only affects 2D structuring elements with even number of rows
or columns.
Parameters
----------
selem : 2D array, shape (M, N)
The input structuring element.
shift_x, shift_y : bool
Whether to move `selem` along each axis.
Returns
-------
out : 2D array, shape (M + int(shift_x), N + int(shift_y))
The shifted structuring element.
"""
if selem.ndim != 2:
# do nothing for 1D or 3D or higher structuring elements
return selem
m, n = selem.shape
if m % 2 == 0:
extra_row = np.zeros((1, n), selem.dtype)
if shift_x:
selem = np.vstack((selem, extra_row))
else:
selem = np.vstack((extra_row, selem))
m += 1
if n % 2 == 0:
extra_col = np.zeros((m, 1), selem.dtype)
if shift_y:
selem = np.hstack((selem, extra_col))
else:
selem = np.hstack((extra_col, selem))
return selem
def _invert_selem(selem):
"""Change the order of the values in `selem`.
This is a patch for the *weird* footprint inversion in
`ndi.grey_morphology` [1]_.
Parameters
----------
selem : array
The input structuring element.
Returns
-------
inverted : array, same shape and type as `selem`
The structuring element, in opposite order.
Examples
--------
>>> selem = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], np.uint8)
>>> _invert_selem(selem)
array([[1, 1, 0],
[1, 1, 0],
[0, 0, 0]], dtype=uint8)
References
----------
.. [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285
"""
inverted = selem[(slice(None, None, -1),) * selem.ndim]
return inverted
def pad_for_eccentric_selems(func):
"""Pad input images for certain morphological operations.
Parameters
----------
func : callable
A morphological function, either opening or closing, that
supports eccentric structuring elements. Its parameters must
include at least `image`, `selem`, and `out`.
Returns
-------
func_out : callable
The same function, but correctly padding the input image before
applying the input function.
See Also
--------
opening, closing.
"""
@functools.wraps(func)
def func_out(image, selem, out=None, *args, **kwargs):
pad_widths = []
padding = False
if out is None:
out = np.empty_like(image)
for axis_len in selem.shape:
if axis_len % 2 == 0:
axis_pad_width = axis_len - 1
padding = True
else:
axis_pad_width = 0
pad_widths.append((axis_pad_width,) * 2)
if padding:
image = np.pad(image, pad_widths, mode='edge')
out_temp = np.empty_like(image)
else:
out_temp = out
out_temp = func(image, selem, out=out_temp, *args, **kwargs)
if padding:
out[:] = crop(out_temp, pad_widths)
else:
out = out_temp
return out
return func_out
@default_selem
def erosion(image, selem=None, out=None, shift_x=False, shift_y=False):
"""Return greyscale morphological erosion of an image.
Morphological erosion sets a pixel at (i,j) to the minimum over all pixels
in the neighborhood centered at (i,j). Erosion shrinks bright regions and
enlarges dark regions.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as an array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarrays, optional
The array to store the result of the morphology. If None is
passed, a new array will be allocated.
shift_x, shift_y : bool, optional
shift structuring element about center point. This only affects
eccentric structuring elements (i.e. selem with even numbered sides).
Returns
-------
eroded : array, same shape as `image`
The result of the morphological erosion.
Notes
-----
For ``uint8`` (and ``uint16`` up to a certain bit-depth) data, the
lower algorithm complexity makes the `skimage.filters.rank.minimum`
function more efficient for larger images and structuring elements.
Examples
--------
>>> # Erosion shrinks bright regions
>>> import numpy as np
>>> from skimage.morphology import square
>>> bright_square = np.array([[0, 0, 0, 0, 0],
... [0, 1, 1, 1, 0],
... [0, 1, 1, 1, 0],
... [0, 1, 1, 1, 0],
... [0, 0, 0, 0, 0]], dtype=np.uint8)
>>> erosion(bright_square, square(3))
array([[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]], dtype=uint8)
"""
selem = np.array(selem)
selem = _shift_selem(selem, shift_x, shift_y)
if out is None:
out = np.empty_like(image)
ndi.grey_erosion(image, footprint=selem, output=out)
return out
@default_selem
def dilation(image, selem=None, out=None, shift_x=False, shift_y=False):
"""Return greyscale morphological dilation of an image.
Morphological dilation sets a pixel at (i,j) to the maximum over all pixels
in the neighborhood centered at (i,j). Dilation enlarges bright regions
and shrinks dark regions.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarray, optional
The array to store the result of the morphology. If None, is
passed, a new array will be allocated.
shift_x, shift_y : bool, optional
shift structuring element about center point. This only affects
eccentric structuring elements (i.e. selem with even numbered sides).
Returns
-------
dilated : uint8 array, same shape and type as `image`
The result of the morphological dilation.
Notes
-----
For `uint8` (and `uint16` up to a certain bit-depth) data, the lower
algorithm complexity makes the `skimage.filters.rank.maximum` function more
efficient for larger images and structuring elements.
Examples
--------
>>> # Dilation enlarges bright regions
>>> import numpy as np
>>> from skimage.morphology import square
>>> bright_pixel = np.array([[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]], dtype=np.uint8)
>>> dilation(bright_pixel, square(3))
array([[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
"""
selem = np.array(selem)
selem = _shift_selem(selem, shift_x, shift_y)
# Inside ndimage.grey_dilation, the structuring element is inverted,
# eg. `selem = selem[::-1, ::-1]` for 2D [1]_, for reasons unknown to
# this author (@jni). To "patch" this behaviour, we invert our own
# selem before passing it to `ndi.grey_dilation`.
# [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285
selem = _invert_selem(selem)
if out is None:
out = np.empty_like(image)
ndi.grey_dilation(image, footprint=selem, output=out)
return out
@default_selem
@pad_for_eccentric_selems
def opening(image, selem=None, out=None):
"""Return greyscale morphological opening of an image.
The morphological opening on an image is defined as an erosion followed by
a dilation. Opening can remove small bright spots (i.e. "salt") and connect
small dark cracks. This tends to "open" up (dark) gaps between (bright)
features.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as an array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarray, optional
The array to store the result of the morphology. If None
is passed, a new array will be allocated.
Returns
-------
opening : array, same shape and type as `image`
The result of the morphological opening.
Examples
--------
>>> # Open up gap between two bright regions (but also shrink regions)
>>> import numpy as np
>>> from skimage.morphology import square
>>> bad_connection = np.array([[1, 0, 0, 0, 1],
... [1, 1, 0, 1, 1],
... [1, 1, 1, 1, 1],
... [1, 1, 0, 1, 1],
... [1, 0, 0, 0, 1]], dtype=np.uint8)
>>> opening(bad_connection, square(3))
array([[0, 0, 0, 0, 0],
[1, 1, 0, 1, 1],
[1, 1, 0, 1, 1],
[1, 1, 0, 1, 1],
[0, 0, 0, 0, 0]], dtype=uint8)
"""
eroded = erosion(image, selem)
# note: shift_x, shift_y do nothing if selem side length is odd
out = dilation(eroded, selem, out=out, shift_x=True, shift_y=True)
return out
@default_selem
@pad_for_eccentric_selems
def closing(image, selem=None, out=None):
"""Return greyscale morphological closing of an image.
The morphological closing on an image is defined as a dilation followed by
an erosion. Closing can remove small dark spots (i.e. "pepper") and connect
small bright cracks. This tends to "close" up (dark) gaps between (bright)
features.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as an array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarray, optional
The array to store the result of the morphology. If None,
is passed, a new array will be allocated.
Returns
-------
closing : array, same shape and type as `image`
The result of the morphological closing.
Examples
--------
>>> # Close a gap between two bright lines
>>> import numpy as np
>>> from skimage.morphology import square
>>> broken_line = np.array([[0, 0, 0, 0, 0],
... [0, 0, 0, 0, 0],
... [1, 1, 0, 1, 1],
... [0, 0, 0, 0, 0],
... [0, 0, 0, 0, 0]], dtype=np.uint8)
>>> closing(broken_line, square(3))
array([[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
"""
dilated = dilation(image, selem)
# note: shift_x, shift_y do nothing if selem side length is odd
out = erosion(dilated, selem, out=out, shift_x=True, shift_y=True)
return out
@default_selem
def white_tophat(image, selem=None, out=None):
"""Return white top hat of an image.
The white top hat of an image is defined as the image minus its
morphological opening. This operation returns the bright spots of the image
that are smaller than the structuring element.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as an array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarray, optional
The array to store the result of the morphology. If None
is passed, a new array will be allocated.
Returns
-------
out : array, same shape and type as `image`
The result of the morphological white top hat.
See also
--------
black_tophat
References
----------
.. [1] https://en.wikipedia.org/wiki/Top-hat_transform
Examples
--------
>>> # Subtract grey background from bright peak
>>> import numpy as np
>>> from skimage.morphology import square
>>> bright_on_grey = np.array([[2, 3, 3, 3, 2],
... [3, 4, 5, 4, 3],
... [3, 5, 9, 5, 3],
... [3, 4, 5, 4, 3],
... [2, 3, 3, 3, 2]], dtype=np.uint8)
>>> white_tophat(bright_on_grey, square(3))
array([[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 5, 1, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
"""
selem = np.array(selem)
if out is image:
opened = opening(image, selem)
if np.issubdtype(opened.dtype, np.bool_):
np.logical_xor(out, opened, out=out)
else:
out -= opened
return out
elif out is None:
out = np.empty_like(image)
# work-around for NumPy deprecation warning for arithmetic
# operations on bool arrays
if isinstance(image, np.ndarray) and image.dtype == np.bool:
image_ = image.view(dtype=np.uint8)
else:
image_ = image
if isinstance(out, np.ndarray) and out.dtype == np.bool:
out_ = out.view(dtype=np.uint8)
else:
out_ = out
out_ = ndi.white_tophat(image_, footprint=selem, output=out_)
return out
@default_selem
def black_tophat(image, selem=None, out=None):
"""Return black top hat of an image.
The black top hat of an image is defined as its morphological closing minus
the original image. This operation returns the dark spots of the image that
are smaller than the structuring element. Note that dark spots in the
original image are bright spots after the black top hat.
Parameters
----------
image : ndarray
Image array.
selem : ndarray, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use cross-shaped structuring element (connectivity=1).
out : ndarray, optional
The array to store the result of the morphology. If None
is passed, a new array will be allocated.
Returns
-------
out : array, same shape and type as `image`
The result of the morphological black top hat.
See also
--------
white_tophat
References
----------
.. [1] https://en.wikipedia.org/wiki/Top-hat_transform
Examples
--------
>>> # Change dark peak to bright peak and subtract background
>>> import numpy as np
>>> from skimage.morphology import square
>>> dark_on_grey = np.array([[7, 6, 6, 6, 7],
... [6, 5, 4, 5, 6],
... [6, 4, 0, 4, 6],
... [6, 5, 4, 5, 6],
... [7, 6, 6, 6, 7]], dtype=np.uint8)
>>> black_tophat(dark_on_grey, square(3))
array([[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 5, 1, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
"""
if out is image:
original = image.copy()
else:
original = image
out = closing(image, selem, out=out)
if np.issubdtype(out.dtype, np.bool_):
np.logical_xor(out, original, out=out)
else:
out -= original
return out

View file

@ -0,0 +1,211 @@
"""
This morphological reconstruction routine was adapted from 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 ..filters._rank_order import rank_order
def reconstruction(seed, mask, method='dilation', selem=None, offset=None):
"""Perform a morphological reconstruction of an image.
Morphological reconstruction by dilation is similar to basic morphological
dilation: high-intensity values will replace nearby low-intensity values.
The basic dilation operator, however, uses a structuring element to
determine how far a value in the input image can spread. In contrast,
reconstruction uses two images: a "seed" image, which specifies the values
that spread, and a "mask" image, which gives the maximum allowed value at
each pixel. The mask image, like the structuring element, limits the spread
of high-intensity values. Reconstruction by erosion is simply the inverse:
low-intensity values spread from the seed image and are limited by the mask
image, which represents the minimum allowed value.
Alternatively, you can think of reconstruction as a way to isolate the
connected regions of an image. For dilation, reconstruction connects
regions marked by local maxima in the seed image: neighboring pixels
less-than-or-equal-to those seeds are connected to the seeded region.
Local maxima with values larger than the seed image will get truncated to
the seed value.
Parameters
----------
seed : ndarray
The seed image (a.k.a. marker image), which specifies the values that
are dilated or eroded.
mask : ndarray
The maximum (dilation) / minimum (erosion) allowed value at each pixel.
method : {'dilation'|'erosion'}, optional
Perform reconstruction by dilation or erosion. In dilation (or
erosion), the seed image is dilated (or eroded) until limited by the
mask image. For dilation, each seed value must be less than or equal
to the corresponding mask value; for erosion, the reverse is true.
Default is 'dilation'.
selem : ndarray, optional
The neighborhood expressed as an n-D array of 1's and 0's.
Default is the n-D square of radius equal to 1 (i.e. a 3x3 square
for 2D images, a 3x3x3 cube for 3D images, etc.)
offset : ndarray, optional
The coordinates of the center of the structuring element.
Default is located on the geometrical center of the selem, in that case
selem dimensions must be odd.
Returns
-------
reconstructed : ndarray
The result of morphological reconstruction.
Examples
--------
>>> import numpy as np
>>> from skimage.morphology import reconstruction
First, we create a sinusoidal mask image with peaks at middle and ends.
>>> x = np.linspace(0, 4 * np.pi)
>>> y_mask = np.cos(x)
Then, we create a seed image initialized to the minimum mask value (for
reconstruction by dilation, min-intensity values don't spread) and add
"seeds" to the left and right peak, but at a fraction of peak value (1).
>>> y_seed = y_mask.min() * np.ones_like(x)
>>> y_seed[0] = 0.5
>>> y_seed[-1] = 0
>>> y_rec = reconstruction(y_seed, y_mask)
The reconstructed image (or curve, in this case) is exactly the same as the
mask image, except that the peaks are truncated to 0.5 and 0. The middle
peak disappears completely: Since there were no seed values in this peak
region, its reconstructed value is truncated to the surrounding value (-1).
As a more practical example, we try to extract the bright features of an
image by subtracting a background image created by reconstruction.
>>> y, x = np.mgrid[:20:0.5, :20:0.5]
>>> bumps = np.sin(x) + np.sin(y)
To create the background image, set the mask image to the original image,
and the seed image to the original image with an intensity offset, `h`.
>>> h = 0.3
>>> seed = bumps - h
>>> background = reconstruction(seed, bumps)
The resulting reconstructed image looks exactly like the original image,
but with the peaks of the bumps cut off. Subtracting this reconstructed
image from the original image leaves just the peaks of the bumps
>>> hdome = bumps - background
This operation is known as the h-dome of the image and leaves features
of height `h` in the subtracted image.
Notes
-----
The algorithm is taken from [1]_. Applications for greyscale reconstruction
are discussed in [2]_ and [3]_.
References
----------
.. [1] Robinson, "Efficient morphological reconstruction: a downhill
filter", Pattern Recognition Letters 25 (2004) 1759-1767.
.. [2] Vincent, L., "Morphological Grayscale Reconstruction in Image
Analysis: Applications and Efficient Algorithms", IEEE Transactions
on Image Processing (1993)
.. [3] Soille, P., "Morphological Image Analysis: Principles and
Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883.
"""
assert tuple(seed.shape) == tuple(mask.shape)
if method == 'dilation' and np.any(seed > mask):
raise ValueError("Intensity of seed image must be less than that "
"of the mask image for reconstruction by dilation.")
elif method == 'erosion' and np.any(seed < mask):
raise ValueError("Intensity of seed image must be greater than that "
"of the mask image for reconstruction by erosion.")
try:
from ._greyreconstruct import reconstruction_loop
except ImportError:
raise ImportError("_greyreconstruct extension not available.")
if selem is None:
selem = np.ones([3] * seed.ndim, dtype=bool)
else:
selem = selem.astype(bool)
if offset is None:
if not all([d % 2 == 1 for d in selem.shape]):
raise ValueError("Footprint dimensions must all be odd")
offset = np.array([d // 2 for d in selem.shape])
else:
if offset.ndim != selem.ndim:
raise ValueError("Offset and selem ndims must be equal.")
if not all([(0 <= o < d) for o, d in zip(offset, selem.shape)]):
raise ValueError("Offset must be included inside selem")
# Cross out the center of the selem
selem[tuple(slice(d, d + 1) for d in offset)] = False
# Make padding for edges of reconstructed image so we can ignore boundaries
dims = np.zeros(seed.ndim + 1, dtype=int)
dims[1:] = np.array(seed.shape) + (np.array(selem.shape) - 1)
dims[0] = 2
inside_slices = tuple(slice(o, o + s) for o, s in zip(offset, seed.shape))
# Set padded region to minimum image intensity and mask along first axis so
# we can interleave image and mask pixels when sorting.
if method == 'dilation':
pad_value = np.min(seed)
elif method == 'erosion':
pad_value = np.max(seed)
else:
raise ValueError("Reconstruction method can be one of 'erosion' "
"or 'dilation'. Got '%s'." % method)
images = np.full(dims, pad_value, dtype='float64')
images[(0, *inside_slices)] = seed
images[(1, *inside_slices)] = mask
# Create a list of strides across the array to get the neighbors within
# a flattened array
value_stride = np.array(images.strides[1:]) // images.dtype.itemsize
image_stride = images.strides[0] // images.dtype.itemsize
selem_mgrid = np.mgrid[[slice(-o, d - o)
for d, o in zip(selem.shape, offset)]]
selem_offsets = selem_mgrid[:, selem].transpose()
nb_strides = np.array([np.sum(value_stride * selem_offset)
for selem_offset in selem_offsets], np.int32)
images = images.flatten()
# Erosion goes smallest to largest; dilation goes largest to smallest.
index_sorted = np.argsort(images).astype(np.int32)
if method == 'dilation':
index_sorted = index_sorted[::-1]
# Make a linked list of pixels sorted by value. -1 is the list terminator.
prev = np.full(len(images), -1, np.int32)
next = np.full(len(images), -1, np.int32)
prev[index_sorted[1:]] = index_sorted[:-1]
next[index_sorted[:-1]] = index_sorted[1:]
# Cython inner-loop compares the rank of pixel values.
if method == 'dilation':
value_rank, value_map = rank_order(images)
elif method == 'erosion':
value_rank, value_map = rank_order(-images)
value_map = -value_map
start = index_sorted[0]
reconstruction_loop(value_rank, prev, next, nb_strides, start,
image_stride)
# Reshape reconstructed image to original image shape and remove padding.
rec_img = value_map[value_rank[:image_stride]]
rec_img.shape = np.array(seed.shape) + (np.array(selem.shape) - 1)
return rec_img[inside_slices]

View file

@ -0,0 +1,670 @@
"""max_tree.py - max_tree representation of images.
This module provides operators based on the max-tree representation of images.
A grayscale image can be seen as a pile of nested sets, each of which is the
result of a threshold operation. These sets can be efficiently represented by
max-trees, where the inclusion relation between connected components at
different levels are represented by parent-child relationships.
These representations allow efficient implementations of many algorithms, such
as attribute operators. Unlike morphological openings and closings, these
operators do not require a fixed structuring element, but rather act with a
flexible structuring element that meets a certain criterion.
This implementation provides functions for:
1. max-tree generation
2. area openings / closings
3. diameter openings / closings
4. local maxima
References:
.. [1] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive
Connected Operators for Image and Sequence Processing.
IEEE Transactions on Image Processing, 7(4), 555-570.
:DOI:10.1109/83.663500
.. [2] Berger, C., Geraud, T., Levillain, R., Widynski, N., Baillard, A.,
Bertin, E. (2007). Effective Component Tree Computation with
Application to Pattern Recognition in Astronomical Imaging.
In International Conference on Image Processing (ICIP) (pp. 41-44).
:DOI:10.1109/ICIP.2007.4379949
.. [3] Najman, L., & Couprie, M. (2006). Building the component tree in
quasi-linear time. IEEE Transactions on Image Processing, 15(11),
3531-3539.
:DOI:10.1109/TIP.2006.877518
.. [4] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:10.1109/TIP.2014.2336551
"""
import numpy as np
from ._util import _validate_connectivity, _offsets_to_raveled_neighbors
from ..util import invert
from . import _max_tree
unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint64]
signed_int_types = [np.int8, np.int16, np.int32, np.int64]
signed_float_types = [np.float16, np.float32, np.float64]
# building the max tree.
def max_tree(image, connectivity=1):
"""Build the max tree from an image.
Component trees represent the hierarchical structure of the connected
components resulting from sequential thresholding operations applied to an
image. A connected component at one level is parent of a component at a
higher level if the latter is included in the first. A max-tree is an
efficient representation of a component tree. A connected component at
one level is represented by one reference pixel at this level, which is
parent to all other pixels at that level and to the reference pixel at the
level above. The max-tree is the basis for many morphological operators,
namely connected operators.
Parameters
----------
image : ndarray
The input image for which the max-tree is to be calculated.
This image can be of any type.
connectivity : unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
Returns
-------
parent : ndarray, int64
Array of same shape as image. The value of each pixel is the index of
its parent in the ravelled array.
tree_traverser : 1D array, int64
The ordered pixel indices (referring to the ravelled array). The pixels
are ordered such that every pixel is preceded by its parent (except for
the root which has no parent).
References
----------
.. [1] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive
Connected Operators for Image and Sequence Processing.
IEEE Transactions on Image Processing, 7(4), 555-570.
:DOI:`10.1109/83.663500`
.. [2] Berger, C., Geraud, T., Levillain, R., Widynski, N., Baillard, A.,
Bertin, E. (2007). Effective Component Tree Computation with
Application to Pattern Recognition in Astronomical Imaging.
In International Conference on Image Processing (ICIP) (pp. 41-44).
:DOI:`10.1109/ICIP.2007.4379949`
.. [3] Najman, L., & Couprie, M. (2006). Building the component tree in
quasi-linear time. IEEE Transactions on Image Processing, 15(11),
3531-3539.
:DOI:`10.1109/TIP.2006.877518`
.. [4] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:`10.1109/TIP.2014.2336551`
Examples
--------
We create a small sample image (Figure 1 from [4]) and build the max-tree.
>>> image = np.array([[15, 13, 16], [12, 12, 10], [16, 12, 14]])
>>> P, S = max_tree(image, connectivity=2)
"""
# User defined masks are not allowed, as there might be more than one
# connected component in the mask (and therefore not a single tree that
# represents the image). Mask here is an image that is 0 on the border
# and 1 everywhere else.
mask = np.ones(image.shape)
for k in range(len(image.shape)):
np.moveaxis(mask, k, 0)[0] = 0
np.moveaxis(mask, k, 0)[-1] = 0
neighbors, offset = _validate_connectivity(image.ndim, connectivity,
offset=None)
# initialization of the parent image
parent = np.zeros(image.shape, dtype=np.int64)
# flat_neighborhood contains a list of offsets allowing one to find the
# neighbors in the ravelled image.
flat_neighborhood = _offsets_to_raveled_neighbors(image.shape, neighbors,
offset).astype(np.int32)
# pixels need to be sorted according to their gray level.
tree_traverser = np.argsort(image.ravel()).astype(np.int64)
# call of cython function.
_max_tree._max_tree(image.ravel(), mask.ravel().astype(np.uint8),
flat_neighborhood, offset.astype(np.int32),
np.array(image.shape, dtype=np.int32),
parent.ravel(), tree_traverser)
return parent, tree_traverser
def area_opening(image, area_threshold=64, connectivity=1,
parent=None, tree_traverser=None):
"""Perform an area opening of the image.
Area opening removes all bright structures of an image with
a surface smaller than area_threshold.
The output image is thus the largest image smaller than the input
for which all local maxima have at least a surface of
area_threshold pixels.
Area openings are similar to morphological openings, but
they do not use a fixed structuring element, but rather a deformable
one, with surface = area_threshold. Consequently, the area_opening
with area_threshold=1 is the identity.
In the binary case, area openings are equivalent to
remove_small_objects; this operator is thus extended to gray-level images.
Technically, this operator is based on the max-tree representation of
the image.
Parameters
----------
image : ndarray
The input image for which the area_opening is to be calculated.
This image can be of any type.
area_threshold : unsigned int
The size parameter (number of pixels). The default value is arbitrarily
chosen to be 64.
connectivity : unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
parent : ndarray, int64, optional
Parent image representing the max tree of the image. The
value of each pixel is the index of its parent in the ravelled array.
tree_traverser : 1D array, int64, optional
The ordered pixel indices (referring to the ravelled array). The pixels
are ordered such that every pixel is preceded by its parent (except for
the root which has no parent).
Returns
-------
output : ndarray
Output image of the same shape and type as the input image.
See also
--------
skimage.morphology.area_closing
skimage.morphology.diameter_opening
skimage.morphology.diameter_closing
skimage.morphology.max_tree
skimage.morphology.remove_small_objects
skimage.morphology.remove_small_holes
References
----------
.. [1] Vincent L., Proc. "Grayscale area openings and closings,
their efficient implementation and applications",
EURASIP Workshop on Mathematical Morphology and its
Applications to Signal Processing, Barcelona, Spain, pp.22-27,
May 1993.
.. [2] Soille, P., "Morphological Image Analysis: Principles and
Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883.
DOI:10.1007/978-3-662-05088-0
.. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive
Connected Operators for Image and Sequence Processing.
IEEE Transactions on Image Processing, 7(4), 555-570.
DOI:10.1109/83.663500
.. [4] Najman, L., & Couprie, M. (2006). Building the component tree in
quasi-linear time. IEEE Transactions on Image Processing, 15(11),
3531-3539.
DOI:10.1109/TIP.2006.877518
.. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
DOI:10.1109/TIP.2014.2336551
Examples
--------
We create an image (quadratic function with a maximum in the center and
4 additional local maxima.
>>> w = 12
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:3,1:5] = 40; f[2:4,9:11] = 60; f[9:11,2:4] = 80
>>> f[9:10,9:11] = 100; f[10,10] = 100
>>> f = f.astype(np.int)
We can calculate the area opening:
>>> open = area_opening(f, 8, connectivity=1)
The peaks with a surface smaller than 8 are removed.
"""
output = image.copy()
if parent is None or tree_traverser is None:
parent, tree_traverser = max_tree(image, connectivity)
area = _max_tree._compute_area(image.ravel(),
parent.ravel(), tree_traverser)
_max_tree._direct_filter(image.ravel(), output.ravel(), parent.ravel(),
tree_traverser, area, area_threshold)
return output
def diameter_opening(image, diameter_threshold=8, connectivity=1,
parent=None, tree_traverser=None):
"""Perform a diameter opening of the image.
Diameter opening removes all bright structures of an image with
maximal extension smaller than diameter_threshold. The maximal
extension is defined as the maximal extension of the bounding box.
The operator is also called Bounding Box Opening. In practice,
the result is similar to a morphological opening, but long and thin
structures are not removed.
Technically, this operator is based on the max-tree representation of
the image.
Parameters
----------
image : ndarray
The input image for which the area_opening is to be calculated.
This image can be of any type.
diameter_threshold : unsigned int
The maximal extension parameter (number of pixels). The default value
is 8.
connectivity : unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
parent : ndarray, int64, optional
Parent image representing the max tree of the image. The
value of each pixel is the index of its parent in the ravelled array.
tree_traverser : 1D array, int64, optional
The ordered pixel indices (referring to the ravelled array). The pixels
are ordered such that every pixel is preceded by its parent (except for
the root which has no parent).
Returns
-------
output : ndarray
Output image of the same shape and type as the input image.
See also
--------
skimage.morphology.area_opening
skimage.morphology.area_closing
skimage.morphology.diameter_closing
skimage.morphology.max_tree
References
----------
.. [1] Walter, T., & Klein, J.-C. (2002). Automatic Detection of
Microaneurysms in Color Fundus Images of the Human Retina by Means
of the Bounding Box Closing. In A. Colosimo, P. Sirabella,
A. Giuliani (Eds.), Medical Data Analysis. Lecture Notes in Computer
Science, vol 2526, pp. 210-220. Springer Berlin Heidelberg.
:DOI:`10.1007/3-540-36104-9_23`
.. [2] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:`10.1109/TIP.2014.2336551`
Examples
--------
We create an image (quadratic function with a maximum in the center and
4 additional local maxima.
>>> w = 12
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:3,1:5] = 40; f[2:4,9:11] = 60; f[9:11,2:4] = 80
>>> f[9:10,9:11] = 100; f[10,10] = 100
>>> f = f.astype(np.int)
We can calculate the diameter opening:
>>> open = diameter_opening(f, 3, connectivity=1)
The peaks with a maximal extension of 2 or less are removed.
The remaining peaks have all a maximal extension of at least 3.
"""
output = image.copy()
if parent is None or tree_traverser is None:
parent, tree_traverser = max_tree(image, connectivity)
diam = _max_tree._compute_extension(image.ravel(),
np.array(image.shape, dtype=np.int32),
parent.ravel(), tree_traverser)
_max_tree._direct_filter(image.ravel(), output.ravel(), parent.ravel(),
tree_traverser, diam, diameter_threshold)
return output
def area_closing(image, area_threshold=64, connectivity=1,
parent=None, tree_traverser=None):
"""Perform an area closing of the image.
Area closing removes all dark structures of an image with
a surface smaller than area_threshold.
The output image is larger than or equal to the input image
for every pixel and all local minima have at least a surface of
area_threshold pixels.
Area closings are similar to morphological closings, but
they do not use a fixed structuring element, but rather a deformable
one, with surface = area_threshold.
In the binary case, area closings are equivalent to
remove_small_holes; this operator is thus extended to gray-level images.
Technically, this operator is based on the max-tree representation of
the image.
Parameters
----------
image : ndarray
The input image for which the area_closing is to be calculated.
This image can be of any type.
area_threshold : unsigned int
The size parameter (number of pixels). The default value is arbitrarily
chosen to be 64.
connectivity : unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
parent : ndarray, int64, optional
Parent image representing the max tree of the inverted image. The
value of each pixel is the index of its parent in the ravelled array.
See Note for further details.
tree_traverser : 1D array, int64, optional
The ordered pixel indices (referring to the ravelled array). The pixels
are ordered such that every pixel is preceded by its parent (except for
the root which has no parent).
Returns
-------
output : ndarray
Output image of the same shape and type as input image.
See also
--------
skimage.morphology.area_opening
skimage.morphology.diameter_opening
skimage.morphology.diameter_closing
skimage.morphology.max_tree
skimage.morphology.remove_small_objects
skimage.morphology.remove_small_holes
References
----------
.. [1] Vincent L., Proc. "Grayscale area openings and closings,
their efficient implementation and applications",
EURASIP Workshop on Mathematical Morphology and its
Applications to Signal Processing, Barcelona, Spain, pp.22-27,
May 1993.
.. [2] Soille, P., "Morphological Image Analysis: Principles and
Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883.
:DOI:`10.1007/978-3-662-05088-0`
.. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive
Connected Operators for Image and Sequence Processing.
IEEE Transactions on Image Processing, 7(4), 555-570.
:DOI:`10.1109/83.663500`
.. [4] Najman, L., & Couprie, M. (2006). Building the component tree in
quasi-linear time. IEEE Transactions on Image Processing, 15(11),
3531-3539.
:DOI:`10.1109/TIP.2006.877518`
.. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:`10.1109/TIP.2014.2336551`
Examples
--------
We create an image (quadratic function with a minimum in the center and
4 additional local minima.
>>> w = 12
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:3,1:5] = 160; f[2:4,9:11] = 140; f[9:11,2:4] = 120
>>> f[9:10,9:11] = 100; f[10,10] = 100
>>> f = f.astype(np.int)
We can calculate the area closing:
>>> closed = area_closing(f, 8, connectivity=1)
All small minima are removed, and the remaining minima have at least
a size of 8.
Notes
-----
If a max-tree representation (parent and tree_traverser) are given to the
function, they must be calculated from the inverted image for this
function, i.e.:
>>> P, S = max_tree(invert(f))
>>> closed = diameter_closing(f, 3, parent=P, tree_traverser=S)
"""
# inversion of the input image
image_inv = invert(image)
output = image_inv.copy()
if parent is None or tree_traverser is None:
parent, tree_traverser = max_tree(image_inv, connectivity)
area = _max_tree._compute_area(image_inv.ravel(),
parent.ravel(), tree_traverser)
_max_tree._direct_filter(image_inv.ravel(), output.ravel(), parent.ravel(),
tree_traverser, area, area_threshold)
# inversion of the output image
output = invert(output)
return output
def diameter_closing(image, diameter_threshold=8, connectivity=1,
parent=None, tree_traverser=None):
"""Perform a diameter closing of the image.
Diameter closing removes all dark structures of an image with
maximal extension smaller than diameter_threshold. The maximal
extension is defined as the maximal extension of the bounding box.
The operator is also called Bounding Box Closing. In practice,
the result is similar to a morphological closing, but long and thin
structures are not removed.
Technically, this operator is based on the max-tree representation of
the image.
Parameters
----------
image : ndarray
The input image for which the diameter_closing is to be calculated.
This image can be of any type.
diameter_threshold : unsigned int
The maximal extension parameter (number of pixels). The default value
is 8.
connectivity : unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
parent : ndarray, int64, optional
Precomputed parent image representing the max tree of the inverted
image. This function is fast, if precomputed parent and tree_traverser
are provided. See Note for further details.
tree_traverser : 1D array, int64, optional
Precomputed traverser, where the pixels are ordered such that every
pixel is preceded by its parent (except for the root which has no
parent). This function is fast, if precomputed parent and
tree_traverser are provided. See Note for further details.
Returns
-------
output : ndarray
Output image of the same shape and type as input image.
See also
--------
skimage.morphology.area_opening
skimage.morphology.area_closing
skimage.morphology.diameter_opening
skimage.morphology.max_tree
References
----------
.. [1] Walter, T., & Klein, J.-C. (2002). Automatic Detection of
Microaneurysms in Color Fundus Images of the Human Retina by Means
of the Bounding Box Closing. In A. Colosimo, P. Sirabella,
A. Giuliani (Eds.), Medical Data Analysis. Lecture Notes in Computer
Science, vol 2526, pp. 210-220. Springer Berlin Heidelberg.
:DOI:`10.1007/3-540-36104-9_23`
.. [2] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:`10.1109/TIP.2014.2336551`
Examples
--------
We create an image (quadratic function with a minimum in the center and
4 additional local minima.
>>> w = 12
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:3,1:5] = 160; f[2:4,9:11] = 140; f[9:11,2:4] = 120
>>> f[9:10,9:11] = 100; f[10,10] = 100
>>> f = f.astype(np.int)
We can calculate the diameter closing:
>>> closed = diameter_closing(f, 3, connectivity=1)
All small minima with a maximal extension of 2 or less are removed.
The remaining minima have all a maximal extension of at least 3.
Notes
-----
If a max-tree representation (parent and tree_traverser) are given to the
function, they must be calculated from the inverted image for this
function, i.e.:
>>> P, S = max_tree(invert(f))
>>> closed = diameter_closing(f, 3, parent=P, tree_traverser=S)
"""
# inversion of the input image
image_inv = invert(image)
output = image_inv.copy()
if parent is None or tree_traverser is None:
parent, tree_traverser = max_tree(image_inv, connectivity)
diam = _max_tree._compute_extension(image_inv.ravel(),
np.array(image_inv.shape,
dtype=np.int32),
parent.ravel(), tree_traverser)
_max_tree._direct_filter(image_inv.ravel(), output.ravel(), parent.ravel(),
tree_traverser, diam, diameter_threshold)
output = invert(output)
return output
def max_tree_local_maxima(image, connectivity=1,
parent=None, tree_traverser=None):
"""Determine all local maxima of the image.
The local maxima are defined as connected sets of pixels with equal
gray level strictly greater than the gray levels of all pixels in direct
neighborhood of the set. The function labels the local maxima.
Technically, the implementation is based on the max-tree representation
of an image. The function is very efficient if the max-tree representation
has already been computed. Otherwise, it is preferable to use
the function local_maxima.
Parameters
----------
image : ndarray
The input image for which the maxima are to be calculated.
connectivity: unsigned int, optional
The neighborhood connectivity. The integer represents the maximum
number of orthogonal steps to reach a neighbor. In 2D, it is 1 for
a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1.
parent: ndarray, int64, optional
The value of each pixel is the index of its parent in the ravelled
array.
tree_traverser: 1D array, int64, optional
The ordered pixel indices (referring to the ravelled array). The pixels
are ordered such that every pixel is preceded by its parent (except for
the root which has no parent).
Returns
-------
local_max : ndarray, uint64
Labeled local maxima of the image.
See also
--------
skimage.morphology.local_maxima
skimage.morphology.max_tree
References
----------
.. [1] Vincent L., Proc. "Grayscale area openings and closings,
their efficient implementation and applications",
EURASIP Workshop on Mathematical Morphology and its
Applications to Signal Processing, Barcelona, Spain, pp.22-27,
May 1993.
.. [2] Soille, P., "Morphological Image Analysis: Principles and
Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883.
:DOI:`10.1007/978-3-662-05088-0`
.. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive
Connected Operators for Image and Sequence Processing.
IEEE Transactions on Image Processing, 7(4), 555-570.
:DOI:`10.1109/83.663500`
.. [4] Najman, L., & Couprie, M. (2006). Building the component tree in
quasi-linear time. IEEE Transactions on Image Processing, 15(11),
3531-3539.
:DOI:`10.1109/TIP.2006.877518`
.. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of
Component Tree Computation Algorithms. IEEE Transactions on Image
Processing, 23(9), 3885-3895.
:DOI:`10.1109/TIP.2014.2336551`
Examples
--------
We create an image (quadratic function with a maximum in the center and
4 additional constant maxima.
>>> w = 10
>>> x, y = np.mgrid[0:w,0:w]
>>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2)
>>> f[2:4,2:4] = 40; f[2:4,7:9] = 60; f[7:9,2:4] = 80; f[7:9,7:9] = 100
>>> f = f.astype(np.int)
We can calculate all local maxima:
>>> maxima = max_tree_local_maxima(f)
The resulting image contains the labeled local maxima.
"""
output = np.ones(image.shape, dtype=np.uint64)
if parent is None or tree_traverser is None:
parent, tree_traverser = max_tree(image, connectivity)
_max_tree._max_tree_local_maxima(image.ravel(), output.ravel(),
parent.ravel(), tree_traverser)
return output

View file

@ -0,0 +1,227 @@
"""Miscellaneous morphology functions."""
import numpy as np
import functools
from scipy import ndimage as ndi
from .._shared.utils import warn
from .selem import _default_selem
# Our function names don't exactly correspond to ndimages.
# This dictionary translates from our names to scipy's.
funcs = ('erosion', 'dilation', 'opening', 'closing')
skimage2ndimage = {x: 'grey_' + x for x in funcs}
# These function names are the same in ndimage.
funcs = ('binary_erosion', 'binary_dilation', 'binary_opening',
'binary_closing', 'black_tophat', 'white_tophat')
skimage2ndimage.update({x: x for x in funcs})
def default_selem(func):
"""Decorator to add a default structuring element to morphology functions.
Parameters
----------
func : function
A morphology function such as erosion, dilation, opening, closing,
white_tophat, or black_tophat.
Returns
-------
func_out : function
The function, using a default structuring element of same dimension
as the input image with connectivity 1.
"""
@functools.wraps(func)
def func_out(image, selem=None, *args, **kwargs):
if selem is None:
selem = _default_selem(image.ndim)
return func(image, selem=selem, *args, **kwargs)
return func_out
def _check_dtype_supported(ar):
# Should use `issubdtype` for bool below, but there's a bug in numpy 1.7
if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)):
raise TypeError("Only bool or integer image types are supported. "
"Got %s." % ar.dtype)
def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False):
"""Remove objects smaller than the specified size.
Expects ar to be an array with labeled objects, and removes objects
smaller than min_size. If `ar` is bool, the image is first labeled.
This leads to potentially different behavior for bool and 0-and-1
arrays.
Parameters
----------
ar : ndarray (arbitrary shape, int or bool type)
The array containing the objects of interest. If the array type is
int, the ints must be non-negative.
min_size : int, optional (default: 64)
The smallest allowable object size.
connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1)
The connectivity defining the neighborhood of a pixel. Used during
labelling if `ar` is bool.
in_place : bool, optional (default: False)
If ``True``, remove the objects in the input array itself.
Otherwise, make a copy.
Raises
------
TypeError
If the input array is of an invalid type, such as float or string.
ValueError
If the input array contains negative values.
Returns
-------
out : ndarray, same shape and type as input `ar`
The input array with small connected components removed.
Examples
--------
>>> from skimage import morphology
>>> a = np.array([[0, 0, 0, 1, 0],
... [1, 1, 1, 0, 0],
... [1, 1, 1, 0, 1]], bool)
>>> b = morphology.remove_small_objects(a, 6)
>>> b
array([[False, False, False, False, False],
[ True, True, True, False, False],
[ True, True, True, False, False]])
>>> c = morphology.remove_small_objects(a, 7, connectivity=2)
>>> c
array([[False, False, False, True, False],
[ True, True, True, False, False],
[ True, True, True, False, False]])
>>> d = morphology.remove_small_objects(a, 6, in_place=True)
>>> d is a
True
"""
# Raising type error if not int or bool
_check_dtype_supported(ar)
if in_place:
out = ar
else:
out = ar.copy()
if min_size == 0: # shortcut for efficiency
return out
if out.dtype == bool:
selem = ndi.generate_binary_structure(ar.ndim, connectivity)
ccs = np.zeros_like(ar, dtype=np.int32)
ndi.label(ar, selem, output=ccs)
else:
ccs = out
try:
component_sizes = np.bincount(ccs.ravel())
except ValueError:
raise ValueError("Negative value labels are not supported. Try "
"relabeling the input with `scipy.ndimage.label` or "
"`skimage.morphology.label`.")
if len(component_sizes) == 2 and out.dtype != bool:
warn("Only one label was provided to `remove_small_objects`. "
"Did you mean to use a boolean array?")
too_small = component_sizes < min_size
too_small_mask = too_small[ccs]
out[too_small_mask] = 0
return out
def remove_small_holes(ar, area_threshold=64, connectivity=1, in_place=False):
"""Remove contiguous holes smaller than the specified size.
Parameters
----------
ar : ndarray (arbitrary shape, int or bool type)
The array containing the connected components of interest.
area_threshold : int, optional (default: 64)
The maximum area, in pixels, of a contiguous hole that will be filled.
Replaces `min_size`.
connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1)
The connectivity defining the neighborhood of a pixel.
in_place : bool, optional (default: False)
If `True`, remove the connected components in the input array itself.
Otherwise, make a copy.
Raises
------
TypeError
If the input array is of an invalid type, such as float or string.
ValueError
If the input array contains negative values.
Returns
-------
out : ndarray, same shape and type as input `ar`
The input array with small holes within connected components removed.
Examples
--------
>>> from skimage import morphology
>>> a = np.array([[1, 1, 1, 1, 1, 0],
... [1, 1, 1, 0, 1, 0],
... [1, 0, 0, 1, 1, 0],
... [1, 1, 1, 1, 1, 0]], bool)
>>> b = morphology.remove_small_holes(a, 2)
>>> b
array([[ True, True, True, True, True, False],
[ True, True, True, True, True, False],
[ True, False, False, True, True, False],
[ True, True, True, True, True, False]])
>>> c = morphology.remove_small_holes(a, 2, connectivity=2)
>>> c
array([[ True, True, True, True, True, False],
[ True, True, True, False, True, False],
[ True, False, False, True, True, False],
[ True, True, True, True, True, False]])
>>> d = morphology.remove_small_holes(a, 2, in_place=True)
>>> d is a
True
Notes
-----
If the array type is int, it is assumed that it contains already-labeled
objects. The labels are not kept in the output image (this function always
outputs a bool image). It is suggested that labeling is completed after
using this function.
"""
_check_dtype_supported(ar)
# Creates warning if image is an integer image
if ar.dtype != bool:
warn("Any labeled images will be returned as a boolean array. "
"Did you mean to use a boolean array?", UserWarning)
if in_place:
out = ar
else:
out = ar.copy()
# Creating the inverse of ar
if in_place:
np.logical_not(out, out=out)
else:
out = np.logical_not(out)
# removing small objects from the inverse of ar
out = remove_small_objects(out, area_threshold, connectivity, in_place)
if in_place:
np.logical_not(out, out=out)
else:
out = np.logical_not(out)
return out

View file

@ -0,0 +1,359 @@
import numpy as np
from scipy import ndimage as ndi
from .. import draw
def square(width, dtype=np.uint8):
"""Generates a flat, square-shaped structuring element.
Every pixel along the perimeter has a chessboard distance
no greater than radius (radius=floor(width/2)) pixels.
Parameters
----------
width : int
The width and height of the square.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
A structuring element consisting only of ones, i.e. every
pixel belongs to the neighborhood.
"""
return np.ones((width, width), dtype=dtype)
def rectangle(width, height, dtype=np.uint8):
"""Generates a flat, rectangular-shaped structuring element.
Every pixel in the rectangle generated for a given width and given height
belongs to the neighborhood.
Parameters
----------
width : int
The width of the rectangle.
height : int
The height of the rectangle.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
A structuring element consisting only of ones, i.e. every
pixel belongs to the neighborhood.
"""
return np.ones((width, height), dtype=dtype)
def diamond(radius, dtype=np.uint8):
"""Generates a flat, diamond-shaped structuring element.
A pixel is part of the neighborhood (i.e. labeled 1) if
the city block/Manhattan distance between it and the center of
the neighborhood is no greater than radius.
Parameters
----------
radius : int
The radius of the diamond-shaped structuring element.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
L = np.arange(0, radius * 2 + 1)
I, J = np.meshgrid(L, L)
return np.array(np.abs(I - radius) + np.abs(J - radius) <= radius,
dtype=dtype)
def disk(radius, dtype=np.uint8):
"""Generates a flat, disk-shaped structuring element.
A pixel is within the neighborhood if the Euclidean distance between
it and the origin is no greater than radius.
Parameters
----------
radius : int
The radius of the disk-shaped structuring element.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
L = np.arange(-radius, radius + 1)
X, Y = np.meshgrid(L, L)
return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype)
def ellipse(width, height, dtype=np.uint8):
"""Generates a flat, ellipse-shaped structuring element.
Every pixel along the perimeter of ellipse satisfies
the equation ``(x/width+1)**2 + (y/height+1)**2 = 1``.
Parameters
----------
width : int
The width of the ellipse-shaped structuring element.
height : int
The height of the ellipse-shaped structuring element.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
Examples
--------
>>> from skimage.morphology import selem
>>> selem.ellipse(5, 3)
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, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=uint8)
"""
selem = np.zeros((2 * height + 1, 2 * width + 1), dtype=dtype)
rows, cols = draw.ellipse(height, width, height + 1, width + 1)
selem[rows, cols] = 1
return selem
def cube(width, dtype=np.uint8):
""" Generates a cube-shaped structuring element.
This is the 3D equivalent of a square.
Every pixel along the perimeter has a chessboard distance
no greater than radius (radius=floor(width/2)) pixels.
Parameters
----------
width : int
The width, height and depth of the cube.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
A structuring element consisting only of ones, i.e. every
pixel belongs to the neighborhood.
"""
return np.ones((width, width, width), dtype=dtype)
def octahedron(radius, dtype=np.uint8):
"""Generates a octahedron-shaped structuring element.
This is the 3D equivalent of a diamond.
A pixel is part of the neighborhood (i.e. labeled 1) if
the city block/Manhattan distance between it and the center of
the neighborhood is no greater than radius.
Parameters
----------
radius : int
The radius of the octahedron-shaped structuring element.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
# note that in contrast to diamond(), this method allows non-integer radii
n = 2 * radius + 1
Z, Y, X = np.mgrid[-radius:radius:n * 1j,
-radius:radius:n * 1j,
-radius:radius:n * 1j]
s = np.abs(X) + np.abs(Y) + np.abs(Z)
return np.array(s <= radius, dtype=dtype)
def ball(radius, dtype=np.uint8):
"""Generates a ball-shaped structuring element.
This is the 3D equivalent of a disk.
A pixel is within the neighborhood if the Euclidean distance between
it and the origin is no greater than radius.
Parameters
----------
radius : int
The radius of the ball-shaped structuring element.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
n = 2 * radius + 1
Z, Y, X = np.mgrid[-radius:radius:n * 1j,
-radius:radius:n * 1j,
-radius:radius:n * 1j]
s = X ** 2 + Y ** 2 + Z ** 2
return np.array(s <= radius * radius, dtype=dtype)
def octagon(m, n, dtype=np.uint8):
"""Generates an octagon shaped structuring element.
For a given size of (m) horizontal and vertical sides
and a given (n) height or width of slanted sides octagon is generated.
The slanted sides are 45 or 135 degrees to the horizontal axis
and hence the widths and heights are equal.
Parameters
----------
m : int
The size of the horizontal and vertical sides.
n : int
The height or width of the slanted sides.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
from . import convex_hull_image
selem = np.zeros((m + 2 * n, m + 2 * n))
selem[0, n] = 1
selem[n, 0] = 1
selem[0, m + n - 1] = 1
selem[m + n - 1, 0] = 1
selem[-1, n] = 1
selem[n, -1] = 1
selem[-1, m + n - 1] = 1
selem[m + n - 1, -1] = 1
selem = convex_hull_image(selem).astype(dtype)
return selem
def star(a, dtype=np.uint8):
"""Generates a star shaped structuring element.
Start has 8 vertices and is an overlap of square of size `2*a + 1`
with its 45 degree rotated version.
The slanted sides are 45 or 135 degrees to the horizontal axis.
Parameters
----------
a : int
Parameter deciding the size of the star structural element. The side
of the square array returned is `2*a + 1 + 2*floor(a / 2)`.
Other Parameters
----------------
dtype : data-type
The data type of the structuring element.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
from . import convex_hull_image
if a == 1:
bfilter = np.zeros((3, 3), dtype)
bfilter[:] = 1
return bfilter
m = 2 * a + 1
n = a // 2
selem_square = np.zeros((m + 2 * n, m + 2 * n))
selem_square[n: m + n, n: m + n] = 1
c = (m + 2 * n - 1) // 2
selem_rotated = np.zeros((m + 2 * n, m + 2 * n))
selem_rotated[0, c] = selem_rotated[-1, c] = 1
selem_rotated[c, 0] = selem_rotated[c, -1] = 1
selem_rotated = convex_hull_image(selem_rotated).astype(int)
selem = selem_square + selem_rotated
selem[selem > 0] = 1
return selem.astype(dtype)
def _default_selem(ndim):
"""Generates a cross-shaped structuring element (connectivity=1).
This is the default structuring element (selem) if no selem was specified.
Parameters
----------
ndim : int
Number of dimensions of the image.
Returns
-------
selem : ndarray
The structuring element where elements of the neighborhood
are 1 and 0 otherwise.
"""
return ndi.morphology.generate_binary_structure(ndim, 1)

View file

@ -0,0 +1,52 @@
#!/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('morphology', parent_package, top_path)
cython(['_skeletonize_cy.pyx',
'_convex_hull.pyx',
'_greyreconstruct.pyx',
'_extrema_cy.pyx'], working_path=base_path)
# _skeletonize_3d uses c++, so it must be cythonized separately
cython(['_skeletonize_3d_cy.pyx.in'], working_path=base_path)
cython(['_extrema_cy.pyx'], working_path=base_path)
cython(['_flood_fill_cy.pyx'], working_path=base_path)
cython(['_max_tree.pyx'], working_path=base_path)
config.add_extension('_skeletonize_cy', sources=['_skeletonize_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_convex_hull', sources=['_convex_hull.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_max_tree', sources=['_max_tree.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_skeletonize_3d_cy',
sources=['_skeletonize_3d_cy.cpp'],
include_dirs=[get_numpy_include_dirs()],
language='c++')
config.add_extension('_extrema_cy', sources=['_extrema_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_flood_fill_cy', sources=['_flood_fill_cy.c'],
include_dirs=[get_numpy_include_dirs()])
return config
if __name__ == '__main__':
from numpy.distutils.core import setup
setup(maintainer='scikit-image Developers',
author='Damian Eads',
maintainer_email='scikit-image@python.org',
description='Morphology Wrapper',
url='https://github.com/scikit-image/scikit-image',
license='SciPy License (BSD Style)',
**(configuration(top_path='').todict())
)

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,178 @@
import numpy as np
from numpy import testing
from skimage import data, color
from skimage.util import img_as_bool
from skimage.morphology import binary, grey, selem
from scipy import ndimage as ndi
import pytest
img = color.rgb2gray(data.astronaut())
bw_img = img > 100 / 255.
def test_non_square_image():
strel = selem.square(3)
binary_res = binary.binary_erosion(bw_img[:100, :200], strel)
grey_res = img_as_bool(grey.erosion(bw_img[:100, :200], strel))
testing.assert_array_equal(binary_res, grey_res)
def test_binary_erosion():
strel = selem.square(3)
binary_res = binary.binary_erosion(bw_img, strel)
grey_res = img_as_bool(grey.erosion(bw_img, strel))
testing.assert_array_equal(binary_res, grey_res)
def test_binary_dilation():
strel = selem.square(3)
binary_res = binary.binary_dilation(bw_img, strel)
grey_res = img_as_bool(grey.dilation(bw_img, strel))
testing.assert_array_equal(binary_res, grey_res)
def test_binary_closing():
strel = selem.square(3)
binary_res = binary.binary_closing(bw_img, strel)
grey_res = img_as_bool(grey.closing(bw_img, strel))
testing.assert_array_equal(binary_res, grey_res)
def test_binary_opening():
strel = selem.square(3)
binary_res = binary.binary_opening(bw_img, strel)
grey_res = img_as_bool(grey.opening(bw_img, strel))
testing.assert_array_equal(binary_res, grey_res)
def test_selem_overflow():
strel = np.ones((17, 17), dtype=np.uint8)
img = np.zeros((20, 20), dtype=bool)
img[2:19, 2:19] = True
binary_res = binary.binary_erosion(img, strel)
grey_res = img_as_bool(grey.erosion(img, strel))
testing.assert_array_equal(binary_res, grey_res)
def test_out_argument():
for func in (binary.binary_erosion, binary.binary_dilation):
strel = np.ones((3, 3), dtype=np.uint8)
img = np.ones((10, 10))
out = np.zeros_like(img)
out_saved = out.copy()
func(img, strel, out=out)
testing.assert_(np.any(out != out_saved))
testing.assert_array_equal(out, func(img, strel))
binary_functions = [binary.binary_erosion, binary.binary_dilation,
binary.binary_opening, binary.binary_closing]
@pytest.mark.parametrize("function", binary_functions)
def test_default_selem(function):
strel = selem.diamond(radius=1)
image = 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, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 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]], np.uint8)
im_expected = function(image, strel)
im_test = function(image)
testing.assert_array_equal(im_expected, im_test)
def test_3d_fallback_default_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
opened = binary.binary_opening(image)
# expect a "hyper-cross" centered in the 5x5x5:
image_expected = np.zeros((7, 7, 7), dtype=bool)
image_expected[2:5, 2:5, 2:5] = ndi.generate_binary_structure(3, 1)
testing.assert_array_equal(opened, image_expected)
binary_3d_fallback_functions = [binary.binary_opening, binary.binary_closing]
@pytest.mark.parametrize("function", binary_3d_fallback_functions)
def test_3d_fallback_cube_selem(function):
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
new_image = function(image, cube)
testing.assert_array_equal(new_image, image)
def test_2d_ndimage_equivalence():
image = np.zeros((9, 9), np.uint16)
image[2:-2, 2:-2] = 2**14
image[3:-3, 3:-3] = 2**15
image[4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
selem = ndi.generate_binary_structure(2, 1)
ndimage_opened = ndi.binary_opening(image, structure=selem)
ndimage_closed = ndi.binary_closing(image, structure=selem)
testing.assert_array_equal(bin_opened, ndimage_opened)
testing.assert_array_equal(bin_closed, ndimage_closed)
def test_binary_output_2d():
image = np.zeros((9, 9), np.uint16)
image[2:-2, 2:-2] = 2**14
image[3:-3, 3:-3] = 2**15
image[4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
int_opened = np.empty_like(image, dtype=np.uint8)
int_closed = np.empty_like(image, dtype=np.uint8)
binary.binary_opening(image, out=int_opened)
binary.binary_closing(image, out=int_closed)
testing.assert_equal(bin_opened.dtype, np.bool)
testing.assert_equal(bin_closed.dtype, np.bool)
testing.assert_equal(int_opened.dtype, np.uint8)
testing.assert_equal(int_closed.dtype, np.uint8)
def test_binary_output_3d():
image = np.zeros((9, 9, 9), np.uint16)
image[2:-2, 2:-2, 2:-2] = 2**14
image[3:-3, 3:-3, 3:-3] = 2**15
image[4, 4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
int_opened = np.empty_like(image, dtype=np.uint8)
int_closed = np.empty_like(image, dtype=np.uint8)
binary.binary_opening(image, out=int_opened)
binary.binary_closing(image, out=int_closed)
testing.assert_equal(bin_opened.dtype, np.bool)
testing.assert_equal(bin_closed.dtype, np.bool)
testing.assert_equal(int_opened.dtype, np.uint8)
testing.assert_equal(int_closed.dtype, np.uint8)
if __name__ == '__main__':
testing.run_module_suite()

View file

@ -0,0 +1,313 @@
import numpy as np
from skimage.measure import label
import skimage.measure._ccomp as ccomp
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
from skimage._shared._warnings import expected_warnings
BG = 0 # background value
class TestConnectedComponents:
def setup(self):
self.x = np.array([
[0, 0, 3, 2, 1, 9],
[0, 1, 1, 9, 2, 9],
[0, 0, 1, 9, 9, 9],
[3, 1, 1, 5, 3, 0]])
self.labels = np.array([
[0, 0, 1, 2, 3, 4],
[0, 5, 5, 4, 2, 4],
[0, 0, 5, 4, 4, 4],
[6, 5, 5, 7, 8, 0]])
# No background - there is no label 0, instead, labelling starts with 1
# and all labels are incremented by 1.
self.labels_nobg = self.labels + 1
# The 0 at lower right corner is isolated, so it should get a new label
self.labels_nobg[-1, -1] = 10
# We say that background value is 9 (and bg label is 0)
self.labels_bg_9 = self.labels_nobg.copy()
self.labels_bg_9[self.x == 9] = 0
# Then, where there was the label 5, we now expect 4 etc.
# (we assume that the label of value 9 would normally be 5)
self.labels_bg_9[self.labels_bg_9 > 5] -= 1
def test_basic(self):
assert_array_equal(label(self.x), self.labels)
# Make sure data wasn't modified
assert self.x[0, 2] == 3
# Check that everything works if there is no background
assert_array_equal(label(self.x, background=99), self.labels_nobg)
# Check that everything works if background value != 0
assert_array_equal(label(self.x, background=9), self.labels_bg_9)
def test_random(self):
x = (np.random.rand(20, 30) * 5).astype(np.int)
labels = label(x)
n = labels.max()
for i in range(n):
values = x[labels == i]
assert np.all(values == values[0])
def test_diag(self):
x = np.array([[0, 0, 1],
[0, 1, 0],
[1, 0, 0]])
assert_array_equal(label(x), x)
def test_4_vs_8(self):
x = np.array([[0, 1],
[1, 0]], dtype=int)
with expected_warnings(["use 'connectivity'"]):
assert_array_equal(label(x, 4),
[[0, 1],
[2, 0]])
assert_array_equal(label(x, 8),
[[0, 1],
[1, 0]])
assert_array_equal(label(x, connectivity=1),
[[0, 1],
[2, 0]])
assert_array_equal(label(x, connectivity=2),
[[0, 1],
[1, 0]])
def test_background(self):
x = np.array([[1, 0, 0],
[1, 1, 5],
[0, 0, 0]])
assert_array_equal(label(x), [[1, 0, 0],
[1, 1, 2],
[0, 0, 0]])
assert_array_equal(label(x, background=0),
[[1, 0, 0],
[1, 1, 2],
[0, 0, 0]])
def test_background_two_regions(self):
x = np.array([[0, 0, 6],
[0, 0, 6],
[5, 5, 5]])
res = label(x, background=0)
assert_array_equal(res,
[[0, 0, 1],
[0, 0, 1],
[2, 2, 2]])
def test_background_one_region_center(self):
x = np.array([[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
with expected_warnings(["use 'connectivity'"]):
assert_array_equal(label(x, neighbors=4, background=0),
[[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
assert_array_equal(label(x, connectivity=1, background=0),
[[0, 0, 0],
[0, 1, 0],
[0, 0, 0]])
def test_return_num(self):
x = np.array([[1, 0, 6],
[0, 0, 6],
[5, 5, 5]])
assert_array_equal(label(x, return_num=True)[1], 3)
assert_array_equal(label(x, background=-1, return_num=True)[1], 4)
class TestConnectedComponents3d:
def setup(self):
self.x = np.zeros((3, 4, 5), int)
self.x[0] = np.array([[0, 3, 2, 1, 9],
[0, 1, 9, 2, 9],
[0, 1, 9, 9, 9],
[3, 1, 5, 3, 0]])
self.x[1] = np.array([[3, 3, 2, 1, 9],
[0, 3, 9, 2, 1],
[0, 3, 3, 1, 1],
[3, 1, 3, 3, 0]])
self.x[2] = np.array([[3, 3, 8, 8, 0],
[2, 3, 9, 8, 8],
[2, 3, 0, 8, 0],
[2, 1, 0, 0, 0]])
self.labels = np.zeros((3, 4, 5), int)
self.labels[0] = np.array([[0, 1, 2, 3, 4],
[0, 5, 4, 2, 4],
[0, 5, 4, 4, 4],
[1, 5, 6, 1, 0]])
self.labels[1] = np.array([[1, 1, 2, 3, 4],
[0, 1, 4, 2, 3],
[0, 1, 1, 3, 3],
[1, 5, 1, 1, 0]])
self.labels[2] = np.array([[1, 1, 7, 7, 0],
[8, 1, 4, 7, 7],
[8, 1, 0, 7, 0],
[8, 5, 0, 0, 0]])
def test_basic(self):
labels = label(self.x)
assert_array_equal(labels, self.labels)
assert self.x[0, 0, 2] == 2, \
"Data was modified!"
def test_random(self):
x = (np.random.rand(20, 30) * 5).astype(np.int)
labels = label(x)
n = labels.max()
for i in range(n):
values = x[labels == i]
assert np.all(values == values[0])
def test_diag(self):
x = np.zeros((3, 3, 3), int)
x[0, 2, 2] = 1
x[1, 1, 1] = 1
x[2, 0, 0] = 1
assert_array_equal(label(x), x)
def test_4_vs_8(self):
x = np.zeros((2, 2, 2), int)
x[0, 1, 1] = 1
x[1, 0, 0] = 1
label4 = x.copy()
label4[1, 0, 0] = 2
with expected_warnings(["use 'connectivity'"]):
assert_array_equal(label(x, 4), label4)
assert_array_equal(label(x, 8), x)
def test_connectivity_1_vs_2(self):
x = np.zeros((2, 2, 2), int)
x[0, 1, 1] = 1
x[1, 0, 0] = 1
label1 = x.copy()
label1[1, 0, 0] = 2
assert_array_equal(label(x, connectivity=1), label1)
assert_array_equal(label(x, connectivity=3), x)
def test_background(self):
x = np.zeros((2, 3, 3), int)
x[0] = np.array([[1, 0, 0],
[1, 0, 0],
[0, 0, 0]])
x[1] = np.array([[0, 0, 0],
[0, 1, 5],
[0, 0, 0]])
lnb = x.copy()
lnb[0] = np.array([[1, 2, 2],
[1, 2, 2],
[2, 2, 2]])
lnb[1] = np.array([[2, 2, 2],
[2, 1, 3],
[2, 2, 2]])
lb = x.copy()
lb[0] = np.array([[1, BG, BG],
[1, BG, BG],
[BG, BG, BG]])
lb[1] = np.array([[BG, BG, BG],
[BG, 1, 2],
[BG, BG, BG]])
assert_array_equal(label(x), lb)
assert_array_equal(label(x, background=-1), lnb)
def test_background_two_regions(self):
x = np.zeros((2, 3, 3), int)
x[0] = np.array([[0, 0, 6],
[0, 0, 6],
[5, 5, 5]])
x[1] = np.array([[6, 6, 0],
[5, 0, 0],
[0, 0, 0]])
lb = x.copy()
lb[0] = np.array([[BG, BG, 1],
[BG, BG, 1],
[2, 2, 2]])
lb[1] = np.array([[1, 1, BG],
[2, BG, BG],
[BG, BG, BG]])
res = label(x, background=0)
assert_array_equal(res, lb)
def test_background_one_region_center(self):
x = np.zeros((3, 3, 3), int)
x[1, 1, 1] = 1
lb = np.ones_like(x) * BG
lb[1, 1, 1] = 1
with expected_warnings(["use 'connectivity'"]):
assert_array_equal(label(x, neighbors=4, background=0), lb)
assert_array_equal(label(x, connectivity=1, background=0), lb)
def test_return_num(self):
x = np.array([[1, 0, 6],
[0, 0, 6],
[5, 5, 5]])
assert_array_equal(label(x, return_num=True)[1], 3)
assert_array_equal(label(x, background=-1, return_num=True)[1], 4)
def test_1D(self):
x = np.array((0, 1, 2, 2, 1, 1, 0, 0))
xlen = len(x)
y = np.array((0, 1, 2, 2, 3, 3, 0, 0))
reshapes = ((xlen,),
(1, xlen), (xlen, 1),
(1, xlen, 1), (xlen, 1, 1), (1, 1, xlen))
for reshape in reshapes:
x2 = x.reshape(reshape)
labelled = label(x2)
assert_array_equal(y, labelled.flatten())
def test_nd(self):
x = np.ones((1, 2, 3, 4))
with testing.raises(NotImplementedError):
label(x)
class TestSupport:
def test_reshape(self):
shapes_in = ((3, 1, 2), (1, 4, 5), (3, 1, 1), (2, 1), (1,))
for shape in shapes_in:
shape = np.array(shape)
numones = sum(shape == 1)
inp = np.random.random(shape)
fixed, swaps = ccomp.reshape_array(inp)
shape2 = fixed.shape
# now check that all ones are at the beginning
for i in range(numones):
assert shape2[i] == 1
back = ccomp.undo_reshape_array(fixed, swaps)
# check that the undo works as expected
assert_array_equal(inp, back)

View file

@ -0,0 +1,171 @@
import numpy as np
from skimage.morphology import convex_hull_image, convex_hull_object
from skimage.morphology._convex_hull import possible_hull
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
from skimage._shared._warnings import expected_warnings
def test_basic():
image = np.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, 1, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool)
expected = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool)
assert_array_equal(convex_hull_image(image), expected)
def test_empty_image():
image = np.zeros((6, 6), dtype=bool)
with expected_warnings(['entirely zero']):
assert_array_equal(convex_hull_image(image), image)
def test_qhull_offset_example():
nonzeros = (([1367, 1368, 1368, 1368, 1369, 1369, 1369, 1369, 1369, 1370,
1370, 1370, 1370, 1370, 1370, 1370, 1371, 1371, 1371, 1371,
1371, 1371, 1371, 1371, 1371, 1372, 1372, 1372, 1372, 1372,
1372, 1372, 1372, 1372, 1373, 1373, 1373, 1373, 1373, 1373,
1373, 1373, 1373, 1374, 1374, 1374, 1374, 1374, 1374, 1374,
1375, 1375, 1375, 1375, 1375, 1376, 1376, 1376, 1377]),
([151, 150, 151, 152, 149, 150, 151, 152, 153, 148, 149, 150,
151, 152, 153, 154, 147, 148, 149, 150, 151, 152, 153, 154,
155, 146, 147, 148, 149, 150, 151, 152, 153, 154, 146, 147,
148, 149, 150, 151, 152, 153, 154, 147, 148, 149, 150, 151,
152, 153, 148, 149, 150, 151, 152, 149, 150, 151, 150]))
image = np.zeros((1392, 1040), dtype=bool)
image[nonzeros] = True
expected = image.copy()
assert_array_equal(convex_hull_image(image), expected)
def test_pathological_qhull_example():
image = np.array(
[[0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 0]], dtype=bool)
expected = np.array(
[[0, 0, 0, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 0]], dtype=bool)
assert_array_equal(convex_hull_image(image), expected)
def test_possible_hull():
image = np.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, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
expected = np.array([[1, 4],
[2, 3],
[3, 2],
[4, 1],
[4, 1],
[3, 2],
[2, 3],
[1, 4],
[2, 5],
[3, 6],
[4, 7],
[2, 5],
[3, 6],
[4, 7],
[4, 2],
[4, 3],
[4, 4],
[4, 5],
[4, 6]])
ph = possible_hull(image)
assert_array_equal(ph, expected)
def test_object():
image = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool)
expected_conn_1 = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 1, 0, 1],
[1, 1, 1, 0, 0, 0, 0, 1, 0],
[1, 1, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool)
assert_array_equal(convex_hull_object(image, connectivity=1),
expected_conn_1)
expected_conn_2 = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 0, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool)
assert_array_equal(convex_hull_object(image, connectivity=2),
expected_conn_2)
with testing.raises(ValueError):
convex_hull_object(image, connectivity=3)
with expected_warnings(['`neighbors` is deprecated']):
out = convex_hull_object(image, neighbors=4)
assert_array_equal(out, expected_conn_1)
def test_non_c_contiguous():
# 2D Fortran-contiguous
image = np.ones((2, 2), order='F', dtype=bool)
assert_array_equal(convex_hull_image(image), image)
# 3D Fortran-contiguous
image = np.ones((2, 2, 2), order='F', dtype=bool)
assert_array_equal(convex_hull_image(image), image)
# 3D non-contiguous
image = np.transpose(np.ones((2, 2, 2), dtype=bool), [0, 2, 1])
assert_array_equal(convex_hull_image(image), image)
@testing.fixture
def images2d3d():
from ...measure.tests.test_regionprops import SAMPLE as image
image3d = np.stack((image, image, image))
return image, image3d
def test_consistent_2d_3d_hulls(images2d3d):
image, image3d = images2d3d
chimage = convex_hull_image(image)
chimage[8, 0] = True # correct for single point exactly on hull edge
chimage3d = convex_hull_image(image3d)
assert_array_equal(chimage3d[1], chimage)

View file

@ -0,0 +1,623 @@
import math
import unittest
import numpy as np
from numpy.testing import assert_equal
from pytest import raises, warns
from skimage.morphology import extrema
eps = 1e-12
def diff(a, b):
a = np.asarray(a, dtype=np.float64)
b = np.asarray(b, dtype=np.float64)
t = ((a - b) ** 2).sum()
return math.sqrt(t)
class TestExtrema(unittest.TestCase):
def test_saturated_arithmetic(self):
"""Adding/subtracting a constant and clipping"""
# Test for unsigned integer
data = np.array([[250, 251, 5, 5],
[100, 200, 253, 252],
[4, 10, 1, 3]],
dtype=np.uint8)
# adding the constant
img_constant_added = extrema._add_constant_clip(data, 4)
expected = np.array([[254, 255, 9, 9],
[104, 204, 255, 255],
[8, 14, 5, 7]],
dtype=np.uint8)
error = diff(img_constant_added, expected)
assert error < eps
img_constant_subtracted = extrema._subtract_constant_clip(data, 4)
expected = np.array([[246, 247, 1, 1],
[96, 196, 249, 248],
[0, 6, 0, 0]],
dtype=np.uint8)
error = diff(img_constant_subtracted, expected)
assert error < eps
# Test for signed integer
data = np.array([[32767, 32766],
[-32768, -32767]],
dtype=np.int16)
img_constant_added = extrema._add_constant_clip(data, 1)
expected = np.array([[32767, 32767],
[-32767, -32766]],
dtype=np.int16)
error = diff(img_constant_added, expected)
assert error < eps
img_constant_subtracted = extrema._subtract_constant_clip(data, 1)
expected = np.array([[32766, 32765],
[-32768, -32768]],
dtype=np.int16)
error = diff(img_constant_subtracted, expected)
assert error < eps
def test_h_maxima(self):
"""h-maxima for various data types"""
data = np.array([[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
[13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
[14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
[13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
dtype=np.uint8)
expected_result = 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, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
for dtype in [np.uint8, np.uint64, np.int8, np.int64]:
data = data.astype(dtype)
out = extrema.h_maxima(data, 40)
error = diff(expected_result, out)
assert error < eps
def test_h_minima(self):
"""h-minima for various data types"""
data = np.array([[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
[13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
[14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
[13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
dtype=np.uint8)
data = 100 - data
expected_result = 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, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
for dtype in [np.uint8, np.uint64, np.int8, np.int64]:
data = data.astype(dtype)
out = extrema.h_minima(data, 40)
error = diff(expected_result, out)
assert error < eps
assert out.dtype == expected_result.dtype
def test_extrema_float(self):
"""specific tests for float type"""
data = np.array([[0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14,
0.14, 0.13, 0.11],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
0.16, 0.15, 0.13],
[0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18,
0.60, 0.60, 0.15],
[0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19,
0.60, 0.60, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
0.19, 0.18, 0.16],
[0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19,
0.19, 0.18, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
0.19, 0.18, 0.16],
[0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19,
1.0, 1.0, 0.16],
[0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18,
1.0, 1.0, 0.15],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
0.16, 0.15, 0.13]],
dtype=np.float32)
inverted_data = 1.0 - data
out = extrema.h_maxima(data, 0.003)
expected_result = 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, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
error = diff(expected_result, out)
assert error < eps
out = extrema.h_minima(inverted_data, 0.003)
error = diff(expected_result, out)
assert error < eps
def test_h_maxima_float_image(self):
"""specific tests for h-maxima float image type"""
w = 10
x, y = np.mgrid[0:w, 0:w]
data = 20 - 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2)
data[2:4, 2:4] = 40
data[2:4, 7:9] = 60
data[7:9, 2:4] = 80
data[7:9, 7:9] = 100
data = data.astype(np.float32)
expected_result = np.zeros_like(data)
expected_result[(data > 19.9)] = 1.0
for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]:
out = extrema.h_maxima(data, h)
error = diff(expected_result, out)
assert error < eps
def test_h_maxima_float_h(self):
"""specific tests for h-maxima float h parameter"""
data = np.array([[0, 0, 0, 0, 0],
[0, 3, 3, 3, 0],
[0, 3, 4, 3, 0],
[0, 3, 3, 3, 0],
[0, 0, 0, 0, 0]], dtype=np.uint8)
h_vals = np.linspace(1.0, 2.0, 100)
failures = 0
for i in range(h_vals.size):
maxima = extrema.h_maxima(data, h_vals[i])
if (maxima[2, 2] == 0):
failures += 1
assert (failures == 0)
def test_h_maxima_large_h(self):
"""test that h-maxima works correctly for large h"""
data = np.array([[10, 10, 10, 10, 10],
[10, 13, 13, 13, 10],
[10, 13, 14, 13, 10],
[10, 13, 13, 13, 10],
[10, 10, 10, 10, 10]], dtype=np.uint8)
maxima = extrema.h_maxima(data, 5)
assert (np.sum(maxima) == 0)
data = np.array([[10, 10, 10, 10, 10],
[10, 13, 13, 13, 10],
[10, 13, 14, 13, 10],
[10, 13, 13, 13, 10],
[10, 10, 10, 10, 10]], dtype=np.float32)
maxima = extrema.h_maxima(data, 5.0)
assert (np.sum(maxima) == 0)
def test_h_minima_float_image(self):
"""specific tests for h-minima float image type"""
w = 10
x, y = np.mgrid[0:w, 0:w]
data = 180 + 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2)
data[2:4, 2:4] = 160
data[2:4, 7:9] = 140
data[7:9, 2:4] = 120
data[7:9, 7:9] = 100
data = data.astype(np.float32)
expected_result = np.zeros_like(data)
expected_result[(data < 180.1)] = 1.0
for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]:
out = extrema.h_minima(data, h)
error = diff(expected_result, out)
assert error < eps
def test_h_minima_float_h(self):
"""specific tests for h-minima float h parameter"""
data = np.array([[4, 4, 4, 4, 4],
[4, 1, 1, 1, 4],
[4, 1, 0, 1, 4],
[4, 1, 1, 1, 4],
[4, 4, 4, 4, 4]], dtype=np.uint8)
h_vals = np.linspace(1.0, 2.0, 100)
failures = 0
for i in range(h_vals.size):
minima = extrema.h_minima(data, h_vals[i])
if (minima[2, 2] == 0):
failures += 1
assert (failures == 0)
def test_h_minima_large_h(self):
"""test that h-minima works correctly for large h"""
data = np.array([[14, 14, 14, 14, 14],
[14, 11, 11, 11, 14],
[14, 11, 10, 11, 14],
[14, 11, 11, 11, 14],
[14, 14, 14, 14, 14]], dtype=np.uint8)
maxima = extrema.h_minima(data, 5)
assert (np.sum(maxima) == 0)
data = np.array([[14, 14, 14, 14, 14],
[14, 11, 11, 11, 14],
[14, 11, 10, 11, 14],
[14, 11, 11, 11, 14],
[14, 14, 14, 14, 14]], dtype=np.float32)
maxima = extrema.h_minima(data, 5.0)
assert (np.sum(maxima) == 0)
class TestLocalMaxima(unittest.TestCase):
"""Some tests for local_minima are included as well."""
supported_dtypes = [
np.uint8, np.uint16, np.uint32, np.uint64,
np.int8, np.int16, np.int32, np.int64,
np.float32, np.float64
]
image = np.array(
[[1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 3, 3, 0, 0, 4, 0, 2, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 4, 4, 0, 3, 0, 0, 0],
[0, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 3, 0, 0, 0],
[0, 0, 2, 0, 2, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0]],
dtype=np.uint8
)
# Connectivity 2, maxima can touch border, returned with default values
expected_default = np.array(
[[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]],
dtype=np.bool
)
# Connectivity 1 (cross), maxima can touch border
expected_cross = np.array(
[[1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]],
dtype=np.bool
)
def test_empty(self):
"""Test result with empty image."""
result = extrema.local_maxima(np.array([[]]), indices=False)
assert result.size == 0
assert result.dtype == np.bool
assert result.shape == (1, 0)
result = extrema.local_maxima(np.array([]), indices=True)
assert isinstance(result, tuple)
assert len(result) == 1
assert result[0].size == 0
assert result[0].dtype == np.intp
result = extrema.local_maxima(np.array([[]]), indices=True)
assert isinstance(result, tuple)
assert len(result) == 2
assert result[0].size == 0
assert result[0].dtype == np.intp
assert result[1].size == 0
assert result[1].dtype == np.intp
def test_dtypes(self):
"""Test results with default configuration for all supported dtypes."""
for dtype in self.supported_dtypes:
result = extrema.local_maxima(self.image.astype(dtype))
assert result.dtype == np.bool
assert_equal(result, self.expected_default)
def test_dtypes_old(self):
"""
Test results with default configuration and data copied from old unit
tests for all supported dtypes.
"""
data = np.array(
[[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
[13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
[14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
[13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
dtype=np.uint8
)
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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.bool
)
for dtype in self.supported_dtypes:
image = data.astype(dtype)
result = extrema.local_maxima(image)
assert result.dtype == np.bool
assert_equal(result, expected)
def test_connectivity(self):
"""Test results if selem is a scalar."""
# Connectivity 1: generates cross shaped structuring element
result_conn1 = extrema.local_maxima(self.image, connectivity=1)
assert result_conn1.dtype == np.bool
assert_equal(result_conn1, self.expected_cross)
# Connectivity 2: generates square shaped structuring element
result_conn2 = extrema.local_maxima(self.image, connectivity=2)
assert result_conn2.dtype == np.bool
assert_equal(result_conn2, self.expected_default)
# Connectivity 3: generates square shaped structuring element
result_conn3 = extrema.local_maxima(self.image, connectivity=3)
assert result_conn3.dtype == np.bool
assert_equal(result_conn3, self.expected_default)
def test_selem(self):
"""Test results if selem is given."""
selem_cross = np.array(
[[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.bool)
result_selem_cross = extrema.local_maxima(
self.image, selem=selem_cross)
assert result_selem_cross.dtype == np.bool
assert_equal(result_selem_cross, self.expected_cross)
for selem in [
((True,) * 3,) * 3,
np.ones((3, 3), dtype=np.float64),
np.ones((3, 3), dtype=np.uint8),
np.ones((3, 3), dtype=np.bool),
]:
# Test different dtypes for selem which expects a boolean array but
# will accept and convert other types if possible
result_selem_square = extrema.local_maxima(self.image, selem=selem)
assert result_selem_square.dtype == np.bool
assert_equal(result_selem_square, self.expected_default)
selem_x = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=np.bool)
expected_selem_x = np.array(
[[1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0]],
dtype=np.bool
)
result_selem_x = extrema.local_maxima(self.image, selem=selem_x)
assert result_selem_x.dtype == np.bool
assert_equal(result_selem_x, expected_selem_x)
def test_indices(self):
"""Test output if indices of peaks are desired."""
# Connectivity 1
expected_conn1 = np.nonzero(self.expected_cross)
result_conn1 = extrema.local_maxima(self.image, connectivity=1,
indices=True)
assert_equal(result_conn1, expected_conn1)
# Connectivity 2
expected_conn2 = np.nonzero(self.expected_default)
result_conn2 = extrema.local_maxima(self.image, connectivity=2,
indices=True)
assert_equal(result_conn2, expected_conn2)
def test_allow_borders(self):
"""Test maxima detection at the image border."""
# Use connectivity 1 to allow many maxima, only filtering at border is
# of interest
result_with_boder = extrema.local_maxima(
self.image, connectivity=1, allow_borders=True)
assert result_with_boder.dtype == np.bool
assert_equal(result_with_boder, self.expected_cross)
expected_without_border = 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, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.bool
)
result_without_border = extrema.local_maxima(
self.image, connectivity=1, allow_borders=False)
assert result_with_boder.dtype == np.bool
assert_equal(result_without_border, expected_without_border)
def test_nd(self):
"""Test one- and three-dimensional case."""
# One-dimension
x_1d = np.array([1, 1, 0, 1, 2, 3, 0, 2, 1, 2, 0])
expected_1d = np.array([1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0],
dtype=np.bool)
result_1d = extrema.local_maxima(x_1d)
assert result_1d.dtype == np.bool
assert_equal(result_1d, expected_1d)
# 3-dimensions (adapted from old unit test)
x_3d = np.zeros((8, 8, 8), dtype=np.uint8)
expected_3d = np.zeros((8, 8, 8), dtype=np.bool)
# first maximum: only one pixel
x_3d[1, 1:3, 1:3] = 100
x_3d[2, 2, 2] = 200
x_3d[3, 1:3, 1:3] = 100
expected_3d[2, 2, 2] = 1
# second maximum: three pixels in z-direction
x_3d[5:8, 1, 1] = 200
expected_3d[5:8, 1, 1] = 1
# third: two maxima in 0 and 3.
x_3d[0, 5:8, 5:8] = 200
x_3d[1, 6, 6] = 100
x_3d[2, 5:7, 5:7] = 200
x_3d[0:3, 5:8, 5:8] += 50
expected_3d[0, 5:8, 5:8] = 1
expected_3d[2, 5:7, 5:7] = 1
# four : one maximum in the corner of the square
x_3d[6:8, 6:8, 6:8] = 200
x_3d[7, 7, 7] = 255
expected_3d[7, 7, 7] = 1
result_3d = extrema.local_maxima(x_3d)
assert result_3d.dtype == np.bool
assert_equal(result_3d, expected_3d)
def test_constant(self):
"""Test behaviour for 'flat' images."""
const_image = np.full((7, 6), 42, dtype=np.uint8)
expected = np.zeros((7, 6), dtype=np.uint8)
for dtype in self.supported_dtypes:
const_image = const_image.astype(dtype)
# test for local maxima
result = extrema.local_maxima(const_image)
assert result.dtype == np.bool
assert_equal(result, expected)
# test for local minima
result = extrema.local_minima(const_image)
assert result.dtype == np.bool
assert_equal(result, expected)
def test_extrema_float(self):
"""Specific tests for float type."""
# Copied from old unit test for local_maxma
image = np.array(
[[0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14, 0.14, 0.13, 0.11],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13],
[0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18, 0.60, 0.60, 0.15],
[0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19, 0.60, 0.60, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16],
[0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19, 0.19, 0.18, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16],
[0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19, 1.0, 1.0, 0.16],
[0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18, 1.0, 1.0, 0.15],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13]],
dtype=np.float32
)
inverted_image = 1.0 - image
expected_result = 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, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.bool
)
# Test for local maxima with automatic step calculation
result = extrema.local_maxima(image)
assert result.dtype == np.bool
assert_equal(result, expected_result)
# Test for local minima with automatic step calculation
result = extrema.local_minima(inverted_image)
assert result.dtype == np.bool
assert_equal(result, expected_result)
def test_exceptions(self):
"""Test if input validation triggers correct exceptions."""
# Mismatching number of dimensions
with raises(ValueError, match="number of dimensions"):
extrema.local_maxima(
self.image, selem=np.ones((3, 3, 3), dtype=np.bool))
with raises(ValueError, match="number of dimensions"):
extrema.local_maxima(
self.image, selem=np.ones((3,), dtype=np.bool))
# All dimensions in selem must be of size 3
with raises(ValueError, match="dimension size"):
extrema.local_maxima(
self.image, selem=np.ones((2, 3), dtype=np.bool))
with raises(ValueError, match="dimension size"):
extrema.local_maxima(
self.image, selem=np.ones((5, 5), dtype=np.bool))
with raises(TypeError, match="float16 which is not supported"):
extrema.local_maxima(np.empty(1, dtype=np.float16))
def test_small_array(self):
"""Test output for arrays with dimension smaller 3.
If any dimension of an array is smaller than 3 and `allow_borders` is
false a structuring element, which has at least 3 elements in each
dimension, can't be applied. This is an implementation detail so
`local_maxima` should still return valid output (see gh-3261).
If `allow_borders` is true the array is padded internally and there is
no problem.
"""
warning_msg = "maxima can't exist .* any dimension smaller 3 .*"
x = np.array([0, 1])
extrema.local_maxima(x, allow_borders=True) # no warning
with warns(UserWarning, match=warning_msg):
result = extrema.local_maxima(x, allow_borders=False)
assert_equal(result, [0, 0])
assert result.dtype == np.bool
x = np.array([[1, 2], [2, 2]])
extrema.local_maxima(x, allow_borders=True, indices=True) # no warning
with warns(UserWarning, match=warning_msg):
result = extrema.local_maxima(x, allow_borders=False, indices=True)
assert_equal(result, np.zeros((2, 0), dtype=np.intp))
assert result[0].dtype == np.intp
assert result[1].dtype == np.intp
if __name__ == "__main__":
np.testing.run_module_suite()

View file

@ -0,0 +1,279 @@
import numpy as np
import pytest
from pytest import raises
from skimage.morphology import flood, flood_fill
from skimage._shared.testing import expected_warnings
eps = 1e-12
def test_empty_input():
# Test shortcut
output = flood_fill(np.empty(0), (), 2)
assert output.size == 0
# Boolean output type
assert flood(np.empty(0), ()).dtype == np.bool
# Maintain shape, even with zero size present
assert flood(np.empty((20, 0, 4)), ()).shape == (20, 0, 4)
def test_float16():
image = np.array([9., 0.1, 42], dtype=np.float16)
with raises(TypeError, match="dtype of `image` is float16"):
flood_fill(image, 0, 1)
def test_overrange_tolerance_int():
image = np.arange(256, dtype=np.uint8).reshape((8, 8, 4))
expected = np.zeros_like(image)
output = flood_fill(image, (7, 7, 3), 0, tolerance=379)
np.testing.assert_equal(output, expected)
def test_overrange_tolerance_float():
max_value = np.finfo(np.float32).max
min_value = np.finfo(np.float32).min
image = np.random.uniform(size=(64, 64), low=-1., high=1.).astype(
np.float32)
image *= max_value
expected = np.ones_like(image)
output = flood_fill(image, (0, 1), 1., tolerance=max_value * 10)
np.testing.assert_equal(output, expected)
def test_inplace_int():
image = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3],
[0, 1, 1, 1, 3, 3, 4]])
flood_fill(image, (0, 0), 5, in_place=True)
expected = np.array([[5, 5, 5, 5, 5, 5, 5],
[5, 1, 1, 5, 2, 2, 5],
[5, 1, 1, 5, 2, 2, 5],
[1, 5, 5, 5, 5, 5, 3],
[5, 1, 1, 1, 3, 3, 4]])
np.testing.assert_array_equal(image, expected)
def test_inplace_float():
image = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3],
[0, 1, 1, 1, 3, 3, 4]], dtype=np.float32)
flood_fill(image, (0, 0), 5, in_place=True)
expected = np.array([[5., 5., 5., 5., 5., 5., 5.],
[5., 1., 1., 5., 2., 2., 5.],
[5., 1., 1., 5., 2., 2., 5.],
[1., 5., 5., 5., 5., 5., 3.],
[5., 1., 1., 1., 3., 3., 4.]], dtype=np.float32)
np.testing.assert_allclose(image, expected)
def test_inplace_noncontiguous():
image = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3],
[0, 1, 1, 1, 3, 3, 4]])
# Transpose is noncontiguous
image2 = image[::2, ::2]
flood_fill(image2, (0, 0), 5, in_place=True)
# The inplace modified result
expected2 = np.array([[5, 5, 5, 5],
[5, 1, 2, 5],
[5, 1, 3, 4]])
np.testing.assert_allclose(image2, expected2)
# Projected back through the view, `image` also modified
expected = np.array([[5, 0, 5, 0, 5, 0, 5],
[0, 1, 1, 0, 2, 2, 0],
[5, 1, 1, 0, 2, 2, 5],
[1, 0, 0, 0, 0, 0, 3],
[5, 1, 1, 1, 3, 3, 4]])
np.testing.assert_allclose(image, expected)
def test_inplace_int_deprecated():
"""This test is deprecated and will be removed in
version 0.19.0. See #4248.
"""
image = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3],
[0, 1, 1, 1, 3, 3, 4]])
with expected_warnings(['The `inplace`']):
flood_fill(image, (0, 0), 5, inplace=True)
expected = np.array([[5, 5, 5, 5, 5, 5, 5],
[5, 1, 1, 5, 2, 2, 5],
[5, 1, 1, 5, 2, 2, 5],
[1, 5, 5, 5, 5, 5, 3],
[5, 1, 1, 1, 3, 3, 4]])
np.testing.assert_array_equal(image, expected)
def test_inplace_float_deprecated():
"""This test is deprecated and will be removed in
version 0.19.0. See #4248.
"""
image = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 2, 2, 0],
[0, 1, 1, 0, 2, 2, 0],
[1, 0, 0, 0, 0, 0, 3],
[0, 1, 1, 1, 3, 3, 4]], dtype=np.float32)
with expected_warnings(['The `inplace`']):
flood_fill(image, (0, 0), 5, inplace=True)
expected = np.array([[5., 5., 5., 5., 5., 5., 5.],
[5., 1., 1., 5., 2., 2., 5.],
[5., 1., 1., 5., 2., 2., 5.],
[1., 5., 5., 5., 5., 5., 3.],
[5., 1., 1., 1., 3., 3., 4.]], dtype=np.float32)
np.testing.assert_allclose(image, expected)
def test_1d():
image = np.arange(11)
expected = np.array([0, 1, -20, -20, -20, -20, -20, -20, -20, 9, 10])
output = flood_fill(image, 5, -20, tolerance=3)
output2 = flood_fill(image, (5,), -20, tolerance=3)
np.testing.assert_equal(output, expected)
np.testing.assert_equal(output, output2)
def test_wraparound():
# If the borders (or neighbors) aren't correctly accounted for, this fails,
# because the algorithm uses an ravelled array.
test = np.zeros((5, 7), dtype=np.float64)
test[:, 3] = 100
expected = np.array([[-1., -1., -1., 100., 0., 0., 0.],
[-1., -1., -1., 100., 0., 0., 0.],
[-1., -1., -1., 100., 0., 0., 0.],
[-1., -1., -1., 100., 0., 0., 0.],
[-1., -1., -1., 100., 0., 0., 0.]])
np.testing.assert_equal(flood_fill(test, (0, 0), -1), expected)
def test_neighbors():
# This test will only pass if the neighbors are exactly correct
test = np.zeros((5, 7), dtype=np.float64)
test[:, 3] = 100
expected = np.array([[0, 0, 0, 255, 0, 0, 0],
[0, 0, 0, 255, 0, 0, 0],
[0, 0, 0, 255, 0, 0, 0],
[0, 0, 0, 255, 0, 0, 0],
[0, 0, 0, 255, 0, 0, 0]])
output = flood_fill(test, (0, 3), 255)
np.testing.assert_equal(output, expected)
test[2] = 100
expected[2] = 255
output2 = flood_fill(test, (2, 3), 255)
np.testing.assert_equal(output2, expected)
def test_selem():
# Basic tests for nonstandard structuring elements
selem = np.array([[0, 1, 1],
[0, 1, 1],
[0, 0, 0]]) # Cannot grow left or down
output = flood_fill(np.zeros((5, 6), dtype=np.uint8), (3, 1), 255,
selem=selem)
expected = np.array([[0, 255, 255, 255, 255, 255],
[0, 255, 255, 255, 255, 255],
[0, 255, 255, 255, 255, 255],
[0, 255, 255, 255, 255, 255],
[0, 0, 0, 0, 0, 0]], dtype=np.uint8)
np.testing.assert_equal(output, expected)
selem = np.array([[0, 0, 0],
[1, 1, 0],
[1, 1, 0]]) # Cannot grow right or up
output = flood_fill(np.zeros((5, 6), dtype=np.uint8), (1, 4), 255,
selem=selem)
expected = np.array([[ 0, 0, 0, 0, 0, 0],
[255, 255, 255, 255, 255, 0],
[255, 255, 255, 255, 255, 0],
[255, 255, 255, 255, 255, 0],
[255, 255, 255, 255, 255, 0]], dtype=np.uint8)
np.testing.assert_equal(output, expected)
def test_basic_nd():
for dimension in (3, 4, 5):
shape = (5,) * dimension
hypercube = np.zeros(shape)
slice_mid = tuple(slice(1, -1, None) for dim in range(dimension))
hypercube[slice_mid] = 1 # sum is 3**dimension
filled = flood_fill(hypercube, (2,)*dimension, 2)
# Test that the middle sum is correct
assert filled.sum() == 3**dimension * 2
# Test that the entire array is as expected
np.testing.assert_equal(
filled, np.pad(np.ones((3,)*dimension) * 2, 1, 'constant'))
@pytest.mark.parametrize("tolerance", [None, 0])
def test_f_order(tolerance):
image = np.array([
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 0, 0],
], order="F")
expected = np.array([
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 0, 0],
], dtype=bool)
mask = flood(image, seed_point=(1, 0), tolerance=tolerance)
np.testing.assert_array_equal(expected, mask)
mask = flood(image, seed_point=(2, 1), tolerance=tolerance)
np.testing.assert_array_equal(expected, mask)
if __name__ == "__main__":
np.testing.run_module_suite()

View file

@ -0,0 +1,276 @@
import numpy as np
from scipy import ndimage as ndi
from skimage import color, data, transform
from skimage.util import img_as_uint, img_as_ubyte
from skimage.morphology import grey, selem
from skimage._shared._warnings import expected_warnings
from skimage._shared import testing
from skimage._shared.testing import (assert_array_equal, assert_equal,
TestCase, parametrize, fetch)
class TestMorphology(TestCase):
# These expected outputs were generated with skimage v0.12.1
# using:
#
# from skimage.morphology.tests.test_grey import TestMorphology
# import numpy as np
# output = TestMorphology()._build_expected_output()
# np.savez_compressed('gray_morph_output.npz', **output)
def _build_expected_output(self):
funcs = (grey.erosion, grey.dilation, grey.opening, grey.closing,
grey.white_tophat, grey.black_tophat)
selems_2D = (selem.square, selem.diamond,
selem.disk, selem.star)
image = img_as_ubyte(transform.downscale_local_mean(
color.rgb2gray(data.coffee()), (20, 20)))
output = {}
for n in range(1, 4):
for strel in selems_2D:
for func in funcs:
key = '{0}_{1}_{2}'.format(
strel.__name__, n, func.__name__)
output[key] = func(image, strel(n))
return output
def test_gray_morphology(self):
expected = dict(np.load(fetch('data/gray_morph_output.npz')))
calculated = self._build_expected_output()
assert_equal(expected, calculated)
class TestEccentricStructuringElements(TestCase):
def setUp(self):
self.black_pixel = 255 * np.ones((4, 4), dtype=np.uint8)
self.black_pixel[1, 1] = 0
self.white_pixel = 255 - self.black_pixel
self.selems = [selem.square(2), selem.rectangle(2, 2),
selem.rectangle(2, 1), selem.rectangle(1, 2)]
def test_dilate_erode_symmetry(self):
for s in self.selems:
c = grey.erosion(self.black_pixel, s)
d = grey.dilation(self.white_pixel, s)
assert np.all(c == (255 - d))
def test_open_black_pixel(self):
for s in self.selems:
grey_open = grey.opening(self.black_pixel, s)
assert np.all(grey_open == self.black_pixel)
def test_close_white_pixel(self):
for s in self.selems:
grey_close = grey.closing(self.white_pixel, s)
assert np.all(grey_close == self.white_pixel)
def test_open_white_pixel(self):
for s in self.selems:
assert np.all(grey.opening(self.white_pixel, s) == 0)
def test_close_black_pixel(self):
for s in self.selems:
assert np.all(grey.closing(self.black_pixel, s) == 255)
def test_white_tophat_white_pixel(self):
for s in self.selems:
tophat = grey.white_tophat(self.white_pixel, s)
assert np.all(tophat == self.white_pixel)
def test_black_tophat_black_pixel(self):
for s in self.selems:
tophat = grey.black_tophat(self.black_pixel, s)
assert np.all(tophat == (255 - self.black_pixel))
def test_white_tophat_black_pixel(self):
for s in self.selems:
tophat = grey.white_tophat(self.black_pixel, s)
assert np.all(tophat == 0)
def test_black_tophat_white_pixel(self):
for s in self.selems:
tophat = grey.black_tophat(self.white_pixel, s)
assert np.all(tophat == 0)
grey_functions = [grey.erosion, grey.dilation,
grey.opening, grey.closing,
grey.white_tophat, grey.black_tophat]
@parametrize("function", grey_functions)
def test_default_selem(function):
strel = selem.diamond(radius=1)
image = 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, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 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]], np.uint8)
im_expected = function(image, strel)
im_test = function(image)
testing.assert_array_equal(im_expected, im_test)
def test_3d_fallback_default_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
opened = grey.opening(image)
# expect a "hyper-cross" centered in the 5x5x5:
image_expected = np.zeros((7, 7, 7), dtype=bool)
image_expected[2:5, 2:5, 2:5] = ndi.generate_binary_structure(3, 1)
assert_array_equal(opened, image_expected)
grey_3d_fallback_functions = [grey.closing, grey.opening]
@parametrize("function", grey_3d_fallback_functions)
def test_3d_fallback_cube_selem(function):
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
new_image = function(image, cube)
testing.assert_array_equal(new_image, image)
def test_3d_fallback_white_tophat():
image = np.zeros((7, 7, 7), dtype=bool)
image[2, 2:4, 2:4] = 1
image[3, 2:5, 2:5] = 1
image[4, 3:5, 3:5] = 1
with expected_warnings([r'operator.*deprecated|\A\Z']):
new_image = grey.white_tophat(image)
footprint = ndi.generate_binary_structure(3, 1)
with expected_warnings([r'operator.*deprecated|\A\Z']):
image_expected = ndi.white_tophat(
image.view(dtype=np.uint8), footprint=footprint)
assert_array_equal(new_image, image_expected)
def test_3d_fallback_black_tophat():
image = np.ones((7, 7, 7), dtype=bool)
image[2, 2:4, 2:4] = 0
image[3, 2:5, 2:5] = 0
image[4, 3:5, 3:5] = 0
with expected_warnings([r'operator.*deprecated|\A\Z']):
new_image = grey.black_tophat(image)
footprint = ndi.generate_binary_structure(3, 1)
with expected_warnings([r'operator.*deprecated|\A\Z']):
image_expected = ndi.black_tophat(
image.view(dtype=np.uint8), footprint=footprint)
assert_array_equal(new_image, image_expected)
def test_2d_ndimage_equivalence():
image = np.zeros((9, 9), np.uint8)
image[2:-2, 2:-2] = 128
image[3:-3, 3:-3] = 196
image[4, 4] = 255
opened = grey.opening(image)
closed = grey.closing(image)
selem = ndi.generate_binary_structure(2, 1)
ndimage_opened = ndi.grey_opening(image, footprint=selem)
ndimage_closed = ndi.grey_closing(image, footprint=selem)
assert_array_equal(opened, ndimage_opened)
assert_array_equal(closed, ndimage_closed)
# float test images
im = np.array([[ 0.55, 0.72, 0.6 , 0.54, 0.42],
[ 0.65, 0.44, 0.89, 0.96, 0.38],
[ 0.79, 0.53, 0.57, 0.93, 0.07],
[ 0.09, 0.02, 0.83, 0.78, 0.87],
[ 0.98, 0.8 , 0.46, 0.78, 0.12]])
eroded = np.array([[ 0.55, 0.44, 0.54, 0.42, 0.38],
[ 0.44, 0.44, 0.44, 0.38, 0.07],
[ 0.09, 0.02, 0.53, 0.07, 0.07],
[ 0.02, 0.02, 0.02, 0.78, 0.07],
[ 0.09, 0.02, 0.46, 0.12, 0.12]])
dilated = np.array([[ 0.72, 0.72, 0.89, 0.96, 0.54],
[ 0.79, 0.89, 0.96, 0.96, 0.96],
[ 0.79, 0.79, 0.93, 0.96, 0.93],
[ 0.98, 0.83, 0.83, 0.93, 0.87],
[ 0.98, 0.98, 0.83, 0.78, 0.87]])
opened = np.array([[ 0.55, 0.55, 0.54, 0.54, 0.42],
[ 0.55, 0.44, 0.54, 0.44, 0.38],
[ 0.44, 0.53, 0.53, 0.78, 0.07],
[ 0.09, 0.02, 0.78, 0.78, 0.78],
[ 0.09, 0.46, 0.46, 0.78, 0.12]])
closed = np.array([[ 0.72, 0.72, 0.72, 0.54, 0.54],
[ 0.72, 0.72, 0.89, 0.96, 0.54],
[ 0.79, 0.79, 0.79, 0.93, 0.87],
[ 0.79, 0.79, 0.83, 0.78, 0.87],
[ 0.98, 0.83, 0.78, 0.78, 0.78]])
def test_float():
np.testing.assert_allclose(grey.erosion(im), eroded)
np.testing.assert_allclose(grey.dilation(im), dilated)
np.testing.assert_allclose(grey.opening(im), opened)
np.testing.assert_allclose(grey.closing(im), closed)
def test_uint16():
im16, eroded16, dilated16, opened16, closed16 = (
map(img_as_uint, [im, eroded, dilated, opened, closed]))
np.testing.assert_allclose(grey.erosion(im16), eroded16)
np.testing.assert_allclose(grey.dilation(im16), dilated16)
np.testing.assert_allclose(grey.opening(im16), opened16)
np.testing.assert_allclose(grey.closing(im16), closed16)
def test_discontiguous_out_array():
image = np.array([[5, 6, 2],
[7, 2, 2],
[3, 5, 1]], np.uint8)
out_array_big = np.zeros((5, 5), np.uint8)
out_array = out_array_big[::2, ::2]
expected_dilation = np.array([[7, 0, 6, 0, 6],
[0, 0, 0, 0, 0],
[7, 0, 7, 0, 2],
[0, 0, 0, 0, 0],
[7, 0, 5, 0, 5]], np.uint8)
expected_erosion = np.array([[5, 0, 2, 0, 2],
[0, 0, 0, 0, 0],
[2, 0, 2, 0, 1],
[0, 0, 0, 0, 0],
[3, 0, 1, 0, 1]], np.uint8)
grey.dilation(image, out=out_array)
assert_array_equal(out_array_big, expected_dilation)
grey.erosion(image, out=out_array)
testing.assert_array_equal(out_array_big, expected_erosion)
def test_1d_erosion():
image = np.array([1, 2, 3, 2, 1])
expected = np.array([1, 1, 2, 1, 1])
eroded = grey.erosion(image)
testing.assert_array_equal(eroded, expected)

View file

@ -0,0 +1,457 @@
import numpy as np
from skimage.morphology import max_tree, area_closing, area_opening
from skimage.morphology import max_tree_local_maxima, diameter_opening
from skimage.morphology import diameter_closing
from skimage.util import invert
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal, TestCase
eps = 1e-12
def _full_type_test(img, param, expected, func, param_scale=False,
**keywords):
# images as they are
out = func(img, param, **keywords)
assert_array_equal(out, expected)
# unsigned int
for dt in [np.uint32, np.uint64]:
img_cast = img.astype(dt)
out = func(img_cast, param, **keywords)
exp_cast = expected.astype(dt)
assert_array_equal(out, exp_cast)
# float
data_float = img.astype(np.float64)
data_float = data_float / 255.0
expected_float = expected.astype(np.float64)
expected_float = expected_float / 255.0
if param_scale:
param_cast = param / 255.0
else:
param_cast = param
for dt in [np.float32, np.float64]:
data_cast = data_float.astype(dt)
out = func(data_cast, param_cast, **keywords)
exp_cast = expected_float.astype(dt)
error_img = 255.0 * exp_cast - 255.0 * out
error = (error_img >= 1.0).sum()
assert error < eps
# signed images
img_signed = img.astype(np.int16)
img_signed = img_signed - 128
exp_signed = expected.astype(np.int16)
exp_signed = exp_signed - 128
for dt in [np.int8, np.int16, np.int32, np.int64]:
img_s = img_signed.astype(dt)
out = func(img_s, param, **keywords)
exp_s = exp_signed.astype(dt)
assert_array_equal(out, exp_s)
class TestMaxtree(TestCase):
def test_max_tree(self):
"Test for max tree"
img_type = np.uint8
img = np.array([[10, 8, 8, 9],
[7, 7, 9, 9],
[8, 7, 10, 10],
[9, 9, 10, 10]], dtype=img_type)
P_exp = np.array([[1, 4, 1, 1],
[4, 4, 3, 3],
[1, 4, 3, 10],
[3, 3, 10, 10]], dtype=np.int64)
S_exp = np.array([4, 5, 9, 1, 2, 8, 3, 6, 7,
12, 13, 0, 10, 11, 14, 15],
dtype=np.int64)
for img_type in [np.uint8, np.uint16, np.uint32, np.uint64]:
img = img.astype(img_type)
P, S = max_tree(img, connectivity=2)
assert_array_equal(P, P_exp)
assert_array_equal(S, S_exp)
for img_type in [np.int8, np.int16, np.int32, np.int64]:
img = img.astype(img_type)
img_shifted = img - 9
P, S = max_tree(img_shifted, connectivity=2)
assert_array_equal(P, P_exp)
assert_array_equal(S, S_exp)
img_float = img.astype(np.float)
img_float = (img_float - 8) / 2.0
for img_type in [np.float32, np.float64]:
img_float = img_float.astype(img_type)
P, S = max_tree(img_float, connectivity=2)
assert_array_equal(P, P_exp)
assert_array_equal(S, S_exp)
return
def test_area_closing(self):
"Test for Area Closing (2 thresholds, all types)"
# original image
img = np.array(
[[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 200, 200, 240, 200, 240, 200, 200, 240, 240, 200, 240],
[240, 200, 40, 240, 240, 240, 240, 240, 240, 240, 40, 240],
[240, 240, 240, 240, 100, 240, 100, 100, 240, 240, 200, 240],
[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255],
[200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 40],
[200, 200, 200, 100, 200, 200, 200, 240, 255, 255, 255, 255],
[200, 200, 200, 100, 200, 200, 200, 240, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 40, 200, 240, 240, 100, 255, 255],
[200, 40, 255, 255, 255, 40, 200, 255, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255]],
dtype=np.uint8)
# expected area closing with area 2
expected_2 = np.array(
[[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 200, 200, 240, 240, 240, 200, 200, 240, 240, 200, 240],
[240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 200, 240],
[240, 240, 240, 240, 240, 240, 100, 100, 240, 240, 200, 240],
[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255],
[200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 255],
[200, 200, 200, 100, 200, 200, 200, 240, 255, 255, 255, 255],
[200, 200, 200, 100, 200, 200, 200, 240, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 40, 200, 240, 240, 200, 255, 255],
[200, 200, 255, 255, 255, 40, 200, 255, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255]],
dtype=np.uint8)
# expected diameter closing with diameter 4
expected_4 = np.array(
[[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240],
[200, 200, 200, 200, 200, 200, 200, 240, 240, 240, 255, 255],
[200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 240, 255, 255, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 240, 240, 200, 255, 255],
[200, 200, 255, 255, 255, 200, 200, 255, 200, 200, 255, 255],
[200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255]],
dtype=np.uint8)
# _full_type_test makes a test with many image types.
_full_type_test(img, 2, expected_2, area_closing, connectivity=2)
_full_type_test(img, 4, expected_4, area_closing, connectivity=2)
P, S = max_tree(invert(img), connectivity=2)
_full_type_test(img, 4, expected_4, area_closing,
parent=P, tree_traverser=S)
def test_area_opening(self):
"Test for Area Opening (2 thresholds, all types)"
# original image
img = np.array([[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15],
[15, 55, 55, 15, 55, 15, 55, 55, 15, 15, 55, 15],
[15, 55, 215, 15, 15, 15, 15, 15, 15, 15, 215, 15],
[15, 15, 15, 15, 155, 15, 155, 155, 15, 15, 55, 15],
[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15],
[55, 55, 55, 55, 55, 55, 55, 15, 55, 55, 0, 0],
[55, 0, 55, 55, 55, 0, 55, 15, 0, 0, 0, 215],
[55, 55, 55, 155, 55, 55, 55, 15, 0, 0, 0, 0],
[55, 55, 55, 155, 55, 55, 55, 15, 55, 55, 0, 0],
[55, 55, 55, 55, 55, 215, 55, 15, 15, 155, 0, 0],
[55, 215, 0, 0, 0, 215, 55, 0, 55, 55, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 0, 0, 0, 0, 0]],
dtype=np.uint8)
# expected area closing with area 2
expected_2 = np.array([[15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[15, 55, 55, 15, 15, 15, 55, 55, 15,
15, 55, 15],
[15, 55, 55, 15, 15, 15, 15, 15, 15,
15, 55, 15],
[15, 15, 15, 15, 15, 15, 155, 155, 15,
15, 55, 15],
[15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[55, 55, 55, 55, 55, 55, 55, 15, 55,
55, 0, 0],
[55, 0, 55, 55, 55, 0, 55, 15, 0,
0, 0, 0],
[55, 55, 55, 155, 55, 55, 55, 15, 0,
0, 0, 0],
[55, 55, 55, 155, 55, 55, 55, 15, 55,
55, 0, 0],
[55, 55, 55, 55, 55, 215, 55, 15, 15,
55, 0, 0],
[55, 55, 0, 0, 0, 215, 55, 0, 55,
55, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 0, 0,
0, 0, 0]],
dtype=np.uint8)
# expected diameter closing with diameter 4
expected_4 = np.array([[15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[15, 55, 55, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[15, 55, 55, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15],
[55, 55, 55, 55, 55, 55, 55, 15, 15,
15, 0, 0],
[55, 0, 55, 55, 55, 0, 55, 15, 0,
0, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 15, 0,
0, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 15, 55,
55, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 15, 15,
55, 0, 0],
[55, 55, 0, 0, 0, 55, 55, 0, 55,
55, 0, 0],
[55, 55, 55, 55, 55, 55, 55, 0, 0,
0, 0, 0]],
dtype=np.uint8)
# _full_type_test makes a test with many image types.
_full_type_test(img, 2, expected_2, area_opening, connectivity=2)
_full_type_test(img, 4, expected_4, area_opening, connectivity=2)
P, S = max_tree(img, connectivity=2)
_full_type_test(img, 4, expected_4, area_opening,
parent=P, tree_traverser=S)
def test_diameter_closing(self):
"Test for Diameter Opening (2 thresholds, all types)"
img = np.array([[97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93],
[93, 63, 63, 63, 63, 86, 86, 86, 87, 43, 43, 91],
[92, 89, 88, 86, 85, 85, 84, 85, 85, 43, 43, 89],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[90, 88, 86, 84, 83, 83, 82, 83, 83, 84, 86, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[92, 89, 23, 23, 85, 85, 84, 85, 85, 3, 3, 89],
[93, 91, 23, 23, 87, 86, 86, 86, 87, 88, 3, 91],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93]],
dtype=np.uint8)
ex2 = np.array([[97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93],
[93, 63, 63, 63, 63, 86, 86, 86, 87, 43, 43, 91],
[92, 89, 88, 86, 85, 85, 84, 85, 85, 43, 43, 89],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[90, 88, 86, 84, 83, 83, 83, 83, 83, 84, 86, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[92, 89, 23, 23, 85, 85, 84, 85, 85, 3, 3, 89],
[93, 91, 23, 23, 87, 86, 86, 86, 87, 88, 3, 91],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93]],
dtype=np.uint8)
ex4 = np.array([[97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93],
[93, 63, 63, 63, 63, 86, 86, 86, 87, 84, 84, 91],
[92, 89, 88, 86, 85, 85, 84, 85, 85, 84, 84, 89],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[90, 88, 86, 84, 83, 83, 83, 83, 83, 84, 86, 88],
[90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88],
[91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88],
[92, 89, 84, 84, 85, 85, 84, 85, 85, 84, 84, 89],
[93, 91, 84, 84, 87, 86, 86, 86, 87, 88, 84, 91],
[95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93]],
dtype=np.uint8)
# _full_type_test makes a test with many image types.
_full_type_test(img, 2, ex2, diameter_closing, connectivity=2)
_full_type_test(img, 4, ex4, diameter_closing, connectivity=2)
P, S = max_tree(invert(img), connectivity=2)
_full_type_test(img, 4, ex4, diameter_opening,
parent=P, tree_traverser=S)
def test_diameter_opening(self):
"Test for Diameter Opening (2 thresholds, all types)"
img = np.array([[5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10],
[9, 40, 40, 40, 40, 16, 16, 16, 16, 60, 60, 11],
[11, 13, 15, 16, 17, 18, 18, 18, 17, 60, 60, 13],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 15, 16, 18, 19, 19, 20, 19, 19, 18, 16, 15],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[11, 13, 80, 80, 17, 18, 18, 18, 17, 100, 100, 13],
[9, 11, 80, 80, 16, 16, 16, 16, 16, 15, 100, 11],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10]])
ex2 = np.array([[5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10],
[9, 40, 40, 40, 40, 16, 16, 16, 16, 60, 60, 11],
[11, 13, 15, 16, 17, 18, 18, 18, 17, 60, 60, 13],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 15, 16, 18, 19, 19, 19, 19, 19, 18, 16, 15],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[11, 13, 80, 80, 17, 18, 18, 18, 17, 100, 100, 13],
[9, 11, 80, 80, 16, 16, 16, 16, 16, 15, 100, 11],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10]])
ex4 = np.array([[5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10],
[9, 40, 40, 40, 40, 16, 16, 16, 16, 18, 18, 11],
[11, 13, 15, 16, 17, 18, 18, 18, 17, 18, 18, 13],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 15, 16, 18, 19, 19, 19, 19, 19, 18, 16, 15],
[12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14],
[12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14],
[11, 13, 18, 18, 17, 18, 18, 18, 17, 18, 18, 13],
[9, 11, 18, 18, 16, 16, 16, 16, 16, 15, 18, 11],
[7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10]])
# _full_type_test makes a test with many image types.
_full_type_test(img, 2, ex2, diameter_opening, connectivity=2)
_full_type_test(img, 4, ex4, diameter_opening, connectivity=2)
P, S = max_tree(img, connectivity=2)
_full_type_test(img, 4, ex4, diameter_opening,
parent=P, tree_traverser=S)
def test_local_maxima(self):
"local maxima for various data types"
data = np.array([[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
[13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
[14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
[14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
[14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
[13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
[11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
dtype=np.uint8)
expected_result = 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, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 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, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint64)
for dtype in [np.uint8, np.uint64, np.int8, np.int64]:
test_data = data.astype(dtype)
out = max_tree_local_maxima(test_data, connectivity=1)
out_bin = out > 0
assert_array_equal(expected_result, out_bin)
assert out.dtype == expected_result.dtype
assert np.max(out) == 5
P, S = max_tree(test_data)
out = max_tree_local_maxima(test_data,
parent=P,
tree_traverser=S)
assert_array_equal(expected_result, out_bin)
assert out.dtype == expected_result.dtype
assert np.max(out) == 5
def test_extrema_float(self):
"specific tests for float type"
data = np.array([[0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14,
0.14, 0.13, 0.11],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
0.16, 0.15, 0.13],
[0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18,
0.60, 0.60, 0.15],
[0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19,
0.60, 0.60, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
0.19, 0.18, 0.16],
[0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19,
0.19, 0.18, 0.16],
[0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
0.19, 0.18, 0.16],
[0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19,
4.0, 1.0, 0.16],
[0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18,
1.0, 1.0, 0.15],
[0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
0.16, 0.15, 0.13]],
dtype=np.float32)
expected_result = 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, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
# test for local maxima
out = max_tree_local_maxima(data, connectivity=1)
out_bin = out > 0
assert_array_equal(expected_result, out_bin)
assert np.max(out) == 6
def test_3d(self):
"""tests the detection of maxima in 3D."""
img = np.zeros((8, 8, 8), dtype=np.uint8)
local_maxima = np.zeros((8, 8, 8), dtype=np.uint64)
# first maximum: only one pixel
img[1, 1:3, 1:3] = 100
img[2, 2, 2] = 200
img[3, 1:3, 1:3] = 100
local_maxima[2, 2, 2] = 1
# second maximum: three pixels in z-direction
img[5:8, 1, 1] = 200
local_maxima[5:8, 1, 1] = 1
# third: two maxima in 0 and 3.
img[0, 5:8, 5:8] = 200
img[1, 6, 6] = 100
img[2, 5:7, 5:7] = 200
img[0:3, 5:8, 5:8] += 50
local_maxima[0, 5:8, 5:8] = 1
local_maxima[2, 5:7, 5:7] = 1
# four : one maximum in the corner of the square
img[6:8, 6:8, 6:8] = 200
img[7, 7, 7] = 255
local_maxima[7, 7, 7] = 1
out = max_tree_local_maxima(img)
out_bin = out > 0
assert_array_equal(local_maxima, out_bin)
assert np.max(out) == 5
if __name__ == "__main__":
np.testing.run_module_suite()

View file

@ -0,0 +1,191 @@
import numpy as np
from skimage.morphology import remove_small_objects, remove_small_holes
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal, assert_equal
from skimage._shared._warnings import expected_warnings
test_image = np.array([[0, 0, 0, 1, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 1]], bool)
def test_one_connectivity():
expected = np.array([[0, 0, 0, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0]], bool)
observed = remove_small_objects(test_image, min_size=6)
assert_array_equal(observed, expected)
def test_two_connectivity():
expected = np.array([[0, 0, 0, 1, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0]], bool)
observed = remove_small_objects(test_image, min_size=7, connectivity=2)
assert_array_equal(observed, expected)
def test_in_place():
image = test_image.copy()
observed = remove_small_objects(image, min_size=6, in_place=True)
assert_equal(observed is image, True,
"remove_small_objects in_place argument failed.")
def test_labeled_image():
labeled_image = np.array([[2, 2, 2, 0, 1],
[2, 2, 2, 0, 1],
[2, 0, 0, 0, 0],
[0, 0, 3, 3, 3]], dtype=int)
expected = np.array([[2, 2, 2, 0, 0],
[2, 2, 2, 0, 0],
[2, 0, 0, 0, 0],
[0, 0, 3, 3, 3]], dtype=int)
observed = remove_small_objects(labeled_image, min_size=3)
assert_array_equal(observed, expected)
def test_uint_image():
labeled_image = np.array([[2, 2, 2, 0, 1],
[2, 2, 2, 0, 1],
[2, 0, 0, 0, 0],
[0, 0, 3, 3, 3]], dtype=np.uint8)
expected = np.array([[2, 2, 2, 0, 0],
[2, 2, 2, 0, 0],
[2, 0, 0, 0, 0],
[0, 0, 3, 3, 3]], dtype=np.uint8)
observed = remove_small_objects(labeled_image, min_size=3)
assert_array_equal(observed, expected)
def test_single_label_warning():
image = np.array([[0, 0, 0, 1, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 0, 0]], int)
with expected_warnings(['use a boolean array?']):
remove_small_objects(image, min_size=6)
def test_float_input():
float_test = np.random.rand(5, 5)
with testing.raises(TypeError):
remove_small_objects(float_test)
def test_negative_input():
negative_int = np.random.randint(-4, -1, size=(5, 5))
with testing.raises(ValueError):
remove_small_objects(negative_int)
test_holes_image = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]], np.bool_)
def test_one_connectivity_holes():
expected = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]], np.bool_)
observed = remove_small_holes(test_holes_image, area_threshold=3)
assert_array_equal(observed, expected)
def test_two_connectivity_holes():
expected = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]], np.bool_)
observed = remove_small_holes(test_holes_image, area_threshold=3,
connectivity=2)
assert_array_equal(observed, expected)
def test_in_place_holes():
image = test_holes_image.copy()
observed = remove_small_holes(image, area_threshold=3, in_place=True)
assert_equal(observed is image, True,
"remove_small_holes in_place argument failed.")
def test_labeled_image_holes():
labeled_holes_image = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 0, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2]],
dtype=np.int_)
expected = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]], dtype=np.bool_)
with expected_warnings(['returned as a boolean array']):
observed = remove_small_holes(labeled_holes_image, area_threshold=3)
assert_array_equal(observed, expected)
def test_uint_image_holes():
labeled_holes_image = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 0, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2]],
dtype=np.uint8)
expected = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1]], dtype=np.bool_)
with expected_warnings(['returned as a boolean array']):
observed = remove_small_holes(labeled_holes_image, area_threshold=3)
assert_array_equal(observed, expected)
def test_label_warning_holes():
labeled_holes_image = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 0, 2],
[0, 0, 0, 0, 0, 0, 0, 2, 2, 2]],
dtype=np.int_)
with expected_warnings(['use a boolean array?']):
remove_small_holes(labeled_holes_image, area_threshold=3)
remove_small_holes(labeled_holes_image.astype(bool), area_threshold=3)
def test_float_input_holes():
float_test = np.random.rand(5, 5)
with testing.raises(TypeError):
remove_small_holes(float_test)

View file

@ -0,0 +1,141 @@
"""
These tests are 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 skimage.morphology.greyreconstruct import reconstruction
from skimage._shared import testing
from skimage._shared.testing import assert_array_almost_equal
def test_zeros():
"""Test reconstruction with image and mask of zeros"""
assert_array_almost_equal(
reconstruction(np.zeros((5, 7)), np.zeros((5, 7))), 0)
def test_image_equals_mask():
"""Test reconstruction where the image and mask are the same"""
assert_array_almost_equal(
reconstruction(np.ones((7, 5)), np.ones((7, 5))), 1)
def test_image_less_than_mask():
"""Test reconstruction where the image is uniform and less than mask"""
image = np.ones((5, 5))
mask = np.ones((5, 5)) * 2
assert_array_almost_equal(reconstruction(image, mask), 1)
def test_one_image_peak():
"""Test reconstruction with one peak pixel"""
image = np.ones((5, 5))
image[2, 2] = 2
mask = np.ones((5, 5)) * 3
assert_array_almost_equal(reconstruction(image, mask), 2)
def test_two_image_peaks():
"""Test reconstruction with two peak pixels isolated by the mask"""
image = np.array([[1, 1, 1, 1, 1, 1, 1, 1],
[1, 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, 3, 1],
[1, 1, 1, 1, 1, 1, 1, 1]])
mask = np.array([[4, 4, 4, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 4, 4, 4],
[1, 1, 1, 1, 1, 4, 4, 4],
[1, 1, 1, 1, 1, 4, 4, 4]])
expected = np.array([[2, 2, 2, 1, 1, 1, 1, 1],
[2, 2, 2, 1, 1, 1, 1, 1],
[2, 2, 2, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 3, 3, 3],
[1, 1, 1, 1, 1, 3, 3, 3],
[1, 1, 1, 1, 1, 3, 3, 3]])
assert_array_almost_equal(reconstruction(image, mask), expected)
def test_zero_image_one_mask():
"""Test reconstruction with an image of all zeros and a mask that's not"""
result = reconstruction(np.zeros((10, 10)), np.ones((10, 10)))
assert_array_almost_equal(result, 0)
def test_fill_hole():
"""Test reconstruction by erosion, which should fill holes in mask."""
seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0])
mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0])
result = reconstruction(seed, mask, method='erosion')
assert_array_almost_equal(result, np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0]))
def test_invalid_seed():
seed = np.ones((5, 5))
mask = np.ones((5, 5))
with testing.raises(ValueError):
reconstruction(seed * 2, mask,
method='dilation')
with testing.raises(ValueError):
reconstruction(seed * 0.5, mask,
method='erosion')
def test_invalid_selem():
seed = np.ones((5, 5))
mask = np.ones((5, 5))
with testing.raises(ValueError):
reconstruction(seed, mask,
selem=np.ones((4, 4)))
with testing.raises(ValueError):
reconstruction(seed, mask,
selem=np.ones((3, 4)))
reconstruction(seed, mask, selem=np.ones((3, 3)))
def test_invalid_method():
seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0])
mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0])
with testing.raises(ValueError):
reconstruction(seed, mask, method='foo')
def test_invalid_offset_not_none():
"""Test reconstruction with invalid not None offset parameter"""
image = np.array([[1, 1, 1, 1, 1, 1, 1, 1],
[1, 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, 3, 1],
[1, 1, 1, 1, 1, 1, 1, 1]])
mask = np.array([[4, 4, 4, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 4, 4, 4],
[1, 1, 1, 1, 1, 4, 4, 4],
[1, 1, 1, 1, 1, 4, 4, 4]])
with testing.raises(ValueError):
reconstruction(image, mask, method='dilation',
selem=np.ones((3, 3)), offset=np.array([3, 0]))
def test_offset_not_none():
"""Test reconstruction with valid offset parameter"""
seed = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0])
mask = np.array([0, 8, 6, 8, 8, 8, 8, 4, 4, 0])
expected = np.array([0, 3, 6, 6, 6, 6, 6, 4, 4, 0])
assert_array_almost_equal(
reconstruction(seed, mask, method='dilation',
selem=np.ones(3), offset=np.array([0])), expected)

View file

@ -0,0 +1,148 @@
"""
Tests for Morphological structuring elements
(skimage.morphology.selem)
Author: Damian Eads
"""
import numpy as np
from skimage import data
from skimage.morphology import selem
from skimage._shared.testing import assert_equal, fetch
from skimage._shared import testing
class TestSElem():
def test_square_selem(self):
"""Test square structuring elements"""
for k in range(0, 5):
actual_mask = selem.square(k)
expected_mask = np.ones((k, k), dtype='uint8')
assert_equal(expected_mask, actual_mask)
def test_rectangle_selem(self):
"""Test rectangle structuring elements"""
for i in range(0, 5):
for j in range(0, 5):
actual_mask = selem.rectangle(i, j)
expected_mask = np.ones((i, j), dtype='uint8')
assert_equal(expected_mask, actual_mask)
def test_cube_selem(self):
"""Test cube structuring elements"""
for k in range(0, 5):
actual_mask = selem.cube(k)
expected_mask = np.ones((k, k, k), dtype='uint8')
assert_equal(expected_mask, actual_mask)
def strel_worker(self, fn, func):
matlab_masks = np.load(fetch(fn))
k = 0
for arrname in sorted(matlab_masks):
expected_mask = matlab_masks[arrname]
actual_mask = func(k)
if expected_mask.shape == (1,):
expected_mask = expected_mask[:, np.newaxis]
assert_equal(expected_mask, actual_mask)
k = k + 1
def strel_worker_3d(self, fn, func):
matlab_masks = np.load(fetch(fn))
k = 0
for arrname in sorted(matlab_masks):
expected_mask = matlab_masks[arrname]
actual_mask = func(k)
if expected_mask.shape == (1,):
expected_mask = expected_mask[:, np.newaxis]
# Test center slice for each dimension. This gives a good
# indication of validity without the need for a 3D reference
# mask.
c = int(expected_mask.shape[0]/2)
assert_equal(expected_mask, actual_mask[c, :, :])
assert_equal(expected_mask, actual_mask[:, c, :])
assert_equal(expected_mask, actual_mask[:, :, c])
k = k + 1
def test_selem_disk(self):
"""Test disk structuring elements"""
self.strel_worker("data/disk-matlab-output.npz", selem.disk)
def test_selem_diamond(self):
"""Test diamond structuring elements"""
self.strel_worker("data/diamond-matlab-output.npz", selem.diamond)
def test_selem_ball(self):
"""Test ball structuring elements"""
self.strel_worker_3d("data/disk-matlab-output.npz", selem.ball)
def test_selem_octahedron(self):
"""Test octahedron structuring elements"""
self.strel_worker_3d("data/diamond-matlab-output.npz",
selem.octahedron)
def test_selem_octagon(self):
"""Test octagon structuring elements"""
expected_mask1 = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]],
dtype=np.uint8)
actual_mask1 = selem.octagon(5, 3)
expected_mask2 = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=np.uint8)
actual_mask2 = selem.octagon(1, 1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)
def test_selem_ellipse(self):
"""Test ellipse structuring elements"""
expected_mask1 = 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, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=np.uint8)
actual_mask1 = selem.ellipse(5, 3)
expected_mask2 = np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 1]], dtype=np.uint8)
actual_mask2 = selem.ellipse(1, 1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)
assert_equal(expected_mask1, selem.ellipse(3, 5).T)
assert_equal(expected_mask2, selem.ellipse(1, 1).T)
def test_selem_star(self):
"""Test star structuring elements"""
expected_mask1 = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
actual_mask1 = selem.star(4)
expected_mask2 = np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 1]], dtype=np.uint8)
actual_mask2 = selem.star(1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)

View file

@ -0,0 +1,242 @@
import numpy as np
from skimage.morphology import skeletonize, medial_axis, thin
from skimage.morphology._skeletonize import (_generate_thin_luts,
G123_LUT, G123P_LUT)
from skimage import draw
from scipy.ndimage import correlate
from skimage.io import imread
from skimage import data
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal, fetch
class TestSkeletonize():
def test_skeletonize_no_foreground(self):
im = np.zeros((5, 5))
result = skeletonize(im)
assert_array_equal(result, np.zeros((5, 5)))
def test_skeletonize_wrong_dim1(self):
im = np.zeros((5))
with testing.raises(ValueError):
skeletonize(im)
def test_skeletonize_wrong_dim2(self):
im = np.zeros((5, 5, 5))
with testing.raises(ValueError):
skeletonize(im, method='zhang')
def test_skeletonize_not_binary(self):
im = np.zeros((5, 5))
im[0, 0] = 1
im[0, 1] = 2
with testing.raises(ValueError):
skeletonize(im)
def test_skeletonize_unexpected_value(self):
im = np.zeros((5, 5))
im[0, 0] = 2
with testing.raises(ValueError):
skeletonize(im)
def test_skeletonize_all_foreground(self):
im = np.ones((3, 4))
skeletonize(im)
def test_skeletonize_single_point(self):
im = np.zeros((5, 5), np.uint8)
im[3, 3] = 1
result = skeletonize(im)
assert_array_equal(result, im)
def test_skeletonize_already_thinned(self):
im = np.zeros((5, 5), np.uint8)
im[3, 1:-1] = 1
im[2, -1] = 1
im[4, 0] = 1
result = skeletonize(im)
assert_array_equal(result, im)
def test_skeletonize_output(self):
im = imread(fetch("data/bw_text.png"), as_gray=True)
# make black the foreground
im = (im == 0)
result = skeletonize(im)
expected = np.load(fetch("data/bw_text_skeleton.npy"))
assert_array_equal(result, expected)
def test_skeletonize_num_neighbours(self):
# an empty image
image = np.zeros((300, 300))
# foreground object 1
image[10:-10, 10:100] = 1
image[-100:-10, 10:-10] = 1
image[10:-10, -100:-10] = 1
# foreground object 2
rs, cs = draw.line(250, 150, 10, 280)
for i in range(10):
image[rs + i, cs] = 1
rs, cs = draw.line(10, 150, 250, 280)
for i in range(20):
image[rs + i, cs] = 1
# foreground object 3
ir, ic = np.indices(image.shape)
circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2
circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2
image[circle1] = 1
image[circle2] = 0
result = skeletonize(image)
# there should never be a 2x2 block of foreground pixels in a skeleton
mask = np.array([[1, 1],
[1, 1]], np.uint8)
blocks = correlate(result, mask, mode='constant')
assert not np.any(blocks == 4)
def test_lut_fix(self):
im = np.zeros((6, 6), np.uint8)
im[1, 2] = 1
im[2, 2] = 1
im[2, 3] = 1
im[3, 3] = 1
im[3, 4] = 1
im[4, 4] = 1
im[4, 5] = 1
result = skeletonize(im)
expected = np.array([[0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0]], dtype=np.uint8)
assert np.all(result == expected)
class TestThin():
@property
def input_image(self):
"""image to test thinning with"""
ii = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
return ii
def test_zeros(self):
assert np.all(thin(np.zeros((10, 10))) == False)
def test_iter_1(self):
result = thin(self.input_image, 1).astype(np.uint8)
expected = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
assert_array_equal(result, expected)
def test_noiter(self):
result = thin(self.input_image).astype(np.uint8)
expected = np.array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 1, 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]], dtype=np.uint8)
assert_array_equal(result, expected)
def test_baddim(self):
for ii in [np.zeros((3)), np.zeros((3, 3, 3))]:
with testing.raises(ValueError):
thin(ii)
def test_lut_generation(self):
g123, g123p = _generate_thin_luts()
assert_array_equal(g123, G123_LUT)
assert_array_equal(g123p, G123P_LUT)
class TestMedialAxis():
def test_00_00_zeros(self):
'''Test skeletonize on an array of all zeros'''
result = medial_axis(np.zeros((10, 10), bool))
assert np.all(result == False)
def test_00_01_zeros_masked(self):
'''Test skeletonize on an array that is completely masked'''
result = medial_axis(np.zeros((10, 10), bool),
np.zeros((10, 10), bool))
assert np.all(result == False)
def test_vertical_line(self):
'''Test a thick vertical line, issue #3861'''
img = np.zeros((9, 9))
img[:, 2] = 1
img[:, 3] = 1
img[:, 4] = 1
expected = np.full(img.shape, False)
expected[:, 3] = True
result = medial_axis(img)
assert_array_equal(result, expected)
def test_01_01_rectangle(self):
'''Test skeletonize on a rectangle'''
image = np.zeros((9, 15), bool)
image[1:-1, 1:-1] = True
#
# The result should be four diagonals from the
# corners, meeting in a horizontal line
#
expected = np.array([[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, 1, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 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]],
dtype=np.bool_)
result = medial_axis(image)
assert np.all(result == expected)
result, distance = medial_axis(image, return_distance=True)
assert distance.max() == 4
def test_01_02_hole(self):
'''Test skeletonize on a rectangle with a hole in the middle'''
image = np.zeros((9, 15), bool)
image[1:-1, 1:-1] = True
image[4, 4:-4] = False
expected = np.array([[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, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 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]],
dtype=np.bool_)
result = medial_axis(image)
assert np.all(result == expected)
def test_narrow_image(self):
"""Test skeletonize on a 1-pixel thin strip"""
image = np.zeros((1, 5), bool)
image[:, 1:-1] = True
result = medial_axis(image)
assert np.all(result == image)

View file

@ -0,0 +1,188 @@
import os
import numpy as np
import scipy.ndimage as ndi
from skimage import io, draw, data
from skimage.data import binary_blobs
from skimage.util import img_as_ubyte
from skimage.morphology import skeletonize, skeletonize_3d
from skimage._shared import testing
from skimage._shared.testing import assert_equal, assert_, parametrize, fetch
# basic behavior tests (mostly copied over from 2D skeletonize)
def test_skeletonize_wrong_dim():
im = np.zeros(5, dtype=np.uint8)
with testing.raises(ValueError):
skeletonize(im, method='lee')
im = np.zeros((5, 5, 5, 5), dtype=np.uint8)
with testing.raises(ValueError):
skeletonize(im, method='lee')
def test_skeletonize_1D_old_api():
# a corner case of an image of a shape(1, N)
im = np.ones((5, 1), dtype=np.uint8)
res = skeletonize_3d(im)
assert_equal(res, im)
def test_skeletonize_1D():
# a corner case of an image of a shape(1, N)
im = np.ones((5, 1), dtype=np.uint8)
res = skeletonize(im, method='lee')
assert_equal(res, im)
def test_skeletonize_no_foreground():
im = np.zeros((5, 5), dtype=np.uint8)
result = skeletonize(im, method='lee')
assert_equal(result, im)
def test_skeletonize_all_foreground():
im = np.ones((3, 4), dtype=np.uint8)
assert_equal(skeletonize(im, method='lee'),
np.array([[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0]], dtype=np.uint8))
def test_skeletonize_single_point():
im = np.zeros((5, 5), dtype=np.uint8)
im[3, 3] = 1
result = skeletonize(im, method='lee')
assert_equal(result, im)
def test_skeletonize_already_thinned():
im = np.zeros((5, 5), dtype=np.uint8)
im[3, 1:-1] = 1
im[2, -1] = 1
im[4, 0] = 1
result = skeletonize(im, method='lee')
assert_equal(result, im)
def test_dtype_conv():
# check that the operation does the right thing with floats etc
# also check non-contiguous input
img = np.random.random((16, 16))[::2, ::2]
img[img < 0.5] = 0
orig = img.copy()
res = skeletonize(img, method='lee')
img_max = img_as_ubyte(img).max()
assert_equal(res.dtype, np.uint8)
assert_equal(img, orig) # operation does not clobber the original
assert_equal(res.max(), img_max) # the intensity range is preserved
@parametrize("img", [
np.ones((8, 8), dtype=float), np.ones((4, 8, 8), dtype=float)
])
def test_input_with_warning(img):
# check that the input is not clobbered
# for 2D and 3D images of varying dtypes
check_input(img)
@parametrize("img", [
np.ones((8, 8), dtype=np.uint8), np.ones((4, 8, 8), dtype=np.uint8),
np.ones((8, 8), dtype=bool), np.ones((4, 8, 8), dtype=bool)
])
def test_input_without_warning(img):
# check that the input is not clobbered
# for 2D and 3D images of varying dtypes
check_input(img)
def check_input(img):
orig = img.copy()
skeletonize(img, method='lee')
assert_equal(img, orig)
def test_skeletonize_num_neighbours():
# an empty image
image = np.zeros((300, 300))
# foreground object 1
image[10:-10, 10:100] = 1
image[-100:-10, 10:-10] = 1
image[10:-10, -100:-10] = 1
# foreground object 2
rs, cs = draw.line(250, 150, 10, 280)
for i in range(10):
image[rs + i, cs] = 1
rs, cs = draw.line(10, 150, 250, 280)
for i in range(20):
image[rs + i, cs] = 1
# foreground object 3
ir, ic = np.indices(image.shape)
circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2
circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2
image[circle1] = 1
image[circle2] = 0
result = skeletonize(image, method='lee')
# there should never be a 2x2 block of foreground pixels in a skeleton
mask = np.array([[1, 1],
[1, 1]], np.uint8)
blocks = ndi.correlate(result, mask, mode='constant')
assert_(not np.any(blocks == 4))
def test_two_hole_image():
# test a simple 2D image against FIJI
img_o = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
[0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
img_f = 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, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
dtype=np.uint8)
res = skeletonize(img_o, method='lee')
assert_equal(res, img_f)
def test_3d_vs_fiji():
# generate an image with blobs and compate its skeleton to
# the skeleton generated by FIJI
img = binary_blobs(32, 0.05, n_dim=3, seed=1234)
img = img[:-2, ...]
img = img.astype(np.uint8)*255
img_s = skeletonize(img)
img_f = io.imread(fetch("data/_blobs_3d_fiji_skeleton.tif"))
assert_equal(img_s, img_f)

View file

@ -0,0 +1,127 @@
"""Tests for `_util`."""
import pytest
import numpy as np
from numpy.testing import assert_array_equal
from skimage.morphology import _util
@pytest.mark.parametrize("image_shape", [
(111,), (33, 44), (22, 55, 11), (6, 5, 4, 3)
])
@pytest.mark.parametrize("order", ["C", "F"])
def test_offsets_to_raveled_neighbors_highest_connectivity(image_shape, order):
"""
Check scenarios where selem is always of the highest connectivity
and all dimensions are > 2.
"""
selem = np.ones((3,) * len(image_shape), dtype=bool)
center = (1,) * len(image_shape)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, selem, center, order
)
# Assert only neighbors are present, center was removed
assert len(offsets) == selem.sum() - 1
assert 0 not in offsets
# Assert uniqueness
assert len(set(offsets)) == offsets.size
# offsets form pairs of with same value but different signs
# if selem is symmetric around center
assert all(-x in offsets for x in offsets)
# Construct image whose values are the Manhattan distance to its center
image_center = tuple(s // 2 for s in image_shape)
coords = [
np.abs(np.arange(s, dtype=np.intp) - c)
for s, c in zip(image_shape, image_center)
]
grid = np.meshgrid(*coords, indexing="ij")
image = np.sum(grid, axis=0)
image_raveled = image.ravel(order)
image_center_raveled = np.ravel_multi_index(
image_center, image_shape, order=order
)
# Sample raveled image around its center
samples = []
for offset in offsets:
index = image_center_raveled + offset
samples.append(image_raveled[index])
# Assert that center with value 0 wasn't selected
assert np.min(samples) == 1
# Assert that only neighbors where selected
# (highest value == connectivity)
assert np.max(samples) == len(image_shape)
# Assert that nearest neighbors are selected first
assert list(sorted(samples)) == samples
@pytest.mark.parametrize("image_shape", [
(2,), (2, 2), (2, 1, 2), (2, 2, 1, 2), (0, 2, 1, 2)
])
@pytest.mark.parametrize("order", ["C", "F"])
def test_offsets_to_raveled_neighbors_selem_smaller_image(image_shape, order):
"""
Test if a dimension indicated by `image_shape` is smaller than in
`selem`.
"""
selem = np.ones((3,) * len(image_shape), dtype=bool)
center = (1,) * len(image_shape)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, selem, center, order
)
# Assert only neighbors are present, center and duplicates (possible
# for this scenario) where removed
assert len(offsets) <= selem.sum() - 1
assert 0 not in offsets
# Assert uniqueness
assert len(set(offsets)) == offsets.size
# offsets form pairs of with same value but different signs
# if selem is symmetric around center
assert all(-x in offsets for x in offsets)
def test_offsets_to_raveled_neighbors_explicit_0():
"""Check reviewed example."""
image_shape = (100, 200, 3)
selem = np.ones((3, 3, 3), dtype=bool)
center = (1, 1, 1)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, selem, center
)
desired = np.array([
3, -600, 1, -1, 600, -3, 4, 2, 603, -2, -4,
-597, 601, -599, -601, -603, 599, 597, 602, -604, 596, -596,
-598, -602, 598, 604
])
assert_array_equal(offsets, desired)
def test_offsets_to_raveled_neighbors_explicit_1():
"""Check reviewed example where selem is larger in last dimension."""
image_shape = (10, 9, 8, 3)
selem = np.ones((3, 3, 3, 4), dtype=bool)
center = (1, 1, 1, 1)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, selem, center
)
desired = np.array([
24, 3, 1, -1, -3, -24, -216, 216, -192, 215, -2,
-21, -23, 2, -25, -27, 4, 217, 21, 219, -4, 23,
25, -240, 240, 192, 27, -213, -219, 213, -215, -217, -243,
191, -241, 195, 189, 212, 26, 5, 20, 28, 22, 214,
243, -237, -22, 241, -214, -212, 237, -218, -195, -20, 220,
-193, -191, 218, -189, -28, -26, 193, -239, -220, 239, 196,
221, 242, 236, 238, 194, -244, -188, -238, -211, -196, -194,
-190, -236, -19, 244, 29, 188, -242, 190, -187, 197, -235,
245
])
assert_array_equal(offsets, desired)