scikit-image
322 строки · 12.1 Кб
1#cython: cdivision=True
2#cython: boundscheck=False
3#cython: nonecheck=False
4#cython: wraparound=False
5
6import numpy as np
7cimport numpy as cnp
8cnp.import_array()
9
10
11def _fast_skeletonize(cnp.uint8_t [:, ::1] image):
12"""Optimized parts of the Zhang-Suen [1]_ skeletonization.
13Iteratively, pixels meeting removal criteria are removed,
14till only the skeleton remains (that is, no further removable pixel
15was found).
16
17Performs a hard-coded correlation to assign every neighborhood of 8 a
18unique number, which in turn is used in conjunction with a look up
19table to select the appropriate thinning criteria.
20
21Parameters
22----------
23image : numpy.ndarray
24A binary image containing the objects to be skeletonized. '1'
25represents foreground, and '0' represents background.
26
27Returns
28-------
29skeleton : ndarray
30A matrix containing the thinned image.
31
32References
33----------
34.. [1] A fast parallel algorithm for thinning digital patterns,
35T. Y. Zhang and C. Y. Suen, Communications of the ACM,
36March 1984, Volume 27, Number 3.
37
38"""
39
40# look up table - there is one entry for each of the 2^8=256 possible
41# combinations of 8 binary neighbors. 1's, 2's and 3's are candidates
42# for removal at each iteration of the algorithm.
43cdef cnp.uint8_t *lut = [0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 3, 1, 1, 0,
441, 3, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0,
453, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
460, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
472, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0, 0, 0, 0,
480, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
490, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
500, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0,
513, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0,
522, 0, 0, 0, 3, 1, 0, 0, 1, 3, 0, 0, 0, 0,
530, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
540, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0,
550, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0,
560, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 1, 3,
570, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
580, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
592, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
600, 0, 3, 3, 0, 1, 0, 0, 0, 0, 2, 2, 0, 0,
612, 0, 0, 0]
62
63cdef cnp.uint8_t first_pass, pixel_removed, neighbors
64
65# indices for fast iteration
66cdef Py_ssize_t row, col, nrows, ncols, pass_num
67nrows, ncols = image.shape[:2]
68nrows += 2
69ncols += 2
70
71# Copy over the image into a larger version with a single pixel border
72# this removes the need to handle border cases below
73cdef cnp.uint8_t [:, ::1] skeleton = np.zeros((nrows, ncols),
74dtype=np.uint8)
75skeleton[1:-1, 1:-1] = image
76cdef cnp.uint8_t [:, ::1] cleaned_skeleton = skeleton.copy()
77
78pixel_removed = True
79
80# the algorithm reiterates the thinning till
81# no further thinning occurred (variable pixel_removed set)
82with nogil:
83while pixel_removed:
84pixel_removed = False
85
86# there are two phases, in the first phase, pixels labeled
87# (see below) 1 and 3 are removed, in the second 2 and 3
88
89# nogil can't iterate through `(True, False)` because it is a Python
90# tuple. Use the fact that 0 is Falsy, and 1 is truthy in C
91# for the iteration instead.
92# for first_pass in (True, False):
93for pass_num in range(2):
94first_pass = (pass_num == 0)
95for row in range(1, nrows-1):
96for col in range(1, ncols-1):
97# all set pixels ...
98
99if skeleton[row, col]:
100# are correlated with a kernel
101# (coefficients spread around here ...)
102# to apply a unique number to every
103# possible neighborhood ...
104
105# which is used with the lut to find the
106# "connectivity type"
107
108neighbors = lut[skeleton[row - 1, col - 1] +
1092 * skeleton[row - 1, col] +
1104 * skeleton[row - 1, col + 1] +
1118 * skeleton[row, col + 1] +
11216 * skeleton[row + 1, col + 1] +
11332 * skeleton[row + 1, col] +
11464 * skeleton[row + 1, col - 1] +
115128 * skeleton[row, col - 1]]
116
117if (neighbors == 0):
118continue
119elif ((neighbors == 3) or
120(neighbors == 1 and first_pass) or
121(neighbors == 2 and not first_pass)):
122# Remove the pixel
123cleaned_skeleton[row, col] = 0
124pixel_removed = True
125
126# once a step has been processed, the original skeleton
127# is overwritten with the cleaned version
128skeleton[:, :] = cleaned_skeleton[:, :]
129
130return skeleton.base[1:-1, 1:-1].astype(bool)
131
132
133def _skeletonize_loop(cnp.uint8_t[:, ::1] result,
134Py_ssize_t[::1] i, Py_ssize_t[::1] j,
135cnp.int32_t[::1] order, cnp.uint8_t[::1] table):
136"""
137Inner loop of skeletonize function
138
139Parameters
140----------
141
142result : ndarray of uint8
143On input, the image to be skeletonized, on output the skeletonized
144image.
145
146i, j : ndarrays
147The coordinates of each foreground pixel in the image
148
149order : ndarray
150The index of each pixel, in the order of processing (order[0] is
151the first pixel to process, etc.)
152
153table : ndarray
154The 512-element lookup table of values after transformation
155(whether to keep or not each configuration in a binary 3x3 array)
156
157Notes
158-----
159
160The loop determines whether each pixel in the image can be removed without
161changing the Euler number of the image. The pixels are ordered by
162increasing distance from the background which means a point nearer to
163the quench-line of the brushfire will be evaluated later than a
164point closer to the edge.
165
166Note that the neighborhood of a pixel may evolve before the loop
167arrives at this pixel. This is why it is possible to compute the
168skeleton in only one pass, thanks to an adapted ordering of the
169pixels.
170"""
171cdef:
172Py_ssize_t accumulator
173Py_ssize_t index, order_index
174Py_ssize_t ii, jj
175Py_ssize_t rows = result.shape[0]
176Py_ssize_t cols = result.shape[1]
177
178with nogil:
179for index in range(order.shape[0]):
180accumulator = 16
181order_index = order[index]
182ii = i[order_index]
183jj = j[order_index]
184# Compute the configuration around the pixel
185if ii > 0:
186if jj > 0 and result[ii - 1, jj - 1]:
187accumulator += 1
188if result[ii - 1, jj]:
189accumulator += 2
190if jj < cols - 1 and result[ii - 1, jj + 1]:
191accumulator += 4
192if jj > 0 and result[ii, jj - 1]:
193accumulator += 8
194if jj < cols - 1 and result[ii, jj + 1]:
195accumulator += 32
196if ii < rows - 1:
197if jj > 0 and result[ii + 1, jj - 1]:
198accumulator += 64
199if result[ii + 1, jj]:
200accumulator += 128
201if jj < cols - 1 and result[ii + 1, jj + 1]:
202accumulator += 256
203# Assign the value of table corresponding to the configuration
204result[ii, jj] = table[accumulator]
205
206
207
208def _table_lookup_index(cnp.uint8_t[:, ::1] image):
209"""
210Return an index into a table per pixel of a binary image
211
212Take the sum of true neighborhood pixel values where the neighborhood
213looks like this::
214
2151 2 4
2168 16 32
21764 128 256
218
219This code could be replaced by a convolution with the kernel::
220
221256 128 64
22232 16 8
2234 2 1
224
225but this runs about twice as fast because of inlining and the
226hardwired kernel.
227"""
228cdef:
229Py_ssize_t[:, ::1] indexer
230Py_ssize_t *p_indexer
231cnp.uint8_t *p_image
232Py_ssize_t i_stride
233Py_ssize_t i_shape
234Py_ssize_t j_shape
235Py_ssize_t i
236Py_ssize_t j
237Py_ssize_t offset
238
239i_shape = image.shape[0]
240j_shape = image.shape[1]
241indexer = np.zeros((i_shape, j_shape), dtype=np.intp)
242p_indexer = &indexer[0, 0]
243p_image = &image[0, 0]
244i_stride = image.strides[0]
245assert i_shape >= 3 and j_shape >= 3, \
246"Please use the slow method for arrays < 3x3"
247with nogil:
248for i in range(1, i_shape-1):
249offset = i_stride* i + 1
250for j in range(1, j_shape - 1):
251if p_image[offset]:
252p_indexer[offset + i_stride + 1] += 1
253p_indexer[offset + i_stride] += 2
254p_indexer[offset + i_stride - 1] += 4
255p_indexer[offset + 1] += 8
256p_indexer[offset] += 16
257p_indexer[offset - 1] += 32
258p_indexer[offset - i_stride + 1] += 64
259p_indexer[offset - i_stride] += 128
260p_indexer[offset - i_stride - 1] += 256
261offset += 1
262#
263# Do the corner cases (literally)
264#
265if image[0, 0]:
266indexer[0, 0] += 16
267indexer[0, 1] += 8
268indexer[1, 0] += 2
269indexer[1, 1] += 1
270
271if image[0, j_shape - 1]:
272indexer[0, j_shape - 2] += 32
273indexer[0, j_shape - 1] += 16
274indexer[1, j_shape - 2] += 4
275indexer[1, j_shape - 1] += 2
276
277if image[i_shape - 1, 0]:
278indexer[i_shape - 2, 0] += 128
279indexer[i_shape - 2, 1] += 64
280indexer[i_shape - 1, 0] += 16
281indexer[i_shape - 1, 1] += 8
282
283if image[i_shape - 1, j_shape - 1]:
284indexer[i_shape - 2, j_shape - 2] += 256
285indexer[i_shape - 2, j_shape - 1] += 128
286indexer[i_shape - 1, j_shape - 2] += 32
287indexer[i_shape - 1, j_shape - 1] += 16
288#
289# Do the edges
290#
291for j in range(1, j_shape - 1):
292if image[0, j]:
293indexer[0, j - 1] += 32
294indexer[0, j] += 16
295indexer[0, j + 1] += 8
296indexer[1, j - 1] += 4
297indexer[1, j] += 2
298indexer[1, j + 1] += 1
299if image[i_shape - 1, j]:
300indexer[i_shape - 2, j - 1] += 256
301indexer[i_shape - 2, j] += 128
302indexer[i_shape - 2, j + 1] += 64
303indexer[i_shape - 1, j - 1] += 32
304indexer[i_shape - 1, j] += 16
305indexer[i_shape - 1, j + 1] += 8
306
307for i in range(1, i_shape - 1):
308if image[i, 0]:
309indexer[i - 1, 0] += 128
310indexer[i, 0] += 16
311indexer[i + 1, 0] += 2
312indexer[i - 1, 1] += 64
313indexer[i, 1] += 8
314indexer[i + 1, 1] += 1
315if image[i, j_shape - 1]:
316indexer[i - 1, j_shape - 2] += 256
317indexer[i, j_shape - 2] += 32
318indexer[i + 1, j_shape - 2] += 4
319indexer[i - 1, j_shape - 1] += 128
320indexer[i, j_shape - 1] += 16
321indexer[i + 1, j_shape - 1] += 2
322return np.asarray(indexer)
323