diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 4612a35d..216e3690 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,9 +1,14 @@ import warnings import numpy as np from scipy import ndimage -from .selem import _default_selem +from .misc import default_fallback +# Our functions only work in 2D, so for 3D or higher input we should fall back +# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring +# element of the appropriate dimension for each of these functions. +# The `default_callback` provides all these. +@default_fallback def binary_erosion(image, selem=None, out=None): """Return fast binary morphological erosion of an image. @@ -32,10 +37,6 @@ def binary_erosion(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - selem = (selem != 0) selem_sum = np.sum(selem) @@ -48,10 +49,11 @@ def binary_erosion(image, selem=None, out=None): ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) if out is None: - out = conv + out = np.empty_like(conv, dtype=np.bool) return np.equal(conv, selem_sum, out=out) +@default_fallback def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. @@ -81,10 +83,6 @@ def binary_dilation(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - selem = (selem != 0) if np.sum(selem) <= 255: @@ -96,10 +94,11 @@ def binary_dilation(image, selem=None, out=None): ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) if out is None: - out = conv + out = np.empty_like(conv, dtype=np.bool) return np.not_equal(conv, 0, out=out) +@default_fallback def binary_opening(image, selem=None, out=None): """Return fast binary morphological opening of an image. @@ -133,6 +132,7 @@ def binary_opening(image, selem=None, out=None): return out +@default_fallback def binary_closing(image, selem=None, out=None): """Return fast binary morphological closing of an image. diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 565ec9a6..19a4a346 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,8 +1,6 @@ import warnings from skimage import img_as_ubyte -from scipy import ndimage -from .selem import _default_selem - +from .misc import default_fallback from . import cmorph @@ -10,7 +8,7 @@ from . import cmorph __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] - +@default_fallback def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -56,10 +54,6 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) @@ -68,6 +62,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): shift_x=shift_x, shift_y=shift_y) +@default_fallback def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. @@ -114,18 +109,16 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("In-place dilation not supported!") + image = img_as_ubyte(image) selem = img_as_ubyte(selem) return cmorph._dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) +@default_fallback def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. @@ -169,10 +162,6 @@ def opening(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -182,6 +171,7 @@ def opening(image, selem=None, out=None): return out +@default_fallback def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. @@ -225,10 +215,6 @@ def closing(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - h, w = selem.shape shift_x = True if (w % 2) == 0 else False shift_y = True if (h % 2) == 0 else False @@ -238,6 +224,7 @@ def closing(image, selem=None, out=None): return out +@default_fallback def white_tophat(image, selem=None, out=None): """Return white top hat of an image. @@ -280,10 +267,6 @@ def white_tophat(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("Cannot perform white top hat in place.") @@ -292,6 +275,7 @@ def white_tophat(image, selem=None, out=None): return out +@default_fallback def black_tophat(image, selem=None, out=None): """Return black top hat of an image. @@ -335,10 +319,6 @@ def black_tophat(image, selem=None, out=None): """ - # Default structure element - if selem is None: - selem = _default_selem(image.ndim) - if image is out: raise NotImplementedError("Cannot perform white top hat in place.") diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index dc8290d2..94aadf7a 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -1,5 +1,53 @@ import numpy as np import scipy.ndimage as nd +from .selem import _default_selem + +# Our function names don't exactly correspond to ndimages. +# This dictionary translates from our names to scipy's. +funcs = ('erosion', 'dilation', 'opening', 'closing') +skimage2ndimage = {x: 'grey_' + x for x in funcs} + +# These function names are the same in ndimage. +funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', + 'binary_closing', 'black_tophat', 'white_tophat') +skimage2ndimage.update({x: x for x in funcs}) + +def default_fallback(func): + """Decorator to fall back on ndimage for images with more than 2 dimensions + + Decorator also provides a default structuring element, `selem`, with the + appropriate dimensionality if none is specified. + + Parameters + ---------- + func : function + A morphology function such as erosion, dilation, opening, closing, + white_tophat, or black_tophat. + + Returns + ------- + func_out : function + If the image dimentionality is greater than 2D, the ndimage + function is returned, otherwise skimage function is used. + """ + + def func_out(image, selem=None, out=None, **kwargs): + # Default structure element + if selem is None: + selem = _default_selem(image.ndim) + + # If image has more than 2 dimensions, use scipy.ndimage + if image.ndim > 2: + function = getattr(nd, skimage2ndimage[func.__name__]) + try: + return function(image, footprint=selem, output=out, **kwargs) + except TypeError: + # nd.binary_* take structure instead of footprint + return function(image, structure=selem, output=out, **kwargs) + else: + return func(image, selem=selem, out=out, **kwargs) + + return func_out def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 1822c8b3..5e0606c0 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -311,10 +311,3 @@ def _default_selem(ndim): """ return ndimage.morphology.generate_binary_structure(ndim, 1) - - - - - - - diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index 6ac5e8d8..a8284103 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -4,6 +4,7 @@ from numpy import testing from skimage import data, color from skimage.util import img_as_bool from skimage.morphology import binary, grey, selem +from scipy import ndimage lena = color.rgb2gray(data.lena()) @@ -86,5 +87,84 @@ def test_default_selem(): im_test = function(image) yield testing.assert_array_equal, im_expected, im_test +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = binary.binary_opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) + +def test_3d_fallback_cube_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + for function in [binary.binary_closing, binary.binary_opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.binary_opening(image, structure=selem) + ndimage_closed = ndimage.binary_closing(image, structure=selem) + + testing.assert_array_equal(bin_opened, ndimage_opened) + testing.assert_array_equal(bin_closed, ndimage_closed) + +def test_binary_output_2d(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + +def test_binary_output_3d(): + image = np.zeros((9, 9, 9), np.uint16) + image[2:-2, 2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3, 3:-3] = 2**15 + image[4, 4, 4] = 2**16-1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + testing.assert_equal(bin_opened.dtype, np.bool) + testing.assert_equal(bin_closed.dtype, np.bool) + + testing.assert_equal(int_opened.dtype, np.uint8) + testing.assert_equal(int_closed.dtype, np.uint8) + if __name__ == '__main__': testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 359a2832..83d10b45 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -2,6 +2,7 @@ import os.path import numpy as np from numpy import testing +from scipy import ndimage import skimage from skimage import data_dir @@ -142,6 +143,65 @@ def test_default_selem(): im_test = function(image) yield testing.assert_array_equal, im_expected, im_test +def test_3d_fallback_default_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = grey.opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1) + testing.assert_array_equal(opened, image_expected) + +def test_3d_fallback_cube_selem(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), np.bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + for function in [grey.closing, grey.opening]: + new_image = function(image, cube) + yield testing.assert_array_equal, new_image, image + +def test_3d_fallback_white_tophat(): + image = np.zeros((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 1 + image[3, 2:5, 2:5] = 1 + image[4, 3:5, 3:5] = 1 + new_image = grey.white_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + image_expected = ndimage.white_tophat(image,footprint=footprint) + testing.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 + new_image = grey.black_tophat(image) + footprint = ndimage.generate_binary_structure(3,1) + image_expected = ndimage.black_tophat(image,footprint=footprint) + testing.assert_array_equal(new_image, image_expected) + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint8) + image[2:-2, 2:-2] = 128 + image[3:-3, 3:-3] = 196 + image[4, 4] = 255 + + opened = grey.opening(image) + closed = grey.closing(image) + + selem = ndimage.generate_binary_structure(2, 1) + ndimage_opened = ndimage.grey_opening(image, footprint=selem) + ndimage_closed = ndimage.grey_closing(image, footprint=selem) + + testing.assert_array_equal(opened, ndimage_opened) + testing.assert_array_equal(closed, ndimage_closed) + class TestDTypes(): def setUp(self):