diff --git a/doc/examples/applications/plot_coins_segmentation.py b/doc/examples/applications/plot_coins_segmentation.py index f46bbd33..0d1d8a34 100644 --- a/doc/examples/applications/plot_coins_segmentation.py +++ b/doc/examples/applications/plot_coins_segmentation.py @@ -90,12 +90,8 @@ plt.title('Filling the holes') Small spurious objects are easily removed by setting a minimum size for valid objects. """ - -label_objects, nb_labels = ndimage.label(fill_coins) -sizes = np.bincount(label_objects.ravel()) -mask_sizes = sizes > 20 -mask_sizes[0] = 0 -coins_cleaned = mask_sizes[label_objects] +from skimage import morphology +coins_cleaned = morphology.remove_small_objects(fill_coins, 21) plt.figure(figsize=(4, 3)) plt.imshow(coins_cleaned, cmap=plt.cm.gray, interpolation='nearest') @@ -149,8 +145,7 @@ plt.title('markers') Finally, we use the watershed transform to fill regions of the elevation map starting from the markers determined above: """ -from skimage.morphology import watershed -segmentation = watershed(elevation_map, markers) +segmentation = morphology.watershed(elevation_map, markers) plt.figure(figsize=(4, 3)) plt.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest') diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index d4c775eb..044fcf89 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -7,3 +7,4 @@ from .watershed import watershed, is_local_maximum from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image from .greyreconstruct import reconstruction +from .misc import remove_small_objects diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py new file mode 100644 index 00000000..bb0ff687 --- /dev/null +++ b/skimage/morphology/misc.py @@ -0,0 +1,78 @@ +import numpy as np +import scipy.ndimage as nd + +def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): + """Remove connected components smaller than the specified size. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the connected components of interest. If the array + type is int, it is assumed that it contains already-labeled objects. + The ints must be non-negative. + min_size : int, optional (default: 64) + The smallest allowable connected component size. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. + in_place : bool, optional (default: False) + If `True`, remove the connected components in the input array itself. + Otherwise, make a copy. + + Raises + ------ + ValueError + If the input array is of int type but contains negative values. + TypeError + If the input array is of an invalid type, such as float or string. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small connected components removed. + + Examples + -------- + >>> from skimage import morphology + >>> from scipy import ndimage as nd + >>> a = np.array([[0, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0], + ... [1, 1, 1, 0, 1]], bool) + >>> b = morphology.remove_small_connected_components(a, 6) + >>> b + array([[False, False, False, False, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> c = morphology.remove_small_connected_components(a, 7, connectivity=2) + >>> c + array([[False, False, False, True, False], + [ True, True, True, False, False], + [ True, True, True, False, False]], dtype=bool) + >>> d = morphology.remove_small_connected_components(a, 6, in_place=True) + >>> d is a + True + """ + errmsg = "Only numpy.ndarrays of bool or integer type are supported. " + if type(ar) != np.ndarray: + raise TypeError(errmsg + "Got a %s." % type(ar)) + elif ar.dtype != bool and not np.issubdtype(ar.dtype, np.integer): + raise TypeError(errmsg + "Got a numpy.ndarray of type %s" % ar.dtype) + if in_place: + out = ar + else: + out = ar.copy() + if min_size == 0: # shortcut for efficiency + return out + if out.dtype == bool: + selem = nd.generate_binary_structure(ar.ndim, connectivity) + ccs = nd.label(ar, selem)[0] + 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`.") + too_small = component_sizes < min_size + too_small_mask = too_small[ccs] + out[too_small_mask] = 0 + return out diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py new file mode 100644 index 00000000..4b6e6e01 --- /dev/null +++ b/skimage/morphology/tests/test_misc.py @@ -0,0 +1,56 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_equal, assert_raises +from skimage.morphology import remove_small_objects + +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(): + observed = remove_small_objects(test_image, min_size=6, in_place=True) + assert_equal(observed is test_image, True, + "remove_small_objects in_place argument failed.") + + +def test_labeled_image(): + labeled_image = np.array([[2, 2, 2, 0, 1], + [2, 2, 2, 0, 1], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=int) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + +def test_float_input(): + float_test = np.random.rand(5, 5) + assert_raises(TypeError, remove_small_objects, float_test) + + +def test_negative_input(): + negative_int = np.random.randint(-4, -1, size=(5, 5)) + assert_raises(ValueError, remove_small_objects, negative_int) + + +if __name__ == "__main__": + np.testing.run_module_suite()