fixed docstrings, renamed variables for clarity, removed old skeletonize function body

This commit is contained in:
Christian Sachs
2015-06-30 09:39:25 +02:00
parent 5301a9a8e3
commit f54c254421
2 changed files with 35 additions and 138 deletions
+5 -64
View File
@@ -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 --------
+30 -74
View File
@@ -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)
"""