fastremap 1.16.0__cp310-cp310-win32.whl → 1.17.0__cp310-cp310-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastremap/__init__.py +47 -0
- fastremap/fastremap.cp310-win32.pyd +0 -0
- fastremap/fastremap.pxd +100 -0
- fastremap/fastremap.pyi +493 -0
- fastremap/fastremap.pyx +1630 -0
- fastremap/ipt.hpp +354 -0
- fastremap/py.typed +0 -0
- fastremap/ska_flat_hash_map.hpp +1572 -0
- {fastremap-1.16.0.dist-info → fastremap-1.17.0.dist-info}/METADATA +4 -4
- fastremap-1.17.0.dist-info/RECORD +15 -0
- {fastremap-1.16.0.dist-info → fastremap-1.17.0.dist-info}/WHEEL +1 -1
- fastremap-1.17.0.dist-info/licenses/AUTHORS +1 -0
- fastremap-1.17.0.dist-info/pbr.json +1 -0
- fastremap-1.16.0.dist-info/RECORD +0 -8
- fastremap-1.16.0.dist-info/licenses/AUTHORS +0 -1
- fastremap-1.16.0.dist-info/pbr.json +0 -1
- fastremap.cp310-win32.pyd +0 -0
- {fastremap-1.16.0.dist-info → fastremap-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {fastremap-1.16.0.dist-info → fastremap-1.17.0.dist-info}/top_level.txt +0 -0
fastremap/fastremap.pyx
ADDED
@@ -0,0 +1,1630 @@
|
|
1
|
+
# cython: language_level=3
|
2
|
+
"""
|
3
|
+
Functions related to remapping image volumes.
|
4
|
+
|
5
|
+
Renumber volumes into smaller data types, mask out labels
|
6
|
+
or their complement, and remap the values of image volumes.
|
7
|
+
|
8
|
+
This module also constains the facilities for performing
|
9
|
+
and in-place matrix transposition for up to 4D arrays. This is
|
10
|
+
helpful for converting between C and Fortran order in memory
|
11
|
+
constrained environments when format shifting.
|
12
|
+
|
13
|
+
Author: William Silversmith
|
14
|
+
Affiliation: Seung Lab, Princeton Neuroscience Institute
|
15
|
+
Date: August 2018 - May 2025
|
16
|
+
"""
|
17
|
+
from typing import Sequence, List
|
18
|
+
cimport cython
|
19
|
+
from libc.stdint cimport (
|
20
|
+
uint8_t, uint16_t, uint32_t, uint64_t,
|
21
|
+
int8_t, int16_t, int32_t, int64_t,
|
22
|
+
uintptr_t
|
23
|
+
)
|
24
|
+
cimport fastremap
|
25
|
+
|
26
|
+
from collections import defaultdict
|
27
|
+
from functools import reduce
|
28
|
+
import operator
|
29
|
+
|
30
|
+
import numpy as np
|
31
|
+
cimport numpy as cnp
|
32
|
+
cnp.import_array()
|
33
|
+
|
34
|
+
from libcpp.vector cimport vector
|
35
|
+
|
36
|
+
ctypedef fused UINT:
|
37
|
+
uint8_t
|
38
|
+
uint16_t
|
39
|
+
uint32_t
|
40
|
+
uint64_t
|
41
|
+
|
42
|
+
ctypedef fused ALLINT:
|
43
|
+
UINT
|
44
|
+
int8_t
|
45
|
+
int16_t
|
46
|
+
int32_t
|
47
|
+
int64_t
|
48
|
+
|
49
|
+
ctypedef fused ALLINT_2:
|
50
|
+
ALLINT
|
51
|
+
|
52
|
+
ctypedef fused NUMBER:
|
53
|
+
ALLINT
|
54
|
+
float
|
55
|
+
double
|
56
|
+
|
57
|
+
ctypedef fused COMPLEX_NUMBER:
|
58
|
+
NUMBER
|
59
|
+
float complex
|
60
|
+
|
61
|
+
cdef extern from "ipt.hpp" namespace "pyipt":
|
62
|
+
cdef void _ipt2d[T](T* arr, size_t sx, size_t sy)
|
63
|
+
cdef void _ipt3d[T](
|
64
|
+
T* arr, size_t sx, size_t sy, size_t sz
|
65
|
+
)
|
66
|
+
cdef void _ipt4d[T](
|
67
|
+
T* arr, size_t sx, size_t sy, size_t sz, size_t sw
|
68
|
+
)
|
69
|
+
|
70
|
+
def minmax(arr):
|
71
|
+
"""
|
72
|
+
Returns (min(arr), max(arr)) computed in a single pass.
|
73
|
+
Returns (None, None) if array is size zero.
|
74
|
+
"""
|
75
|
+
return _minmax(_reshape(arr, (arr.size,)))
|
76
|
+
|
77
|
+
def _minmax(cnp.ndarray[NUMBER, ndim=1] arr):
|
78
|
+
cdef size_t i = 0
|
79
|
+
cdef size_t size = arr.size
|
80
|
+
|
81
|
+
if size == 0:
|
82
|
+
return None, None
|
83
|
+
|
84
|
+
cdef NUMBER minval = arr[0]
|
85
|
+
cdef NUMBER maxval = arr[0]
|
86
|
+
|
87
|
+
for i in range(1, size):
|
88
|
+
if minval > arr[i]:
|
89
|
+
minval = arr[i]
|
90
|
+
if maxval < arr[i]:
|
91
|
+
maxval = arr[i]
|
92
|
+
|
93
|
+
return minval, maxval
|
94
|
+
|
95
|
+
def _match_array_orders(*arrs, order="K"):
|
96
|
+
if len(arrs) == 0:
|
97
|
+
return []
|
98
|
+
|
99
|
+
if order == "C" or (order == "K" and arrs[0].flags.c_contiguous):
|
100
|
+
return [ np.ascontiguousarray(arr) for arr in arrs ]
|
101
|
+
else:
|
102
|
+
return [ np.asfortranarray(arr) for arr in arrs ]
|
103
|
+
|
104
|
+
@cython.boundscheck(False)
|
105
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
106
|
+
@cython.nonecheck(False)
|
107
|
+
def indices(cnp.ndarray[NUMBER, cast=True, ndim=1] arr, NUMBER value):
|
108
|
+
"""
|
109
|
+
Search through an array and identify the indices where value matches the array.
|
110
|
+
"""
|
111
|
+
cdef vector[uint64_t] all_indices
|
112
|
+
cdef uint64_t i = 0
|
113
|
+
cdef uint64_t size = arr.size
|
114
|
+
|
115
|
+
for i in range(size):
|
116
|
+
if arr[i] == value:
|
117
|
+
all_indices.push_back(i)
|
118
|
+
|
119
|
+
return np.asarray(all_indices, dtype=np.uint64)
|
120
|
+
|
121
|
+
def renumber(arr, start=1, preserve_zero=True, in_place=False):
|
122
|
+
"""
|
123
|
+
renumber(arr, start=1, preserve_zero=True, in_place=False)
|
124
|
+
|
125
|
+
Given an array of integers, renumber all the unique values starting
|
126
|
+
from 1. This can allow us to reduce the size of the data width required
|
127
|
+
to represent it.
|
128
|
+
|
129
|
+
arr: A numpy array
|
130
|
+
start (default: 1): Start renumbering from this value
|
131
|
+
preserve_zero (default: True): Don't renumber zero.
|
132
|
+
in_place (default: False): Perform the renumbering in-place to avoid
|
133
|
+
an extra copy. This option depends on a fortran or C contiguous
|
134
|
+
array. A copy will be made if the array is not contiguous.
|
135
|
+
|
136
|
+
Return: a renumbered array, dict with remapping of oldval => newval
|
137
|
+
"""
|
138
|
+
arr = np.asarray(arr)
|
139
|
+
|
140
|
+
if arr.size == 0:
|
141
|
+
return arr, {}
|
142
|
+
|
143
|
+
if arr.dtype == bool and preserve_zero:
|
144
|
+
return arr, { 0: 0, 1: start }
|
145
|
+
elif arr.dtype == bool:
|
146
|
+
arr = arr.view(np.uint8)
|
147
|
+
|
148
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
149
|
+
|
150
|
+
shape = arr.shape
|
151
|
+
order = 'F' if arr.flags['F_CONTIGUOUS'] else 'C'
|
152
|
+
in_place = in_place and (arr.flags['F_CONTIGUOUS'] or arr.flags['C_CONTIGUOUS'])
|
153
|
+
|
154
|
+
if not in_place:
|
155
|
+
arr = np.copy(arr, order=order)
|
156
|
+
|
157
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=(arr.size,), strides=(nbytes,))
|
158
|
+
arr, remap_dict = _renumber(arr, <int64_t>start, preserve_zero)
|
159
|
+
arr = _reshape(arr, shape, order)
|
160
|
+
|
161
|
+
return arr, remap_dict
|
162
|
+
|
163
|
+
def _reshape(arr, shape, order=None):
|
164
|
+
"""
|
165
|
+
If the array is contiguous, attempt an in place reshape
|
166
|
+
rather than potentially making a copy.
|
167
|
+
|
168
|
+
Required:
|
169
|
+
arr: The input numpy array.
|
170
|
+
shape: The desired shape (must be the same size as arr)
|
171
|
+
|
172
|
+
Optional:
|
173
|
+
order: 'C', 'F', or None (determine automatically)
|
174
|
+
|
175
|
+
Returns: reshaped array
|
176
|
+
"""
|
177
|
+
if order is None:
|
178
|
+
if arr.flags['F_CONTIGUOUS']:
|
179
|
+
order = 'F'
|
180
|
+
elif arr.flags['C_CONTIGUOUS']:
|
181
|
+
order = 'C'
|
182
|
+
else:
|
183
|
+
return arr.reshape(shape)
|
184
|
+
|
185
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
186
|
+
|
187
|
+
if order == 'C':
|
188
|
+
strides = [ reduce(operator.mul, shape[i:]) * nbytes for i in range(1, len(shape)) ]
|
189
|
+
strides += [ nbytes ]
|
190
|
+
return np.lib.stride_tricks.as_strided(arr, shape=shape, strides=strides)
|
191
|
+
else:
|
192
|
+
strides = [ reduce(operator.mul, shape[:i]) * nbytes for i in range(1, len(shape)) ]
|
193
|
+
strides = [ nbytes ] + strides
|
194
|
+
return np.lib.stride_tricks.as_strided(arr, shape=shape, strides=strides)
|
195
|
+
|
196
|
+
@cython.boundscheck(False)
|
197
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
198
|
+
@cython.nonecheck(False)
|
199
|
+
def _renumber(cnp.ndarray[NUMBER, cast=True, ndim=1] arr, int64_t start=1, preserve_zero=True):
|
200
|
+
"""
|
201
|
+
renumber(arr, int64_t start=1, preserve_zero=True)
|
202
|
+
|
203
|
+
Given an array of integers, renumber all the unique values starting
|
204
|
+
from 1. This can allow us to reduce the size of the data width required
|
205
|
+
to represent it.
|
206
|
+
|
207
|
+
arr: A numpy array
|
208
|
+
start (default: 1): Start renumbering from this value
|
209
|
+
preserve_zero (default ):
|
210
|
+
|
211
|
+
Return: a renumbered array, dict with remapping of oldval => newval
|
212
|
+
"""
|
213
|
+
cdef flat_hash_map[NUMBER, NUMBER] remap_dict
|
214
|
+
|
215
|
+
if arr.size == 0:
|
216
|
+
return refit(np.zeros((0,), dtype=arr.dtype), 0), {}
|
217
|
+
|
218
|
+
remap_dict.reserve(1024)
|
219
|
+
|
220
|
+
if preserve_zero:
|
221
|
+
remap_dict[0] = 0
|
222
|
+
|
223
|
+
cdef NUMBER[:] arrview = arr
|
224
|
+
|
225
|
+
cdef NUMBER remap_id = start
|
226
|
+
cdef NUMBER elem
|
227
|
+
|
228
|
+
# some value that isn't the first value
|
229
|
+
# and won't cause an overflow
|
230
|
+
cdef NUMBER last_elem = <NUMBER>(~<uint64_t>arr[0])
|
231
|
+
cdef NUMBER last_remap_id = start
|
232
|
+
|
233
|
+
cdef size_t size = arr.size
|
234
|
+
cdef size_t i = 0
|
235
|
+
|
236
|
+
for i in range(size):
|
237
|
+
elem = arrview[i]
|
238
|
+
|
239
|
+
if elem == last_elem:
|
240
|
+
arrview[i] = last_remap_id
|
241
|
+
continue
|
242
|
+
|
243
|
+
if remap_dict.find(elem) == remap_dict.end():
|
244
|
+
arrview[i] = remap_id
|
245
|
+
remap_dict[elem] = remap_id
|
246
|
+
remap_id += 1
|
247
|
+
else:
|
248
|
+
arrview[i] = remap_dict[elem]
|
249
|
+
|
250
|
+
last_elem = elem
|
251
|
+
last_remap_id = arrview[i]
|
252
|
+
|
253
|
+
factor = remap_id
|
254
|
+
if abs(start) > abs(factor):
|
255
|
+
factor = start
|
256
|
+
|
257
|
+
return refit(arr, factor), { k:v for k,v in remap_dict }
|
258
|
+
|
259
|
+
def refit(arr, value=None, increase_only=False, exotics=False):
|
260
|
+
"""
|
261
|
+
Resize the array to the smallest dtype of the
|
262
|
+
same kind that will fit a given value.
|
263
|
+
|
264
|
+
For example, if the input array is uint8 and
|
265
|
+
the value is 2^20 return the array as a
|
266
|
+
uint32.
|
267
|
+
|
268
|
+
Works for standard floating, integer,
|
269
|
+
unsigned integer, and complex types.
|
270
|
+
|
271
|
+
arr: numpy array
|
272
|
+
value: value to fit array to. if None,
|
273
|
+
it is set to the value of the absolutely
|
274
|
+
larger of the min and max value in the array.
|
275
|
+
increase_only: if true, only resize the array if it can't
|
276
|
+
contain value. if false, always resize to the
|
277
|
+
smallest size that fits.
|
278
|
+
exotics: if true, allow e.g. half precision floats (16-bit)
|
279
|
+
or double complex (128-bit)
|
280
|
+
|
281
|
+
Return: refitted array
|
282
|
+
"""
|
283
|
+
|
284
|
+
if value is None:
|
285
|
+
min_value, max_value = minmax(arr)
|
286
|
+
if min_value is None or max_value is None:
|
287
|
+
min_value = 0
|
288
|
+
max_value = 0
|
289
|
+
|
290
|
+
if abs(max_value) > abs(min_value):
|
291
|
+
value = max_value
|
292
|
+
else:
|
293
|
+
value = min_value
|
294
|
+
|
295
|
+
dtype = _fit_dtype(arr.dtype, value, exotics=exotics)
|
296
|
+
|
297
|
+
if increase_only and np.dtype(dtype).itemsize <= np.dtype(arr.dtype).itemsize:
|
298
|
+
return arr
|
299
|
+
elif dtype == arr.dtype:
|
300
|
+
return arr
|
301
|
+
return arr.astype(dtype)
|
302
|
+
|
303
|
+
def _fit_dtype(dtype, value, exotics=False):
|
304
|
+
"""
|
305
|
+
Find the smallest dtype of the
|
306
|
+
same kind that will fit a given value.
|
307
|
+
|
308
|
+
For example, if the input array is uint8 and
|
309
|
+
the value is 2^20 return the array as a
|
310
|
+
uint32.
|
311
|
+
|
312
|
+
Works for standard floating, integer,
|
313
|
+
unsigned integer, and complex types.
|
314
|
+
|
315
|
+
exotics: if True, allow fitting to
|
316
|
+
e.g. float16 (half-precision, 16-bits)
|
317
|
+
or double complex (which takes 128-bits).
|
318
|
+
|
319
|
+
Return: refitted dtype
|
320
|
+
"""
|
321
|
+
dtype = np.dtype(dtype)
|
322
|
+
if np.issubdtype(dtype, np.floating):
|
323
|
+
if exotics:
|
324
|
+
sequence = [ np.float16, np.float32, np.float64 ]
|
325
|
+
else:
|
326
|
+
sequence = [ np.float32, np.float64 ]
|
327
|
+
infofn = np.finfo
|
328
|
+
elif np.issubdtype(dtype, np.unsignedinteger):
|
329
|
+
sequence = [ np.uint8, np.uint16, np.uint32, np.uint64 ]
|
330
|
+
infofn = np.iinfo
|
331
|
+
if value < 0:
|
332
|
+
raise ValueError(str(value) + " is negative but unsigned data type {} is selected.".format(dtype))
|
333
|
+
elif np.issubdtype(dtype, np.complexfloating):
|
334
|
+
if exotics:
|
335
|
+
sequence = [ np.csingle, np.cdouble ]
|
336
|
+
else:
|
337
|
+
sequence = [ np.csingle ]
|
338
|
+
infofn = np.finfo
|
339
|
+
elif np.issubdtype(dtype, np.integer):
|
340
|
+
sequence = [ np.int8, np.int16, np.int32, np.int64 ]
|
341
|
+
infofn = np.iinfo
|
342
|
+
else:
|
343
|
+
raise ValueError(
|
344
|
+
"Unsupported dtype: {} Only standard floats, integers, and complex types are supported.".format(dtype)
|
345
|
+
)
|
346
|
+
|
347
|
+
test_value = np.real(value)
|
348
|
+
if abs(np.real(value)) < abs(np.imag(value)):
|
349
|
+
test_value = np.imag(value)
|
350
|
+
|
351
|
+
for seq_dtype in sequence:
|
352
|
+
if test_value >= 0 and infofn(seq_dtype).max >= test_value:
|
353
|
+
return seq_dtype
|
354
|
+
elif test_value < 0 and infofn(seq_dtype).min <= test_value:
|
355
|
+
return seq_dtype
|
356
|
+
|
357
|
+
raise ValueError("Unable to find a compatible dtype for {} that can fit {}".format(
|
358
|
+
dtype, value
|
359
|
+
))
|
360
|
+
|
361
|
+
def widen_dtype(dtype, exotics:bool = False):
|
362
|
+
"""
|
363
|
+
Widen the given dtype to the next size
|
364
|
+
of the same type. For example,
|
365
|
+
int8 -> int16 or uint32 -> uint64
|
366
|
+
|
367
|
+
64-bit types will map to themselves.
|
368
|
+
|
369
|
+
Return: upgraded dtype
|
370
|
+
"""
|
371
|
+
dtype = np.dtype(dtype)
|
372
|
+
|
373
|
+
if np.issubdtype(dtype, np.floating):
|
374
|
+
sequence = [ np.float16, np.float32, np.float64 ]
|
375
|
+
if exotics:
|
376
|
+
sequence += [ np.longdouble ]
|
377
|
+
elif np.issubdtype(dtype, np.unsignedinteger):
|
378
|
+
sequence = [ np.uint8, np.uint16, np.uint32, np.uint64 ]
|
379
|
+
elif np.issubdtype(dtype, np.complexfloating):
|
380
|
+
sequence = [ np.complex64 ]
|
381
|
+
if exotics:
|
382
|
+
sequence += [ np.complex128, np.clongdouble ]
|
383
|
+
elif np.issubdtype(dtype, np.integer):
|
384
|
+
sequence = [ np.int8, np.int16, np.int32, np.int64 ]
|
385
|
+
elif np.issubdtype(dtype, (np.intp, np.uintp)):
|
386
|
+
return dtype
|
387
|
+
elif exotics:
|
388
|
+
raise ValueError(
|
389
|
+
f"Unsupported dtype: {dtype}\n"
|
390
|
+
)
|
391
|
+
else:
|
392
|
+
raise ValueError(
|
393
|
+
f"Unsupported dtype: {dtype}\n"
|
394
|
+
f"Only standard floats, integers, and complex types are supported."
|
395
|
+
f"For additional types (e.g. long double, complex128, clongdouble), enable exotics."
|
396
|
+
)
|
397
|
+
|
398
|
+
idx = sequence.index(dtype)
|
399
|
+
return sequence[min(idx+1, len(sequence) - 1)]
|
400
|
+
|
401
|
+
def narrow_dtype(dtype, exotics:bool = False):
|
402
|
+
"""
|
403
|
+
Widen the given dtype to the next size
|
404
|
+
of the same type. For example,
|
405
|
+
int16 -> int8 or uint64 -> uint32
|
406
|
+
|
407
|
+
8-bit types will map to themselves.
|
408
|
+
|
409
|
+
exotics: include float16
|
410
|
+
|
411
|
+
Return: upgraded dtype
|
412
|
+
"""
|
413
|
+
dtype = np.dtype(dtype)
|
414
|
+
if dtype.itemsize == 1:
|
415
|
+
return dtype
|
416
|
+
|
417
|
+
if np.issubdtype(dtype, np.floating):
|
418
|
+
sequence = [ np.float32, np.float64, np.longdouble ]
|
419
|
+
if exotics:
|
420
|
+
sequence = [ np.float16 ] + sequence
|
421
|
+
elif np.issubdtype(dtype, np.unsignedinteger):
|
422
|
+
sequence = [ np.uint8, np.uint16, np.uint32, np.uint64 ]
|
423
|
+
elif np.issubdtype(dtype, np.complexfloating):
|
424
|
+
sequence = [ np.complex64, np.complex128, np.clongdouble ]
|
425
|
+
elif np.issubdtype(dtype, np.integer):
|
426
|
+
sequence = [ np.int8, np.int16, np.int32, np.int64 ]
|
427
|
+
elif np.issubdtype(dtype, (np.intp, np.uintp)):
|
428
|
+
return dtype
|
429
|
+
else:
|
430
|
+
raise ValueError(
|
431
|
+
f"Unsupported dtype: {dtype}\n"
|
432
|
+
f"Only standard floats, integers, and complex types are supported."
|
433
|
+
)
|
434
|
+
|
435
|
+
idx = sequence.index(dtype)
|
436
|
+
return sequence[max(idx-1, 0)]
|
437
|
+
|
438
|
+
def mask(arr, labels, in_place=False, value=0):
|
439
|
+
"""
|
440
|
+
mask(arr, labels, in_place=False, value=0)
|
441
|
+
|
442
|
+
Mask out designated labels in an array with the
|
443
|
+
given value.
|
444
|
+
|
445
|
+
Alternative implementation of:
|
446
|
+
|
447
|
+
arr[np.isin(labels)] = value
|
448
|
+
|
449
|
+
arr: an N-dimensional numpy array
|
450
|
+
labels: an iterable list of integers
|
451
|
+
in_place: if True, modify the input array to reduce
|
452
|
+
memory consumption.
|
453
|
+
value: mask value
|
454
|
+
|
455
|
+
Returns: arr with `labels` masked out
|
456
|
+
"""
|
457
|
+
labels = { lbl: value for lbl in labels }
|
458
|
+
return remap(arr, labels, preserve_missing_labels=True, in_place=in_place)
|
459
|
+
|
460
|
+
def mask_except(arr, labels, in_place=False, value=0):
|
461
|
+
"""
|
462
|
+
mask_except(arr, labels, in_place=False, value=0)
|
463
|
+
|
464
|
+
Mask out all labels except the provided list.
|
465
|
+
|
466
|
+
Alternative implementation of:
|
467
|
+
|
468
|
+
arr[~np.isin(labels)] = value
|
469
|
+
|
470
|
+
arr: an N-dimensional numpy array
|
471
|
+
labels: an iterable list of integers
|
472
|
+
in_place: if True, modify the input array to reduce
|
473
|
+
memory consumption.
|
474
|
+
value: mask value
|
475
|
+
|
476
|
+
Returns: arr with all labels except `labels` masked out
|
477
|
+
"""
|
478
|
+
shape = arr.shape
|
479
|
+
|
480
|
+
if arr.flags['F_CONTIGUOUS']:
|
481
|
+
order = 'F'
|
482
|
+
else:
|
483
|
+
order = 'C'
|
484
|
+
|
485
|
+
if not in_place:
|
486
|
+
arr = np.copy(arr, order=order)
|
487
|
+
|
488
|
+
arr = _reshape(arr, (arr.size,))
|
489
|
+
arr = _mask_except(arr, labels, value)
|
490
|
+
return _reshape(arr, shape, order=order)
|
491
|
+
|
492
|
+
@cython.boundscheck(False)
|
493
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
494
|
+
@cython.nonecheck(False)
|
495
|
+
def _mask_except(cnp.ndarray[ALLINT] arr, list labels, ALLINT value):
|
496
|
+
cdef ALLINT[:] arrview = arr
|
497
|
+
cdef size_t i = 0
|
498
|
+
cdef size_t size = arr.size
|
499
|
+
|
500
|
+
if size == 0:
|
501
|
+
return arr
|
502
|
+
|
503
|
+
cdef flat_hash_map[ALLINT, ALLINT] tbl
|
504
|
+
|
505
|
+
for label in labels:
|
506
|
+
tbl[label] = label
|
507
|
+
|
508
|
+
cdef ALLINT last_elem = arrview[0]
|
509
|
+
cdef ALLINT last_elem_value = 0
|
510
|
+
|
511
|
+
if tbl.find(last_elem) == tbl.end():
|
512
|
+
last_elem_value = value
|
513
|
+
else:
|
514
|
+
last_elem_value = last_elem
|
515
|
+
|
516
|
+
for i in range(size):
|
517
|
+
if arrview[i] == last_elem:
|
518
|
+
arrview[i] = last_elem_value
|
519
|
+
elif tbl.find(arrview[i]) == tbl.end():
|
520
|
+
last_elem = arrview[i]
|
521
|
+
last_elem_value = value
|
522
|
+
arrview[i] = value
|
523
|
+
else:
|
524
|
+
last_elem = arrview[i]
|
525
|
+
last_elem_value = arrview[i]
|
526
|
+
|
527
|
+
return arr
|
528
|
+
|
529
|
+
def component_map(component_labels, parent_labels):
|
530
|
+
"""
|
531
|
+
Given two sets of images that have a surjective mapping between their labels,
|
532
|
+
generate a dictionary for that mapping.
|
533
|
+
|
534
|
+
For example, generate a mapping from connected components of labels to their
|
535
|
+
parent labels.
|
536
|
+
|
537
|
+
e.g. component_map([ 1, 2, 3, 4 ], [ 5, 5, 6, 7 ])
|
538
|
+
returns { 1: 5, 2: 5, 3: 6, 4: 7 }
|
539
|
+
|
540
|
+
Returns: { $COMPONENT_LABEL: $PARENT_LABEL }
|
541
|
+
"""
|
542
|
+
if not isinstance(component_labels, np.ndarray):
|
543
|
+
component_labels = np.array(component_labels)
|
544
|
+
if not isinstance(parent_labels, np.ndarray):
|
545
|
+
parent_labels = np.array(parent_labels)
|
546
|
+
|
547
|
+
if component_labels.size == 0:
|
548
|
+
return {}
|
549
|
+
|
550
|
+
if component_labels.shape != parent_labels.shape:
|
551
|
+
raise ValueError("The size of the inputs must match: {} vs {}".format(
|
552
|
+
component_labels.shape, parent_labels.shape
|
553
|
+
))
|
554
|
+
|
555
|
+
shape = component_labels.shape
|
556
|
+
|
557
|
+
component_labels, parent_labels = _match_array_orders(
|
558
|
+
component_labels, parent_labels
|
559
|
+
)
|
560
|
+
|
561
|
+
component_labels = _reshape(component_labels, (component_labels.size,))
|
562
|
+
parent_labels = _reshape(parent_labels, (parent_labels.size,))
|
563
|
+
return _component_map(component_labels, parent_labels)
|
564
|
+
|
565
|
+
@cython.boundscheck(False)
|
566
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
567
|
+
@cython.nonecheck(False)
|
568
|
+
def _component_map(
|
569
|
+
cnp.ndarray[ALLINT, ndim=1, cast=True] component_labels,
|
570
|
+
cnp.ndarray[ALLINT_2, ndim=1, cast=True] parent_labels
|
571
|
+
):
|
572
|
+
cdef size_t size = component_labels.size
|
573
|
+
if size == 0:
|
574
|
+
return {}
|
575
|
+
|
576
|
+
cdef dict remap = {}
|
577
|
+
cdef size_t i = 0
|
578
|
+
|
579
|
+
cdef ALLINT last_label = component_labels[0]
|
580
|
+
remap[component_labels[0]] = parent_labels[0]
|
581
|
+
for i in range(size):
|
582
|
+
if last_label == component_labels[i]:
|
583
|
+
continue
|
584
|
+
remap[component_labels[i]] = parent_labels[i]
|
585
|
+
last_label = component_labels[i]
|
586
|
+
|
587
|
+
return remap
|
588
|
+
|
589
|
+
def inverse_component_map(parent_labels, component_labels):
|
590
|
+
"""
|
591
|
+
Given two sets of images that have a mapping between their labels,
|
592
|
+
generate a dictionary for that mapping.
|
593
|
+
|
594
|
+
For example, generate a mapping from connected components of labels to their
|
595
|
+
parent labels.
|
596
|
+
|
597
|
+
e.g. inverse_component_map([ 1, 2, 1, 3 ], [ 4, 4, 5, 6 ])
|
598
|
+
returns { 1: [ 4, 5 ], 2: [ 4 ], 3: [ 6 ] }
|
599
|
+
|
600
|
+
Returns: { $PARENT_LABEL: [ $COMPONENT_LABELS, ... ] }
|
601
|
+
"""
|
602
|
+
if not isinstance(component_labels, np.ndarray):
|
603
|
+
component_labels = np.array(component_labels)
|
604
|
+
if not isinstance(parent_labels, np.ndarray):
|
605
|
+
parent_labels = np.array(parent_labels)
|
606
|
+
|
607
|
+
if component_labels.size == 0:
|
608
|
+
return {}
|
609
|
+
|
610
|
+
if component_labels.shape != parent_labels.shape:
|
611
|
+
raise ValueError("The size of the inputs must match: {} vs {}".format(
|
612
|
+
component_labels.shape, parent_labels.shape
|
613
|
+
))
|
614
|
+
|
615
|
+
shape = component_labels.shape
|
616
|
+
component_labels, parent_labels = _match_array_orders(
|
617
|
+
component_labels, parent_labels
|
618
|
+
)
|
619
|
+
component_labels = _reshape(component_labels, (component_labels.size,))
|
620
|
+
parent_labels = _reshape(parent_labels, (parent_labels.size,))
|
621
|
+
return _inverse_component_map(parent_labels, component_labels)
|
622
|
+
|
623
|
+
@cython.boundscheck(False)
|
624
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
625
|
+
@cython.nonecheck(False)
|
626
|
+
def _inverse_component_map(
|
627
|
+
cnp.ndarray[ALLINT, ndim=1, cast=True] parent_labels,
|
628
|
+
cnp.ndarray[ALLINT_2, ndim=1, cast=True] component_labels
|
629
|
+
):
|
630
|
+
cdef size_t size = parent_labels.size
|
631
|
+
if size == 0:
|
632
|
+
return {}
|
633
|
+
|
634
|
+
remap = defaultdict(set)
|
635
|
+
cdef size_t i = 0
|
636
|
+
|
637
|
+
cdef ALLINT last_label = parent_labels[0]
|
638
|
+
cdef ALLINT_2 last_component = component_labels[0]
|
639
|
+
remap[parent_labels[0]].add(component_labels[0])
|
640
|
+
for i in range(size):
|
641
|
+
if last_label == parent_labels[i] and last_component == component_labels[i]:
|
642
|
+
continue
|
643
|
+
remap[parent_labels[i]].add(component_labels[i])
|
644
|
+
last_label = parent_labels[i]
|
645
|
+
last_component = component_labels[i]
|
646
|
+
|
647
|
+
# for backwards compatibility
|
648
|
+
for key in remap:
|
649
|
+
remap[key] = list(remap[key])
|
650
|
+
remap.default_factory = list
|
651
|
+
|
652
|
+
return remap
|
653
|
+
|
654
|
+
def remap(arr, table, preserve_missing_labels=False, in_place=False):
|
655
|
+
"""
|
656
|
+
remap(cnp.ndarray[COMPLEX_NUMBER] arr, dict table,
|
657
|
+
preserve_missing_labels=False, in_place=False)
|
658
|
+
|
659
|
+
Remap an input numpy array in-place according to the values in the given
|
660
|
+
dictionary "table".
|
661
|
+
|
662
|
+
arr: an N-dimensional numpy array
|
663
|
+
table: { label: new_label_value, ... }
|
664
|
+
preserve_missing_labels: If an array value is not present in "table"...
|
665
|
+
True: Leave it alone.
|
666
|
+
False: Throw a KeyError.
|
667
|
+
in_place: if True, modify the input array to reduce
|
668
|
+
memory consumption.
|
669
|
+
|
670
|
+
Returns: remapped array
|
671
|
+
"""
|
672
|
+
if type(arr) == list:
|
673
|
+
arr = np.array(arr)
|
674
|
+
|
675
|
+
shape = arr.shape
|
676
|
+
|
677
|
+
if arr.flags['F_CONTIGUOUS']:
|
678
|
+
order = 'F'
|
679
|
+
else:
|
680
|
+
order = 'C'
|
681
|
+
|
682
|
+
original_dtype = arr.dtype
|
683
|
+
if len(table):
|
684
|
+
min_label, max_label = min(table.values()), max(table.values())
|
685
|
+
fit_value = min_label if abs(min_label) > abs(max_label) else max_label
|
686
|
+
arr = refit(arr, fit_value, increase_only=True)
|
687
|
+
|
688
|
+
if not in_place and original_dtype == arr.dtype:
|
689
|
+
arr = np.copy(arr, order=order)
|
690
|
+
|
691
|
+
if all([ k == v for k,v in table.items() ]) and preserve_missing_labels:
|
692
|
+
return arr
|
693
|
+
|
694
|
+
arr = _reshape(arr, (arr.size,))
|
695
|
+
arr = _remap(arr, table, preserve_missing_labels)
|
696
|
+
return _reshape(arr, shape, order=order)
|
697
|
+
|
698
|
+
@cython.boundscheck(False)
|
699
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
700
|
+
@cython.nonecheck(False)
|
701
|
+
def _remap(cnp.ndarray[NUMBER] arr, dict table, uint8_t preserve_missing_labels):
|
702
|
+
cdef NUMBER[:] arrview = arr
|
703
|
+
cdef size_t i = 0
|
704
|
+
cdef size_t size = arr.size
|
705
|
+
cdef NUMBER elem = 0
|
706
|
+
|
707
|
+
if size == 0:
|
708
|
+
return arr
|
709
|
+
|
710
|
+
# fast path for remapping only a single label
|
711
|
+
# e.g. for masking something out
|
712
|
+
cdef NUMBER before = 0
|
713
|
+
cdef NUMBER after = 0
|
714
|
+
if preserve_missing_labels and len(table) == 1:
|
715
|
+
before = next(iter(table.keys()))
|
716
|
+
after = table[before]
|
717
|
+
if before == after:
|
718
|
+
return arr
|
719
|
+
for i in range(size):
|
720
|
+
if arr[i] == before:
|
721
|
+
arr[i] = after
|
722
|
+
return arr
|
723
|
+
|
724
|
+
cdef flat_hash_map[NUMBER, NUMBER] tbl
|
725
|
+
|
726
|
+
for k, v in table.items():
|
727
|
+
tbl[k] = v
|
728
|
+
|
729
|
+
cdef NUMBER last_elem = arrview[0]
|
730
|
+
cdef NUMBER last_remap_id = 0
|
731
|
+
|
732
|
+
with nogil:
|
733
|
+
if tbl.find(last_elem) == tbl.end():
|
734
|
+
if not preserve_missing_labels:
|
735
|
+
raise KeyError("{} was not in the remap table.".format(last_elem))
|
736
|
+
else:
|
737
|
+
last_remap_id = last_elem
|
738
|
+
else:
|
739
|
+
arrview[0] = tbl[last_elem]
|
740
|
+
last_remap_id = arrview[0]
|
741
|
+
|
742
|
+
for i in range(1, size):
|
743
|
+
elem = arrview[i]
|
744
|
+
|
745
|
+
if elem == last_elem:
|
746
|
+
arrview[i] = last_remap_id
|
747
|
+
continue
|
748
|
+
|
749
|
+
if tbl.find(elem) == tbl.end():
|
750
|
+
if preserve_missing_labels:
|
751
|
+
last_elem = elem
|
752
|
+
last_remap_id = elem
|
753
|
+
continue
|
754
|
+
else:
|
755
|
+
raise KeyError("{} was not in the remap table.".format(elem))
|
756
|
+
else:
|
757
|
+
arrview[i] = tbl[elem]
|
758
|
+
|
759
|
+
last_elem = elem
|
760
|
+
last_remap_id = arrview[i]
|
761
|
+
|
762
|
+
return arr
|
763
|
+
|
764
|
+
@cython.boundscheck(False)
|
765
|
+
def remap_from_array(cnp.ndarray[UINT] arr, cnp.ndarray[UINT] vals, in_place=True):
|
766
|
+
"""
|
767
|
+
remap_from_array(cnp.ndarray[UINT] arr, cnp.ndarray[UINT] vals)
|
768
|
+
"""
|
769
|
+
cdef size_t i = 0
|
770
|
+
cdef size_t size = arr.size
|
771
|
+
cdef size_t maxkey = vals.size - 1
|
772
|
+
cdef UINT elem
|
773
|
+
|
774
|
+
if not in_place:
|
775
|
+
arr = np.copy(arr)
|
776
|
+
|
777
|
+
with nogil:
|
778
|
+
for i in range(size):
|
779
|
+
elem = arr[i]
|
780
|
+
if elem < 0 or elem > maxkey:
|
781
|
+
continue
|
782
|
+
arr[i] = vals[elem]
|
783
|
+
|
784
|
+
return arr
|
785
|
+
|
786
|
+
@cython.boundscheck(False)
|
787
|
+
def remap_from_array_kv(cnp.ndarray[ALLINT] arr, cnp.ndarray[ALLINT] keys, cnp.ndarray[ALLINT] vals, bint preserve_missing_labels=True, in_place=True):
|
788
|
+
"""
|
789
|
+
remap_from_array_kv(cnp.ndarray[ALLINT] arr, cnp.ndarray[ALLINT] keys, cnp.ndarray[ALLINT] vals)
|
790
|
+
"""
|
791
|
+
cdef flat_hash_map[ALLINT, ALLINT] remap_dict
|
792
|
+
|
793
|
+
assert keys.size == vals.size
|
794
|
+
|
795
|
+
cdef size_t i = 0
|
796
|
+
cdef size_t size = keys.size
|
797
|
+
cdef ALLINT elem
|
798
|
+
|
799
|
+
if not in_place:
|
800
|
+
arr = np.copy(arr)
|
801
|
+
|
802
|
+
with nogil:
|
803
|
+
for i in range(size):
|
804
|
+
remap_dict[keys[i]] = vals[i]
|
805
|
+
|
806
|
+
i = 0
|
807
|
+
size = arr.size
|
808
|
+
|
809
|
+
with nogil:
|
810
|
+
for i in range(size):
|
811
|
+
elem = arr[i]
|
812
|
+
if remap_dict.find(elem) == remap_dict.end():
|
813
|
+
if preserve_missing_labels:
|
814
|
+
continue
|
815
|
+
else:
|
816
|
+
raise KeyError("{} was not in the remap keys.".format(elem))
|
817
|
+
else:
|
818
|
+
arr[i] = remap_dict[elem]
|
819
|
+
|
820
|
+
return arr
|
821
|
+
|
822
|
+
def pixel_pairs(labels):
|
823
|
+
"""
|
824
|
+
Computes the number of matching adjacent memory locations.
|
825
|
+
|
826
|
+
This is useful for rapidly evaluating whether an image is
|
827
|
+
more binary or more connectomics like.
|
828
|
+
"""
|
829
|
+
if labels.size == 0:
|
830
|
+
return 0
|
831
|
+
return _pixel_pairs(_reshape(labels, (labels.size,)))
|
832
|
+
|
833
|
+
def _pixel_pairs(cnp.ndarray[ALLINT, ndim=1] labels):
|
834
|
+
cdef size_t voxels = labels.size
|
835
|
+
|
836
|
+
cdef size_t pairs = 0
|
837
|
+
cdef ALLINT label = labels[0]
|
838
|
+
|
839
|
+
cdef size_t i = 0
|
840
|
+
for i in range(1, voxels):
|
841
|
+
if label == labels[i]:
|
842
|
+
pairs += 1
|
843
|
+
else:
|
844
|
+
label = labels[i]
|
845
|
+
|
846
|
+
return pairs
|
847
|
+
|
848
|
+
@cython.binding(True)
|
849
|
+
def unique(labels, return_index=False, return_inverse=False, return_counts=False, axis=None):
|
850
|
+
"""
|
851
|
+
Compute the sorted set of unique labels in the input array.
|
852
|
+
|
853
|
+
return_index: also return the index of the first detected occurance
|
854
|
+
of each label.
|
855
|
+
return_inverse: If True, also return the indices of the unique array
|
856
|
+
(for the specified axis, if provided) that can be used to reconstruct
|
857
|
+
the input array.
|
858
|
+
return_counts: also return the unique label frequency as an array.
|
859
|
+
|
860
|
+
Returns:
|
861
|
+
unique ndarray
|
862
|
+
The sorted unique values.
|
863
|
+
|
864
|
+
unique_indices ndarray, optional
|
865
|
+
The indices of the first occurrences of the unique values in the original array.
|
866
|
+
Only provided if return_index is True.
|
867
|
+
|
868
|
+
unique_inverse ndarray, optional
|
869
|
+
The indices to reconstruct the original array from the unique array.
|
870
|
+
Only provided if return_inverse is True.
|
871
|
+
|
872
|
+
unique_counts ndarray, optional
|
873
|
+
The number of times each of the unique values comes up in the original array.
|
874
|
+
Only provided if return_counts is True.
|
875
|
+
"""
|
876
|
+
if not isinstance(labels, np.ndarray):
|
877
|
+
labels = np.array(labels)
|
878
|
+
|
879
|
+
# These flags are currently unsupported so call uncle and
|
880
|
+
# use the standard implementation instead.
|
881
|
+
if (axis is not None) or (not np.issubdtype(labels.dtype, np.integer)):
|
882
|
+
if (
|
883
|
+
axis == 0
|
884
|
+
and (
|
885
|
+
labels.ndim == 2
|
886
|
+
and labels.shape[1] == 2
|
887
|
+
and np.dtype(labels.dtype).itemsize < 8
|
888
|
+
and np.issubdtype(labels.dtype, np.integer)
|
889
|
+
)
|
890
|
+
and not (return_index or return_inverse or return_counts)
|
891
|
+
and labels.flags.c_contiguous
|
892
|
+
):
|
893
|
+
return _two_axis_unique(labels)
|
894
|
+
else:
|
895
|
+
return np.unique(
|
896
|
+
labels,
|
897
|
+
return_index=return_index,
|
898
|
+
return_inverse=return_inverse,
|
899
|
+
return_counts=return_counts,
|
900
|
+
axis=axis
|
901
|
+
)
|
902
|
+
|
903
|
+
cdef size_t voxels = labels.size
|
904
|
+
|
905
|
+
shape = labels.shape
|
906
|
+
fortran_order = labels.flags.f_contiguous
|
907
|
+
order = "F" if fortran_order else "C"
|
908
|
+
labels_orig = labels
|
909
|
+
labels = _reshape(labels, (voxels,))
|
910
|
+
|
911
|
+
max_label = 0
|
912
|
+
min_label = 0
|
913
|
+
if voxels > 0:
|
914
|
+
min_label, max_label = minmax(labels)
|
915
|
+
|
916
|
+
def c_order_index(arr):
|
917
|
+
if len(shape) > 1 and fortran_order:
|
918
|
+
return np.ravel_multi_index(
|
919
|
+
np.unravel_index(arr, shape, order='F'),
|
920
|
+
shape, order='C'
|
921
|
+
)
|
922
|
+
return arr
|
923
|
+
|
924
|
+
if voxels == 0:
|
925
|
+
uniq = np.array([], dtype=labels.dtype)
|
926
|
+
counts = np.array([], dtype=np.uint32)
|
927
|
+
index = np.array([], dtype=np.uint64)
|
928
|
+
inverse = np.array([], dtype=np.uintp)
|
929
|
+
elif min_label >= 0 and max_label < int(voxels):
|
930
|
+
uniq, index, counts, inverse = _unique_via_array(labels, max_label, return_index=return_index, return_inverse=return_inverse)
|
931
|
+
elif (max_label - min_label) <= int(voxels):
|
932
|
+
uniq, index, counts, inverse = _unique_via_shifted_array(labels, min_label, max_label, return_index=return_index, return_inverse=return_inverse)
|
933
|
+
elif float(pixel_pairs(labels)) / float(voxels) > 0.66:
|
934
|
+
uniq, index, counts, inverse = _unique_via_renumber(labels, return_index=return_index, return_inverse=return_inverse)
|
935
|
+
elif return_index or return_inverse:
|
936
|
+
return np.unique(labels_orig, return_index=return_index, return_counts=return_counts, return_inverse=return_inverse)
|
937
|
+
else:
|
938
|
+
uniq, counts = _unique_via_sort(labels)
|
939
|
+
index = None
|
940
|
+
inverse = None
|
941
|
+
|
942
|
+
results = [ uniq ]
|
943
|
+
if return_index:
|
944
|
+
# This is required to match numpy's behavior
|
945
|
+
results.append(c_order_index(index))
|
946
|
+
if return_inverse:
|
947
|
+
results.append(_reshape(inverse, shape, order=order))
|
948
|
+
if return_counts:
|
949
|
+
results.append(counts)
|
950
|
+
|
951
|
+
if len(results) > 1:
|
952
|
+
return tuple(results)
|
953
|
+
return uniq
|
954
|
+
|
955
|
+
def _two_axis_unique(labels):
|
956
|
+
"""
|
957
|
+
Faster replacement for np.unique(labels, axis=0)
|
958
|
+
when ndim = 2 and the dtype can be widened.
|
959
|
+
|
960
|
+
This special case is useful for sorting edge lists.
|
961
|
+
"""
|
962
|
+
dtype = labels.dtype
|
963
|
+
wide_dtype = widen_dtype(dtype)
|
964
|
+
|
965
|
+
labels = labels[:, [1,0]].reshape(-1, order="C")
|
966
|
+
labels = labels.view(wide_dtype)
|
967
|
+
labels = unique(labels)
|
968
|
+
N = len(labels)
|
969
|
+
labels = labels.view(dtype).reshape((N, 2), order="C")
|
970
|
+
return labels[:,[1,0]]
|
971
|
+
|
972
|
+
def _unique_via_shifted_array(labels, min_label=None, max_label=None, return_index=False, return_inverse=False):
|
973
|
+
if min_label is None or max_label is None:
|
974
|
+
min_label, max_label = minmax(labels)
|
975
|
+
|
976
|
+
labels -= min_label
|
977
|
+
uniq, idx, counts, inverse = _unique_via_array(labels, max_label - min_label + 1, return_index, return_inverse)
|
978
|
+
labels += min_label
|
979
|
+
uniq += min_label
|
980
|
+
return uniq, idx, counts, inverse
|
981
|
+
|
982
|
+
def _unique_via_renumber(labels, return_index=False, return_inverse=False):
|
983
|
+
dtype = labels.dtype
|
984
|
+
labels, remap = renumber(labels)
|
985
|
+
remap = { v:k for k,v in remap.items() }
|
986
|
+
uniq, idx, counts, inverse = _unique_via_array(labels, max(remap.keys()), return_index, return_inverse)
|
987
|
+
uniq = np.array([ remap[segid] for segid in uniq ], dtype=dtype)
|
988
|
+
|
989
|
+
if not return_index and not return_inverse:
|
990
|
+
uniq.sort()
|
991
|
+
return uniq, idx, counts, inverse
|
992
|
+
|
993
|
+
uniq, idx2 = np.unique(uniq, return_index=return_index)
|
994
|
+
if idx is not None:
|
995
|
+
idx = idx[idx2]
|
996
|
+
if counts is not None:
|
997
|
+
counts = counts[idx2]
|
998
|
+
if inverse is not None:
|
999
|
+
inverse = idx2[inverse]
|
1000
|
+
|
1001
|
+
return uniq, idx, counts, inverse
|
1002
|
+
|
1003
|
+
@cython.boundscheck(False)
|
1004
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1005
|
+
@cython.nonecheck(False)
|
1006
|
+
def _unique_via_sort(cnp.ndarray[ALLINT, ndim=1] labels):
|
1007
|
+
"""Slower than _unique_via_array but can handle any label."""
|
1008
|
+
labels = np.copy(labels)
|
1009
|
+
labels.sort()
|
1010
|
+
|
1011
|
+
cdef size_t voxels = labels.size
|
1012
|
+
|
1013
|
+
cdef vector[ALLINT] uniq
|
1014
|
+
uniq.reserve(100)
|
1015
|
+
|
1016
|
+
cdef vector[uint64_t] counts
|
1017
|
+
counts.reserve(100)
|
1018
|
+
|
1019
|
+
cdef size_t i = 0
|
1020
|
+
|
1021
|
+
cdef ALLINT cur = labels[0]
|
1022
|
+
cdef uint64_t accum = 1
|
1023
|
+
for i in range(1, voxels):
|
1024
|
+
if cur == labels[i]:
|
1025
|
+
accum += 1
|
1026
|
+
else:
|
1027
|
+
uniq.push_back(cur)
|
1028
|
+
counts.push_back(accum)
|
1029
|
+
accum = 1
|
1030
|
+
cur = labels[i]
|
1031
|
+
|
1032
|
+
uniq.push_back(cur)
|
1033
|
+
counts.push_back(accum)
|
1034
|
+
|
1035
|
+
dtype = labels.dtype
|
1036
|
+
del labels
|
1037
|
+
|
1038
|
+
return np.array(uniq, dtype=dtype), np.array(counts, dtype=np.uint64)
|
1039
|
+
|
1040
|
+
@cython.boundscheck(False)
|
1041
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1042
|
+
@cython.nonecheck(False)
|
1043
|
+
def _unique_via_array(
|
1044
|
+
cnp.ndarray[ALLINT, ndim=1] labels,
|
1045
|
+
size_t max_label,
|
1046
|
+
return_index, return_inverse,
|
1047
|
+
):
|
1048
|
+
cdef cnp.ndarray[uint64_t, ndim=1] counts = np.zeros(
|
1049
|
+
(max_label+1,), dtype=np.uint64
|
1050
|
+
)
|
1051
|
+
cdef cnp.ndarray[uintptr_t, ndim=1] index
|
1052
|
+
|
1053
|
+
cdef uintptr_t sentinel = np.iinfo(np.uintp).max
|
1054
|
+
if return_index:
|
1055
|
+
index = np.full(
|
1056
|
+
(max_label+1,), sentinel, dtype=np.uintp
|
1057
|
+
)
|
1058
|
+
|
1059
|
+
cdef size_t voxels = labels.shape[0]
|
1060
|
+
cdef size_t i = 0
|
1061
|
+
for i in range(voxels):
|
1062
|
+
counts[labels[i]] += 1
|
1063
|
+
|
1064
|
+
if return_index:
|
1065
|
+
for i in range(voxels):
|
1066
|
+
if index[labels[i]] == sentinel:
|
1067
|
+
index[labels[i]] = i
|
1068
|
+
|
1069
|
+
cdef size_t real_size = 0
|
1070
|
+
for i in range(max_label + 1):
|
1071
|
+
if counts[i] > 0:
|
1072
|
+
real_size += 1
|
1073
|
+
|
1074
|
+
cdef cnp.ndarray[ALLINT, ndim=1] segids = np.zeros(
|
1075
|
+
(real_size,), dtype=labels.dtype
|
1076
|
+
)
|
1077
|
+
cdef cnp.ndarray[uint64_t, ndim=1] cts = np.zeros(
|
1078
|
+
(real_size,), dtype=np.uint64
|
1079
|
+
)
|
1080
|
+
cdef cnp.ndarray[uintptr_t, ndim=1] idx
|
1081
|
+
|
1082
|
+
cdef size_t j = 0
|
1083
|
+
for i in range(max_label + 1):
|
1084
|
+
if counts[i] > 0:
|
1085
|
+
segids[j] = i
|
1086
|
+
cts[j] = counts[i]
|
1087
|
+
j += 1
|
1088
|
+
|
1089
|
+
if return_index:
|
1090
|
+
idx = np.zeros( (real_size,), dtype=np.uintp)
|
1091
|
+
j = 0
|
1092
|
+
for i in range(max_label + 1):
|
1093
|
+
if counts[i] > 0:
|
1094
|
+
idx[j] = index[i]
|
1095
|
+
j += 1
|
1096
|
+
|
1097
|
+
cdef cnp.ndarray[uintptr_t, ndim=1] mapping
|
1098
|
+
|
1099
|
+
if return_inverse:
|
1100
|
+
if segids.size:
|
1101
|
+
mapping = np.zeros([segids[segids.size - 1] + 1], dtype=np.uintp)
|
1102
|
+
for i in range(real_size):
|
1103
|
+
mapping[segids[i]] = i
|
1104
|
+
inverse_idx = mapping[labels]
|
1105
|
+
else:
|
1106
|
+
inverse_idx = np.zeros([0], dtype=np.uintp)
|
1107
|
+
|
1108
|
+
ret = [ segids, None, cts, None ]
|
1109
|
+
if return_index:
|
1110
|
+
ret[1] = idx
|
1111
|
+
if return_inverse:
|
1112
|
+
ret[3] = inverse_idx
|
1113
|
+
|
1114
|
+
return ret
|
1115
|
+
|
1116
|
+
def transpose(arr):
|
1117
|
+
"""
|
1118
|
+
transpose(arr)
|
1119
|
+
|
1120
|
+
For up to four dimensional matrices, perform in-place transposition.
|
1121
|
+
Square matrices up to three dimensions are faster than numpy's out-of-place
|
1122
|
+
algorithm. Default to the out-of-place implementation numpy uses for cases
|
1123
|
+
that aren't specially handled.
|
1124
|
+
|
1125
|
+
Returns: transposed numpy array
|
1126
|
+
"""
|
1127
|
+
if not arr.flags['F_CONTIGUOUS'] and not arr.flags['C_CONTIGUOUS']:
|
1128
|
+
arr = np.copy(arr, order='C')
|
1129
|
+
|
1130
|
+
shape = arr.shape
|
1131
|
+
strides = arr.strides
|
1132
|
+
|
1133
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1134
|
+
|
1135
|
+
dtype = arr.dtype
|
1136
|
+
if arr.dtype == bool:
|
1137
|
+
arr = arr.view(np.uint8)
|
1138
|
+
|
1139
|
+
if arr.ndim == 2:
|
1140
|
+
arr = _internal_ipt2d(arr)
|
1141
|
+
return arr.view(dtype)
|
1142
|
+
elif arr.ndim == 3:
|
1143
|
+
arr = _internal_ipt3d(arr)
|
1144
|
+
return arr.view(dtype)
|
1145
|
+
elif arr.ndim == 4:
|
1146
|
+
arr = _internal_ipt4d(arr)
|
1147
|
+
return arr.view(dtype)
|
1148
|
+
else:
|
1149
|
+
return arr.T
|
1150
|
+
|
1151
|
+
def asfortranarray(arr):
|
1152
|
+
"""
|
1153
|
+
asfortranarray(arr)
|
1154
|
+
|
1155
|
+
For up to four dimensional matrices, perform in-place transposition.
|
1156
|
+
Square matrices up to three dimensions are faster than numpy's out-of-place
|
1157
|
+
algorithm. Default to the out-of-place implementation numpy uses for cases
|
1158
|
+
that aren't specially handled.
|
1159
|
+
|
1160
|
+
Returns: transposed numpy array
|
1161
|
+
"""
|
1162
|
+
if arr.flags['F_CONTIGUOUS']:
|
1163
|
+
return arr
|
1164
|
+
elif not arr.flags['C_CONTIGUOUS']:
|
1165
|
+
return np.asfortranarray(arr)
|
1166
|
+
elif arr.ndim == 1:
|
1167
|
+
return arr
|
1168
|
+
|
1169
|
+
shape = arr.shape
|
1170
|
+
strides = arr.strides
|
1171
|
+
|
1172
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1173
|
+
|
1174
|
+
dtype = arr.dtype
|
1175
|
+
if arr.dtype == bool:
|
1176
|
+
arr = arr.view(np.uint8)
|
1177
|
+
|
1178
|
+
if arr.ndim == 2:
|
1179
|
+
arr = _internal_ipt2d(arr)
|
1180
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape, strides=(nbytes, shape[0] * nbytes))
|
1181
|
+
return arr.view(dtype)
|
1182
|
+
elif arr.ndim == 3:
|
1183
|
+
arr = _internal_ipt3d(arr)
|
1184
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape, strides=(nbytes, shape[0] * nbytes, shape[0] * shape[1] * nbytes))
|
1185
|
+
return arr.view(dtype)
|
1186
|
+
elif arr.ndim == 4:
|
1187
|
+
arr = _internal_ipt4d(arr)
|
1188
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape,
|
1189
|
+
strides=(
|
1190
|
+
nbytes,
|
1191
|
+
shape[0] * nbytes,
|
1192
|
+
shape[0] * shape[1] * nbytes,
|
1193
|
+
shape[0] * shape[1] * shape[2] * nbytes
|
1194
|
+
))
|
1195
|
+
return arr.view(dtype)
|
1196
|
+
else:
|
1197
|
+
return np.asfortranarray(arr)
|
1198
|
+
|
1199
|
+
def ascontiguousarray(arr):
|
1200
|
+
"""
|
1201
|
+
ascontiguousarray(arr)
|
1202
|
+
|
1203
|
+
For up to four dimensional matrices, perform in-place transposition.
|
1204
|
+
Square matrices up to three dimensions are faster than numpy's out-of-place
|
1205
|
+
algorithm. Default to the out-of-place implementation numpy uses for cases
|
1206
|
+
that aren't specially handled.
|
1207
|
+
|
1208
|
+
Returns: transposed numpy array
|
1209
|
+
"""
|
1210
|
+
if arr.flags['C_CONTIGUOUS']:
|
1211
|
+
return arr
|
1212
|
+
elif not arr.flags['F_CONTIGUOUS']:
|
1213
|
+
return np.ascontiguousarray(arr)
|
1214
|
+
elif arr.ndim == 1:
|
1215
|
+
return arr
|
1216
|
+
|
1217
|
+
shape = arr.shape
|
1218
|
+
strides = arr.strides
|
1219
|
+
|
1220
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1221
|
+
|
1222
|
+
dtype = arr.dtype
|
1223
|
+
if arr.dtype == bool:
|
1224
|
+
arr = arr.view(np.uint8)
|
1225
|
+
|
1226
|
+
if arr.ndim == 2:
|
1227
|
+
arr = _internal_ipt2d(arr)
|
1228
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape, strides=(shape[1] * nbytes, nbytes))
|
1229
|
+
return arr.view(dtype)
|
1230
|
+
elif arr.ndim == 3:
|
1231
|
+
arr = _internal_ipt3d(arr)
|
1232
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape, strides=(
|
1233
|
+
shape[2] * shape[1] * nbytes,
|
1234
|
+
shape[2] * nbytes,
|
1235
|
+
nbytes,
|
1236
|
+
))
|
1237
|
+
return arr.view(dtype)
|
1238
|
+
elif arr.ndim == 4:
|
1239
|
+
arr = _internal_ipt4d(arr)
|
1240
|
+
arr = np.lib.stride_tricks.as_strided(arr, shape=shape,
|
1241
|
+
strides=(
|
1242
|
+
shape[3] * shape[2] * shape[1] * nbytes,
|
1243
|
+
shape[3] * shape[2] * nbytes,
|
1244
|
+
shape[3] * nbytes,
|
1245
|
+
nbytes,
|
1246
|
+
))
|
1247
|
+
return arr.view(dtype)
|
1248
|
+
else:
|
1249
|
+
return np.ascontiguousarray(arr)
|
1250
|
+
|
1251
|
+
def _internal_ipt2d(cnp.ndarray[COMPLEX_NUMBER, cast=True, ndim=2] arr):
|
1252
|
+
cdef COMPLEX_NUMBER[:,:] arrview = arr
|
1253
|
+
|
1254
|
+
cdef size_t sx
|
1255
|
+
cdef size_t sy
|
1256
|
+
|
1257
|
+
if arr.flags['F_CONTIGUOUS']:
|
1258
|
+
sx = arr.shape[0]
|
1259
|
+
sy = arr.shape[1]
|
1260
|
+
else:
|
1261
|
+
sx = arr.shape[1]
|
1262
|
+
sy = arr.shape[0]
|
1263
|
+
|
1264
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1265
|
+
|
1266
|
+
# ipt doesn't do anything with values,
|
1267
|
+
# just moves them around, so only bit width matters
|
1268
|
+
# int, uint, float, bool who cares
|
1269
|
+
if nbytes == 1:
|
1270
|
+
_ipt2d[uint8_t](
|
1271
|
+
<uint8_t*>&arrview[0,0],
|
1272
|
+
sx, sy
|
1273
|
+
)
|
1274
|
+
elif nbytes == 2:
|
1275
|
+
_ipt2d[uint16_t](
|
1276
|
+
<uint16_t*>&arrview[0,0],
|
1277
|
+
sx, sy
|
1278
|
+
)
|
1279
|
+
elif nbytes == 4:
|
1280
|
+
_ipt2d[uint32_t](
|
1281
|
+
<uint32_t*>&arrview[0,0],
|
1282
|
+
sx, sy
|
1283
|
+
)
|
1284
|
+
else:
|
1285
|
+
_ipt2d[uint64_t](
|
1286
|
+
<uint64_t*>&arrview[0,0],
|
1287
|
+
sx, sy
|
1288
|
+
)
|
1289
|
+
|
1290
|
+
return arr
|
1291
|
+
|
1292
|
+
def _internal_ipt3d(cnp.ndarray[COMPLEX_NUMBER, cast=True, ndim=3] arr):
|
1293
|
+
cdef COMPLEX_NUMBER[:,:,:] arrview = arr
|
1294
|
+
|
1295
|
+
cdef size_t sx
|
1296
|
+
cdef size_t sy
|
1297
|
+
cdef size_t sz
|
1298
|
+
|
1299
|
+
if arr.flags['F_CONTIGUOUS']:
|
1300
|
+
sx = arr.shape[0]
|
1301
|
+
sy = arr.shape[1]
|
1302
|
+
sz = arr.shape[2]
|
1303
|
+
else:
|
1304
|
+
sx = arr.shape[2]
|
1305
|
+
sy = arr.shape[1]
|
1306
|
+
sz = arr.shape[0]
|
1307
|
+
|
1308
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1309
|
+
|
1310
|
+
# ipt doesn't do anything with values,
|
1311
|
+
# just moves them around, so only bit width matters
|
1312
|
+
# int, uint, float, bool who cares
|
1313
|
+
if nbytes == 1:
|
1314
|
+
_ipt3d[uint8_t](
|
1315
|
+
<uint8_t*>&arrview[0,0,0],
|
1316
|
+
sx, sy, sz
|
1317
|
+
)
|
1318
|
+
elif nbytes == 2:
|
1319
|
+
_ipt3d[uint16_t](
|
1320
|
+
<uint16_t*>&arrview[0,0,0],
|
1321
|
+
sx, sy, sz
|
1322
|
+
)
|
1323
|
+
elif nbytes == 4:
|
1324
|
+
_ipt3d[uint32_t](
|
1325
|
+
<uint32_t*>&arrview[0,0,0],
|
1326
|
+
sx, sy, sz
|
1327
|
+
)
|
1328
|
+
else:
|
1329
|
+
_ipt3d[uint64_t](
|
1330
|
+
<uint64_t*>&arrview[0,0,0],
|
1331
|
+
sx, sy, sz
|
1332
|
+
)
|
1333
|
+
|
1334
|
+
return arr
|
1335
|
+
|
1336
|
+
def _internal_ipt4d(cnp.ndarray[COMPLEX_NUMBER, cast=True, ndim=4] arr):
|
1337
|
+
cdef COMPLEX_NUMBER[:,:,:,:] arrview = arr
|
1338
|
+
|
1339
|
+
cdef size_t sx
|
1340
|
+
cdef size_t sy
|
1341
|
+
cdef size_t sz
|
1342
|
+
cdef size_t sw
|
1343
|
+
|
1344
|
+
if arr.flags['F_CONTIGUOUS']:
|
1345
|
+
sx = arr.shape[0]
|
1346
|
+
sy = arr.shape[1]
|
1347
|
+
sz = arr.shape[2]
|
1348
|
+
sw = arr.shape[3]
|
1349
|
+
else:
|
1350
|
+
sx = arr.shape[3]
|
1351
|
+
sy = arr.shape[2]
|
1352
|
+
sz = arr.shape[1]
|
1353
|
+
sw = arr.shape[0]
|
1354
|
+
|
1355
|
+
cdef int nbytes = np.dtype(arr.dtype).itemsize
|
1356
|
+
|
1357
|
+
# ipt doesn't do anything with values,
|
1358
|
+
# just moves them around, so only bit width matters
|
1359
|
+
# int, uint, float, bool who cares
|
1360
|
+
if nbytes == 1:
|
1361
|
+
_ipt4d[uint8_t](
|
1362
|
+
<uint8_t*>&arrview[0,0,0,0],
|
1363
|
+
sx, sy, sz, sw
|
1364
|
+
)
|
1365
|
+
elif nbytes == 2:
|
1366
|
+
_ipt4d[uint16_t](
|
1367
|
+
<uint16_t*>&arrview[0,0,0,0],
|
1368
|
+
sx, sy, sz, sw
|
1369
|
+
)
|
1370
|
+
elif nbytes == 4:
|
1371
|
+
_ipt4d[uint32_t](
|
1372
|
+
<uint32_t*>&arrview[0,0,0,0],
|
1373
|
+
sx, sy, sz, sw
|
1374
|
+
)
|
1375
|
+
else:
|
1376
|
+
_ipt4d[uint64_t](
|
1377
|
+
<uint64_t*>&arrview[0,0,0,0],
|
1378
|
+
sx, sy, sz, sw
|
1379
|
+
)
|
1380
|
+
|
1381
|
+
return arr
|
1382
|
+
|
1383
|
+
def foreground(arr):
|
1384
|
+
"""Returns the number of non-zero voxels in an array."""
|
1385
|
+
arr = _reshape(arr, (arr.size,))
|
1386
|
+
return _foreground(arr)
|
1387
|
+
|
1388
|
+
@cython.boundscheck(False)
|
1389
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1390
|
+
@cython.nonecheck(False)
|
1391
|
+
def _foreground(cnp.ndarray[ALLINT, ndim=1] arr):
|
1392
|
+
cdef size_t i = 0
|
1393
|
+
cdef size_t sz = arr.size
|
1394
|
+
cdef size_t n_foreground = 0
|
1395
|
+
for i in range(sz):
|
1396
|
+
n_foreground += <size_t>(arr[i] != 0)
|
1397
|
+
return n_foreground
|
1398
|
+
|
1399
|
+
def point_cloud(arr):
|
1400
|
+
"""
|
1401
|
+
point_cloud(arr)
|
1402
|
+
|
1403
|
+
Given a 2D or 3D integer image, return a mapping from
|
1404
|
+
labels to their (x,y,z) position in the image.
|
1405
|
+
|
1406
|
+
Zero is considered a background label.
|
1407
|
+
|
1408
|
+
Returns: ndarray(N, 2 or 3, dtype=uint16)
|
1409
|
+
"""
|
1410
|
+
if arr.dtype == bool:
|
1411
|
+
arr = arr.view(np.uint8)
|
1412
|
+
|
1413
|
+
if arr.ndim == 2:
|
1414
|
+
return _point_cloud_2d(arr)
|
1415
|
+
else:
|
1416
|
+
return _point_cloud_3d(arr)
|
1417
|
+
|
1418
|
+
@cython.boundscheck(False)
|
1419
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1420
|
+
@cython.nonecheck(False)
|
1421
|
+
def _point_cloud_2d(cnp.ndarray[ALLINT, ndim=2] arr):
|
1422
|
+
cdef size_t n_foreground = foreground(arr)
|
1423
|
+
|
1424
|
+
cdef size_t sx = arr.shape[0]
|
1425
|
+
cdef size_t sy = arr.shape[1]
|
1426
|
+
|
1427
|
+
if n_foreground == 0:
|
1428
|
+
return {}
|
1429
|
+
|
1430
|
+
cdef cnp.ndarray[ALLINT, ndim=1] ptlabel = np.zeros((n_foreground,), dtype=arr.dtype)
|
1431
|
+
cdef cnp.ndarray[uint16_t, ndim=2] ptcloud = np.zeros((n_foreground, 2), dtype=np.uint16)
|
1432
|
+
|
1433
|
+
cdef size_t i = 0
|
1434
|
+
cdef size_t j = 0
|
1435
|
+
|
1436
|
+
cdef size_t idx = 0
|
1437
|
+
for i in range(sx):
|
1438
|
+
for j in range(sy):
|
1439
|
+
if arr[i,j] != 0:
|
1440
|
+
ptlabel[idx] = arr[i,j]
|
1441
|
+
ptcloud[idx,0] = i
|
1442
|
+
ptcloud[idx,1] = j
|
1443
|
+
idx += 1
|
1444
|
+
|
1445
|
+
sortidx = ptlabel.argsort()
|
1446
|
+
ptlabel = ptlabel[sortidx]
|
1447
|
+
ptcloud = ptcloud[sortidx]
|
1448
|
+
del sortidx
|
1449
|
+
|
1450
|
+
ptcloud_by_label = {}
|
1451
|
+
if n_foreground == 1:
|
1452
|
+
ptcloud_by_label[ptlabel[0]] = ptcloud
|
1453
|
+
return ptcloud_by_label
|
1454
|
+
|
1455
|
+
cdef size_t start = 0
|
1456
|
+
cdef size_t end = 0
|
1457
|
+
for end in range(1, n_foreground):
|
1458
|
+
if ptlabel[end] != ptlabel[end - 1]:
|
1459
|
+
ptcloud_by_label[ptlabel[end - 1]] = ptcloud[start:end,:]
|
1460
|
+
start = end
|
1461
|
+
|
1462
|
+
ptcloud_by_label[ptlabel[end]] = ptcloud[start:,:]
|
1463
|
+
|
1464
|
+
return ptcloud_by_label
|
1465
|
+
|
1466
|
+
@cython.boundscheck(False)
|
1467
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1468
|
+
@cython.nonecheck(False)
|
1469
|
+
def _point_cloud_3d(cnp.ndarray[ALLINT, ndim=3] arr):
|
1470
|
+
cdef size_t n_foreground = foreground(arr)
|
1471
|
+
|
1472
|
+
cdef size_t sx = arr.shape[0]
|
1473
|
+
cdef size_t sy = arr.shape[1]
|
1474
|
+
cdef size_t sz = arr.shape[2]
|
1475
|
+
|
1476
|
+
if n_foreground == 0:
|
1477
|
+
return {}
|
1478
|
+
|
1479
|
+
cdef cnp.ndarray[ALLINT, ndim=1] ptlabel = np.zeros((n_foreground,), dtype=arr.dtype)
|
1480
|
+
cdef cnp.ndarray[uint16_t, ndim=2] ptcloud = np.zeros((n_foreground, 3), dtype=np.uint16)
|
1481
|
+
|
1482
|
+
cdef size_t i = 0
|
1483
|
+
cdef size_t j = 0
|
1484
|
+
cdef size_t k = 0
|
1485
|
+
|
1486
|
+
cdef size_t idx = 0
|
1487
|
+
for i in range(sx):
|
1488
|
+
for j in range(sy):
|
1489
|
+
for k in range(sz):
|
1490
|
+
if arr[i,j,k] != 0:
|
1491
|
+
ptlabel[idx] = arr[i,j,k]
|
1492
|
+
ptcloud[idx,0] = i
|
1493
|
+
ptcloud[idx,1] = j
|
1494
|
+
ptcloud[idx,2] = k
|
1495
|
+
idx += 1
|
1496
|
+
|
1497
|
+
sortidx = ptlabel.argsort()
|
1498
|
+
ptlabel = ptlabel[sortidx]
|
1499
|
+
ptcloud = ptcloud[sortidx]
|
1500
|
+
del sortidx
|
1501
|
+
|
1502
|
+
ptcloud_by_label = {}
|
1503
|
+
if n_foreground == 1:
|
1504
|
+
ptcloud_by_label[ptlabel[0]] = ptcloud
|
1505
|
+
return ptcloud_by_label
|
1506
|
+
|
1507
|
+
cdef size_t start = 0
|
1508
|
+
cdef size_t end = 0
|
1509
|
+
for end in range(1, n_foreground):
|
1510
|
+
if ptlabel[end] != ptlabel[end - 1]:
|
1511
|
+
ptcloud_by_label[ptlabel[end - 1]] = ptcloud[start:end,:]
|
1512
|
+
start = end
|
1513
|
+
|
1514
|
+
ptcloud_by_label[ptlabel[end]] = ptcloud[start:,:]
|
1515
|
+
|
1516
|
+
return ptcloud_by_label
|
1517
|
+
|
1518
|
+
|
1519
|
+
@cython.binding(True)
|
1520
|
+
@cython.boundscheck(False)
|
1521
|
+
@cython.wraparound(False) # turn off negative index wrapping for entire function
|
1522
|
+
@cython.nonecheck(False)
|
1523
|
+
def tobytes(
|
1524
|
+
cnp.ndarray[NUMBER, ndim=3] image,
|
1525
|
+
chunk_size:Sequence[int,int,int],
|
1526
|
+
order:str="C"
|
1527
|
+
) -> List[bytes]:
|
1528
|
+
"""
|
1529
|
+
Compute the cutout.tobytes(order) with the image divided into
|
1530
|
+
a grid of cutouts. Return the resultant binaries indexed by
|
1531
|
+
their cutout's gridpoint in fortran order.
|
1532
|
+
|
1533
|
+
This is faster than calling tobytes on each cutout individually
|
1534
|
+
if the input and output orders match.
|
1535
|
+
"""
|
1536
|
+
if order not in ["C", "F"]:
|
1537
|
+
raise ValueError(f"order must be C or F. Got: {order}")
|
1538
|
+
|
1539
|
+
chunk_size = np.array(chunk_size, dtype=float)
|
1540
|
+
shape = np.array((image.shape[0], image.shape[1], image.shape[2]), dtype=float)
|
1541
|
+
grid_size = np.ceil(shape / chunk_size).astype(int)
|
1542
|
+
|
1543
|
+
if np.any(np.remainder(shape, chunk_size)):
|
1544
|
+
raise ValueError(f"chunk_size ({chunk_size}) must evenly divide the image shape ({shape}).")
|
1545
|
+
|
1546
|
+
chunk_array_size = int(reduce(operator.mul, chunk_size))
|
1547
|
+
chunk_size = chunk_size.astype(int)
|
1548
|
+
shape = shape.astype(int)
|
1549
|
+
|
1550
|
+
num_grid = int(reduce(operator.mul, grid_size))
|
1551
|
+
|
1552
|
+
cdef int64_t img_i = 0
|
1553
|
+
|
1554
|
+
cdef int64_t sgx = grid_size[0]
|
1555
|
+
cdef int64_t sgy = grid_size[1]
|
1556
|
+
cdef int64_t sgz = grid_size[2]
|
1557
|
+
|
1558
|
+
cdef int64_t sx = shape[0]
|
1559
|
+
cdef int64_t sy = shape[1]
|
1560
|
+
cdef int64_t sz = shape[2]
|
1561
|
+
cdef int64_t sxy = sx * sy
|
1562
|
+
|
1563
|
+
cdef int64_t cx = chunk_size[0]
|
1564
|
+
cdef int64_t cy = chunk_size[1]
|
1565
|
+
cdef int64_t cz = chunk_size[2]
|
1566
|
+
|
1567
|
+
cdef int64_t gx = 0
|
1568
|
+
cdef int64_t gy = 0
|
1569
|
+
cdef int64_t gz = 0
|
1570
|
+
cdef int64_t gi = 0
|
1571
|
+
|
1572
|
+
cdef int64_t idx = 0
|
1573
|
+
cdef int64_t x = 0
|
1574
|
+
cdef int64_t y = 0
|
1575
|
+
cdef int64_t z = 0
|
1576
|
+
|
1577
|
+
# It's difficult to do better than numpy when f and c or c and f
|
1578
|
+
# because at least one of the arrays must be transversed substantially
|
1579
|
+
# out of order. However, when f and f or c and c you can do strips in
|
1580
|
+
# order.
|
1581
|
+
if (
|
1582
|
+
(not image.flags.f_contiguous and not image.flags.c_contiguous)
|
1583
|
+
or (image.flags.f_contiguous and order == "C")
|
1584
|
+
or (image.flags.c_contiguous and order == "F")
|
1585
|
+
):
|
1586
|
+
res = []
|
1587
|
+
for gz in range(sgz):
|
1588
|
+
for gy in range(sgy):
|
1589
|
+
for gx in range(sgx):
|
1590
|
+
cutout = image[gx*cx:(gx+1)*cx, gy*cy:(gy+1)*cy, gz*cz:(gz+1)*cz]
|
1591
|
+
res.append(cutout.tobytes(order))
|
1592
|
+
return res
|
1593
|
+
elif (cx == sx and cy == sy and cz == sz):
|
1594
|
+
return [ image.tobytes(order) ]
|
1595
|
+
|
1596
|
+
cdef cnp.ndarray[NUMBER] arr
|
1597
|
+
|
1598
|
+
cdef list[cnp.ndarray[NUMBER]] array_grid = [
|
1599
|
+
np.zeros((chunk_array_size,), dtype=image.dtype)
|
1600
|
+
for i in range(num_grid)
|
1601
|
+
]
|
1602
|
+
|
1603
|
+
cdef cnp.ndarray[NUMBER, ndim=1] img = _reshape(image, (image.size,))
|
1604
|
+
|
1605
|
+
if order == "F": # b/c of guard above, this is F to F order
|
1606
|
+
for gz in range(sgz):
|
1607
|
+
for z in range(cz):
|
1608
|
+
for gy in range(sgy):
|
1609
|
+
for gx in range(sgx):
|
1610
|
+
gi = gx + sgx * (gy + sgy * gz)
|
1611
|
+
arr = array_grid[gi]
|
1612
|
+
for y in range(cy):
|
1613
|
+
img_i = cx * gx + sx * ((cy * gy + y) + sy * (cz * gz + z))
|
1614
|
+
idx = cx * (y + cy * z)
|
1615
|
+
for x in range(cx):
|
1616
|
+
arr[idx + x] = img[img_i + x]
|
1617
|
+
else: # b/c of guard above, this is C to C order
|
1618
|
+
for gx in range(sgx):
|
1619
|
+
for x in range(cx):
|
1620
|
+
for gy in range(sgy):
|
1621
|
+
for gz in range(sgz):
|
1622
|
+
gi = gx + sgx * (gy + sgy * gz)
|
1623
|
+
arr = array_grid[gi]
|
1624
|
+
for y in range(cy):
|
1625
|
+
img_i = cz * gz + sz * ((cy * gy + y) + sy * (cx * gx + x))
|
1626
|
+
idx = cz * (y + cy * x)
|
1627
|
+
for z in range(cz):
|
1628
|
+
arr[idx + z] = img[img_i + z]
|
1629
|
+
|
1630
|
+
return [ bytes(memoryview(ar)) for ar in array_grid ]
|