From 772c469ec1302092215f0f018909fc461a8f1c60 Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Wed, 12 Oct 2011 13:38:36 +0100 Subject: [PATCH 1/8] Initial revision of skeletonization --- scikits/image/morphology/skeletonize.py | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 scikits/image/morphology/skeletonize.py diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py new file mode 100644 index 00000000..696eafdd --- /dev/null +++ b/scikits/image/morphology/skeletonize.py @@ -0,0 +1,82 @@ +"""skeletonize.py - ??? + +Original author: Neil Yager +""" + +import numpy as np +from scipy.ndimage import correlate + +def skeletonize(image): + """ + Return a single pixel wide skeleton of all connected + components in a binary image + + Parameters + ---------- + + image: + + Returns + ------- + + out: ndarray + A matrix containing the thinned image + + References + ---------- + 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 + -------- + """ + + # look up table + 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,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,1,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] + + skeleton = image.copy().astype(np.int8) + mask = np.array([[ 1, 2, 4], + [128, 0, 8], + [ 64, 32, 16]], np.int8) + + pixelRemoved = True + while pixelRemoved: + pixelRemoved = False; + + # pass 1 + neighbours = correlate(skeleton, mask, mode='constant') + neighbours[skeleton == 0] = 0 + codes = np.take(lut, neighbours) + if np.any(codes == 1): + pixelRemoved = True + skeleton[codes == 1] = 0 + if np.any(codes == 3): + pixelRemoved = True + skeleton[codes == 3] = 0 + + # pass 2 + neighbours = correlate(skeleton, mask, mode='constant') + neighbours[skeleton == 0] = 0 + codes = np.take(lut, neighbours) + if np.any(codes == 2): + pixelRemoved = True + skeleton[codes == 2] = 0 + if np.any(codes == 3): + pixelRemoved = True + skeleton[codes == 3] = 0 + + + + return skeleton + + + \ No newline at end of file From 74bde135c9b3b48b369317ef8471aae5afacd7c1 Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Thu, 13 Oct 2011 08:29:32 +0100 Subject: [PATCH 2/8] Added skeleonize example --- doc/examples/plot_skeleton.py | 27 +++++++++++++++++++++++++ scikits/image/morphology/skeletonize.py | 9 ++------- 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 doc/examples/plot_skeleton.py diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py new file mode 100644 index 00000000..2c6fed27 --- /dev/null +++ b/doc/examples/plot_skeleton.py @@ -0,0 +1,27 @@ +from scikits.image.morphology import skeletonize +import numpy as np +import matplotlib.pyplot as plt + +image = np.zeros((400, 400)) +image[10:-10, 10:100] = 1 +image[-100:-10, 10:-10] = 1 +image[10:-10, -100:-10] = 1 + +skeleton = skeletonize.skeletonize(image) + +plt.figure(figsize=(8,5)) + +plt.subplot(121) +plt.imshow(image, cmap=plt.cm.gray) +plt.axis('off') +plt.title('original', fontsize=20) + +plt.subplot(122) +plt.imshow(skeleton, cmap=plt.cm.gray) +plt.axis('off') +plt.title('skeleton', fontsize=20) + +plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.98, + bottom=0.02, left=0.02, right=0.98) + +plt.show() \ No newline at end of file diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index 696eafdd..325448a0 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -52,7 +52,7 @@ def skeletonize(image): while pixelRemoved: pixelRemoved = False; - # pass 1 + # pass 1 - remove the 1's and 3's neighbours = correlate(skeleton, mask, mode='constant') neighbours[skeleton == 0] = 0 codes = np.take(lut, neighbours) @@ -63,7 +63,7 @@ def skeletonize(image): pixelRemoved = True skeleton[codes == 3] = 0 - # pass 2 + # pass 2 - remove the 2's and 3's neighbours = correlate(skeleton, mask, mode='constant') neighbours[skeleton == 0] = 0 codes = np.take(lut, neighbours) @@ -74,9 +74,4 @@ def skeletonize(image): pixelRemoved = True skeleton[codes == 3] = 0 - - return skeleton - - - \ No newline at end of file From 8ad8946900527acf88c28210ee5e313de3dd28dc Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Thu, 13 Oct 2011 09:51:27 +0100 Subject: [PATCH 3/8] Added more objects to skeletonize demo --- doc/examples/plot_skeleton.py | 28 +++++++++++++++++++++- scikits/image/morphology/skeletonize.py | 32 +++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index 2c6fed27..68958c6d 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -1,15 +1,41 @@ +""" +=========== +Skeletonize +=========== + +An example of thinning a binary image using skeletonize. +""" from scikits.image.morphology import skeletonize +from scikits.image.draw import draw import numpy as np import matplotlib.pyplot as plt +# an empty image image = np.zeros((400, 400)) + +# foreground object 1 image[10:-10, 10:100] = 1 image[-100:-10, 10:-10] = 1 image[10:-10, -100:-10] = 1 +# foreground object 2 +rs, cs = draw.bresenham(250, 150, 10, 280) +for i in range(10): image[rs+i, cs] = 1 +rs, cs = draw.bresenham(10, 150, 250, 280) +for i in range(20): image[rs+i, cs] = 1 + +# foreground object 3 +ir, ic = np.indices(image.shape) +circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 +circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 +image[circle1] = 1 +image[circle2] = 0 + +# perform skeletonization skeleton = skeletonize.skeletonize(image) -plt.figure(figsize=(8,5)) +# display results +plt.figure(figsize=(10,6)) plt.subplot(121) plt.imshow(image, cmap=plt.cm.gray) diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index 325448a0..c5588370 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -1,4 +1,5 @@ -"""skeletonize.py - ??? +"""skeletonize.py - Use an iterative thinning algorithm to find the + skeletons of binary objects in an image. Original author: Neil Yager """ @@ -8,13 +9,32 @@ from scipy.ndimage import correlate def skeletonize(image): """ - Return a single pixel wide skeleton of all connected - components in a binary image + Return a single pixel wide skeleton of all connected components + in a binary image. + + The algorithm 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. Parameters ---------- - image: + image: ndarray (2D) + A binary image containing the objects to be skeletonized. '1' + represents foreground, and '0' represents background. + + Notes + ----- + + This implementation gives different results than a medial + axis transforrmation, which can be can be implemented using + morphological operations. This implementation is generally much + faster. Returns ------- @@ -43,7 +63,11 @@ def skeletonize(image): 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] + # initialize the skeleton to the original image + # TODO: how to handle data types skeleton = image.copy().astype(np.int8) + + # create the mask that will assign a value based on neighbouring pixels mask = np.array([[ 1, 2, 4], [128, 0, 8], [ 64, 32, 16]], np.int8) From 3d2613cdfa4d137646be394933c4a1b46bba977f Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Thu, 13 Oct 2011 14:35:37 +0100 Subject: [PATCH 4/8] Added unit tests to skeletonization --- scikits/image/morphology/skeletonize.py | 25 ++++-- .../morphology/tests/test_skeletonize.py | 83 +++++++++++++++++++ 2 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 scikits/image/morphology/tests/test_skeletonize.py diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index c5588370..853aba3e 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -6,6 +6,7 @@ Original author: Neil Yager import numpy as np from scipy.ndimage import correlate +from .. import util def skeletonize(image): """ @@ -26,7 +27,8 @@ def skeletonize(image): image: ndarray (2D) A binary image containing the objects to be skeletonized. '1' - represents foreground, and '0' represents background. + represents foreground, and '0' represents background. It + also accepts arrays of boolean values where True is foreground. Notes ----- @@ -62,15 +64,24 @@ def skeletonize(image): 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 = np.array(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: + raise ValueError('Skeletonize requires a 2D array') + for val in np.unique(skeleton): + if val not in [0, 1]: + raise ValueError('Invalid value in the image: %d'%(val)) - # initialize the skeleton to the original image - # TODO: how to handle data types - skeleton = image.copy().astype(np.int8) - - # create the mask that will assign a value based on neighbouring pixels + # 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.int8) + [ 64, 32, 16]], np.uint8) pixelRemoved = True while pixelRemoved: diff --git a/scikits/image/morphology/tests/test_skeletonize.py b/scikits/image/morphology/tests/test_skeletonize.py new file mode 100644 index 00000000..5a6bacb9 --- /dev/null +++ b/scikits/image/morphology/tests/test_skeletonize.py @@ -0,0 +1,83 @@ +import unittest +import numpy as np +from scikits.image.morphology import skeletonize +import numpy.testing +from scikits.image.draw import draw +from scipy.ndimage import correlate + +class TestSkeletonize(unittest.TestCase): + def test_skeletonize_no_foreground(self): + im = np.zeros((5,5)) + result = skeletonize.skeletonize(im) + numpy.testing.assert_array_equal(result, np.zeros((5,5))) + + def test_skeletonize_wrong_dim1(self): + im = np.zeros((5)) + self.assertRaises(ValueError, skeletonize.skeletonize, im) + + def test_skeletonize_wrong_dim2(self): + im = np.zeros((5, 5, 5)) + self.assertRaises(ValueError, skeletonize.skeletonize, im) + + def test_skeletonize_not_binary(self): + im = np.zeros((5, 5)) + im[0, 0] = 1 + im[0, 1] = 2 + self.assertRaises(ValueError, skeletonize.skeletonize, im) + + def test_skeletonize_unexpected_value(self): + im = np.zeros((5, 5)) + im[0, 0] = 2 + self.assertRaises(ValueError, skeletonize.skeletonize, im) + + def test_skeletonize_all_foreground(self): + im = np.ones((3,4)) + result = skeletonize.skeletonize(im) + + def test_skeletonize_single_point(self): + im = np.zeros((5, 5), np.uint8) + im[3, 3] = 1 + result = skeletonize.skeletonize(im) + numpy.testing.assert_array_equal(result, im) + + def test_skeletonize_already_thinned(self): + im = np.zeros((5, 5), np.uint8) + im[3,1:-1] = 1 + im[2, -1] = 1 + im[4, 0] = 1 + result = skeletonize.skeletonize(im) + numpy.testing.assert_array_equal(result, im) + + def test_skeletonize_num_neighbours(self): + # an empty image + image = np.zeros((300, 300)) + + # foreground object 1 + image[10:-10, 10:100] = 1 + image[-100:-10, 10:-10] = 1 + image[10:-10, -100:-10] = 1 + + # foreground object 2 + rs, cs = draw.bresenham(250, 150, 10, 280) + for i in range(10): image[rs+i, cs] = 1 + rs, cs = draw.bresenham(10, 150, 250, 280) + for i in range(20): image[rs+i, cs] = 1 + + # foreground object 3 + ir, ic = np.indices(image.shape) + circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 + circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 + image[circle1] = 1 + image[circle2] = 0 + result = skeletonize.skeletonize(image) + + # there should never be a 2x2 block of foreground pixels + # in a skeleton + mask = np.array([[1, 1], + [1, 1]], np.uint8) + blocks = correlate(result, mask, mode='constant') + self.assertFalse(numpy.any(blocks == 4)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From fb810577749a9fa67719e8fa9196bf84cc5b22d8 Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Fri, 14 Oct 2011 12:53:06 +0100 Subject: [PATCH 5/8] Added a new unit test --- scikits/image/data/bw_text.png | Bin 0 -> 8348 bytes scikits/image/data/bw_text_skeleton.npy | Bin 0 -> 171908 bytes .../image/morphology/tests/test_skeletonize.py | 14 ++++++++++++++ 3 files changed, 14 insertions(+) create mode 100644 scikits/image/data/bw_text.png create mode 100644 scikits/image/data/bw_text_skeleton.npy diff --git a/scikits/image/data/bw_text.png b/scikits/image/data/bw_text.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1a4c7c5cc29441d9187fca305cc08dc6a70819 GIT binary patch literal 8348 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 literal 0 HcmV?d00001 diff --git a/scikits/image/data/bw_text_skeleton.npy b/scikits/image/data/bw_text_skeleton.npy new file mode 100644 index 0000000000000000000000000000000000000000..9492cb647a5c5b9401c3526d4e9632ad416cba04 GIT binary patch literal 171908 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 literal 0 HcmV?d00001 diff --git a/scikits/image/morphology/tests/test_skeletonize.py b/scikits/image/morphology/tests/test_skeletonize.py index 5a6bacb9..841fcfbe 100644 --- a/scikits/image/morphology/tests/test_skeletonize.py +++ b/scikits/image/morphology/tests/test_skeletonize.py @@ -4,6 +4,9 @@ from scikits.image.morphology import skeletonize import numpy.testing from scikits.image.draw import draw from scipy.ndimage import correlate +from scikits.image.io import imread +from scikits.image import data_dir +import os.path class TestSkeletonize(unittest.TestCase): def test_skeletonize_no_foreground(self): @@ -48,6 +51,17 @@ class TestSkeletonize(unittest.TestCase): result = skeletonize.skeletonize(im) numpy.testing.assert_array_equal(result, im) + def test_skeletonize_output(self): + im = imread(os.path.join(data_dir, "bw_text.png"), as_grey=True) + + # make black the foreground + im = (im==0) + result = skeletonize.skeletonize(im) + + expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy")) + numpy.testing.assert_array_equal(result, expected) + + def test_skeletonize_num_neighbours(self): # an empty image image = np.zeros((300, 300)) From 3ddfdbdb9c1935cf99c52a94d282ce7c45d5aa28 Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Mon, 17 Oct 2011 08:32:19 +0100 Subject: [PATCH 6/8] Updates based on review comments --- CONTRIBUTORS.txt | 2 + doc/examples/plot_skeleton.py | 16 ++++- scikits/image/morphology/__init__.py | 1 + scikits/image/morphology/skeletonize.py | 61 +++++++++++-------- .../morphology/tests/test_skeletonize.py | 30 +++++---- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index aba19f3b..dbcb17d3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -78,3 +78,5 @@ - Christoph Gohlke Windows packaging and Python 3 compatibility. +- Neil Yager + Skeletonization. \ No newline at end of file diff --git a/doc/examples/plot_skeleton.py b/doc/examples/plot_skeleton.py index 68958c6d..a61bbbdd 100644 --- a/doc/examples/plot_skeleton.py +++ b/doc/examples/plot_skeleton.py @@ -3,7 +3,17 @@ 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.draw import draw @@ -32,7 +42,7 @@ image[circle1] = 1 image[circle2] = 0 # perform skeletonization -skeleton = skeletonize.skeletonize(image) +skeleton = skeletonize(image) # display results plt.figure(figsize=(10,6)) @@ -50,4 +60,4 @@ plt.title('skeleton', fontsize=20) plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.98, bottom=0.02, left=0.02, right=0.98) -plt.show() \ No newline at end of file +plt.show() diff --git a/scikits/image/morphology/__init__.py b/scikits/image/morphology/__init__.py index 03cf49e0..d57cc859 100644 --- a/scikits/image/morphology/__init__.py +++ b/scikits/image/morphology/__init__.py @@ -2,3 +2,4 @@ from grey import * from selem import * from .ccomp import label from watershed import watershed, is_local_maximum +from skeletonize import skeletonize diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index 853aba3e..dc04e7fa 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -1,7 +1,5 @@ """skeletonize.py - Use an iterative thinning algorithm to find the skeletons of binary objects in an image. - -Original author: Neil Yager """ import numpy as np @@ -11,16 +9,16 @@ from .. import util def skeletonize(image): """ 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, - 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. + 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. Parameters ---------- @@ -34,9 +32,9 @@ def skeletonize(image): ----- This implementation gives different results than a medial - axis transforrmation, which can be can be implemented using - morphological operations. This implementation is generally much - faster. + axis transformation, which can be can be implemented using + morphological operations. This implementation is generally much + faster. 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, 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, @@ -73,9 +73,8 @@ def skeletonize(image): # - binary image with only 0's and 1's if skeleton.ndim != 2: raise ValueError('Skeletonize requires a 2D array') - for val in np.unique(skeleton): - if val not in [0, 1]: - raise ValueError('Invalid value in the image: %d'%(val)) + if not np.all(np.in1d(skeleton.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 @@ -87,26 +86,36 @@ def skeletonize(image): while pixelRemoved: 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') + + # ignore background neighbours[skeleton == 0] = 0 + + # use LUT to categorize each foreground pixel as a 0, 1, 2 or 3 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 - skeleton[codes == 1] = 0 - if np.any(codes == 3): + skeleton[code_mask] = 0 + code_mask = (codes == 3) + if np.any(code_mask): pixelRemoved = True - skeleton[codes == 3] = 0 + skeleton[code_mask] = 0 # pass 2 - remove the 2's and 3's neighbours = correlate(skeleton, mask, mode='constant') neighbours[skeleton == 0] = 0 codes = np.take(lut, neighbours) - if np.any(codes == 2): + code_mask = (codes == 2) + if np.any(code_mask): pixelRemoved = True - skeleton[codes == 2] = 0 - if np.any(codes == 3): + skeleton[code_mask] = 0 + code_mask = (codes == 3) + if np.any(code_mask): pixelRemoved = True - skeleton[codes == 3] = 0 + skeleton[code_mask] = 0 return skeleton diff --git a/scikits/image/morphology/tests/test_skeletonize.py b/scikits/image/morphology/tests/test_skeletonize.py index 841fcfbe..aada8a46 100644 --- a/scikits/image/morphology/tests/test_skeletonize.py +++ b/scikits/image/morphology/tests/test_skeletonize.py @@ -1,4 +1,3 @@ -import unittest import numpy as np from scikits.image.morphology import skeletonize import numpy.testing @@ -8,39 +7,39 @@ from scikits.image.io import imread from scikits.image import data_dir import os.path -class TestSkeletonize(unittest.TestCase): +class TestSkeletonize(): def test_skeletonize_no_foreground(self): im = np.zeros((5,5)) - result = skeletonize.skeletonize(im) + result = skeletonize(im) numpy.testing.assert_array_equal(result, np.zeros((5,5))) def test_skeletonize_wrong_dim1(self): im = np.zeros((5)) - self.assertRaises(ValueError, skeletonize.skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_wrong_dim2(self): 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): im = np.zeros((5, 5)) im[0, 0] = 1 im[0, 1] = 2 - self.assertRaises(ValueError, skeletonize.skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_unexpected_value(self): im = np.zeros((5, 5)) im[0, 0] = 2 - self.assertRaises(ValueError, skeletonize.skeletonize, im) + numpy.testing.assert_raises(ValueError, skeletonize, im) def test_skeletonize_all_foreground(self): im = np.ones((3,4)) - result = skeletonize.skeletonize(im) + result = skeletonize(im) def test_skeletonize_single_point(self): im = np.zeros((5, 5), np.uint8) im[3, 3] = 1 - result = skeletonize.skeletonize(im) + result = skeletonize(im) numpy.testing.assert_array_equal(result, im) def test_skeletonize_already_thinned(self): @@ -48,7 +47,7 @@ class TestSkeletonize(unittest.TestCase): im[3,1:-1] = 1 im[2, -1] = 1 im[4, 0] = 1 - result = skeletonize.skeletonize(im) + result = skeletonize(im) numpy.testing.assert_array_equal(result, im) def test_skeletonize_output(self): @@ -56,7 +55,7 @@ class TestSkeletonize(unittest.TestCase): # make black the foreground im = (im==0) - result = skeletonize.skeletonize(im) + result = skeletonize(im) expected = np.load(os.path.join(data_dir, "bw_text_skeleton.npy")) numpy.testing.assert_array_equal(result, expected) @@ -83,15 +82,14 @@ class TestSkeletonize(unittest.TestCase): circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 image[circle1] = 1 image[circle2] = 0 - result = skeletonize.skeletonize(image) + result = skeletonize(image) - # there should never be a 2x2 block of foreground pixels - # in a skeleton + # there should never be a 2x2 block of foreground pixels in a skeleton mask = np.array([[1, 1], [1, 1]], np.uint8) blocks = correlate(result, mask, mode='constant') - self.assertFalse(numpy.any(blocks == 4)) + assert not numpy.any(blocks == 4) if __name__ == '__main__': - unittest.main() \ No newline at end of file + np.testing.run_module_suite() From c48aa63323201a197018707bd898c1daeda7d72a Mon Sep 17 00:00:00 2001 From: Neil Yager Date: Fri, 21 Oct 2011 15:27:33 +0100 Subject: [PATCH 7/8] Address 2nd round of review comments --- CONTRIBUTORS.txt | 3 +- scikits/image/morphology/skeletonize.py | 88 +++++++++++++++---------- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index dbcb17d3..0cda885d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,4 +79,5 @@ Windows packaging and Python 3 compatibility. - Neil Yager - Skeletonization. \ No newline at end of file + Skeletonization. + \ No newline at end of file diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index dc04e7fa..f424d81d 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -1,58 +1,78 @@ -"""skeletonize.py - Use an iterative thinning algorithm to find the - skeletons of binary objects in an image. +"""Use an iterative thinning algorithm to find the skeletons of binary +objects in an image. + """ import numpy as np from scipy.ndimage import correlate -from .. import util def skeletonize(image): - """ - Return a single pixel wide skeleton of all connected components - in a binary 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. - The algorithm works by making successive passes of the image, + 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. + + Returns + ------- + skeleton : ndarray + A matrix containing the thinned image. + + 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. - - Parameters - ---------- + the iterations. - image: ndarray (2D) - 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. - - Notes - ----- - - This implementation gives different results than a medial - axis transformation, which can be can be implemented using - morphological operations. This implementation is generally much - faster. - - Returns - ------- - - out: ndarray - A matrix containing the thinned image + Note that this algorithm will give different results than a + medial axis transform, which is also often referred to as + "skeletonization". References ---------- - 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 + .. [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 + 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) + """ - # 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. @@ -90,7 +110,7 @@ def skeletonize(image): neighbours = correlate(skeleton, mask, mode='constant') # ignore background - neighbours[skeleton == 0] = 0 + neighbours *= skeleton # use LUT to categorize each foreground pixel as a 0, 1, 2 or 3 codes = np.take(lut, neighbours) From c45db9f4cc5557025b44526ad9643f6281cb05c5 Mon Sep 17 00:00:00 2001 From: emmanuelle Date: Sat, 22 Oct 2011 12:10:13 +0200 Subject: [PATCH 8/8] ENH: more efficient binary operation in skeletonize --- scikits/image/morphology/skeletonize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scikits/image/morphology/skeletonize.py b/scikits/image/morphology/skeletonize.py index f424d81d..ea478a9a 100644 --- a/scikits/image/morphology/skeletonize.py +++ b/scikits/image/morphology/skeletonize.py @@ -127,7 +127,7 @@ def skeletonize(image): # pass 2 - remove the 2's and 3's neighbours = correlate(skeleton, mask, mode='constant') - neighbours[skeleton == 0] = 0 + neighbours *= skeleton codes = np.take(lut, neighbours) code_mask = (codes == 2) if np.any(code_mask):