Last active
April 3, 2023 02:47
-
-
Save joefutrelle/562f25bbcf20691217b8 to your computer and use it in GitHub Desktop.
bwmorph('thin') in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numpy as np | |
from scipy import ndimage as ndi | |
# lookup tables for bwmorph_thin | |
G123_LUT = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, | |
0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, | |
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, | |
1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, | |
0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 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, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, | |
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, | |
0, 0, 0], dtype=np.bool) | |
G123P_LUT = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, | |
0, 0, 0, 0, 0, 1, 1, 1, 1, 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, 1, 0, 1, 0, | |
1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 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, 1, 0, 1, 0, 0, | |
0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 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, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, | |
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, | |
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, | |
0, 0, 0], dtype=np.bool) | |
def bwmorph_thin(image, n_iter=None): | |
""" | |
Perform morphological thinning of a binary image | |
Parameters | |
---------- | |
image : binary (M, N) ndarray | |
The image to be thinned. | |
n_iter : int, number of iterations, optional | |
Regardless of the value of this parameter, the thinned image | |
is returned immediately if an iteration produces no change. | |
If this parameter is specified it thus sets an upper bound on | |
the number of iterations performed. | |
Returns | |
------- | |
out : ndarray of bools | |
Thinned image. | |
See also | |
-------- | |
skeletonize | |
Notes | |
----- | |
This algorithm [1]_ works by making multiple passes over the image, | |
removing pixels matching a set of criteria designed to thin | |
connected regions while preserving eight-connected components and | |
2 x 2 squares [2]_. In each of the two sub-iterations the algorithm | |
correlates the intermediate skeleton image with a neighborhood mask, | |
then looks up each neighborhood in a lookup table indicating whether | |
the central pixel should be deleted in that sub-iteration. | |
References | |
---------- | |
.. [1] Z. Guo and R. W. Hall, "Parallel thinning with | |
two-subiteration algorithms," Comm. ACM, vol. 32, no. 3, | |
pp. 359-373, 1989. | |
.. [2] Lam, L., Seong-Whan Lee, and Ching Y. Suen, "Thinning | |
Methodologies-A Comprehensive Survey," IEEE Transactions on | |
Pattern Analysis and Machine Intelligence, Vol 14, No. 9, | |
September 1992, p. 879 | |
Examples | |
-------- | |
>>> square = np.zeros((7, 7), dtype=np.uint8) | |
>>> square[1:-1, 2:-2] = 1 | |
>>> square[0,1] = 1 | |
>>> square | |
array([[0, 1, 0, 0, 0, 0, 0], | |
[0, 0, 1, 1, 1, 0, 0], | |
[0, 0, 1, 1, 1, 0, 0], | |
[0, 0, 1, 1, 1, 0, 0], | |
[0, 0, 1, 1, 1, 0, 0], | |
[0, 0, 1, 1, 1, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8) | |
>>> skel = bwmorph_thin(square) | |
>>> skel.astype(np.uint8) | |
array([[0, 1, 0, 0, 0, 0, 0], | |
[0, 0, 1, 0, 0, 0, 0], | |
[0, 0, 0, 1, 0, 0, 0], | |
[0, 0, 0, 1, 0, 0, 0], | |
[0, 0, 0, 1, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8) | |
""" | |
# check parameters | |
if n_iter is None: | |
n = -1 | |
elif n_iter <= 0: | |
raise ValueError('n_iter must be > 0') | |
else: | |
n = n_iter | |
# check that we have a 2d binary image, and convert it | |
# to uint8 | |
skel = np.array(image).astype(np.uint8) | |
if skel.ndim != 2: | |
raise ValueError('2D array required') | |
if not np.all(np.in1d(image.flat,(0,1))): | |
raise ValueError('Image contains values other than 0 and 1') | |
# neighborhood mask | |
mask = np.array([[ 8, 4, 2], | |
[16, 0, 1], | |
[32, 64,128]],dtype=np.uint8) | |
# iterate either 1) indefinitely or 2) up to iteration limit | |
while n != 0: | |
before = np.sum(skel) # count points before thinning | |
# for each subiteration | |
for lut in [G123_LUT, G123P_LUT]: | |
# correlate image with neighborhood mask | |
N = ndi.correlate(skel, mask, mode='constant') | |
# take deletion decision from this subiteration's LUT | |
D = np.take(lut, N) | |
# perform deletion | |
skel[D] = 0 | |
after = np.sum(skel) # coint points after thinning | |
if before == after: | |
# iteration had no effect: finish | |
break | |
# count down to iteration limit (or endlessly negative) | |
n -= 1 | |
return skel.astype(np.bool) | |
""" | |
# here's how to make the LUTs | |
def nabe(n): | |
return np.array([n>>i&1 for i in range(0,9)]).astype(np.bool) | |
def hood(n): | |
return np.take(nabe(n), np.array([[3, 2, 1], | |
[4, 8, 0], | |
[5, 6, 7]])) | |
def G1(n): | |
s = 0 | |
bits = nabe(n) | |
for i in (0,2,4,6): | |
if not(bits[i]) and (bits[i+1] or bits[(i+2) % 8]): | |
s += 1 | |
return s==1 | |
g1_lut = np.array([G1(n) for n in range(256)]) | |
def G2(n): | |
n1, n2 = 0, 0 | |
bits = nabe(n) | |
for k in (1,3,5,7): | |
if bits[k] or bits[k-1]: | |
n1 += 1 | |
if bits[k] or bits[(k+1) % 8]: | |
n2 += 1 | |
return min(n1,n2) in [2,3] | |
g2_lut = np.array([G2(n) for n in range(256)]) | |
g12_lut = g1_lut & g2_lut | |
def G3(n): | |
bits = nabe(n) | |
return not((bits[1] or bits[2] or not(bits[7])) and bits[0]) | |
def G3p(n): | |
bits = nabe(n) | |
return not((bits[5] or bits[6] or not(bits[3])) and bits[4]) | |
g3_lut = np.array([G3(n) for n in range(256)]) | |
g3p_lut = np.array([G3p(n) for n in range(256)]) | |
g123_lut = g12_lut & g3_lut | |
g123p_lut = g12_lut & g3p_lut | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment