diff --git a/scikits/image/morphology/cmorph.pyx b/scikits/image/morphology/cmorph.pyx index 5572102c..6870b500 100644 --- a/scikits/image/morphology/cmorph.pyx +++ b/scikits/image/morphology/cmorph.pyx @@ -8,6 +8,7 @@ import numpy as np cimport numpy as np cimport cython +from cpython cimport bool STREL_DTYPE = np.uint8 ctypedef np.uint8_t STREL_DTYPE_t @@ -21,9 +22,15 @@ cdef inline int int_min(int a, int b): return a if a <= b else b @cython.boundscheck(False) def dilate(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out): + np.ndarray[IMAGE_DTYPE_t, ndim=2] out, + bool shift_x, bool shift_y): cdef int hw = selem.shape[0] // 2 cdef int hh = selem.shape[1] // 2 + if shift_x: + hh -= 1 + if shift_y: + hw -= 1 + cdef int width = image.shape[0], height = image.shape[1] if out is None: out = np.zeros([width, height], dtype=IMAGE_DTYPE) @@ -64,9 +71,15 @@ def dilate(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, @cython.boundscheck(False) def erode(np.ndarray[IMAGE_DTYPE_t, ndim=2] image not None, np.ndarray[IMAGE_DTYPE_t, ndim=2] selem not None, - np.ndarray[IMAGE_DTYPE_t, ndim=2] out): + np.ndarray[IMAGE_DTYPE_t, ndim=2] out, + bool shift_x, bool shift_y): cdef int hw = selem.shape[0] // 2 cdef int hh = selem.shape[1] // 2 + if shift_x: + hh -= 1 + if shift_y: + hw -= 1 + cdef int width = image.shape[0], height = image.shape[1] if out is None: out = np.zeros([width, height], dtype=IMAGE_DTYPE) diff --git a/scikits/image/morphology/grey.py b/scikits/image/morphology/grey.py index 2eb94024..1b5552ff 100644 --- a/scikits/image/morphology/grey.py +++ b/scikits/image/morphology/grey.py @@ -9,16 +9,17 @@ import numpy as np eps = np.finfo(float).eps -def greyscale_erode(image, selem, out=None): - """ - Performs a greyscale morphological erosion on an image given a flat - structuring element. The eroded pixel at (i,j) is the minimum over all - pixels in the neighborhood centered at (i,j). +def greyscale_erode(image, selem, out=None, shift_x=False, shift_y=False): + """Return greyscale 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 - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -27,31 +28,54 @@ def greyscale_erode(image, selem, out=None): The array to store the result of the morphology. If None is passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + Returns ------- eroded : ndarray The result of the morphological erosion. + + Examples + -------- + >>> # Erosion shrinks bright regions + >>> from scikits.image.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) + >>> greyscale_erode(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 image is out: raise NotImplementedError("In-place erosion not supported!") try: import scikits.image.morphology.cmorph as cmorph - out = cmorph.erode(image, selem, out=out) + out = cmorph.erode(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) return out; except ImportError: raise ImportError("cmorph extension not available.") -def greyscale_dilate(image, selem, out=None): - """ - Performs a greyscale morphological dilation on an image given a flat - structuring element. The dilated pixel at (i,j) is the maximum over all - pixels in the neighborhood centered at (i,j). +def greyscale_dilate(image, selem, out=None, shift_x=False, shift_y=False): + """Return greyscale morphological dilation of an image. + + 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 - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -60,29 +84,54 @@ def greyscale_dilate(image, selem, out=None): The array to store the result of the morphology. If None, is passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + Returns ------- dilated : ndarray The result of the morphological dilation. + + Examples + -------- + >>> # Dilation enlarges bright regions + >>> from scikits.image.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) + >>> greyscale_dilate(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 image is out: raise NotImplementedError("In-place dilation not supported!") try: from . import cmorph - out = cmorph.dilate(image, selem, out=out) + out = cmorph.dilate(image, selem, out=out, + shift_x=shift_x, shift_y=shift_y) return out; except ImportError: raise ImportError("cmorph extension not available.") def greyscale_open(image, selem, out=None): - """ - Performs a greyscale morphological opening on an image given a flat - structuring element defined as a erosion followed by a dilation. + """Return greyscale morphological opening of an image. + + 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 - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -95,20 +144,45 @@ def greyscale_open(image, selem, out=None): ------- opening : ndarray The result of the morphological opening. + + Examples + -------- + >>> # Open up gap between two bright regions (but also shrink regions) + >>> from scikits.image.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) + >>> greyscale_open(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') + """ + h, w = selem.shape + shift_x = True if (w % 2) == 0 else False + shift_y = True if (h % 2) == 0 else False + eroded = greyscale_erode(image, selem) - out = greyscale_dilate(eroded, selem, out=out) + out = greyscale_dilate(eroded, selem, out=out, + shift_x=shift_x, shift_y=shift_y) return out def greyscale_close(image, selem, out=None): - """ - Performs a greyscale morphological closing on an image given a flat - structuring element defined as a dilation followed by an erosion. + """Return greyscale morphological closing of an image. + + 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 - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -121,19 +195,44 @@ def greyscale_close(image, selem, out=None): ------- opening : ndarray The result of the morphological opening. + + Examples + -------- + >>> # Close a gap between two bright lines + >>> from scikits.image.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) + >>> greyscale_close(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') + """ + h, w = selem.shape + shift_x = True if (w % 2) == 0 else False + shift_y = True if (h % 2) == 0 else False + dilated = greyscale_dilate(image, selem) - out = greyscale_erode(dilated, selem, out=out) + out = greyscale_erode(dilated, selem, out=out, + shift_x=shift_x, shift_y=shift_y) return out def greyscale_white_top_hat(image, selem, out=None): - """ - Applies a white top hat on an image given a flat structuring element. + """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 structuring element. Parameters ---------- image : ndarray - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -146,23 +245,43 @@ def greyscale_white_top_hat(image, selem, out=None): ------- opening : ndarray The result of the morphological white top hat. - """ + + Examples + -------- + >>> # Subtract grey background from bright peak + >>> from scikits.image.morphology import square + >>> bright_on_grey = 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) + >>> greyscale_white_top_hat(bright_on_grey, 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 image is out: raise NotImplementedError("Cannot perform white top hat in place.") - eroded = greyscale_erode(image, selem) - out = greyscale_dilate(eroded, selem, out=out) + out = greyscale_open(image, selem, out=out) out = image - out return out def greyscale_black_top_hat(image, selem, out=None): - """ - Applies a black top hat on an image given a flat structuring element. + """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 structuring element. Note that dark spots in the + original image are bright spots after the black top hat. Parameters ---------- image : ndarray - The image as an ndarray. + The image as a uint8 ndarray. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. @@ -175,13 +294,27 @@ def greyscale_black_top_hat(image, selem, out=None): ------- opening : ndarray The result of the black top filter. + + Examples + -------- + >>> # Change dark peak to bright peak and subtract background + >>> from scikits.image.morphology import square + >>> dark_on_grey = 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) + >>> greyscale_black_top_hat(dark_on_grey, 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 image is out: raise NotImplementedError("Cannot perform white top hat in place.") - dilated = greyscale_dilate(image, selem) - out = greyscale_erode(dilated, selem, out=out) - + out = greyscale_close(image, selem, out=out) out = out - image - if image is out: - raise NotImplementedError("Cannot perform black top hat in place.") return out + diff --git a/scikits/image/morphology/selem.py b/scikits/image/morphology/selem.py index cf786d6d..4a5ee0bd 100644 --- a/scikits/image/morphology/selem.py +++ b/scikits/image/morphology/selem.py @@ -69,7 +69,7 @@ def diamond(radius, dtype=np.uint8): Parameters ---------- radius : string - The radius of the disk-shaped structuring element. + The radius of the diamond-shaped structuring element. dtype : data-type The data type of the structuring element. diff --git a/scikits/image/morphology/tests/test_morphology.py b/scikits/image/morphology/tests/test_morphology.py index ff22c458..330518c2 100644 --- a/scikits/image/morphology/tests/test_morphology.py +++ b/scikits/image/morphology/tests/test_morphology.py @@ -61,3 +61,58 @@ class TestMorphology(): def test_close_disk(self): self.morph_worker(lena, "disk-close-matlab-output.npz", greyscale_close, disk) + + +class TestEccentricStructuringElements(): + + def setUp(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.selems = [square(2), rectangle(2, 2), + rectangle(2, 1), rectangle(1, 2)] + + def test_dilate_erode_symmetry(self): + for s in self.selems: + c = greyscale_erode(self.black_pixel, s) + d = greyscale_dilate(self.white_pixel, s) + assert np.all(c == (255 - d)) + + def test_open_black_pixel(self): + for s in self.selems: + grey_open = greyscale_open(self.black_pixel, s) + assert np.all(grey_open == self.black_pixel) + + def test_close_white_pixel(self): + for s in self.selems: + grey_close = greyscale_close(self.white_pixel, s) + assert np.all(grey_close == self.white_pixel) + + def test_open_white_pixel(self): + for s in self.selems: + assert np.all(greyscale_open(self.white_pixel, s) == 0) + + def test_close_black_pixel(self): + for s in self.selems: + assert np.all(greyscale_close(self.black_pixel, s) == 255) + + def test_white_tophat_white_pixel(self): + for s in self.selems: + tophat = greyscale_white_top_hat(self.white_pixel, s) + assert np.all(tophat == self.white_pixel) + + def test_black_tophat_black_pixel(self): + for s in self.selems: + tophat = greyscale_black_top_hat(self.black_pixel, s) + assert np.all(tophat == (255 - self.black_pixel)) + + def test_white_tophat_black_pixel(self): + for s in self.selems: + tophat = greyscale_white_top_hat(self.black_pixel, s) + assert np.all(tophat == 0) + + def test_black_tophat_white_pixel(self): + for s in self.selems: + tophat = greyscale_black_top_hat(self.white_pixel, s) + assert np.all(tophat == 0) + diff --git a/scikits/image/morphology/watershed.py b/scikits/image/morphology/watershed.py index 1065dbd3..5c74374a 100644 --- a/scikits/image/morphology/watershed.py +++ b/scikits/image/morphology/watershed.py @@ -249,32 +249,32 @@ def is_local_maximum(image, labels=None, footprint=None): >>> image[3, 3] = 1 >>> image array([[ 0., 0., 0., 0.], - [ 0., 0., 2., 0.], - [ 0., 0., 0., 0.], - [ 0., 0., 0., 1.]]) + [ 0., 0., 2., 0.], + [ 0., 0., 0., 0.], + [ 0., 0., 0., 1.]]) >>> is_local_maximum(image) array([[ True, False, False, False], - [ True, False, True, False], - [ True, False, False, False], - [ True, True, False, True]], dtype=bool) + [ True, False, True, False], + [ True, False, False, False], + [ True, True, False, True]], dtype='bool') >>> image = np.arange(16).reshape((4, 4)) >>> labels = np.array([[1, 2], [3, 4]]) >>> labels = np.repeat(np.repeat(labels, 2, axis=0), 2, axis=1) >>> labels array([[1, 1, 2, 2], - [1, 1, 2, 2], - [3, 3, 4, 4], - [3, 3, 4, 4]]) + [1, 1, 2, 2], + [3, 3, 4, 4], + [3, 3, 4, 4]]) >>> image array([[ 0, 1, 2, 3], - [ 4, 5, 6, 7], - [ 8, 9, 10, 11], - [12, 13, 14, 15]]) + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]]) >>> is_local_maximum(image, labels=labels) array([[False, False, False, False], - [False, True, False, True], - [False, False, False, False], - [False, True, False, True]], dtype=bool) + [False, True, False, True], + [False, False, False, False], + [False, True, False, True]], dtype='bool') """ if labels is None: labels = np.ones(image.shape, dtype=np.uint8)