From f54c2544213fd5a9cc725dde8ce90d0427d06ef3 Mon Sep 17 00:00:00 2001 From: Christian Sachs Date: Tue, 30 Jun 2015 09:39:25 +0200 Subject: [PATCH] fixed docstrings, renamed variables for clarity, removed old skeletonize function body --- skimage/morphology/_skeletonize.py | 69 ++-------------- skimage/morphology/_skeletonize_cy.pyx | 104 +++++++------------------ 2 files changed, 35 insertions(+), 138 deletions(-) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index bba03048..1a15c3a6 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -9,8 +9,7 @@ from ._skeletonize_cy import _fast_skeletonize, _skeletonize_loop, _table_lookup # --------- Skeletonization by morphological thinning --------- - -def _slow_skeletonize(image): +def skeletonize(image): """Return the skeleton of a binary image. Thinning is used to reduce each connected component in a binary image @@ -81,78 +80,20 @@ def _slow_skeletonize(image): [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ - # look up table - there is one entry for each of the 2^8=256 possible - # combinations of 8 binary neighbours. 1's, 2's and 3's are candidates - # for removal at each iteration of the algorithm. - lut = [0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 3, 1, 1, 0, 1, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 2, 0, 2, 0, 3, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 2, 2, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, - 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 2, 0, 0, 0, 3, 1, - 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 1, 3, 0, 0, - 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 2, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, - 0, 1, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 0, 0] # convert to unsigned int (this should work for boolean values) - skeleton = image.astype(np.uint8) + image = image.astype(np.uint8) # check some properties of the input image: # - 2D # - binary image with only 0's and 1's - if skeleton.ndim != 2: + if image.ndim != 2: raise ValueError('Skeletonize requires a 2D array') - if not np.all(np.in1d(skeleton.flat, (0, 1))): + if not np.all(np.in1d(image.flat, (0, 1))): raise ValueError('Image contains values other than 0 and 1') - # create the mask that will assign a unique value based on the - # arrangement of neighbouring pixels - mask = np.array([[ 1, 2, 4], - [128, 0, 8], - [ 64, 32, 16]], np.uint8) + return _fast_skeletonize(image) - pixel_removed = True - while pixel_removed: - pixel_removed = False - - # assign each pixel a unique value based on its foreground neighbours - neighbours = ndi.correlate(skeleton, mask, mode='constant') - - # ignore background - neighbours *= skeleton - - # use LUT to categorize each foreground pixel as a 0, 1, 2 or 3 - codes = np.take(lut, neighbours) - - # pass 1 - remove the 1's and 3's - code_mask = (codes == 1) - if np.any(code_mask): - pixel_removed = True - skeleton[code_mask] = 0 - code_mask = (codes == 3) - if np.any(code_mask): - pixel_removed = True - skeleton[code_mask] = 0 - - # pass 2 - remove the 2's and 3's - neighbours = ndi.correlate(skeleton, mask, mode='constant') - neighbours *= skeleton - codes = np.take(lut, neighbours) - code_mask = (codes == 2) - if np.any(code_mask): - pixel_removed = True - skeleton[code_mask] = 0 - code_mask = (codes == 3) - if np.any(code_mask): - pixel_removed = True - skeleton[code_mask] = 0 - - return skeleton.astype(bool) - -skeletonize = _fast_skeletonize # --------- Skeletonization by medial axis transform -------- diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index 87dc087d..4272f294 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -7,71 +7,27 @@ import numpy as np cimport numpy as cnp def _fast_skeletonize(image): - """Return the skeleton of a binary image. - Thinning is used to reduce each connected component in a binary image - to a single-pixel wide skeleton. + """Optimized parts of the Zhang-Suen skeletonization. + Iteratively, pixels meeting removal criteria are removed, + till only the skeleton remains (that is, no further removable pixel + was found). + Performs a hard-coded correlation to assign every neighborhood of 8 a + unique number, which in turn is used in conjunction with a look up + table to select the appropriate thinning criteria. + + Parameters ---------- image : numpy.ndarray A binary image containing the objects to be skeletonized. '1' - represents foreground, and '0' represents background. It - also accepts arrays of boolean values where True is foreground. + represents foreground, and '0' represents background. + Returns ------- skeleton : ndarray A matrix containing the thinned image. - See also - -------- - medial_axis - Notes - ----- - The algorithm [1] works by making successive passes of the image, - removing pixels on object borders. This continues until no - more pixels can be removed. The image is correlated with a - mask that assigns each pixel a number in the range [0...255] - corresponding to each possible pattern of its 8 neighbouring - pixels. A look up table is then used to assign the pixels a - value of 0, 1, 2 or 3, which are selectively removed during - the iterations. - Note that this algorithm will give different results than a - medial axis transform, which is also often referred to as - "skeletonization". - References - ---------- - .. [1] A fast parallel algorithm for thinning digital patterns, - T. Y. ZHANG and C. Y. SUEN, Communications of the ACM, - March 1984, Volume 27, Number 3 - Examples - -------- - >>> X, Y = np.ogrid[0:9, 0:9] - >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(np.uint8) - >>> ellipse - array([[0, 0, 0, 1, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8) - >>> skel = skeletonize(ellipse) - >>> skel.astype(np.uint8) - array([[0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) - """ - if image.ndim != 2: - raise ValueError("Skeletonize requires a 2D array") - if not np.all(np.in1d(image.flat, (0, 1))): - raise ValueError("Image contains values other than 0 and 1") + """ # look up table - there is one entry for each of the 2^8=256 possible # combinations of 8 binary neighbours. 1's, 2's and 3's are candidates @@ -90,17 +46,15 @@ def _fast_skeletonize(image): 0, 0, 0, 0, 2, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 1, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 0, 0] - cdef int pixel_removed, odd_loop, neighbors + cdef int pixel_removed, first_pass, neighbors # indices for fast iteration - cdef Py_ssize_t x, y - - cdef Py_ssize_t ymax = image.shape[0]+2, xmax = image.shape[1]+2 + cdef Py_ssize_t row, col, nrows = image.shape[0]+2, ncols = image.shape[1]+2 # we copy over the image into a larger version with a single pixel border # this removes the need to handle border cases below - _skeleton = np.zeros((ymax, xmax), dtype=np.uint8) - _skeleton[1:ymax-1, 1:xmax-1] = image > 0 + _skeleton = np.zeros((nrows, ncols), dtype=np.uint8) + _skeleton[1:nrows-1, 1:ncols-1] = image > 0 _cleaned_skeleton = _skeleton.copy() @@ -113,7 +67,7 @@ def _fast_skeletonize(image): pixel_removed = True # the algorithm reiterates the thinning till - # no further thinning occured (variable pixel_removed set) + # no further thinning occurred (variable pixel_removed set) while pixel_removed: pixel_removed = False @@ -121,24 +75,26 @@ def _fast_skeletonize(image): # there are two phases, in the first phase, pixels labeled (see below) # 1 and 3 are removed, in the second 2 and 3 - for odd_loop in range(1, -1, -1): - for y in range(1, ymax-1): - for x in range(1, xmax-1): + for first_pass in (True, False): + for row in range(1, nrows-1): + for col in range(1, ncols-1): # all set pixels ... - if skeleton[y, x] > 0: + if skeleton[row, col]: # are correlated with a kernel (coefficients spread around here ...) # to apply a unique number to every possible neighborhood ... # which is used with the lut to find the "connectivity type" - neighbors = lut[ 1*skeleton[y - 1, x - 1] + 2*skeleton[y - 1, x] +\ - 4*skeleton[y - 1, x + 1] + 8*skeleton[y, x + 1] +\ - 16*skeleton[y + 1, x + 1] + 32*skeleton[y + 1, x] +\ - 64*skeleton[y + 1, x - 1] + 128*skeleton[y, x - 1]] + neighbors = lut[ 1*skeleton[row - 1, col - 1] + 2*skeleton[row - 1, col] +\ + 4*skeleton[row - 1, col + 1] + 8*skeleton[row, col + 1] +\ + 16*skeleton[row + 1, col + 1] + 32*skeleton[row + 1, col] +\ + 64*skeleton[row + 1, col - 1] + 128*skeleton[row, col - 1]] # if the condition is met, the pixel is removed (unset) - if (odd_loop and neighbors == 1) or ((not odd_loop) and neighbors == 2) or neighbors == 3: - cleaned_skeleton[y, x] = 0 + if (first_pass and neighbors == 1) or\ + ((not first_pass) and neighbors == 2) or\ + neighbors == 3: + cleaned_skeleton[row, col] = 0 pixel_removed = True # once a step has been processed, the original skeleton @@ -146,7 +102,7 @@ def _fast_skeletonize(image): _skeleton = _cleaned_skeleton.copy() skeleton = _skeleton - return _skeleton[1:ymax-1, 1:xmax-1].astype(np.bool) + return _skeleton[1:nrows-1, 1:ncols-1].astype(np.bool) """