comment here

This commit is contained in:
ton
2023-03-18 20:03:34 +07:00
commit 4553a0a589
14513 changed files with 2685043 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
from .binary import (binary_closing, binary_dilation, binary_erosion,
binary_opening)
from .gray import (black_tophat, closing, dilation, erosion, opening,
white_tophat)
from .isotropic import (isotropic_erosion, isotropic_dilation,
isotropic_opening, isotropic_closing)
from .footprints import (ball, cube, diamond, disk, ellipse,
footprint_from_sequence, octagon, octahedron,
rectangle, square, star)
from ..measure._label import label
from ._skeletonize import medial_axis, skeletonize, skeletonize_3d, thin
from .convex_hull import convex_hull_image, convex_hull_object
from .grayreconstruct import reconstruction
from .misc import remove_small_holes, remove_small_objects
from .extrema import h_maxima, h_minima, local_minima, local_maxima
from ._flood_fill import flood, flood_fill
from .max_tree import (area_opening, area_closing, diameter_closing,
diameter_opening, max_tree,
max_tree_local_maxima)
__all__ = ['area_closing',
'area_opening',
'ball',
'binary_closing',
'binary_dilation',
'binary_erosion',
'binary_opening',
'black_tophat',
'closing',
'convex_hull_image',
'convex_hull_object',
'cube',
'diameter_closing',
'diameter_opening',
'diamond',
'dilation',
'disk',
'ellipse',
'erosion',
'flood',
'flood_fill',
'footprint_from_sequence',
'h_maxima',
'h_minima',
'isotropic_closing',
'isotropic_dilation',
'isotropic_erosion',
'isotropic_opening',
'label',
'local_maxima',
'local_minima',
'max_tree',
'max_tree_local_maxima',
'medial_axis',
'octagon',
'octahedron',
'opening',
'reconstruction',
'rectangle',
'remove_small_holes',
'remove_small_objects',
'skeletonize',
'skeletonize_3d',
'square',
'star',
'thin',
'white_tophat'
]

View File

