Updates based on review comments

This commit is contained in:
Neil Yager
2011-10-17 08:32:19 +01:00
parent fb81057774
commit 3ddfdbdb9c
5 changed files with 65 additions and 45 deletions
+2
View File
@@ -78,3 +78,5 @@
- Christoph Gohlke - Christoph Gohlke
Windows packaging and Python 3 compatibility. Windows packaging and Python 3 compatibility.
- Neil Yager
Skeletonization.
+12 -2
View File
@@ -3,7 +3,17 @@
Skeletonize Skeletonize
=========== ===========
An example of thinning a binary image using skeletonize. Skeletonization reduces binary objects to 1 pixel wide representations. This
can be useful for feature extraction, and/or representing an object's topology.
The algorithm works by making successive passes of the image. On each pass,
border pixels are identified and removed on the condition that they do not
break the connectivity of the corresponding object.
This module provides an example of calling the routine and displaying the
results. The input is a 2D ndarray, with either boolean or integer elements.
In the case of boolean, 'True' indicates foreground, and for integer arrays,
the foreground is 1's.
""" """
from scikits.image.morphology import skeletonize from scikits.image.morphology import skeletonize
from scikits.image.draw import draw from scikits.image.draw import draw
@@ -32,7 +42,7 @@ image[circle1] = 1
image[circle2] = 0 image[circle2] = 0
# perform skeletonization # perform skeletonization
skeleton = skeletonize.skeletonize(image) skeleton = skeletonize(image)
# display results # display results
plt.figure(figsize=(10,6)) plt.figure(figsize=(10,6))
+1
View File
@@ -2,3 +2,4 @@ from grey import *
from selem import * from selem import *
from .ccomp import label from .ccomp import label
from watershed import watershed, is_local_maximum from watershed import watershed, is_local_maximum
from skeletonize import skeletonize
+35 -26
View File
@@ -1,7 +1,5 @@
"""skeletonize.py - Use an iterative thinning algorithm to find the """skeletonize.py - Use an iterative thinning algorithm to find the
skeletons of binary objects in an image. skeletons of binary objects in an image.
Original author: Neil Yager
""" """
import numpy as np import numpy as np
@@ -11,16 +9,16 @@ from .. import util
def skeletonize(image): def skeletonize(image):
""" """
Return a single pixel wide skeleton of all connected components Return a single pixel wide skeleton of all connected components
in a binary image. in a binary image.
The algorithm works by making successive passes of the image, The algorithm works by making successive passes of the image,
removing pixels on object borders. This continues until no removing pixels on object borders. This continues until no
more pixels can be removed. The image is correlated with a more pixels can be removed. The image is correlated with a
mask that assigns each pixel a number in the range [0...255] mask that assigns each pixel a number in the range [0...255]
corresponding to each possible pattern of its 8 neighbouring corresponding to each possible pattern of its 8 neighbouring
pixels. A look up table is then used to assign the pixels a 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 value of 0, 1, 2 or 3, which are selectively removed during
the iterations. the iterations.
Parameters Parameters
---------- ----------
@@ -34,9 +32,9 @@ def skeletonize(image):
----- -----
This implementation gives different results than a medial This implementation gives different results than a medial
axis transforrmation, which can be can be implemented using axis transformation, which can be can be implemented using
morphological operations. This implementation is generally much morphological operations. This implementation is generally much
faster. faster.
Returns Returns
------- -------
@@ -55,7 +53,9 @@ def skeletonize(image):
-------- --------
""" """
# look up table # 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, 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,2,0,0,0,0,0,0,0,2,0,0,0,3,0,2,2, 0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,2,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, 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,
@@ -73,9 +73,8 @@ def skeletonize(image):
# - binary image with only 0's and 1's # - binary image with only 0's and 1's
if skeleton.ndim != 2: if skeleton.ndim != 2:
raise ValueError('Skeletonize requires a 2D array') raise ValueError('Skeletonize requires a 2D array')
for val in np.unique(skeleton): if not np.all(np.in1d(skeleton.flat, (0, 1))):
if val not in [0, 1]: raise ValueError('Image contains values other than 0 and 1')
raise ValueError('Invalid value in the image: %d'%(val))
# create the mask that will assign a unique value based on the # create the mask that will assign a unique value based on the
# arrangement of neighbouring pixels # arrangement of neighbouring pixels
@@ -87,26 +86,36 @@ def skeletonize(image):
while pixelRemoved: while pixelRemoved:
pixelRemoved = False; pixelRemoved = False;
# pass 1 - remove the 1's and 3's # assign each pixel a unique value based on its foreground neighbours
neighbours = correlate(skeleton, mask, mode='constant') neighbours = correlate(skeleton, mask, mode='constant')
# ignore background
neighbours[skeleton == 0] = 0 neighbours[skeleton == 0] = 0
# use LUT to categorize each foreground pixel as a 0, 1, 2 or 3
codes = np.take(lut, neighbours) codes = np.take(lut, neighbours)
if np.any(codes == 1):
# pass 1 - remove the 1's and 3's
code_mask = (codes == 1)
if np.any(code_mask):
pixelRemoved = True pixelRemoved = True
skeleton[codes == 1] = 0 skeleton[code_mask] = 0
if np.any(codes == 3): code_mask = (codes == 3)
if np.any(code_mask):
pixelRemoved = True pixelRemoved = True
skeleton[codes == 3] = 0 skeleton[code_mask] = 0
# pass 2 - remove the 2's and 3's # pass 2 - remove the 2's and 3's
neighbours = correlate(skeleton, mask, mode='constant') neighbours = correlate(skeleton, mask, mode='constant')
neighbours[skeleton == 0] = 0 neighbours[skeleton == 0] = 0
codes = np.take(lut, neighbours) codes = np.take(lut, neighbours)
if np.any(codes == 2): code_mask = (codes == 2)
if np.any(code_mask):
pixelRemoved = True pixelRemoved = True
skeleton[codes == 2] = 0 skeleton[code_mask] = 0
if np.any(codes == 3): code_mask = (codes == 3)
if np.any(code_mask):
pixelRemoved = True pixelRemoved = True
skeleton[codes == 3] = 0 skeleton[code_mask] = 0
return skeleton return skeleton
@@ -1,4 +1,3 @@
import unittest
import numpy as np import numpy as np
from scikits.image.morphology import skeletonize from scikits.image.morphology import skeletonize
import numpy.testing import numpy.testing
@@ -8,39 +7,39 @@ from scikits.image.io import imread
from scikits.image import data_dir from scikits.image import data_dir
import os.path import os.path
class TestSkeletonize(unittest.TestCase): class TestSkeletonize():
def test_skeletonize_no_foreground(self): def test_skeletonize_no_foreground(self):
im = np.zeros((5,5)) im = np.zeros((5,5))
result = skeletonize.skeletonize(im) result = skeletonize(im)
numpy.testing.assert_array_equal(result, np.zeros((5,5))) numpy.testing.assert_array_equal(result, np.zeros((5,5)))
def test_skeletonize_wrong_dim1(self): def test_skeletonize_wrong_dim1(self):
im = np.zeros((5)) im = np.zeros((5))
self.assertRaises(ValueError, skeletonize.skeletonize, im) numpy.testing.assert_raises(ValueError, skeletonize, im)
def test_skeletonize_wrong_dim2(self): def test_skeletonize_wrong_dim2(self):
im = np.zeros((5, 5, 5)) im = np.zeros((5, 5, 5))
self.assertRaises(ValueError, skeletonize.skeletonize, im) numpy.testing.assert_raises(ValueError, skeletonize, im)
def test_skeletonize_not_binary(self): def test_skeletonize_not_binary(self):
im = np.zeros((5, 5)) im = np.zeros((5, 5))
im[0, 0] = 1 im[0, 0] = 1
im[0, 1] = 2 im[0, 1] = 2
self.assertRaises(ValueError, skeletonize.skeletonize, im) numpy.testing.assert_raises(ValueError, skeletonize, im)
def test_skeletonize_unexpected_value(self): def test_skeletonize_unexpected_value(self):
im = np.zeros((5, 5)) im = np.zeros((5, 5))
im[0, 0] = 2 im[0, 0] = 2
self.assertRaises(ValueError, skeletonize.skeletonize, im) numpy.testing.assert_raises(ValueError, skeletonize, im)
def test_skeletonize_all_foreground(self): def test_skeletonize_all_foreground(self):
im = np.ones((3,4)) im = np.ones((3,4))
result = skeletonize.skeletonize(im) result = skeletonize(im)
def test_skeletonize_single_point(self): def test_skeletonize_single_point(self):
im = np.zeros((5, 5), np.uint8) im = np.zeros((5, 5), np.uint8)
im[3, 3] = 1 im[3, 3] = 1
result = skeletonize.skeletonize(im) result = skeletonize(im)
numpy.testing.assert_array_equal(result, im) numpy.testing.assert_array_equal(result, im)
def test_skeletonize_already_thinned(self): def test_skeletonize_already_thinned(self):
@@ -48,7 +47,7 @@ class TestSkeletonize(unittest.TestCase):
im[3,1:-1] = 1 im[3,1:-1] = 1
im[2, -1] = 1 im[2, -1] = 1
im[4, 0] = 1 im[4, 0] = 1
result = skeletonize.skeletonize(im) result = skeletonize(im)
numpy.testing.assert_array_equal(result, im) numpy.testing.assert_array_equal(result, im)
def test_skeletonize_output(self): def test_skeletonize_output(self):
@@ -56,7 +55,7 @@ class TestSkeletonize(unittest.TestCase):
# make black the foreground # make black the foreground
im = (im==0) im = (im==0)
result = skeletonize.skeletonize(im) result = skeletonize(im)
expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy")) expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy"))
numpy.testing.assert_array_equal(result, expected) numpy.testing.assert_array_equal(result, expected)
@@ -83,15 +82,14 @@ class TestSkeletonize(unittest.TestCase):
circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2
image[circle1] = 1 image[circle1] = 1
image[circle2] = 0 image[circle2] = 0
result = skeletonize.skeletonize(image) result = skeletonize(image)
# there should never be a 2x2 block of foreground pixels # there should never be a 2x2 block of foreground pixels in a skeleton
# in a skeleton
mask = np.array([[1, 1], mask = np.array([[1, 1],
[1, 1]], np.uint8) [1, 1]], np.uint8)
blocks = correlate(result, mask, mode='constant') blocks = correlate(result, mask, mode='constant')
self.assertFalse(numpy.any(blocks == 4)) assert not numpy.any(blocks == 4)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() np.testing.run_module_suite()