fastremap 1.16.1__cp39-cp39-win32.whl → 1.17.1__cp39-cp39-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.
@@ -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 ]