@@ -0,0 +1,290 @@
"""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 ..util import crop
from ._flood_fill_cy import _flood_fill_equal, _flood_fill_tolerance
from ._util import (_offsets_to_raveled_neighbors, _resolve_neighborhood,
_set_border_values,)
def flood_fill(image, seed_point, new_value, *, footprint=None,
connectivity=None, tolerance=None, in_place=False):
"""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`.
footprint : ndarray, optional
The footprint (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
`footprint` 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).
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]])
"""
mask = flood(image, seed_point, footprint=footprint,
connectivity=connectivity, tolerance=tolerance)
if not in_place:
image = image.copy()
image[mask] = new_value
return image
def flood(image, seed_point, *, footprint=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.
footprint : ndarray, optional
The footprint (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
`footprint` 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'
# Shortcut for rank zero
if 0 in image.shape:
return np.zeros(image.shape, dtype=bool)
# Convenience for 1d input
try:
iter(seed_point)
except TypeError:
seed_point = (seed_point,)
seed_value = image[seed_point]
seed_point = tuple(np.asarray(seed_point) % image.shape)
footprint = _resolve_neighborhood(
footprint, connectivity, image.ndim, enforce_adjacency=False)
center = tuple(s // 2 for s in footprint.shape)
# Compute padding width as the maximum offset to neighbors on each axis.
# Generates a 2-tuple of (pad_start, pad_end) for each axis.
pad_width = [(np.max(np.abs(idx - c)),) * 2
for idx, c in zip(np.nonzero(footprint), center)]
# Must annotate borders
working_image = np.pad(image, pad_width, mode='constant',
constant_values=image.min())
# Stride-aware neighbors - works for both C- and Fortran-contiguity
ravelled_seed_idx = np.ravel_multi_index(
[i + pad_start
for i, (pad_start, pad_end) in zip(seed_point, pad_width)],
working_image.shape,
order=order
)
neighbor_offsets = _offsets_to_raveled_neighbors(
working_image.shape, footprint, center=center,
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, border_width=pad_width)
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 crop(flags, pad_width, copy=False).view(bool)

View File

@@ -0,0 +1,639 @@
"""
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
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
An 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.astype(bool, copy=False))
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(f'skeletonize requires a 2D or 3D image as input, '
f'got {image.ndim}D.')
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 neighboring
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)
"""
if image.ndim != 2:
raise ValueError("Zhang's skeletonize method requires a 2D array")
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(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=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=bool)
def thin(image, max_num_iter=None):
"""
Perform morphological thinning of a binary image.
Parameters
----------
image : binary (M, N) ndarray
The image to be thinned.
max_num_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_num_iter = max_num_iter or np.inf
num_iter = 0
n_pts_old, n_pts_new = np.inf, np.sum(skel)
while n_pts_old != n_pts_new and num_iter < max_num_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
num_iter += 1
return skel.astype(bool)
# --------- Skeletonization by medial axis transform --------
_eight_connect = ndi.generate_binary_structure(2, 2)
def medial_axis(image, mask=None, return_distance=False, *, random_state=None):
"""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.
random_state : {None, int, `numpy.random.Generator`}, optional
If `random_state` is None the `numpy.random.Generator` singleton is
used.
If `random_state` is an int, a new ``Generator`` instance is used,
seeded with `random_state`.
If `random_state` is already a ``Generator`` instance then that
instance is used.
.. versionadded:: 0.19
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(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 neighborhood 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 neighborhood
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.default_rng(random_state)
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.
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; "
f"got image.ndim = {image.ndim} instead.")
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,333 @@
"""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 footprint 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 footprint 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 footprint.
Returns
-------
c_connectivity : array of bool
The footprint (structuring element) corresponding to the input
`connectivity`.
offset : array of int
The offset corresponding to the center of the footprint.
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 _raveled_offsets_and_distances(
image_shape,
*,
footprint=None,
connectivity=1,
center=None,
spacing=None,
order='C',
):
"""Compute offsets to neighboring pixels in raveled coordinate space.
This function also returns the corresponding distances from the center
pixel given a spacing (assumed to be 1 along each axis by default).
Parameters
----------
image_shape : tuple of int
The shape of the image for which the offsets are being computed.
footprint : array of bool
The footprint of the neighborhood, expressed as an n-dimensional array
of 1s and 0s. If provided, the connectivity argument is ignored.
connectivity : {1, ..., ndim}
The square connectivity of the neighborhood: the number of orthogonal
steps allowed to consider a pixel a neighbor. See
`scipy.ndimage.generate_binary_structure`. Ignored if footprint is
provided.
center : tuple of int
Tuple of indices to the center of the footprint. If not provided, it
is assumed to be the center of the footprint, either provided or
generated by the connectivity argument.
spacing : tuple of float
The spacing between pixels/voxels along each axis.
order : 'C' or 'F'
The ordering of the array, either C or Fortran ordering.
Returns
-------
raveled_offsets : ndarray
Linear offsets to a samples neighbors in the raveled image, sorted by
their distance from the center.
distances : ndarray
The pixel distances corresponding to each offset.
Notes
-----
This function will return values even if `image_shape` contains a dimension
length that is smaller than `footprint`.
Examples
--------
>>> off, d = _raveled_offsets_and_distances(
... (4, 5), footprint=np.ones((4, 3)), center=(1, 1)
... )
>>> off
array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11])
>>> d[0]
1.0
>>> d[-1] # distance from (1, 1) to (3, 2)
2.236...
"""
ndim = len(image_shape)
if footprint is None:
footprint = ndi.generate_binary_structure(
rank=ndim, connectivity=connectivity
)
if center is None:
center = tuple(s // 2 for s in footprint.shape)
if not footprint.ndim == ndim == len(center):
raise ValueError(
"number of dimensions in image shape, footprint and its"
"center index does not match")
offsets = np.stack([(idx - c)
for idx, c in zip(np.nonzero(footprint), center)],
axis=-1)
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
if spacing is None:
spacing = np.ones(ndim)
weighted_offsets = offsets * spacing
distances = np.sqrt(np.sum(weighted_offsets**2, axis=1))
sorted_raveled_offsets = raveled_offsets[np.argsort(distances)]
sorted_distances = np.sort(distances)
# If any dimension in image_shape is smaller than footprint.shape
# duplicates might occur, remove them
if any(x < y for x, y in zip(image_shape, footprint.shape)):
# np.unique reorders, which we don't want
_, indices = np.unique(sorted_raveled_offsets, return_index=True)
sorted_raveled_offsets = sorted_raveled_offsets[np.sort(indices)]
sorted_distances = sorted_distances[np.sort(indices)]
# Remove "offset to center"
sorted_raveled_offsets = sorted_raveled_offsets[1:]
sorted_distances = sorted_distances[1:]
return sorted_raveled_offsets, sorted_distances
def _offsets_to_raveled_neighbors(image_shape, footprint, 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.
footprint : ndarray
The footprint (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 `footprint`.
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 `footprint`.
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])
"""
raveled_offsets = _raveled_offsets_and_distances(
image_shape, footprint=footprint, center=center, order=order
)[0]
return raveled_offsets
def _resolve_neighborhood(footprint, connectivity, ndim,
enforce_adjacency=True):
"""Validate or create a footprint (structuring element).
Depending on the values of `connectivity` and `footprint` this function
either creates a new footprint (`footprint` is None) using `connectivity`
or validates the given footprint (`footprint` is not None).
Parameters
----------
footprint : ndarray
The footprint (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 `footprint` 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
`footprint` is not None.
ndim : int
Number of dimensions `footprint` ought to have.
enforce_adjacency : bool
A boolean that determines whether footprint must only specify direct
neighbors.
Returns
-------
footprint : ndarray
Validated or new footprint 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 footprint is None:
if connectivity is None:
connectivity = ndim
footprint = ndi.generate_binary_structure(ndim, connectivity)
else:
# Validate custom structured element
footprint = np.asarray(footprint, dtype=bool)
# Must specify neighbors for all dimensions
if footprint.ndim != ndim:
raise ValueError(
"number of dimensions in image and footprint do not"
"match"
)
# Must only specify direct neighbors
if enforce_adjacency and any(s != 3 for s in footprint.shape):
raise ValueError("dimension size in footprint is not 3")
elif any((s % 2 != 1) for s in footprint.shape):
raise ValueError("footprint size must be odd along all dimensions")
return footprint
def _set_border_values(image, value, border_width=1):
"""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.
border_width : int or sequence of tuples
A sequence with one 2-tuple per axis where the first and second values
are the width of the border at the start and end of the axis,
respectively. If an int is provided, a uniform border width along all
axes is used.
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]])
>>> image = np.zeros((8, 8), dtype=int)
>>> _set_border_values(image, 1, border_width=((1, 1), (2, 3)))
>>> image
array([[1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1]])
"""
if np.isscalar(border_width):
border_width = ((border_width, border_width),) * image.ndim
elif len(border_width) != image.ndim:
raise ValueError('length of `border_width` must match image.ndim')
for axis, npad in enumerate(border_width):
if len(npad) != 2:
raise ValueError('each sequence in `border_width` must have '
'length 2')
w_start, w_end = npad
if w_start == w_end == 0:
continue
elif w_start == w_end == 1:
# Index first and last element in the current dimension
sl = (slice(None),) * axis + ((0, -1),) + (...,)
image[sl] = value
continue
if w_start > 0:
# set first w_start entries along axis to value
sl = (slice(None),) * axis + (slice(0, w_start),) + (...,)
image[sl] = value
if w_end > 0:
# set last w_end entries along axis to value
sl = (slice(None),) * axis + (slice(-w_end, None),) + (...,)
image[sl] = value

View File

@@ -0,0 +1,249 @@
"""
Binary morphological operations
"""
import functools
import numpy as np
from scipy import ndimage as ndi
from .footprints import _footprint_is_sequence
from .misc import default_footprint
def _iterate_binary_func(binary_func, image, footprint, out):
"""Helper to call `binary_func` for each footprint in a sequence.
binary_func is a binary morphology function that accepts "structure",
"output" and "iterations" keyword arguments
(e.g. `scipy.ndimage.binary_erosion`).
"""
fp, num_iter = footprint[0]
binary_func(image, structure=fp, output=out, iterations=num_iter)
for fp, num_iter in footprint[1:]:
# Note: out.copy() because the computation cannot be in-place!
# SciPy <= 1.7 did not automatically make a copy if needed.
binary_func(out.copy(), structure=fp, output=out, iterations=num_iter)
return out
# The default_footprint decorator provides a diamond footprint as
# default with the same dimension as the input image and size 3 along each
# axis.
@default_footprint
def binary_erosion(image, footprint=None, out=None):
"""Return fast binary morphological erosion of an image.
This function returns the same result as grayscale 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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]``.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
See also
--------
skimage.morphology.isotropic_erosion
"""
if out is None:
out = np.empty(image.shape, dtype=bool)
if _footprint_is_sequence(footprint):
binary_func = functools.partial(ndi.binary_erosion, border_value=True)
return _iterate_binary_func(binary_func, image, footprint, out)
ndi.binary_erosion(image, structure=footprint, output=out,
border_value=True)
return out
@default_footprint
def binary_dilation(image, footprint=None, out=None):
"""Return fast binary morphological dilation of an image.
This function returns the same result as grayscale 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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]``.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
See also
--------
skimage.morphology.isotropic_dilation
"""
if out is None:
out = np.empty(image.shape, dtype=bool)
if _footprint_is_sequence(footprint):
return _iterate_binary_func(ndi.binary_dilation, image, footprint, out)
ndi.binary_dilation(image, structure=footprint, output=out)
return out
@default_footprint
def binary_opening(image, footprint=None, out=None):
"""Return fast binary morphological opening of an image.
This function returns the same result as grayscale 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
See also
--------
skimage.morphology.isotropic_opening
"""
eroded = binary_erosion(image, footprint)
out = binary_dilation(eroded, footprint, out=out)
return out
@default_footprint
def binary_closing(image, footprint=None, out=None):
"""Return fast binary morphological closing of an image.
This function returns the same result as grayscale 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
See also
--------
skimage.morphology.isotropic_closing
"""
dilated = binary_dilation(image, footprint)
out = binary_erosion(dilated, footprint, out=out)
return out

View File

@@ -0,0 +1,215 @@
"""Convex Hull."""
from itertools import product
import numpy as np
from scipy.spatial import ConvexHull, QhullError
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 _check_coords_in_hull(gridcoords, hull_equations, tolerance):
r"""Checks all the coordinates for inclusiveness in the convex hull.
Parameters
----------
gridcoords : (M, N) ndarray
Coordinates of ``N`` points in ``M`` dimensions.
hull_equations : (M, N) ndarray
Hyperplane equations of the facets of the convex hull.
tolerance : float
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
-------
coords_in_hull : ndarray of bool
Binary 1D ndarray representing points in n-dimensional space
with value ``True`` set for points inside the convex hull.
Notes
-----
Checking the inclusiveness of coordinates in a convex hull requires
intermediate calculations of dot products which are memory-intensive.
Thus, the convex hull equations are checked individually with all
coordinates to keep within the memory limit.
References
----------
.. [1] https://github.com/scikit-image/scikit-image/issues/5019
"""
ndim, n_coords = gridcoords.shape
n_hull_equations = hull_equations.shape[0]
coords_in_hull = np.ones(n_coords, dtype=bool)
# Pre-allocate arrays to cache intermediate results for reducing overheads
dot_array = np.empty(n_coords, dtype=np.float64)
test_ineq_temp = np.empty(n_coords, dtype=np.float64)
coords_single_ineq = np.empty(n_coords, dtype=bool)
# A point is in the hull if it satisfies all of the hull's inequalities
for idx in range(n_hull_equations):
# Tests a hyperplane equation on all coordinates of volume
np.dot(hull_equations[idx, :ndim], gridcoords, out=dot_array)
np.add(dot_array, hull_equations[idx, ndim:], out=test_ineq_temp)
np.less(test_ineq_temp, tolerance, out=coords_single_ineq)
coords_in_hull *= coords_single_ineq
return coords_in_hull
def convex_hull_image(image, offset_coordinates=True, tolerance=1e-10,
include_borders=True):
"""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.
include_borders: bool, optional
If ``False``, vertices/edges are excluded from the final hull mask.
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=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.
try:
hull0 = ConvexHull(coords)
except QhullError as err:
warn(f"Failed to get convex hull image. "
f"Returning empty image, see error message below:\n"
f"{err}")
return np.zeros(image.shape, dtype=bool)
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
try:
hull = ConvexHull(coords)
except QhullError as err:
warn(f"Failed to get convex hull image. "
f"Returning empty image, see error message below:\n"
f"{err}")
return np.zeros(image.shape, dtype=bool)
vertices = hull.points[hull.vertices]
# If 2D, use fast Cython function to locate convex hull pixels
if ndim == 2:
labels = grid_points_in_poly(image.shape, vertices, binarize=False)
# If include_borders is True, we include vertices (2) and edge
# points (3) in the mask, otherwise only the inside of the hull (1)
mask = labels >= 1 if include_borders else labels == 1
else:
gridcoords = np.reshape(np.mgrid[tuple(map(slice, image.shape))],
(ndim, -1))
coords_in_hull = _check_coords_in_hull(gridcoords,
hull.equations, tolerance)
mask = np.reshape(coords_in_hull, image.shape)
return mask
def convex_hull_object(image, *, connectivity=2):
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.
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.::
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 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,545 @@
"""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 .._shared.utils import warn
from ..util import dtype_limits, invert, crop
from . import grayreconstruct, _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, footprint=None):
"""Determine all maxima of the image with height >= h.
The local maxima are defined as connected sets of pixels with equal
gray level strictly greater than the gray 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 an equal or higher local 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 to an equal or higher local maximum for which the minimal
value is greater.
The global maxima of the image are also found by this function.
Parameters
----------
image : ndarray
The input image for which the maxima are to be calculated.
h : unsigned integer
The minimal height of all extracted maxima.
footprint : 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 local maxima of height >= h and the global maxima.
The resulting image is a binary image, where pixels belonging to
the determined 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(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(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 = grayreconstruct.reconstruction(shifted_img, image,
method='dilation',
footprint=footprint)
residue_img = image - rec_img
return (residue_img >= h).astype(np.uint8)
def h_minima(image, h, footprint=None):
"""Determine all minima of the image with depth >= h.
The local minima are defined as connected sets of pixels with equal
gray level strictly smaller than the gray 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 an equal or lower local 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 to an equal or lower local minimum for which the maximal
value is smaller.
The global minima of the image are also found by this function.
Parameters
----------
image : ndarray
The input image for which the minima are to be calculated.
h : unsigned integer
The minimal depth of all extracted minima.
footprint : 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 local minima of depth >= h and the global minima.
The resulting image is a binary image, where pixels belonging to
the determined minima take value 1, the others 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(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(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 = grayreconstruct.reconstruction(shifted_img, image,
method='erosion',
footprint=footprint)
residue_img = rec_img - image
return (residue_img >= h).astype(np.uint8)
def local_maxima(image, footprint=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.
footprint : ndarray, optional
The footprint (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 `footprint` 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
`footprint` 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=bool)
if allow_borders:
# Ensure that local maxima are always at least one smaller sample away
# from the image border
image = np.pad(image, 1, mode='constant', constant_values=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 & footprint 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:
footprint = _util._resolve_neighborhood(footprint, connectivity,
image.ndim)
neighbor_offsets = _util._offsets_to_raveled_neighbors(
image.shape, footprint, 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(bool)
def local_minima(image, footprint=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.
footprint : ndarray, optional
The footprint (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 `footprint` 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
`footprint` 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),
footprint=footprint,
connectivity=connectivity,
indices=indices,
allow_borders=allow_borders
)

View File

@@ -0,0 +1,961 @@
import os
from collections.abc import Sequence
from numbers import Integral
import numpy as np
from .. import draw
from skimage import morphology
# Precomputed ball and disk decompositions were saved as 2D arrays where the
# radius of the desired decomposition is used to index into the first axis of
# the array. The values at a given radius corresponds to the number of
# repetitions of 3 different types elementary of structuring elements.
#
# See _nsphere_series_decomposition for full details.
_nsphere_decompositions = {}
_nsphere_decompositions[2] = np.load(
os.path.join(os.path.dirname(__file__), 'disk_decompositions.npy'))
_nsphere_decompositions[3] = np.load(
os.path.join(os.path.dirname(__file__), 'ball_decompositions.npy'))
def _footprint_is_sequence(footprint):
if hasattr(footprint, '__array_interface__'):
return False
def _validate_sequence_element(t):
return (
isinstance(t, Sequence)
and len(t) == 2
and hasattr(t[0], '__array_interface__')
and isinstance(t[1], Integral)
)
if isinstance(footprint, Sequence):
if not all(_validate_sequence_element(t) for t in footprint):
raise ValueError(
"All elements of footprint sequence must be a 2-tuple where "
"the first element of the tuple is an ndarray and the second "
"is an integer indicating the number of iterations."
)
else:
raise ValueError("footprint must be either an ndarray or Sequence")
return True
def _shape_from_sequence(footprints, require_odd_size=False):
"""Determine the shape of composite footprint
In the future if we only want to support odd-sized square, we may want to
change this to require_odd_size
"""
if not _footprint_is_sequence(footprints):
raise ValueError("expected a sequence of footprints")
ndim = footprints[0][0].ndim
shape = [0] * ndim
def _odd_size(size, require_odd_size):
if require_odd_size and size % 2 == 0:
raise ValueError(
"expected all footprint elements to have odd size"
)
for d in range(ndim):
fp, nreps = footprints[0]
_odd_size(fp.shape[d], require_odd_size)
shape[d] = fp.shape[d] + (nreps - 1) * (fp.shape[d] - 1)
for fp, nreps in footprints[1:]:
_odd_size(fp.shape[d], require_odd_size)
shape[d] += nreps * (fp.shape[d] - 1)
return tuple(shape)
def footprint_from_sequence(footprints):
"""Convert a footprint sequence into an equivalent ndarray.
Parameters
----------
footprints : tuple of 2-tuples
A sequence of footprint tuples where the first element of each tuple
is an array corresponding to a footprint and the second element is the
number of times it is to be applied. Currently all footprints should
have odd size.
Returns
-------
footprint : ndarray
An single array equivalent to applying the sequence of `footprints`.
"""
# Create a single pixel image of sufficient size and apply binary dilation.
shape = _shape_from_sequence(footprints)
imag = np.zeros(shape, dtype=bool)
imag[tuple(s // 2 for s in shape)] = 1
return morphology.binary_dilation(imag, footprints)
def square(width, dtype=np.uint8, *, decomposition=None):
"""Generates a flat, square-shaped footprint.
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, optional
The data type of the footprint.
decomposition : {None, 'separable', 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but often with
better computational performance. See Notes for more details.
With 'separable', this function uses separable 1D footprints for each
axis. Whether 'seqeunce' or 'separable' is computationally faster may
be architecture-dependent.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
this will be a tuple whose length is equal to the number of unique
structuring elements to apply (see Notes for more detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For binary morphology, using ``decomposition='sequence'`` or
``decomposition='separable'`` were observed to give better performance than
``decomposition=None``, with the magnitude of the performance increase
rapidly increasing with footprint size. For grayscale morphology with
square footprints, it is recommended to use ``decomposition=None`` since
the internal SciPy functions that are called already have a fast
implementation based on separable 1D sliding windows.
The 'sequence' decomposition mode only supports odd valued `width`. If
`width` is even, the sequence used will be identical to the 'separable'
mode.
"""
if decomposition is None:
return np.ones((width, width), dtype=dtype)
if decomposition == 'separable' or width % 2 == 0:
sequence = [(np.ones((width, 1), dtype=dtype), 1),
(np.ones((1, width), dtype=dtype), 1)]
elif decomposition == 'sequence':
# only handles odd widths
sequence = [(np.ones((3, 3), dtype=dtype), _decompose_size(width, 3))]
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return tuple(sequence)
def _decompose_size(size, kernel_size=3):
"""Determine number of repeated iterations for a `kernel_size` kernel.
Returns how many repeated morphology operations with an element of size
`kernel_size` is equivalent to a morphology with a single kernel of size
`n`.
"""
if kernel_size % 2 != 1:
raise ValueError("only odd length kernel_size is supported")
return 1 + (size - kernel_size) // (kernel_size - 1)
def rectangle(nrows, ncols, dtype=np.uint8, *, decomposition=None):
"""Generates a flat, rectangular-shaped footprint.
Every pixel in the rectangle generated for a given width and given height
belongs to the neighborhood.
Parameters
----------
nrows : int
The number of rows of the rectangle.
ncols : int
The number of columns of the rectangle.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
decomposition : {None, 'separable', 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but often with
better computational performance. See Notes for more details.
With 'separable', this function uses separable 1D footprints for each
axis. Whether 'sequence' or 'separable' is computationally faster may
be architecture-dependent.
Returns
-------
footprint : ndarray or tuple
A footprint consisting only of ones, i.e. every pixel belongs to the
neighborhood. When `decomposition` is None, this is just a
numpy.ndarray. Otherwise, this will be a tuple whose length is equal to
the number of unique structuring elements to apply (see Notes for more
detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For binary morphology, using ``decomposition='sequence'``
was observed to give better performance, with the magnitude of the
performance increase rapidly increasing with footprint size. For grayscale
morphology with rectangular footprints, it is recommended to use
``decomposition=None`` since the internal SciPy functions that are called
already have a fast implementation based on separable 1D sliding windows.
The `sequence` decomposition mode only supports odd valued `nrows` and
`ncols`. If either `nrows` or `ncols` is even, the sequence used will be
identical to ``decomposition='separable'``.
- The use of ``width`` and ``height`` has been deprecated in
version 0.18.0. Use ``nrows`` and ``ncols`` instead.
"""
if decomposition is None: # TODO: check optimal width setting here
return np.ones((nrows, ncols), dtype=dtype)
even_rows = nrows % 2 == 0
even_cols = ncols % 2 == 0
if decomposition == 'separable' or even_rows or even_cols:
sequence = [(np.ones((nrows, 1), dtype=dtype), 1),
(np.ones((1, ncols), dtype=dtype), 1)]
elif decomposition == 'sequence':
# this branch only support odd nrows, ncols
sq_size = 3
sq_reps = _decompose_size(min(nrows, ncols), sq_size)
sequence = [(np.ones((3, 3), dtype=dtype), sq_reps)]
if nrows > ncols:
nextra = nrows - ncols
sequence.append(
(np.ones((nextra + 1, 1), dtype=dtype), 1)
)
elif ncols > nrows:
nextra = ncols - nrows
sequence.append(
(np.ones((1, nextra + 1), dtype=dtype), 1)
)
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return tuple(sequence)
def diamond(radius, dtype=np.uint8, *, decomposition=None):
"""Generates a flat, diamond-shaped footprint.
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 footprint.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
decomposition : {None, 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but with
better computational performance. See Notes for more details.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
this will be a tuple whose length is equal to the number of unique
structuring elements to apply (see Notes for more detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For either binary or grayscale morphology, using
``decomposition='sequence'`` was observed to have a performance benefit,
with the magnitude of the benefit increasing with increasing footprint
size.
"""
if decomposition is None:
L = np.arange(0, radius * 2 + 1)
I, J = np.meshgrid(L, L)
footprint = np.array(np.abs(I - radius) + np.abs(J - radius) <= radius,
dtype=dtype)
elif decomposition == 'sequence':
fp = diamond(1, dtype=dtype, decomposition=None)
nreps = _decompose_size(2 * radius + 1, fp.shape[0])
footprint = ((fp, nreps),)
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return footprint
def _nsphere_series_decomposition(radius, ndim, dtype=np.uint8):
"""Generate a sequence of footprints approximating an n-sphere.
Morphological operations with an n-sphere (hypersphere) footprint can be
approximated by applying a series of smaller footprints of extent 3 along
each axis. Specific solutions for this are given in [1]_ for the case of
2D disks with radius 2 through 10.
Here we used n-dimensional extensions of the "square", "diamond" and
"t-shaped" elements from that publication. All of these elementary elements
have size ``(3,) * ndim``. We numerically computed the number of
repetitions of each element that gives the closest match to the disk
(in 2D) or ball (in 3D) computed with ``decomposition=None``.
The approach can be extended to higher dimensions, but we have only stored
results for 2D and 3D at this point.
Empirically, the shapes at large radius approach a hexadecagon
(16-sides [2]_) in 2D and a rhombicuboctahedron (26-faces, [3]_) in 3D.
References
----------
.. [1] Park, H and Chin R.T. Decomposition of structuring elements for
optimal implementation of morphological operations. In Proceedings:
1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
UK.
https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
.. [2] https://en.wikipedia.org/wiki/Hexadecagon
.. [3] https://en.wikipedia.org/wiki/Rhombicuboctahedron
"""
if radius == 1:
# for radius 1 just use the exact shape (3,) * ndim solution
kwargs = dict(dtype=dtype, strict_radius=False, decomposition=None)
if ndim == 2:
return ((disk(1, **kwargs), 1),)
elif ndim == 3:
return ((ball(1, **kwargs), 1),)
# load precomputed decompositions
if ndim not in _nsphere_decompositions:
raise ValueError(
"sequence decompositions are only currently available for "
"2d disks or 3d balls"
)
precomputed_decompositions = _nsphere_decompositions[ndim]
max_radius = precomputed_decompositions.shape[0]
if radius > max_radius:
raise ValueError(
f"precomputed {ndim}D decomposition unavailable for "
f"radius > {max_radius}"
)
num_t_series, num_diamond, num_square = precomputed_decompositions[radius]
sequence = []
if num_t_series > 0:
# shape (3, ) * ndim "T-shaped" footprints
all_t = _t_shaped_element_series(ndim=ndim, dtype=dtype)
[sequence.append((t, num_t_series)) for t in all_t]
if num_diamond > 0:
d = np.zeros((3,) * ndim, dtype=dtype)
sl = [slice(1, 2)] * ndim
for ax in range(ndim):
sl[ax] = slice(None)
d[tuple(sl)] = 1
sl[ax] = slice(1, 2)
sequence.append((d, num_diamond))
if num_square > 0:
sq = np.ones((3, ) * ndim, dtype=dtype)
sequence.append((sq, num_square))
return tuple(sequence)
def _t_shaped_element_series(ndim=2, dtype=np.uint8):
"""A series of T-shaped structuring elements.
In the 2D case this is a T-shaped element and its rotation at multiples of
90 degrees. This series is used in efficient decompositions of disks of
various radius as published in [1]_.
The generalization to the n-dimensional case can be performed by having the
"top" of the T to extend in (ndim - 1) dimensions and then producing a
series of rotations such that the bottom end of the T points along each of
``2 * ndim`` orthogonal directions.
"""
if ndim == 2:
# The n-dimensional case produces the same set of footprints, but
# the 2D example is retained here for clarity.
t0 = np.array([[1, 1, 1],
[0, 1, 0],
[0, 1, 0]], dtype=dtype)
t90 = np.rot90(t0, 1)
t180 = np.rot90(t0, 2)
t270 = np.rot90(t0, 3)
return t0, t90, t180, t270
else:
# ndimensional generalization of the 2D case above
all_t = []
for ax in range(ndim):
for idx in [0, 2]:
t = np.zeros((3,) * ndim, dtype=dtype)
sl = [slice(None)] * ndim
sl[ax] = slice(idx, idx + 1)
t[tuple(sl)] = 1
sl = [slice(1, 2)] * ndim
sl[ax] = slice(None)
t[tuple(sl)] = 1
all_t.append(t)
return tuple(all_t)
def disk(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None):
"""Generates a flat, disk-shaped footprint.
A pixel is within the neighborhood if the Euclidean distance between
it and the origin is no greater than radius (This is only approximately
True, when `decomposition == 'sequence'`).
Parameters
----------
radius : int
The radius of the disk-shaped footprint.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
strict_radius : bool, optional
If False, extend the radius by 0.5. This allows the circle to expand
further within a cube that remains of size ``2 * radius + 1`` along
each axis. This parameter is ignored if decomposition is not None.
decomposition : {None, 'sequence', 'crosses'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given a result equivalent to a single, larger footprint, but with
better computational performance. For disk footprints, the 'sequence'
or 'crosses' decompositions are not always exactly equivalent to
``decomposition=None``. See Notes for more details.
Returns
-------
footprint : ndarray
The footprint where elements of the neighborhood are 1 and 0 otherwise.
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
The disk produced by the ``decomposition='sequence'`` mode may not be
identical to that with ``decomposition=None``. A disk footprint can be
approximated by applying a series of smaller footprints of extent 3 along
each axis. Specific solutions for this are given in [1]_ for the case of
2D disks with radius 2 through 10. Here, we numerically computed the number
of repetitions of each element that gives the closest match to the disk
computed with kwargs ``strict_radius=False, decomposition=None``.
Empirically, the series decomposition at large radius approaches a
hexadecagon (a 16-sided polygon [2]_). In [3]_, the authors demonstrate
that a hexadecagon is the closest approximation to a disk that can be
achieved for decomposition with footprints of shape (3, 3).
The disk produced by the ``decomposition='crosses'`` is often but not
always identical to that with ``decomposition=None``. It tends to give a
closer approximation than ``decomposition='sequence'``, at a performance
that is fairly comparable. The individual cross-shaped elements are not
limited to extent (3, 3) in size. Unlike the 'seqeuence' decomposition, the
'crosses' decomposition can also accurately approximate the shape of disks
with ``strict_radius=True``. The method is based on an adaption of
algorithm 1 given in [4]_.
References
----------
.. [1] Park, H and Chin R.T. Decomposition of structuring elements for
optimal implementation of morphological operations. In Proceedings:
1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
UK.
https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
.. [2] https://en.wikipedia.org/wiki/Hexadecagon
.. [3] Vanrell, M and Vitrià, J. Optimal 3 × 3 decomposable disks for
morphological transformations. Image and Vision Computing, Vol. 15,
Issue 11, 1997.
:DOI:`10.1016/S0262-8856(97)00026-7`
.. [4] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
Image Processing, (1 November 1990).
:DOI:`10.1117/12.23608`
"""
if decomposition is None:
L = np.arange(-radius, radius + 1)
X, Y = np.meshgrid(L, L)
if not strict_radius:
radius += 0.5
return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype)
elif decomposition == 'sequence':
sequence = _nsphere_series_decomposition(radius, ndim=2, dtype=dtype)
elif decomposition == 'crosses':
fp = disk(radius, dtype, strict_radius=strict_radius,
decomposition=None)
sequence = _cross_decomposition(fp)
return sequence
def _cross(r0, r1, dtype=np.uint8):
"""Cross-shaped structuring element of shape (r0, r1).
Only the central row and column are ones.
"""
s0 = int(2 * r0 + 1)
s1 = int(2 * r1 + 1)
c = np.zeros((s0, s1), dtype=dtype)
if r1 != 0:
c[r0, :] = 1
if r0 != 0:
c[:, r1] = 1
return c
def _cross_decomposition(footprint, dtype=np.uint8):
""" Decompose a symmetric convex footprint into cross-shaped elements.
This is a decomposition of the footprint into a sequence of
(possibly asymmetric) cross-shaped elements. This technique was proposed in
[1]_ and corresponds roughly to algorithm 1 of that publication (some
details had to be modified to get reliable operation).
.. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
Image Processing, (1 November 1990).
:DOI:`10.1117/12.23608`
"""
quadrant = footprint[footprint.shape[0] // 2:, footprint.shape[1] // 2:]
col_sums = quadrant.sum(0, dtype=int)
col_sums = np.concatenate((col_sums, np.asarray([0], dtype=int)))
i_prev = 0
idx = {}
sum0 = 0
for i in range(col_sums.size - 1):
if col_sums[i] > col_sums[i + 1]:
if i == 0:
continue
key = (col_sums[i_prev] - col_sums[i], i - i_prev)
sum0 += key[0]
if key not in idx:
idx[key] = 1
else:
idx[key] += 1
i_prev = i
n = quadrant.shape[0] - 1 - sum0
if n > 0:
key = (n, 0)
idx[key] = idx.get(key, 0) + 1
return tuple([(_cross(r0, r1, dtype), n) for (r0, r1), n in idx.items()])
def ellipse(width, height, dtype=np.uint8, *, decomposition=None):
"""Generates a flat, ellipse-shaped footprint.
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 footprint.
height : int
The height of the ellipse-shaped footprint.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
decomposition : {None, 'crosses'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but with
better computational performance. See Notes for more details.
Returns
-------
footprint : ndarray
The footprint where elements of the neighborhood are 1 and 0 otherwise.
The footprint will have shape ``(2 * height + 1, 2 * width + 1)``.
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
The ellipse produced by the ``decomposition='crosses'`` is often but not
always identical to that with ``decomposition=None``. The method is based
on an adaption of algorithm 1 given in [1]_.
References
----------
.. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
Image Processing, (1 November 1990).
:DOI:`10.1117/12.23608`
Examples
--------
>>> from skimage.morphology import footprints
>>> footprints.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)
"""
if decomposition is None:
footprint = np.zeros((2 * height + 1, 2 * width + 1), dtype=dtype)
rows, cols = draw.ellipse(height, width, height + 1, width + 1)
footprint[rows, cols] = 1
return footprint
elif decomposition == 'crosses':
fp = ellipse(width, height, dtype, decomposition=None)
sequence = _cross_decomposition(fp)
return sequence
def cube(width, dtype=np.uint8, *, decomposition=None):
""" Generates a cube-shaped footprint.
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, optional
The data type of the footprint.
decomposition : {None, 'separable', 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but often with
better computational performance. See Notes for more details.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
this will be a tuple whose length is equal to the number of unique
structuring elements to apply (see Notes for more detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For binary morphology, using ``decomposition='sequence'``
was observed to give better performance, with the magnitude of the
performance increase rapidly increasing with footprint size. For grayscale
morphology with square footprints, it is recommended to use
``decomposition=None`` since the internal SciPy functions that are called
already have a fast implementation based on separable 1D sliding windows.
The 'sequence' decomposition mode only supports odd valued `width`. If
`width` is even, the sequence used will be identical to the 'separable'
mode.
"""
if decomposition is None:
return np.ones((width, width, width), dtype=dtype)
if decomposition == 'separable' or width % 2 == 0:
sequence = [(np.ones((width, 1, 1), dtype=dtype), 1),
(np.ones((1, width, 1), dtype=dtype), 1),
(np.ones((1, 1, width), dtype=dtype), 1)]
elif decomposition == 'sequence':
# only handles odd widths
sequence = [
(np.ones((3, 3, 3), dtype=dtype), _decompose_size(width, 3))
]
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return tuple(sequence)
def octahedron(radius, dtype=np.uint8, *, decomposition=None):
"""Generates a octahedron-shaped footprint.
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 footprint.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
decomposition : {None, 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but with
better computational performance. See Notes for more details.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
this will be a tuple whose length is equal to the number of unique
structuring elements to apply (see Notes for more detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For either binary or grayscale morphology, using
``decomposition='sequence'`` was observed to have a performance benefit,
with the magnitude of the benefit increasing with increasing footprint
size.
"""
# note that in contrast to diamond(), this method allows non-integer radii
if decomposition is None:
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)
footprint = np.array(s <= radius, dtype=dtype)
elif decomposition == 'sequence':
fp = octahedron(1, dtype=dtype, decomposition=None)
nreps = _decompose_size(2 * radius + 1, fp.shape[0])
footprint = ((fp, nreps),)
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return footprint
def ball(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None):
"""Generates a ball-shaped footprint.
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 footprint.
Other Parameters
----------------
dtype : data-type, optional
The data type of the footprint.
strict_radius : bool, optional
If False, extend the radius by 0.5. This allows the circle to expand
further within a cube that remains of size ``2 * radius + 1`` along
each axis. This parameter is ignored if decomposition is not None.
decomposition : {None, 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given a result equivalent to a single, larger footprint, but with
better computational performance. For ball footprints, the sequence
decomposition is not exactly equivalent to decomposition=None.
See Notes for more details.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
Notes
-----
The disk produced by the decomposition='sequence' mode is not identical
to that with decomposition=None. Here we extend the approach taken in [1]_
for disks to the 3D case, using 3-dimensional extensions of the "square",
"diamond" and "t-shaped" elements from that publication. All of these
elementary elements have size ``(3,) * ndim``. We numerically computed the
number of repetitions of each element that gives the closest match to the
ball computed with kwargs ``strict_radius=False, decomposition=None``.
Empirically, the equivalent composite footprint to the sequence
decomposition approaches a rhombicuboctahedron (26-faces [2]_).
References
----------
.. [1] Park, H and Chin R.T. Decomposition of structuring elements for
optimal implementation of morphological operations. In Proceedings:
1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
UK.
https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
.. [2] https://en.wikipedia.org/wiki/Rhombicuboctahedron
"""
if decomposition is None:
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
if not strict_radius:
radius += 0.5
return np.array(s <= radius * radius, dtype=dtype)
elif decomposition == 'sequence':
sequence = _nsphere_series_decomposition(radius, ndim=3, dtype=dtype)
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return sequence
def octagon(m, n, dtype=np.uint8, *, decomposition=None):
"""Generates an octagon shaped footprint.
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. The overall size of the
footprint along a single axis will be ``m + 2 * n``.
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, optional
The data type of the footprint.
decomposition : {None, 'sequence'}, optional
If None, a single array is returned. For 'sequence', a tuple of smaller
footprints is returned. Applying this series of smaller footprints will
given an identical result to a single, larger footprint, but with
better computational performance. See Notes for more details.
Returns
-------
footprint : ndarray or tuple
The footprint where elements of the neighborhood are 1 and 0 otherwise.
When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
this will be a tuple whose length is equal to the number of unique
structuring elements to apply (see Notes for more detail)
Notes
-----
When `decomposition` is not None, each element of the `footprint`
tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
footprint array and the number of iterations it is to be applied.
For either binary or grayscale morphology, using
``decomposition='sequence'`` was observed to have a performance benefit,
with the magnitude of the benefit increasing with increasing footprint
size.
"""
if m == n == 0:
raise ValueError("m and n cannot both be zero")
# TODO?: warn about even footprint size when m is even
if decomposition is None:
from . import convex_hull_image
footprint = np.zeros((m + 2 * n, m + 2 * n))
footprint[0, n] = 1
footprint[n, 0] = 1
footprint[0, m + n - 1] = 1
footprint[m + n - 1, 0] = 1
footprint[-1, n] = 1
footprint[n, -1] = 1
footprint[-1, m + n - 1] = 1
footprint[m + n - 1, -1] = 1
footprint = convex_hull_image(footprint).astype(dtype)
elif decomposition == 'sequence':
# special handling for edge cases with small m and/or n
if m <= 2 and n <= 2:
return ((octagon(m, n, dtype=dtype, decomposition=None), 1),)
# general approach for larger m and/or n
if m == 0:
m = 2
n -= 1
sequence = []
if m > 1:
sequence += list(square(m, dtype=dtype, decomposition='sequence'))
if n > 0:
sequence += [(diamond(1, dtype=dtype, decomposition=None), n)]
footprint = tuple(sequence)
else:
raise ValueError(f"Unrecognized decomposition: {decomposition}")
return footprint
def star(a, dtype=np.uint8):
"""Generates a star shaped footprint.
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, optional
The data type of the footprint.
Returns
-------
footprint : ndarray
The footprint 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
footprint_square = np.zeros((m + 2 * n, m + 2 * n))
footprint_square[n: m + n, n: m + n] = 1
c = (m + 2 * n - 1) // 2
footprint_rotated = np.zeros((m + 2 * n, m + 2 * n))
footprint_rotated[0, c] = footprint_rotated[-1, c] = 1
footprint_rotated[c, 0] = footprint_rotated[c, -1] = 1
footprint_rotated = convex_hull_image(footprint_rotated).astype(int)
footprint = footprint_square + footprint_rotated
footprint[footprint > 0] = 1
return footprint.astype(dtype)

View File

@@ -0,0 +1,633 @@
"""
Grayscale morphological operations
"""
import functools
import numpy as np
from scipy import ndimage as ndi
from ..util import crop
from .footprints import _footprint_is_sequence, _shape_from_sequence
from .misc import default_footprint
__all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat',
'black_tophat']
def _iterate_gray_func(gray_func, image, footprints, out):
"""Helper to call `binary_func` for each footprint in a sequence.
binary_func is a binary morphology function that accepts "structure",
"output" and "iterations" keyword arguments
(e.g. `scipy.ndimage.binary_erosion`).
"""
fp, num_iter = footprints[0]
gray_func(image, footprint=fp, output=out)
for _ in range(1, num_iter):
gray_func(out.copy(), footprint=fp, output=out)
for fp, num_iter in footprints[1:]:
# Note: out.copy() because the computation cannot be in-place!
for _ in range(num_iter):
gray_func(out.copy(), footprint=fp, output=out)
return out
def _shift_footprint(footprint, shift_x, shift_y):
"""Shift the binary image `footprint` in the left and/or up.
This only affects 2D footprints with even number of rows
or columns.
Parameters
----------
footprint : 2D array, shape (M, N)
The input footprint.
shift_x, shift_y : bool
Whether to move `footprint` along each axis.
Returns
-------
out : 2D array, shape (M + int(shift_x), N + int(shift_y))
The shifted footprint.
"""
if footprint.ndim != 2:
# do nothing for 1D or 3D or higher footprints
return footprint
m, n = footprint.shape
if m % 2 == 0:
extra_row = np.zeros((1, n), footprint.dtype)
if shift_x:
footprint = np.vstack((footprint, extra_row))
else:
footprint = np.vstack((extra_row, footprint))
m += 1
if n % 2 == 0:
extra_col = np.zeros((m, 1), footprint.dtype)
if shift_y:
footprint = np.hstack((footprint, extra_col))
else:
footprint = np.hstack((extra_col, footprint))
return footprint
def _invert_footprint(footprint):
"""Change the order of the values in `footprint`.
This is a patch for the *weird* footprint inversion in
`ndi.grey_morphology` [1]_.
Parameters
----------
footprint : array
The input footprint.
Returns
-------
inverted : array, same shape and type as `footprint`
The footprint, in opposite order.
Examples
--------
>>> footprint = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], np.uint8)
>>> _invert_footprint(footprint)
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 # noqa
"""
inverted = footprint[(slice(None, None, -1),) * footprint.ndim]
return inverted
def pad_for_eccentric_footprints(func):
"""Pad input images for certain morphological operations.
Parameters
----------
func : callable
A morphological function, either opening or closing, that
supports eccentric footprints. Its parameters must
include at least `image`, `footprint`, 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, footprint, out=None, *args, **kwargs):
pad_widths = []
padding = False
if out is None:
out = np.empty_like(image)
if _footprint_is_sequence(footprint):
# Note: in practice none of our built-in footprint sequences will
# require padding (all are symmetric and have odd sizes)
footprint_shape = _shape_from_sequence(footprint)
else:
footprint_shape = footprint.shape
for axis_len in footprint_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, footprint, out=out_temp, *args, **kwargs)
if padding:
out[:] = crop(out_temp, pad_widths)
else:
out = out_temp
return out
return func_out
@default_footprint
def erosion(image, footprint=None, out=None, shift_x=False, shift_y=False):
"""Return grayscale 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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 footprint about center point. This only affects
eccentric footprints (i.e. footprint 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 footprints.
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
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)
"""
if out is None:
out = np.empty_like(image)
if _footprint_is_sequence(footprint):
footprints = tuple((_shift_footprint(fp, shift_x, shift_y), n)
for fp, n in footprint)
return _iterate_gray_func(ndi.grey_erosion, image, footprints, out)
footprint = np.array(footprint)
footprint = _shift_footprint(footprint, shift_x, shift_y)
ndi.grey_erosion(image, footprint=footprint, output=out)
return out
@default_footprint
def dilation(image, footprint=None, out=None, shift_x=False, shift_y=False):
"""Return grayscale morphological dilation of an image.
Morphological dilation sets the value of a pixel to the maximum over all
pixel values within a local neighborhood centered about it. The values
where the footprint is 1 define this neighborhood.
Dilation enlarges bright regions and shrinks dark regions.
Parameters
----------
image : ndarray
Image array.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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 footprint about center point. This only affects 2D
eccentric footprints (i.e., footprints 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 footprints.
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
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)
"""
if out is None:
out = np.empty_like(image)
if _footprint_is_sequence(footprint):
# shift and invert (see comment below) each footprint
footprints = tuple(
(_invert_footprint(_shift_footprint(fp, shift_x, shift_y)), n)
for fp, n in footprint
)
return _iterate_gray_func(ndi.grey_dilation, image, footprints, out)
footprint = np.array(footprint)
footprint = _shift_footprint(footprint, shift_x, shift_y)
# Inside ndi.grey_dilation, the footprint is inverted,
# e.g. `footprint = footprint[::-1, ::-1]` for 2D [1]_, for reasons unknown
# to this author (@jni). To "patch" this behaviour, we invert our own
# footprint before passing it to `ndi.grey_dilation`.
# [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285 # noqa
footprint = _invert_footprint(footprint)
ndi.grey_dilation(image, footprint=footprint, output=out)
return out
@default_footprint
@pad_for_eccentric_footprints
def opening(image, footprint=None, out=None):
"""Return grayscale morphological opening of an image.
The morphological opening of 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
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, footprint)
# note: shift_x, shift_y do nothing if footprint side length is odd
out = dilation(eroded, footprint, out=out, shift_x=True, shift_y=True)
return out
@default_footprint
@pad_for_eccentric_footprints
def closing(image, footprint=None, out=None):
"""Return grayscale morphological closing of an image.
The morphological closing of 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.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
out : ndarray, optional
The array to store the result of the morphology. If None,
a new array will be allocated.
Returns
-------
closing : array, same shape and type as `image`
The result of the morphological closing.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
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, footprint)
# note: shift_x, shift_y do nothing if footprint side length is odd
out = erosion(dilated, footprint, out=out, shift_x=True, shift_y=True)
return out
def _white_tophat_seqence(image, footprints, out):
"""Return white top hat for a sequence of footprints.
Like SciPy's implementation, but with ``ndi.grey_erosion`` and
``ndi.grey_dilation`` wrapped with ``_iterate_gray_func``.
"""
tmp = _iterate_gray_func(ndi.grey_erosion, image, footprints, out)
tmp = _iterate_gray_func(ndi.grey_dilation, tmp.copy(), footprints, out)
if tmp is None:
tmp = out
if image.dtype == np.bool_ and tmp.dtype == np.bool_:
np.bitwise_xor(image, tmp, out=tmp)
else:
np.subtract(image, tmp, out=tmp)
return tmp
@default_footprint
def white_tophat(image, footprint=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 footprint.
Parameters
----------
image : ndarray
Image array.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
See Also
--------
black_tophat
References
----------
.. [1] https://en.wikipedia.org/wiki/Top-hat_transform
Examples
--------
>>> # Subtract gray background from bright peak
>>> import numpy as np
>>> from skimage.morphology import square
>>> bright_on_gray = 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_gray, 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:
opened = opening(image, footprint)
if np.issubdtype(opened.dtype, bool):
np.logical_xor(out, opened, out=out)
else:
out -= opened
return out
elif out is None:
out = np.empty_like(image)
# promote bool to a type that allows arithmetic operations
if isinstance(image, np.ndarray) and image.dtype == bool:
image_ = image.view(dtype=np.uint8)
else:
image_ = image
if isinstance(out, np.ndarray) and out.dtype == bool:
out_ = out.view(dtype=np.uint8)
else:
out_ = out
if _footprint_is_sequence(footprint):
return _white_tophat_seqence(image_, footprint, out_)
footprint = np.array(footprint)
out_ = ndi.white_tophat(image_, footprint=footprint, output=out_)
return out
@default_footprint
def black_tophat(image, footprint=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 footprint. Note that dark spots in the
original image are bright spots after the black top hat.
Parameters
----------
image : ndarray
Image array.
footprint : ndarray or tuple, optional
The neighborhood expressed as a 2-D array of 1's and 0's.
If None, use a cross-shaped footprint (connectivity=1). The footprint
can also be provided as a sequence of smaller footprints as described
in the notes below.
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.
Notes
-----
The footprint can also be a provided as a sequence of 2-tuples where the
first element of each 2-tuple is a footprint ndarray and the second element
is an integer describing the number of times it should be iterated. For
example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]``
would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net
effect that is the same as ``footprint=np.ones((9, 9))``, but with lower
computational cost. Most of the builtin footprints such as
``skimage.morphology.disk`` provide an option to automatically generate a
footprint sequence of this type.
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_gray = 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_gray, 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, footprint, 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,207 @@
import numpy as np
from .._shared.utils import _supported_float_type
from ..filters._rank_order import rank_order
from ._grayreconstruct import reconstruction_loop
def reconstruction(seed, mask, method='dilation', footprint=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 footprint 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 footprint, 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'.
footprint : 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 footprint.
Default is located on the geometrical center of the footprint, in that
case footprint 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 grayscale 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.")
if footprint is None:
footprint = np.ones([3] * seed.ndim, dtype=bool)
else:
footprint = footprint.astype(bool, copy=True)
if offset is None:
if not all([d % 2 == 1 for d in footprint.shape]):
raise ValueError("Footprint dimensions must all be odd")
offset = np.array([d // 2 for d in footprint.shape])
else:
if offset.ndim != footprint.ndim:
raise ValueError("Offset and footprint ndims must be equal.")
if not all([(0 <= o < d) for o, d in zip(offset, footprint.shape)]):
raise ValueError("Offset must be included inside footprint")
# Cross out the center of the footprint
footprint[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(footprint.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' "
f"or 'dilation'. Got '{method}'.")
float_dtype = _supported_float_type(mask.dtype)
images = np.full(dims, pad_value, dtype=float_dtype)
images[(0, *inside_slices)] = seed
images[(1, *inside_slices)] = mask
# determine whether image is large enough to require 64-bit integers
isize = images.size
# use -isize so we get a signed dtype rather than an unsigned one
signed_int_dtype = np.result_type(np.min_scalar_type(-isize), np.int32)
# the corresponding unsigned type has same char, but uppercase
unsigned_int_dtype = np.dtype(signed_int_dtype.char.upper())
# 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
footprint_mgrid = np.mgrid[[slice(-o, d - o)
for d, o in zip(footprint.shape, offset)]]
footprint_offsets = footprint_mgrid[:, footprint].transpose()
nb_strides = np.array([np.sum(value_stride * footprint_offset)
for footprint_offset in footprint_offsets],
signed_int_dtype)
images = images.reshape(-1)
# Erosion goes smallest to largest; dilation goes largest to smallest.
index_sorted = np.argsort(images).astype(signed_int_dtype, copy=False)
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(isize, -1, signed_int_dtype)
next = np.full(isize, -1, signed_int_dtype)
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]
value_rank = value_rank.astype(unsigned_int_dtype, copy=False)
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(footprint.shape) - 1)
return rec_img[inside_slices]

View File

@@ -0,0 +1,193 @@
"""
Binary morphological operations
"""
import numpy as np
from scipy import ndimage as ndi
def isotropic_erosion(image, radius, out=None, spacing=None):
"""Return binary morphological erosion of an image.
This function returns the same result as :func:`skimage.morphology.binary_erosion`
but performs faster for large circular structuring elements.
This works by applying a threshold to the exact Euclidean distance map
of the image [1]_, [2]_.
The implementation is based on: func:`scipy.ndimage.distance_transform_edt`.
Parameters
----------
image : ndarray
Binary input image.
radius : float
The radius by which regions should be eroded.
out : ndarray of bool, optional
The array to store the result of the morphology. If None,
a new array will be allocated.
spacing : float, or sequence of float, optional
Spacing of elements along each dimension.
If a sequence, must be of length equal to the input's dimension (number of axes).
If a single number, this value is used for all axes.
If not specified, a grid spacing of unity is implied.
Returns
-------
eroded : ndarray of bool
The result of the morphological erosion taking values in
``[False, True]``.
References
----------
.. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators
using local distance transformation by propagation, and applications,"
Image Processing And Its Applications, 1999. Seventh International
Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2.
:DOI:`10.1049/cp:19990446`
.. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing
and thresholding of distance maps, Pattern Recognition Letters,
Volume 13, Issue 3, 1992, Pages 161-166.
:DOI:`10.1016/0167-8655(92)90055-5`
"""
dist = ndi.distance_transform_edt(image, sampling=spacing)
return np.greater(dist, radius, out=out)
def isotropic_dilation(image, radius, out=None, spacing=None):
"""Return binary morphological dilation of an image.
This function returns the same result as :func:`skimage.morphology.binary_dilation`
but performs faster for large circular structuring elements.
This works by applying a threshold to the exact Euclidean distance map
of the inverted image [1]_, [2]_.
The implementation is based on: func:`scipy.ndimage.distance_transform_edt`.
Parameters
----------
image : ndarray
Binary input image.
radius : float
The radius by which regions should be dilated.
out : ndarray of bool, optional
The array to store the result of the morphology. If None is
passed, a new array will be allocated.
spacing : float, or sequence of float, optional
Spacing of elements along each dimension.
If a sequence, must be of length equal to the input's dimension (number of axes).
If a single number, this value is used for all axes.
If not specified, a grid spacing of unity is implied.
Returns
-------
dilated : ndarray of bool
The result of the morphological dilation with values in
``[False, True]``.
References
----------
.. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators
using local distance transformation by propagation, and applications,"
Image Processing And Its Applications, 1999. Seventh International
Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2.
:DOI:`10.1049/cp:19990446`
.. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing
and thresholding of distance maps, Pattern Recognition Letters,
Volume 13, Issue 3, 1992, Pages 161-166.
:DOI:`10.1016/0167-8655(92)90055-5`
"""
dist = ndi.distance_transform_edt(np.logical_not(image), sampling=spacing)
return np.less_equal(dist, radius, out=out)
def isotropic_opening(image, radius, out=None, spacing=None):
"""Return binary morphological opening of an image.
This function returns the same result as :func:`skimage.morphology.binary_opening`
but performs faster for large circular structuring elements.
This works by thresholding the exact Euclidean distance map [1]_, [2]_.
The implementation is based on: func:`scipy.ndimage.distance_transform_edt`.
Parameters
----------
image : ndarray
Binary input image.
radius : float
The radius with which the regions should be opened.
out : ndarray of bool, optional
The array to store the result of the morphology. If None
is passed, a new array will be allocated.
spacing : float, or sequence of float, optional
Spacing of elements along each dimension.
If a sequence, must be of length equal to the input's dimension (number of axes).
If a single number, this value is used for all axes.
If not specified, a grid spacing of unity is implied.
Returns
-------
opened : ndarray of bool
The result of the morphological opening.
References
----------
.. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators
using local distance transformation by propagation, and applications,"
Image Processing And Its Applications, 1999. Seventh International
Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2.
:DOI:`10.1049/cp:19990446`
.. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing
and thresholding of distance maps, Pattern Recognition Letters,
Volume 13, Issue 3, 1992, Pages 161-166.
:DOI:`10.1016/0167-8655(92)90055-5`
"""
eroded = isotropic_erosion(image, radius, out=out, spacing=spacing)
return isotropic_dilation(eroded, radius, out=out, spacing=spacing)
def isotropic_closing(image, radius, out=None, spacing=None):
"""Return binary morphological closing of an image.
This function returns the same result as binary :func:`skimage.morphology.binary_closing`
but performs faster for large circular structuring elements.
This works by thresholding the exact Euclidean distance map [1]_, [2]_.
The implementation is based on: func:`scipy.ndimage.distance_transform_edt`.
Parameters
----------
image : ndarray
Binary input image.
radius : float
The radius with which the regions should be closed.
out : ndarray of bool, optional
The array to store the result of the morphology. If None,
is passed, a new array will be allocated.
spacing : float, or sequence of float, optional
Spacing of elements along each dimension.
If a sequence, must be of length equal to the input's dimension (number of axes).
If a single number, this value is used for all axes.
If not specified, a grid spacing of unity is implied.
Returns
-------
closed : ndarray of bool
The result of the morphological closing.
References
----------
.. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators
using local distance transformation by propagation, and applications,"
Image Processing And Its Applications, 1999. Seventh International
Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2.
:DOI:`10.1049/cp:19990446`
.. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing
and thresholding of distance maps, Pattern Recognition Letters,
Volume 13, Issue 3, 1992, Pages 161-166.
:DOI:`10.1016/0167-8655(92)90055-5`
"""
dilated = isotropic_dilation(image, radius, out=out, spacing=spacing)
return isotropic_erosion(dilated, radius, out=out, spacing=spacing)

View File

@@ -0,0 +1,664 @@
"""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 footprint, but rather act with a flexible
footprint 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 footprint, 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(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(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 footprint, 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(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(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(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,221 @@
"""Miscellaneous morphology functions."""
import numpy as np
import functools
from scipy import ndimage as ndi
from .._shared.utils import warn
# 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_footprint(func):
"""Decorator to add a default footprint 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 footprint of same dimension
as the input image with connectivity 1.
"""
@functools.wraps(func)
def func_out(image, footprint=None, *args, **kwargs):
if footprint is None:
footprint = ndi.generate_binary_structure(image.ndim, 1)
return func(image, footprint=footprint, *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. "
f"Got {ar.dtype}.")
def remove_small_objects(ar, min_size=64, connectivity=1, *, out=None):
"""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.
out : ndarray
Array of the same shape as `ar`, into which the output is
placed. By default, a new array is created.
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, out=a)
>>> d is a
True
"""
# Raising type error if not int or bool
_check_dtype_supported(ar)
if out is None:
out = ar.copy()
else:
out[:] = ar
if min_size == 0: # shortcut for efficiency
return out
if out.dtype == bool:
footprint = ndi.generate_binary_structure(ar.ndim, connectivity)
ccs = np.zeros_like(ar, dtype=np.int32)
ndi.label(ar, footprint, 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, *, out=None):
"""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.
out : ndarray
Array of the same shape as `ar` and bool dtype, into which the
output is placed. By default, a new array is created.
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, out=a)
>>> 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 out is not None:
if out.dtype != bool:
raise TypeError("out dtype must be bool")
else:
out = ar.astype(bool, copy=True)
# Creating the inverse of ar
np.logical_not(ar, out=out)
# removing small objects from the inverse of ar
out = remove_small_objects(out, area_threshold, connectivity, out=out)
np.logical_not(out, out=out)
return out

View File

@@ -0,0 +1,317 @@
import numpy as np
import pytest
from numpy.testing import assert_array_equal, assert_equal
from scipy import ndimage as ndi
from skimage import data, color, morphology
from skimage.util import img_as_bool
from skimage.morphology import binary, footprints, gray
img = color.rgb2gray(data.astronaut())
bw_img = img > 100 / 255.
def test_non_square_image():
footprint = morphology.square(3)
binary_res = binary.binary_erosion(bw_img[:100, :200], footprint)
gray_res = img_as_bool(gray.erosion(bw_img[:100, :200], footprint))
assert_array_equal(binary_res, gray_res)
def test_binary_erosion():
footprint = morphology.square(3)
binary_res = binary.binary_erosion(bw_img, footprint)
gray_res = img_as_bool(gray.erosion(bw_img, footprint))
assert_array_equal(binary_res, gray_res)
def test_binary_dilation():
footprint = morphology.square(3)
binary_res = binary.binary_dilation(bw_img, footprint)
gray_res = img_as_bool(gray.dilation(bw_img, footprint))
assert_array_equal(binary_res, gray_res)
def test_binary_closing():
footprint = morphology.square(3)
binary_res = binary.binary_closing(bw_img, footprint)
gray_res = img_as_bool(gray.closing(bw_img, footprint))
assert_array_equal(binary_res, gray_res)
def test_binary_opening():
footprint = morphology.square(3)
binary_res = binary.binary_opening(bw_img, footprint)
gray_res = img_as_bool(gray.opening(bw_img, footprint))
assert_array_equal(binary_res, gray_res)
def _get_decomp_test_data(function, ndim=2):
if function == 'binary_erosion':
img = np.ones((17, ) * ndim, dtype=np.uint8)
img[8, 8] = 0
elif function == 'binary_dilation':
img = np.zeros((17, ) * ndim, dtype=np.uint8)
img[8, 8] = 1
else:
img = data.binary_blobs(32, n_dim=ndim, seed=1)
return img
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("size", (3, 4, 11))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_square_decomposition(function, size, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.square(size, decomposition=None)
footprint = footprints.square(size, decomposition=decomposition)
img = _get_decomp_test_data(function)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("nrows", (3, 4, 11))
@pytest.mark.parametrize("ncols", (3, 4, 11))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_rectangle_decomposition(function, nrows, ncols, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.rectangle(nrows, ncols, decomposition=None)
footprint = footprints.rectangle(nrows, ncols, decomposition=decomposition)
img = _get_decomp_test_data(function)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("m", (0, 1, 2, 3, 4, 5))
@pytest.mark.parametrize("n", (0, 1, 2, 3, 4, 5))
@pytest.mark.parametrize("decomposition", ['sequence'])
def test_octagon_decomposition(function, m, n, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
if m == 0 and n == 0:
with pytest.raises(ValueError):
footprints.octagon(m, n, decomposition=decomposition)
else:
footprint_ndarray = footprints.octagon(m, n, decomposition=None)
footprint = footprints.octagon(m, n, decomposition=decomposition)
img = _get_decomp_test_data(function)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("radius", (1, 2, 5))
@pytest.mark.parametrize("decomposition", ['sequence'])
def test_diamond_decomposition(function, radius, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.diamond(radius, decomposition=None)
footprint = footprints.diamond(radius, decomposition=decomposition)
img = _get_decomp_test_data(function)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("size", (3, 4, 5))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_cube_decomposition(function, size, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.cube(size, decomposition=None)
footprint = footprints.cube(size, decomposition=decomposition)
img = _get_decomp_test_data(function, ndim=3)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function",
["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
)
@pytest.mark.parametrize("radius", (1, 2, 3))
@pytest.mark.parametrize("decomposition", ['sequence'])
def test_octahedron_decomposition(function, radius, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.octahedron(radius, decomposition=None)
footprint = footprints.octahedron(radius, decomposition=decomposition)
img = _get_decomp_test_data(function, ndim=3)
func = getattr(binary, function)
expected = func(img, footprint=footprint_ndarray)
out = func(img, footprint=footprint)
assert_array_equal(expected, out)
def test_footprint_overflow():
footprint = 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, footprint)
gray_res = img_as_bool(gray.erosion(img, footprint))
assert_array_equal(binary_res, gray_res)
def test_out_argument():
for func in (binary.binary_erosion, binary.binary_dilation):
footprint = np.ones((3, 3), dtype=np.uint8)
img = np.ones((10, 10))
out = np.zeros_like(img)
out_saved = out.copy()
func(img, footprint, out=out)
assert np.any(out != out_saved)
assert_array_equal(out, func(img, footprint))
binary_functions = [binary.binary_erosion, binary.binary_dilation,
binary.binary_opening, binary.binary_closing]
@pytest.mark.parametrize("function", binary_functions)
def test_default_footprint(function):
footprint = morphology.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, footprint)
im_test = function(image)
assert_array_equal(im_expected, im_test)
def test_3d_fallback_default_footprint():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), 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)
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_footprint(function):
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
new_image = function(image, cube)
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)
footprint = ndi.generate_binary_structure(2, 1)
ndimage_opened = ndi.binary_opening(image, structure=footprint)
ndimage_closed = ndi.binary_closing(image, structure=footprint)
assert_array_equal(bin_opened, ndimage_opened)
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)
assert_equal(bin_opened.dtype, bool)
assert_equal(bin_closed.dtype, bool)
assert_equal(int_opened.dtype, np.uint8)
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)
assert_equal(bin_opened.dtype, bool)
assert_equal(bin_closed.dtype, bool)
assert_equal(int_opened.dtype, np.uint8)
assert_equal(int_closed.dtype, np.uint8)

View File

@@ -0,0 +1,197 @@
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, 1372]),
([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, 155]))
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, 1, 0, 0]], dtype=bool)
assert_array_equal(convex_hull_image(image), expected)
def test_pathological_qhull_labels():
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, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 0]], dtype=bool)
actual = convex_hull_image(image, include_borders=False)
assert_array_equal(actual, 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)
out = convex_hull_object(image, connectivity=1)
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)
def test_few_points():
image = 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, 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.uint8)
image3d = np.stack([image, image, image])
with testing.assert_warns(UserWarning):
chimage3d = convex_hull_image(image3d)
assert_array_equal(chimage3d, np.zeros(image3d.shape, dtype=bool))

View File

@@ -0,0 +1,637 @@
import math
import unittest
import numpy as np
from numpy.testing import assert_equal
from pytest import raises, warns
from skimage._shared.testing import expected_warnings
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():
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 h in h_vals:
if h % 1 != 0:
msgs = ['possible precision loss converting image']
else:
msgs = []
with expected_warnings(msgs):
maxima = extrema.h_maxima(data, h)
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 h in h_vals:
if h % 1 != 0:
msgs = ['possible precision loss converting image']
else:
msgs = []
with expected_warnings(msgs):
minima = extrema.h_minima(data, h)
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=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=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 == 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 == 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=bool
)
for dtype in self.supported_dtypes:
image = data.astype(dtype)
result = extrema.local_maxima(image)
assert result.dtype == bool
assert_equal(result, expected)
def test_connectivity(self):
"""Test results if footprint is a scalar."""
# Connectivity 1: generates cross shaped footprint
result_conn1 = extrema.local_maxima(self.image, connectivity=1)
assert result_conn1.dtype == bool
assert_equal(result_conn1, self.expected_cross)
# Connectivity 2: generates square shaped footprint
result_conn2 = extrema.local_maxima(self.image, connectivity=2)
assert result_conn2.dtype == bool
assert_equal(result_conn2, self.expected_default)
# Connectivity 3: generates square shaped footprint
result_conn3 = extrema.local_maxima(self.image, connectivity=3)
assert result_conn3.dtype == bool
assert_equal(result_conn3, self.expected_default)
def test_footprint(self):
"""Test results if footprint is given."""
footprint_cross = np.array(
[[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=bool)
result_footprint_cross = extrema.local_maxima(
self.image, footprint=footprint_cross)
assert result_footprint_cross.dtype == bool
assert_equal(result_footprint_cross, self.expected_cross)
for footprint in [
((True,) * 3,) * 3,
np.ones((3, 3), dtype=np.float64),
np.ones((3, 3), dtype=np.uint8),
np.ones((3, 3), dtype=bool),
]:
# Test different dtypes for footprint which expects a boolean array
# but will accept and convert other types if possible
result_footprint_square = extrema.local_maxima(
self.image, footprint=footprint
)
assert result_footprint_square.dtype == bool
assert_equal(result_footprint_square, self.expected_default)
footprint_x = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=bool)
expected_footprint_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=bool
)
result_footprint_x = extrema.local_maxima(self.image,
footprint=footprint_x)
assert result_footprint_x.dtype == bool
assert_equal(result_footprint_x, expected_footprint_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 == 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=bool
)
result_without_border = extrema.local_maxima(
self.image, connectivity=1, allow_borders=False)
assert result_with_boder.dtype == 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=bool)
result_1d = extrema.local_maxima(x_1d)
assert result_1d.dtype == 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=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 == 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 == bool
assert_equal(result, expected)
# test for local minima
result = extrema.local_minima(const_image)
assert result.dtype == 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=bool
)
# Test for local maxima with automatic step calculation
result = extrema.local_maxima(image)
assert result.dtype == bool
assert_equal(result, expected_result)
# Test for local minima with automatic step calculation
result = extrema.local_minima(inverted_image)
assert result.dtype == 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, footprint=np.ones((3, 3, 3), dtype=bool))
with raises(ValueError, match="number of dimensions"):
extrema.local_maxima(
self.image, footprint=np.ones((3,), dtype=bool))
# All dimensions in footprint must be of size 3
with raises(ValueError, match="dimension size"):
extrema.local_maxima(
self.image, footprint=np.ones((2, 3), dtype=bool))
with raises(ValueError, match="dimension size"):
extrema.local_maxima(
self.image, footprint=np.ones((5, 5), dtype=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 footprint, 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 == 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

View File

@@ -0,0 +1,286 @@
import numpy as np
import pytest
from skimage.morphology import flood, flood_fill
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 == 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 pytest.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
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_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_footprint():
# Basic tests for nonstandard footprints
footprint = 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,
footprint=footprint)
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)
footprint = 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,
footprint=footprint)
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)
def test_negative_indexing_seed_point():
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)
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)
image = flood_fill(image, (0, -1), 5)
np.testing.assert_allclose(image, expected)
def test_non_adjacent_footprint():
# Basic tests for non-adjacent footprints
footprint = np.array([[1, 0, 0, 0, 1],
[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0],
[1, 0, 0, 0, 1]])
output = flood_fill(np.zeros((5, 6), dtype=np.uint8), (2, 3), 255,
footprint=footprint)
expected = np.array([[0, 255, 0, 0, 0, 255],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 255, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 255, 0, 0, 0, 255]], dtype=np.uint8)
np.testing.assert_equal(output, expected)
footprint = np.array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]])
image = np.zeros((5, 10), dtype=np.uint8)
image[:, (3, 7, 8)] = 100
output = flood_fill(image, (0, 0), 255, footprint=footprint)
expected = np.array([[255, 255, 255, 100, 255, 255, 255, 100, 100, 0],
[255, 255, 255, 100, 255, 255, 255, 100, 100, 0],
[255, 255, 255, 100, 255, 255, 255, 100, 100, 0],
[255, 255, 255, 100, 255, 255, 255, 100, 100, 0],
[255, 255, 255, 100, 255, 255, 255, 100, 100, 0]],
dtype=np.uint8)
np.testing.assert_equal(output, expected)

View File

@@ -0,0 +1,238 @@
"""
Tests for Morphological footprints
(skimage.morphology.footprint)
Author: Damian Eads
"""
import numpy as np
import pytest
from numpy.testing import assert_equal
from skimage._shared.testing import fetch
from skimage.morphology import footprints
class TestFootprints:
def test_square_footprint(self):
"""Test square footprints"""
for k in range(0, 5):
actual_mask = footprints.square(k)
expected_mask = np.ones((k, k), dtype='uint8')
assert_equal(expected_mask, actual_mask)
def test_rectangle_footprint(self):
"""Test rectangle footprints"""
for i in range(0, 5):
for j in range(0, 5):
actual_mask = footprints.rectangle(i, j)
expected_mask = np.ones((i, j), dtype='uint8')
assert_equal(expected_mask, actual_mask)
def test_cube_footprint(self):
"""Test cube footprints"""
for k in range(0, 5):
actual_mask = footprints.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_footprint_disk(self):
"""Test disk footprints"""
self.strel_worker("data/disk-matlab-output.npz", footprints.disk)
def test_footprint_diamond(self):
"""Test diamond footprints"""
self.strel_worker("data/diamond-matlab-output.npz", footprints.diamond)
def test_footprint_ball(self):
"""Test ball footprints"""
self.strel_worker_3d("data/disk-matlab-output.npz", footprints.ball)
def test_footprint_octahedron(self):
"""Test octahedron footprints"""
self.strel_worker_3d("data/diamond-matlab-output.npz",
footprints.octahedron)
def test_footprint_octagon(self):
"""Test octagon footprints"""
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 = footprints.octagon(5, 3)
expected_mask2 = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=np.uint8)
actual_mask2 = footprints.octagon(1, 1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)
def test_footprint_ellipse(self):
"""Test ellipse footprints"""
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 = footprints.ellipse(5, 3)
expected_mask2 = np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 1]], dtype=np.uint8)
actual_mask2 = footprints.ellipse(1, 1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)
assert_equal(expected_mask1, footprints.ellipse(3, 5).T)
assert_equal(expected_mask2, footprints.ellipse(1, 1).T)
def test_footprint_star(self):
"""Test star footprints"""
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 = footprints.star(4)
expected_mask2 = np.array([[1, 1, 1],
[1, 1, 1],
[1, 1, 1]], dtype=np.uint8)
actual_mask2 = footprints.star(1)
assert_equal(expected_mask1, actual_mask1)
assert_equal(expected_mask2, actual_mask2)
@pytest.mark.parametrize(
'function, args, supports_sequence_decomposition',
[
(footprints.disk, (3,), True),
(footprints.ball, (3,), True),
(footprints.square, (3,), True),
(footprints.cube, (3,), True),
(footprints.diamond, (3,), True),
(footprints.octahedron, (3,), True),
(footprints.rectangle, (3, 4), True),
(footprints.ellipse, (3, 4), False),
(footprints.octagon, (3, 4), True),
(footprints.star, (3,), False),
]
)
@pytest.mark.parametrize("dtype", [np.uint8, np.float64])
def test_footprint_dtype(function, args, supports_sequence_decomposition,
dtype):
# make sure footprint dtype matches what was requested
footprint = function(*args, dtype=dtype)
assert footprint.dtype == dtype
if supports_sequence_decomposition:
sequence = function(*args, dtype=dtype, decomposition='sequence')
assert all([fp_tuple[0].dtype == dtype for fp_tuple in sequence])
@pytest.mark.parametrize("function", ["disk", "ball"])
@pytest.mark.parametrize("radius", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 50, 75,
100])
def test_nsphere_series_approximation(function, radius):
fp_func = getattr(footprints, function)
expected = fp_func(radius, strict_radius=False, decomposition=None)
footprint_sequence = fp_func(radius, strict_radius=False,
decomposition="sequence")
approximate = footprints.footprint_from_sequence(footprint_sequence)
assert approximate.shape == expected.shape
# verify that maximum error does not exceed some fraction of the size
error = np.sum(np.abs(expected.astype(int) - approximate.astype(int)))
if radius == 1:
assert error == 0
else:
max_error = 0.1 if function == "disk" else 0.15
assert error / expected.size <= max_error
@pytest.mark.parametrize("radius", [1, 2, 3, 4, 5, 10, 20, 50, 75])
@pytest.mark.parametrize("strict_radius", [False, True])
def test_disk_crosses_approximation(radius, strict_radius):
fp_func = footprints.disk
expected = fp_func(radius, strict_radius=strict_radius, decomposition=None)
footprint_sequence = fp_func(radius, strict_radius=strict_radius,
decomposition="crosses")
approximate = footprints.footprint_from_sequence(footprint_sequence)
assert approximate.shape == expected.shape
# verify that maximum error does not exceed some fraction of the size
error = np.sum(np.abs(expected.astype(int) - approximate.astype(int)))
max_error = 0.05
assert error / expected.size <= max_error
@pytest.mark.parametrize("width", [3, 8, 20, 50])
@pytest.mark.parametrize("height", [3, 8, 20, 50])
def test_ellipse_crosses_approximation(width, height):
fp_func = footprints.ellipse
expected = fp_func(width, height, decomposition=None)
footprint_sequence = fp_func(width, height, decomposition="crosses")
approximate = footprints.footprint_from_sequence(footprint_sequence)
assert approximate.shape == expected.shape
# verify that maximum error does not exceed some fraction of the size
error = np.sum(np.abs(expected.astype(int) - approximate.astype(int)))
max_error = 0.05
assert error / expected.size <= max_error
def test_disk_series_approximation_unavailable():
# ValueError if radius is too large (only precomputed up to radius=250)
with pytest.raises(ValueError):
footprints.disk(radius=10000, decomposition="sequence")
def test_ball_series_approximation_unavailable():
# ValueError if radius is too large (only precomputed up to radius=100)
with pytest.raises(ValueError):
footprints.ball(radius=10000, decomposition="sequence")

View File

@@ -0,0 +1,410 @@
import numpy as np
import pytest
from scipy import ndimage as ndi
from numpy.testing import assert_allclose, assert_array_equal, assert_equal
from skimage import color, data, transform
from skimage._shared._warnings import expected_warnings
from skimage._shared.testing import fetch
from skimage.morphology import gray, footprints
from skimage.util import img_as_uint, img_as_ubyte
@pytest.fixture
def cam_image():
from skimage import data
return np.ascontiguousarray(data.camera()[64:112, 64:96])
@pytest.fixture
def cell3d_image():
from skimage import data
return np.ascontiguousarray(data.cells3d()[30:48, 0, 20:36, 20:32])
class TestMorphology():
# These expected outputs were generated with skimage v0.12.1
# using:
#
# from skimage.morphology.tests.test_gray 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 = (gray.erosion, gray.dilation, gray.opening, gray.closing,
gray.white_tophat, gray.black_tophat)
footprints_2D = (footprints.square, footprints.diamond,
footprints.disk, footprints.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 footprints_2D:
for func in funcs:
key = f'{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():
def setup_class(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.footprints = [footprints.square(2), footprints.rectangle(2, 2),
footprints.rectangle(2, 1),
footprints.rectangle(1, 2)]
def test_dilate_erode_symmetry(self):
for s in self.footprints:
c = gray.erosion(self.black_pixel, s)
d = gray.dilation(self.white_pixel, s)
assert np.all(c == (255 - d))
def test_open_black_pixel(self):
for s in self.footprints:
gray_open = gray.opening(self.black_pixel, s)
assert np.all(gray_open == self.black_pixel)
def test_close_white_pixel(self):
for s in self.footprints:
gray_close = gray.closing(self.white_pixel, s)
assert np.all(gray_close == self.white_pixel)
def test_open_white_pixel(self):
for s in self.footprints:
assert np.all(gray.opening(self.white_pixel, s) == 0)
def test_close_black_pixel(self):
for s in self.footprints:
assert np.all(gray.closing(self.black_pixel, s) == 255)
def test_white_tophat_white_pixel(self):
for s in self.footprints:
tophat = gray.white_tophat(self.white_pixel, s)
assert np.all(tophat == self.white_pixel)
def test_black_tophat_black_pixel(self):
for s in self.footprints:
tophat = gray.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.footprints:
tophat = gray.white_tophat(self.black_pixel, s)
assert np.all(tophat == 0)
def test_black_tophat_white_pixel(self):
for s in self.footprints:
tophat = gray.black_tophat(self.white_pixel, s)
assert np.all(tophat == 0)
gray_functions = [gray.erosion, gray.dilation,
gray.opening, gray.closing,
gray.white_tophat, gray.black_tophat]
@pytest.mark.parametrize("function", gray_functions)
def test_default_footprint(function):
strel = footprints.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)
assert_array_equal(im_expected, im_test)
def test_3d_fallback_default_footprint():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), bool)
image[2:-2, 2:-2, 2:-2] = 1
opened = gray.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)
gray_3d_fallback_functions = [gray.closing, gray.opening]
@pytest.mark.parametrize("function", gray_3d_fallback_functions)
def test_3d_fallback_cube_footprint(function):
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
new_image = function(image, cube)
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 = gray.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 = gray.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 = gray.opening(image)
closed = gray.closing(image)
footprint = ndi.generate_binary_structure(2, 1)
ndimage_opened = ndi.grey_opening(image, footprint=footprint)
ndimage_closed = ndi.grey_closing(image, footprint=footprint)
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():
assert_allclose(gray.erosion(im), eroded)
assert_allclose(gray.dilation(im), dilated)
assert_allclose(gray.opening(im), opened)
assert_allclose(gray.closing(im), closed)
def test_uint16():
im16, eroded16, dilated16, opened16, closed16 = (
map(img_as_uint, [im, eroded, dilated, opened, closed]))
assert_allclose(gray.erosion(im16), eroded16)
assert_allclose(gray.dilation(im16), dilated16)
assert_allclose(gray.opening(im16), opened16)
assert_allclose(gray.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)
gray.dilation(image, out=out_array)
assert_array_equal(out_array_big, expected_dilation)
gray.erosion(image, out=out_array)
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 = gray.erosion(image)
assert_array_equal(eroded, expected)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("size", (7,))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_square_decomposition(cam_image, function, size, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.square(size, decomposition=None)
footprint = footprints.square(size, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cam_image, footprint=footprint_ndarray)
out = func(cam_image, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("nrows", (3, 11))
@pytest.mark.parametrize("ncols", (3, 11))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_rectangle_decomposition(cam_image, function, nrows, ncols,
decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.rectangle(nrows, ncols, decomposition=None)
footprint = footprints.rectangle(nrows, ncols, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cam_image, footprint=footprint_ndarray)
out = func(cam_image, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("radius", (2, 3))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_diamond_decomposition(cam_image, function, radius, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.square(radius, decomposition=None)
footprint = footprints.square(radius, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cam_image, footprint=footprint_ndarray)
out = func(cam_image, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("m", (0, 1, 3, 5))
@pytest.mark.parametrize("n", (0, 1, 2, 3))
@pytest.mark.parametrize("decomposition", ['sequence'])
def test_octagon_decomposition(cam_image, function, m, n, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
if m == 0 and n == 0:
with pytest.raises(ValueError):
footprints.octagon(m, n, decomposition=decomposition)
else:
footprint_ndarray = footprints.octagon(m, n, decomposition=None)
footprint = footprints.octagon(m, n, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cam_image, footprint=footprint_ndarray)
out = func(cam_image, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("size", (5,))
@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
def test_cube_decomposition(cell3d_image, function, size, decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.cube(size, decomposition=None)
footprint = footprints.cube(size, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cell3d_image, footprint=footprint_ndarray)
out = func(cell3d_image, footprint=footprint)
assert_array_equal(expected, out)
@pytest.mark.parametrize(
"function", ["erosion", "dilation", "closing", "opening", "white_tophat",
"black_tophat"],
)
@pytest.mark.parametrize("radius", (3,))
@pytest.mark.parametrize("decomposition", ['sequence'])
def test_octahedron_decomposition(cell3d_image, function, radius,
decomposition):
"""Validate footprint decomposition for various shapes.
comparison is made to the case without decomposition.
"""
footprint_ndarray = footprints.octahedron(radius, decomposition=None)
footprint = footprints.octahedron(radius, decomposition=decomposition)
func = getattr(gray, function)
expected = func(cell3d_image, footprint=footprint_ndarray)
out = func(cell3d_image, footprint=footprint)
assert_array_equal(expected, out)

View File

@@ -0,0 +1,82 @@
import numpy as np
from numpy.testing import assert_array_equal
from skimage import color, data, morphology
from skimage.morphology import binary, isotropic
from skimage.util import img_as_bool
img = color.rgb2gray(data.astronaut())
bw_img = img > 100 / 255.
def test_non_square_image():
isotropic_res = isotropic.isotropic_erosion(bw_img[:100, :200], 3)
binary_res = img_as_bool(binary.binary_erosion(
bw_img[:100, :200], morphology.disk(3)))
assert_array_equal(isotropic_res, binary_res)
def test_isotropic_erosion():
isotropic_res = isotropic.isotropic_erosion(bw_img, 3)
binary_res = img_as_bool(binary.binary_erosion(bw_img, morphology.disk(3)))
assert_array_equal(isotropic_res, binary_res)
def _disk_with_spacing(radius, dtype=np.uint8, *, strict_radius=True, spacing=None):
# Identical to morphology.disk, but with a spacing parameter and without decomposition.
# This is different from morphology.ellipse which produces a slightly different footprint.
L = np.arange(-radius, radius + 1)
X, Y = np.meshgrid(L, L)
if spacing is not None:
X *= spacing[1]
Y *= spacing[0]
if not strict_radius:
radius += 0.5
return np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype)
def test_isotropic_erosion_spacing():
isotropic_res = isotropic.isotropic_dilation(bw_img, 6, spacing=(1,2))
binary_res = img_as_bool(binary.binary_dilation(bw_img, _disk_with_spacing(6, spacing=(1,2))))
assert_array_equal(isotropic_res, binary_res)
def test_isotropic_dilation():
isotropic_res = isotropic.isotropic_dilation(bw_img, 3)
binary_res = img_as_bool(
binary.binary_dilation(
bw_img, morphology.disk(3)))
assert_array_equal(isotropic_res, binary_res)
def test_isotropic_closing():
isotropic_res = isotropic.isotropic_closing(bw_img, 3)
binary_res = img_as_bool(binary.binary_closing(bw_img, morphology.disk(3)))
assert_array_equal(isotropic_res, binary_res)
def test_isotropic_opening():
isotropic_res = isotropic.isotropic_opening(bw_img, 3)
binary_res = img_as_bool(binary.binary_opening(bw_img, morphology.disk(3)))
assert_array_equal(isotropic_res, binary_res)
def test_footprint_overflow():
img = np.zeros((20, 20), dtype=bool)
img[2:19, 2:19] = True
isotropic_res = isotropic.isotropic_erosion(img, 9)
binary_res = img_as_bool(binary.binary_erosion(img, morphology.disk(9)))
assert_array_equal(isotropic_res, binary_res)
def test_out_argument():
for func in (isotropic.isotropic_erosion, isotropic.isotropic_dilation):
radius = 3
img = np.ones((10, 10))
out = np.zeros_like(img)
out_saved = out.copy()
func(img, radius, out=out)
assert np.any(out != out_saved)
assert_array_equal(out, func(img, radius))

View File

@@ -0,0 +1,453 @@
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.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(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

View File

@@ -0,0 +1,225 @@
import numpy as np
import pytest
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, out=image)
assert_equal(observed is image, True,
"remove_small_objects in_place argument failed.")
@pytest.mark.parametrize("in_dtype", [bool, int, np.int32])
@pytest.mark.parametrize("out_dtype", [bool, int, np.int32])
def test_out(in_dtype, out_dtype):
image = test_image.astype(in_dtype, copy=True)
expected_out = np.empty_like(test_image, dtype=out_dtype)
if out_dtype != bool:
# object with only 1 label will warn on non-bool output dtype
exp_warn = ["Only one label was provided"]
else:
exp_warn = []
with expected_warnings(exp_warn):
out = remove_small_objects(image, min_size=6, out=expected_out)
assert out is expected_out
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]], 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]], 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]], 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, out=image)
assert_equal(observed is image, True,
"remove_small_holes in_place argument failed.")
def test_out_remove_small_holes():
image = test_holes_image.copy()
expected_out = np.empty_like(image)
out = remove_small_holes(image, area_threshold=3, out=expected_out)
assert out is expected_out
def test_non_bool_out():
image = test_holes_image.copy()
expected_out = np.empty_like(image, dtype=int)
with testing.raises(TypeError):
remove_small_holes(image, area_threshold=3, out=expected_out)
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=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=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=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=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,152 @@
import math
import numpy as np
import pytest
from numpy.testing import assert_array_almost_equal
from skimage._shared.utils import _supported_float_type
from skimage.morphology.grayreconstruct import reconstruction
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)
# minsize chosen to test sizes covering use of 8, 16 and 32-bit integers
# internally
@pytest.mark.parametrize('minsize', [None, 200, 20000, 40000, 80000])
@pytest.mark.parametrize('dtype', [np.uint8, np.float32])
def test_two_image_peaks(minsize, dtype):
"""Test reconstruction with two peak pixels isolated by the mask"""
image = np.array([[1, 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, 1, 1, 1, 3, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=dtype)
mask = np.array([[4, 4, 4, 1, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1, 1],
[4, 4, 4, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 4, 4, 4, 1],
[1, 1, 1, 1, 1, 4, 4, 4, 1],
[1, 1, 1, 1, 1, 4, 4, 4, 1]], dtype=dtype)
expected = np.array([[2, 2, 2, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 3, 3, 3, 1],
[1, 1, 1, 1, 1, 3, 3, 3, 1],
[1, 1, 1, 1, 1, 3, 3, 3, 1]], dtype=dtype)
if minsize is not None:
# increase data size by tiling (done to test various int types)
nrow = math.ceil(math.sqrt(minsize / image.size))
ncol = math.ceil(minsize / (image.size * nrow))
image = np.tile(image, (nrow, ncol))
mask = np.tile(mask, (nrow, ncol))
expected = np.tile(expected, (nrow, ncol))
out = reconstruction(image, mask)
assert out.dtype == _supported_float_type(mask.dtype)
assert_array_almost_equal(out, 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)
@pytest.mark.parametrize('dtype', [np.int8, np.uint8, np.int16, np.uint16,
np.int32, np.uint32, np.int64, np.uint64,
np.float16, np.float32, np.float64])
def test_fill_hole(dtype):
"""Test reconstruction by erosion, which should fill holes in mask."""
seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0], dtype=dtype)
mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0], dtype=dtype)
result = reconstruction(seed, mask, method='erosion')
assert result.dtype == _supported_float_type(mask.dtype)
expected = np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0], dtype=dtype)
assert_array_almost_equal(result, expected)
def test_invalid_seed():
seed = np.ones((5, 5))
mask = np.ones((5, 5))
with pytest.raises(ValueError):
reconstruction(seed * 2, mask,
method='dilation')
with pytest.raises(ValueError):
reconstruction(seed * 0.5, mask,
method='erosion')
def test_invalid_footprint():
seed = np.ones((5, 5))
mask = np.ones((5, 5))
with pytest.raises(ValueError):
reconstruction(seed, mask,
footprint=np.ones((4, 4)))
with pytest.raises(ValueError):
reconstruction(seed, mask,
footprint=np.ones((3, 4)))
reconstruction(seed, mask, footprint=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 pytest.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 pytest.raises(ValueError):
reconstruction(image, mask, method='dilation',
footprint=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',
footprint=np.ones(3), offset=np.array([0])), expected)

View File

@@ -0,0 +1,229 @@
import numpy as np
import pytest
from numpy.testing import assert_array_equal
from scipy.ndimage import correlate
from skimage import draw
from skimage._shared.testing import fetch
from skimage.io import imread
from skimage.morphology import medial_axis, skeletonize, thin
from skimage.morphology._skeletonize import (_generate_thin_luts,
G123_LUT, G123P_LUT)
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 pytest.raises(ValueError):
skeletonize(im)
def test_skeletonize_wrong_dim2(self):
im = np.zeros((5, 5, 5))
with pytest.raises(ValueError):
skeletonize(im, method='zhang')
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_neighbors(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 pytest.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=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=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,185 @@
import numpy as np
import scipy.ndimage as ndi
from skimage import io, draw
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_neighbors():
# 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 (Plugins>Skeleton->Skeletonize)
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,126 @@
"""Tests for `_util`."""
import numpy as np
import pytest
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 footprint is always of the highest connectivity
and all dimensions are > 2.
"""
footprint = np.ones((3,) * len(image_shape), dtype=bool)
center = (1,) * len(image_shape)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, footprint, center, order
)
# Assert only neighbors are present, center was removed
assert len(offsets) == footprint.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 footprint 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_footprint_smaller_image(image_shape,
order):
"""
Test if a dimension indicated by `image_shape` is smaller than in
`footprint`.
"""
footprint = np.ones((3,) * len(image_shape), dtype=bool)
center = (1,) * len(image_shape)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, footprint, center, order
)
# Assert only neighbors are present, center and duplicates (possible
# for this scenario) where removed
assert len(offsets) <= footprint.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 footprint 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)
footprint = np.ones((3, 3, 3), dtype=bool)
center = (1, 1, 1)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, footprint, 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 footprint is larger in last dimension."""
image_shape = (10, 9, 8, 3)
footprint = np.ones((3, 3, 3, 4), dtype=bool)
center = (1, 1, 1, 1)
offsets = _util._offsets_to_raveled_neighbors(
image_shape, footprint, center
)
desired = np.array([
216, 24, -24, 3, -216, 1, -1, -3, 215, -27, -25, -23, -21, -2,
-192, 192, 2, 4, 21, 23, 25, 27, -4, 217, 213, -219, 219, -217,
-213, -215, 240, -240, 193, 239, -237, 241, -239, 218, -220, 22,
-241, 243, 189, 26, -243, 191, 20, -218, 195, -193, 220, -191,
-212, -189, 214, 28, -195, -214, -28, 212, -22, 237, -20, -26, 236,
196, 190, 242, 238, 194, 188, -244, -188, -196, -194, -190, -238,
-236, 244, -242, 5, 221, -211, -19, 29, -235, -187, 197, 245
])
assert_array_equal(offsets, desired)