diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 9bf311c4..159a4ac2 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -8,7 +8,7 @@ from .watershed import watershed from ._skeletonize import skeletonize, medial_axis from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction -from .misc import remove_small_objects +from .misc import remove_small_objects, remove_small_holes from ..measure._label import label from .._shared.utils import deprecated as _deprecated @@ -40,4 +40,5 @@ __all__ = ['binary_erosion', 'convex_hull_image', 'convex_hull_object', 'reconstruction', - 'remove_small_objects'] + 'remove_small_objects', + 'remove_small_holes'] diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index e743a398..1a024f38 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -37,7 +37,12 @@ def default_selem(func): return func(image, selem=selem, *args, **kwargs) return func_out - + +def _check_dtype_supported(ar): + # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 + if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): + raise TypeError("Only bool or integer image types are supported. " + "Got %s." % ar.dtype) def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): """Remove connected components smaller than the specified size. @@ -88,10 +93,8 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): >>> d is a True """ - # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 - if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): - raise TypeError("Only bool or integer image types are supported. " - "Got %s." % ar.dtype) + # Raising type error if not int or bool + _check_dtype_supported(ar) if in_place: out = ar @@ -124,3 +127,89 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): out[too_small_mask] = 0 return out + +def remove_small_holes(ar, min_size=64, connectivity=1, in_place=False): + """Remove continguous holes smaller than the specified size. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the connected components of interest. + min_size : int, optional (default: 64) + The hole 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 + ------ + 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]], dtype=bool) + >>> 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]], dtype=bool) + >>> d = morphology.remove_small_holes(a, 2, in_place=True) + >>> d is a + True + + Notes + ----- + + If the array type is int, it is assumed that it contains already-labeled + objects. The labels are not kept in the output image (this function always + outputs a bool image). It is suggested that labeling is completed after + using this function. + """ + _check_dtype_supported(ar) + + #Creates warning if image is an integer image + if ar.dtype != bool: + warnings.warn("Any labeled images will be returned as a boolean array. " + "Did you mean to use a boolean array?", UserWarning) + + if in_place: + out = ar + else: + out = ar.copy() + + #Creating the inverse of ar + if in_place: + out = np.logical_not(out,out) + else: + out = np.logical_not(out) + + #removing small objects from the inverse of ar + out = remove_small_objects(out, min_size, connectivity, in_place) + + if in_place: + out = np.logical_not(out,out) + else: + out = np.logical_not(out) + + return out diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py index 8789b22a..b7222cb5 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -1,7 +1,8 @@ import numpy as np from numpy.testing import (assert_array_equal, assert_equal, assert_raises, assert_warns) -from skimage.morphology import remove_small_objects +from skimage.morphology import remove_small_objects, remove_small_holes +from ..._shared._warnings import expected_warnings test_image = np.array([[0, 0, 0, 1, 0], [1, 1, 1, 0, 0], @@ -60,7 +61,8 @@ def test_single_label_warning(): image = np.array([[0, 0, 0, 1, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 0]], int) - assert_warns(UserWarning, remove_small_objects, image, min_size=6) + with expected_warnings(['use a boolean array?']): + remove_small_objects(image, min_size=6) def test_float_input(): @@ -72,6 +74,102 @@ def test_negative_input(): negative_int = np.random.randint(-4, -1, size=(5, 5)) assert_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, min_size=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, min_size=3, connectivity=2) + assert_array_equal(observed, expected) + +def test_in_place_holes(): + observed = remove_small_holes(test_holes_image, min_size=3, in_place=True) + assert_equal(observed is test_holes_image, True, + "remove_small_holes in_place argument failed.") + +def test_labeled_image_holes(): + labeled_holes_image = np.array([[0,0,0,0,0,0,1,0,0,0], + [0,1,1,1,1,1,0,0,0,0], + [0,1,0,0,1,1,0,0,0,0], + [0,1,1,1,0,1,0,0,0,0], + [0,1,1,1,1,1,0,0,0,0], + [0,0,0,0,0,0,0,2,2,2], + [0,0,0,0,0,0,0,2,0,2], + [0,0,0,0,0,0,0,2,2,2]], dtype=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) + observed = remove_small_holes(labeled_holes_image, min_size=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) + observed = remove_small_holes(labeled_holes_image, min_size=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, min_size=3) + +def test_float_input_holes(): + float_test = np.random.rand(5, 5) + assert_raises(TypeError, remove_small_holes, float_test) + if __name__ == "__main__": np.testing.run_module_suite()