forked from scikit-image/scikit-image
-
Notifications
You must be signed in to change notification settings - Fork 0
/
_util.py
290 lines (247 loc) · 9.94 KB
/
_util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""Utility functions used in the morphology subpackage."""
import numpy as np
from scipy import ndimage as ndi
def _validate_connectivity(image_dim, connectivity, offset):
"""Convert any valid connectivity to a footprint and offset.
Parameters
----------
image_dim : int
The number of dimensions of the input image.
connectivity : int, array, or None
The neighborhood connectivity. An integer is interpreted as in
``scipy.ndimage.generate_binary_structure``, as the maximum number
of orthogonal steps to reach a neighbor. An array is directly
interpreted as a footprint and its shape is validated against
the input image shape. ``None`` is interpreted as a connectivity of 1.
offset : tuple of int, or None
The coordinates of the center of the footprint.
Returns
-------
c_connectivity : array of bool
The footprint (structuring element) corresponding to the input
`connectivity`.
offset : array of int
The offset corresponding to the center of the footprint.
Raises
------
ValueError:
If the image dimension and the connectivity or offset dimensions don't
match.
"""
if connectivity is None:
connectivity = 1
if np.isscalar(connectivity):
c_connectivity = ndi.generate_binary_structure(image_dim, connectivity)
else:
c_connectivity = np.array(connectivity, bool)
if c_connectivity.ndim != image_dim:
raise ValueError("Connectivity dimension must be same as image")
if offset is None:
if any([x % 2 == 0 for x in c_connectivity.shape]):
raise ValueError("Connectivity array must have an unambiguous "
"center")
offset = np.array(c_connectivity.shape) // 2
return c_connectivity, offset
def _raveled_offsets_and_distances(
image_shape,
*,
footprint=None,
connectivity=1,
center=None,
spacing=None,
order='C',
):
"""Compute offsets to neighboring pixels in raveled coordinate space.
This function also returns the corresponding distances from the center
pixel given a spacing (assumed to be 1 along each axis by default).
Parameters
----------
image_shape : tuple of int
The shape of the image for which the offsets are being computed.
footprint : array of bool
The footprint of the neighborhood, expressed as an n-dimensional array
of 1s and 0s. If provided, the connectivity argument is ignored.
connectivity : {1, ..., ndim}
The square connectivity of the neighborhood: the number of orthogonal
steps allowed to consider a pixel a neighbor. See
`scipy.ndimage.generate_binary_structure`. Ignored if footprint is
provided.
center : tuple of int
Tuple of indices to the center of the footprint. If not provided, it
is assumed to be the center of the footprint, either provided or
generated by the connectivity argument.
spacing : tuple of float
The spacing between pixels/voxels along each axis.
order : 'C' or 'F'
The ordering of the array, either C or Fortran ordering.
Returns
-------
raveled_offsets : ndarray
Linear offsets to a samples neighbors in the raveled image, sorted by
their distance from the center.
distances : ndarray
The pixel distances correspoding to each offset.
Notes
-----
This function will return values even if `image_shape` contains a dimension
length that is smaller than `footprint`.
Examples
--------
>>> off, d = _raveled_offsets_and_distances(
... (4, 5), footprint=np.ones((4, 3)), center=(1, 1)
... )
>>> off
array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11])
>>> d[0]
1.0
>>> d[-1] # distance from (1, 1) to (3, 2)
2.236...
"""
ndim = len(image_shape)
if footprint is None:
footprint = ndi.generate_binary_structure(
rank=ndim, connectivity=connectivity
)
if center is None:
center = np.array(footprint.shape) // 2
if not footprint.ndim == ndim == len(center):
raise ValueError(
"number of dimensions in image shape, footprint and its"
"center index does not match"
)
footprint_indices = np.stack(np.nonzero(footprint), axis=-1)
offsets = footprint_indices - center
if order == 'F':
offsets = offsets[:, ::-1]
image_shape = image_shape[::-1]
elif order != 'C':
raise ValueError("order must be 'C' or 'F'")
# Scale offsets in each dimension and sum
ravel_factors = image_shape[1:] + (1,)
ravel_factors = np.cumprod(ravel_factors[::-1])[::-1]
raveled_offsets = (offsets * ravel_factors).sum(axis=1)
# Sort by distance
if spacing is None:
spacing = np.ones(ndim)
weighted_offsets = offsets * spacing
distances = np.sqrt(np.sum(weighted_offsets**2, axis=1))
sorted_raveled_offsets = raveled_offsets[np.argsort(distances)]
sorted_distances = np.sort(distances)
# If any dimension in image_shape is smaller than footprint.shape
# duplicates might occur, remove them
if any(x < y for x, y in zip(image_shape, footprint.shape)):
# np.unique reorders, which we don't want
_, indices = np.unique(sorted_raveled_offsets, return_index=True)
sorted_raveled_offsets = sorted_raveled_offsets[np.sort(indices)]
sorted_distances = sorted_distances[np.sort(indices)]
# Remove "offset to center"
sorted_raveled_offsets = sorted_raveled_offsets[1:]
sorted_distances = sorted_distances[1:]
return sorted_raveled_offsets, sorted_distances
def _offsets_to_raveled_neighbors(image_shape, footprint, center, order='C'):
"""Compute offsets to a samples neighbors if the image would be raveled.
Parameters
----------
image_shape : tuple
The shape of the image for which the offsets are computed.
footprint : ndarray
The footprint (structuring element) determining the neighborhood
expressed as an n-D array of 1's and 0's.
center : tuple
Tuple of indices to the center of `footprint`.
order : {"C", "F"}, optional
Whether the image described by `image_shape` is in row-major (C-style)
or column-major (Fortran-style) order.
Returns
-------
raveled_offsets : ndarray
Linear offsets to a samples neighbors in the raveled image, sorted by
their distance from the center.
Notes
-----
This function will return values even if `image_shape` contains a dimension
length that is smaller than `footprint`.
Examples
--------
>>> _offsets_to_raveled_neighbors((4, 5), np.ones((4, 3)), (1, 1))
array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11])
>>> _offsets_to_raveled_neighbors((2, 3, 2), np.ones((3, 3, 3)), (1, 1, 1))
array([ 2, -6, 1, -1, 6, -2, 3, 8, -3, -4, 7, -5, -7, -8, 5, 4, -9,
9])
"""
raveled_offsets = _raveled_offsets_and_distances(
image_shape, footprint=footprint, center=center, order=order
)[0]
return raveled_offsets
def _resolve_neighborhood(footprint, connectivity, ndim):
"""Validate or create a footprint (structuring element).
Depending on the values of `connectivity` and `footprint` this function
either creates a new footprint (`footprint` is None) using `connectivity`
or validates the given footprint (`footprint` is not None).
Parameters
----------
footprint : ndarray
The footprint (structuring) element used to determine the neighborhood
of each evaluated pixel (``True`` denotes a connected pixel). It must
be a boolean array and have the same number of dimensions as `image`.
If neither `footprint` nor `connectivity` are given, all adjacent
pixels are considered as part of the neighborhood.
connectivity : int
A number used to determine the neighborhood of each evaluated pixel.
Adjacent pixels whose squared distance from the center is less than or
equal to `connectivity` are considered neighbors. Ignored if
`footprint` is not None.
ndim : int
Number of dimensions `footprint` ought to have.
Returns
-------
footprint : ndarray
Validated or new footprint specifying the neighborhood.
Examples
--------
>>> _resolve_neighborhood(None, 1, 2)
array([[False, True, False],
[ True, True, True],
[False, True, False]])
>>> _resolve_neighborhood(None, None, 3).shape
(3, 3, 3)
"""
if footprint is None:
if connectivity is None:
connectivity = ndim
footprint = ndi.generate_binary_structure(ndim, connectivity)
else:
# Validate custom structured element
footprint = np.asarray(footprint, dtype=bool)
# Must specify neighbors for all dimensions
if footprint.ndim != ndim:
raise ValueError(
"number of dimensions in image and footprint do not"
"match"
)
# Must only specify direct neighbors
if any(s != 3 for s in footprint.shape):
raise ValueError("dimension size in footprint is not 3")
return footprint
def _set_border_values(image, value):
"""Set edge values along all axes to a constant value.
Parameters
----------
image : ndarray
The array to modify inplace.
value : scalar
The value to use. Should be compatible with `image`'s dtype.
Examples
--------
>>> image = np.zeros((4, 5), dtype=int)
>>> _set_border_values(image, 1)
>>> image
array([[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]])
"""
for axis in range(image.ndim):
# Index first and last element in each dimension
sl = (slice(None),) * axis + ((0, -1),) + (...,)
image[sl] = value