diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index e65e756b..0dbe183d 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -1,17 +1,53 @@ """ Grayscale morphological operations """ -from skimage import img_as_ubyte -from .misc import default_fallback - -from . import cmorph - +import numpy as np +from scipy import ndimage as nd +from .misc import default_selem __all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] -@default_fallback +def _shift_selem(selem, shift_x, shift_y): + """Shift the binary image `selem` in the left and/or up. + + This only affects structuring elements with even number of rows or + columns. + + Parameters + ---------- + selem : 2D array, shape (M, N) + The input structuring element. + shift_x, shift_y : bool + Whether to move `selem` along each axis. + + Returns + ------- + out : 2D array, shape (M + int(shift_x), N + int(shift_y)) + The shifted structuring element. + """ + if selem.ndim > 2: + # do nothing for 3D or higher structuring elements + return selem + m, n = selem.shape + if m % 2 == 0: + extra_row = np.zeros((1, n), selem.dtype) + if shift_x: + selem = np.vstack((selem, extra_row)) + else: + selem = np.vstack((extra_row, selem)) + m += 1 + if n % 2 == 0: + extra_col = np.zeros((m, 1), selem.dtype) + if shift_y: + selem = np.hstack((selem, extra_col)) + else: + selem = np.hstack((extra_col, selem)) + return selem + + +@default_selem def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological erosion of an image. @@ -24,7 +60,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): image : ndarray Image array. selem : ndarray, optional - The neighborhood expressed as a 2-D array of 1's and 0's. + The neighborhood expressed as an array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). out : ndarrays, optional The array to store the result of the morphology. If None is @@ -35,7 +71,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): Returns ------- - eroded : uint8 array + eroded : array, same shape as `image` The result of the morphological erosion. Notes @@ -62,16 +98,15 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("In-place erosion not supported!") - image = img_as_ubyte(image) - selem = img_as_ubyte(selem) - return cmorph._erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + selem = np.array(selem) + selem = _shift_selem(selem, shift_x, shift_y) + if out is None: + out = np.empty_like(image) + nd.grey_erosion(image, footprint=selem, output=out) + return out -@default_fallback +@default_selem def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): """Return greyscale morphological dilation of an image. @@ -123,17 +158,15 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False): [0, 0, 0, 0, 0]], dtype=uint8) """ - - 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) + selem = np.array(selem) + selem = _shift_selem(selem, shift_x, shift_y) + if out is None: + out = np.empty_like(image) + nd.grey_dilation(image, footprint=selem, output=out) + return out -@default_fallback +@default_selem def opening(image, selem=None, out=None): """Return greyscale morphological opening of an image. @@ -147,7 +180,7 @@ def opening(image, selem=None, out=None): image : ndarray Image array. selem : ndarray, optional - The neighborhood expressed as a 2-D array of 1's and 0's. + The neighborhood expressed as an array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). out : ndarray, optional The array to store the result of the morphology. If None @@ -155,7 +188,7 @@ def opening(image, selem=None, out=None): Returns ------- - opening : uint8 array + opening : array The result of the morphological opening. Examples @@ -176,17 +209,13 @@ def opening(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - h, w = selem.shape - shift_x = True if (w % 2) == 0 else False - shift_y = True if (h % 2) == 0 else False - eroded = erosion(image, selem) - out = dilation(eroded, selem, out=out, shift_x=shift_x, shift_y=shift_y) + # note: shift_x, shift_y do nothing if selem side length is odd + out = dilation(eroded, selem, out=out, shift_x=True, shift_y=True) return out -@default_fallback +@default_selem def closing(image, selem=None, out=None): """Return greyscale morphological closing of an image. @@ -200,7 +229,7 @@ def closing(image, selem=None, out=None): image : ndarray Image array. selem : ndarray, optional - The neighborhood expressed as a 2-D array of 1's and 0's. + The neighborhood expressed as an array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). out : ndarray, optional The array to store the result of the morphology. If None, @@ -208,7 +237,7 @@ def closing(image, selem=None, out=None): Returns ------- - closing : uint8 array + closing : array The result of the morphological closing. Examples @@ -229,17 +258,13 @@ def closing(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - h, w = selem.shape - shift_x = True if (w % 2) == 0 else False - shift_y = True if (h % 2) == 0 else False - dilated = dilation(image, selem) - out = erosion(dilated, selem, out=out, shift_x=shift_x, shift_y=shift_y) + # note: shift_x, shift_y do nothing if selem side length is odd + out = erosion(dilated, selem, out=out, shift_x=True, shift_y=True) return out -@default_fallback +@default_selem def white_tophat(image, selem=None, out=None): """Return white top hat of an image. @@ -252,7 +277,7 @@ def white_tophat(image, selem=None, out=None): image : ndarray Image array. selem : ndarray, optional - The neighborhood expressed as a 2-D array of 1's and 0's. + The neighborhood expressed as an array of 1's and 0's. If None, use cross-shaped structuring element (connectivity=1). out : ndarray, optional The array to store the result of the morphology. If None @@ -260,7 +285,7 @@ def white_tophat(image, selem=None, out=None): Returns ------- - opening : uint8 array + out : array The result of the morphological white top hat. Examples @@ -281,16 +306,18 @@ def white_tophat(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("Cannot perform white top hat in place.") - - out = opening(image, selem, out=out) - out = image - out + selem = np.array(selem) + if out is image: + opened = opening(image, selem) + out -= opened + return out + elif out is None: + out = np.empty_like(image) + out = nd.white_tophat(image, footprint=selem, output=out) return out -@default_fallback +@default_selem def black_tophat(image, selem=None, out=None): """Return black top hat of an image. @@ -333,10 +360,10 @@ def black_tophat(image, selem=None, out=None): [0, 0, 0, 0, 0]], dtype=uint8) """ - - if image is out: - raise NotImplementedError("Cannot perform white top hat in place.") - + if out is image: + original = image.copy() + else: + original = image out = closing(image, selem, out=out) - out = out - image + out -= original return out diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 55b279a2..663e1e02 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -15,11 +15,8 @@ funcs = ('binary_erosion', 'binary_dilation', 'binary_opening', skimage2ndimage.update(dict((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. +def default_selem(func): + """Decorator to add a default structuring element to morphology functions. Parameters ---------- @@ -30,25 +27,14 @@ def default_fallback(func): Returns ------- func_out : function - If the image dimensionality is greater than 2D, the ndimage - function is returned, otherwise skimage function is used. + The function, using a default structuring element of same dimension + as the input image with connectivity 1. """ @functools.wraps(func) - def func_out(image, selem=None, out=None, **kwargs): - # Default structure element + def func_out(image, selem=None, *args, **kwargs): 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(image, selem=selem, *args, **kwargs) return func_out