batchgenerators 0.25.2__tar.gz → 0.25.4__tar.gz

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.
Files changed (63) hide show
  1. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/PKG-INFO +1 -1
  2. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/spatial_transformations.py +16 -12
  3. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/utils.py +204 -27
  4. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/multi_threaded_augmenter.py +139 -79
  5. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/nondet_multi_threaded_augmenter.py +131 -56
  6. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/spatial_transforms.py +22 -10
  7. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/PKG-INFO +1 -1
  8. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/SOURCES.txt +1 -0
  9. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/dependency_links.txt +0 -0
  10. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/requires.txt +0 -0
  11. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/top_level.txt +0 -0
  12. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/pyproject.toml +1 -1
  13. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_DataLoader.py +1 -1
  14. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_axis_mirroring.py +1 -2
  15. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_resample_augmentations.py +2 -2
  16. batchgenerators-0.25.4/tests/test_seg_resampling.py +247 -0
  17. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/LICENSE +0 -0
  18. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/README.md +0 -0
  19. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/__init__.py +0 -0
  20. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/__init__.py +0 -0
  21. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/color_augmentations.py +0 -0
  22. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/crop_and_pad_augmentations.py +0 -0
  23. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/noise_augmentations.py +0 -0
  24. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/normalizations.py +0 -0
  25. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/resample_augmentations.py +0 -0
  26. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/__init__.py +0 -0
  27. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/data_loader.py +0 -0
  28. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/dataset.py +0 -0
  29. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/single_threaded_augmenter.py +0 -0
  30. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/datasets/__init__.py +0 -0
  31. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/datasets/cifar.py +0 -0
  32. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/__init__.py +0 -0
  33. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/__init__.py +0 -0
  34. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_dataloader_2D.py +0 -0
  35. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_dataloader_3D.py +0 -0
  36. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_preprocessing.py +0 -0
  37. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/config.py +0 -0
  38. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/cifar10.py +0 -0
  39. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/multithreaded_dataloading.py +0 -0
  40. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/__init__.py +0 -0
  41. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/abstract_transforms.py +0 -0
  42. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/channel_selection_transforms.py +0 -0
  43. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/color_transforms.py +0 -0
  44. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/crop_and_pad_transforms.py +0 -0
  45. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/local_transforms.py +0 -0
  46. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/noise_transforms.py +0 -0
  47. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/resample_transforms.py +0 -0
  48. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/sample_normalization_transforms.py +0 -0
  49. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/utility_transforms.py +0 -0
  50. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/__init__.py +0 -0
  51. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/custom_types.py +0 -0
  52. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/data_splitting.py +0 -0
  53. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/file_and_folder_operations.py +0 -0
  54. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/setup.cfg +0 -0
  55. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/setup.py +0 -0
  56. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_augment_zoom.py +0 -0
  57. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_color_augmentations.py +0 -0
  58. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_crop.py +0 -0
  59. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_multithreaded_augmenter.py +0 -0
  60. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_normalizations.py +0 -0
  61. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_random_crop.py +0 -0
  62. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_sanity.py +0 -0
  63. {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_spatial_transformations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchgenerators
3
- Version: 0.25.2
3
+ Version: 0.25.4
4
4
  Summary: Data augmentation toolkit
5
5
  Author-email: "Division of Medical Image Computing, German Cancer Research Center AND Applied Computer Vision Lab, Helmholtz Imaging Platform" <f.isensee@dkfz-heidelberg.de>
6
6
  License: Apache License
@@ -45,7 +45,7 @@ def augment_rot90(sample_data, sample_seg, num_rot=(1, 2, 3), axes=(0, 1, 2)):
45
45
  return sample_data, sample_seg
46
46
 
47
47
 
48
- def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1):
48
+ def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1, *, seg_tiebreak='nearest'):
49
49
  """
50
50
  Reshapes data (and seg) to target_size
51
51
  :param sample_data: np.ndarray or list/tuple of np.ndarrays, must be (c, x, y(, z))) (if list/tuple then each entry
@@ -71,14 +71,15 @@ def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1):
71
71
  if sample_seg is not None:
72
72
  target_seg = np.ones([sample_seg.shape[0]] + target_size_here)
73
73
  for c in range(sample_seg.shape[0]):
74
- target_seg[c] = resize_segmentation(sample_seg[c], target_size_here, order_seg)
74
+ target_seg[c] = resize_segmentation(sample_seg[c], target_size_here, order_seg,
75
+ seg_tiebreak=seg_tiebreak)
75
76
  else:
76
77
  target_seg = None
77
78
 
78
79
  return sample_data, target_seg
79
80
 
80
81
 
81
- def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1):
82
+ def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1, *, seg_tiebreak='nearest'):
82
83
  """
83
84
  zooms data (and seg) by factor zoom_factors
84
85
  :param sample_data: np.ndarray or list/tuple of np.ndarrays, must be (c, x, y(, z))) (if list/tuple then each entry
@@ -107,7 +108,8 @@ def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1):
107
108
  if sample_seg is not None:
108
109
  target_seg = np.ones([sample_seg.shape[0]] + target_shape_here)
109
110
  for c in range(sample_seg.shape[0]):
110
- target_seg[c] = resize_segmentation(sample_seg[c], target_shape_here, order_seg)
111
+ target_seg[c] = resize_segmentation(sample_seg[c], target_shape_here, order_seg,
112
+ seg_tiebreak=seg_tiebreak)
111
113
  else:
112
114
  target_seg = None
113
115
 
@@ -192,7 +194,8 @@ def augment_spatial(data, seg, patch_size, patch_center_dist_from_border=30,
192
194
  do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
193
195
  border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, p_el_per_sample=1,
194
196
  p_scale_per_sample=1, p_rot_per_sample=1, independent_scale_for_each_axis=False,
195
- p_rot_per_axis: float = 1, p_independent_scale_per_axis: int = 1):
197
+ p_rot_per_axis: float = 1, p_independent_scale_per_axis: int = 1,
198
+ *, seg_tiebreak: str = 'nearest'):
196
199
  dim = len(patch_size)
197
200
  seg_result = None
198
201
  if seg is not None:
@@ -277,7 +280,7 @@ def augment_spatial(data, seg, patch_size, patch_center_dist_from_border=30,
277
280
  for channel_id in range(seg.shape[1]):
278
281
  seg_result[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
279
282
  border_mode_seg, cval=border_cval_seg,
280
- is_seg=True)
283
+ is_seg=True, seg_tiebreak=seg_tiebreak)
281
284
  else:
282
285
  if seg is None:
283
286
  s = None
@@ -300,7 +303,8 @@ def augment_spatial_2(data, seg, patch_size, patch_center_dist_from_border=30,
300
303
  do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
301
304
  border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, p_el_per_sample=1,
302
305
  p_scale_per_sample=1, p_rot_per_sample=1, independent_scale_for_each_axis=False,
303
- p_rot_per_axis: float = 1, p_independent_scale_per_axis: float = 1):
306
+ p_rot_per_axis: float = 1, p_independent_scale_per_axis: float = 1,
307
+ *, seg_tiebreak: str = 'nearest'):
304
308
  """
305
309
 
306
310
  :param data:
@@ -444,7 +448,7 @@ def augment_spatial_2(data, seg, patch_size, patch_center_dist_from_border=30,
444
448
  for channel_id in range(seg.shape[1]):
445
449
  seg_result[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
446
450
  border_mode_seg, cval=border_cval_seg,
447
- is_seg=True)
451
+ is_seg=True, seg_tiebreak=seg_tiebreak)
448
452
  else:
449
453
  if seg is None:
450
454
  s = None
@@ -549,7 +553,7 @@ def augment_misalign(data, seg, data_size,
549
553
  do_transl=False,
550
554
  border_mode_data='constant', border_cval_data=0,
551
555
  border_mode_seg='constant', border_cval_seg=0,
552
- order_data=3, order_seg=0):
556
+ order_data=3, order_seg=0, *, seg_tiebreak='nearest'):
553
557
 
554
558
  dim = len(data_size)
555
559
 
@@ -587,7 +591,7 @@ def augment_misalign(data, seg, data_size,
587
591
  if channel_id in im_channels_2_misalign:
588
592
  seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
589
593
  border_mode_seg, cval=border_cval_seg,
590
- is_seg=True)
594
+ is_seg=True, seg_tiebreak=seg_tiebreak)
591
595
 
592
596
  if do_rotation and np.random.uniform() < p_rot_per_sample:
593
597
  coords = create_zero_centered_coordinate_mesh(data_size)
@@ -620,7 +624,7 @@ def augment_misalign(data, seg, data_size,
620
624
  if channel_id in im_channels_2_misalign:
621
625
  seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
622
626
  border_mode_seg, cval=border_cval_seg,
623
- is_seg=True)
627
+ is_seg=True, seg_tiebreak=seg_tiebreak)
624
628
 
625
629
  if do_transl and np.random.uniform() < p_transl_per_sample:
626
630
  coords = create_zero_centered_coordinate_mesh(data_size)
@@ -654,5 +658,5 @@ def augment_misalign(data, seg, data_size,
654
658
  if channel_id in label_channels_2_misalign:
655
659
  seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
656
660
  border_mode_seg, cval=border_cval_seg,
657
- is_seg=True)
661
+ is_seg=True, seg_tiebreak=seg_tiebreak)
658
662
  return data, seg
@@ -16,11 +16,15 @@
16
16
  import random
17
17
  import numpy as np
18
18
  from copy import deepcopy
19
- from scipy.ndimage import map_coordinates, fourier_gaussian
20
- from scipy.ndimage.filters import gaussian_filter, gaussian_gradient_magnitude
21
- from scipy.ndimage.morphology import grey_dilation
19
+ from scipy.ndimage import (
20
+ fourier_gaussian,
21
+ gaussian_filter,
22
+ gaussian_gradient_magnitude,
23
+ grey_dilation,
24
+ map_coordinates,
25
+ )
26
+ from scipy.ndimage import label as lb
22
27
  from skimage.transform import resize
23
- from scipy.ndimage.measurements import label as lb
24
28
  import pandas as pd
25
29
 
26
30
 
@@ -43,6 +47,24 @@ def create_zero_centered_coordinate_mesh(shape):
43
47
  return coords
44
48
 
45
49
 
50
+ def unique_labels(arr: np.ndarray) -> np.ndarray:
51
+ """
52
+ np.unique(arr): the distinct values, ascending.
53
+
54
+ Which implementation is cheaper depends on the dtype. pd.unique hashes where np.unique sorts, and
55
+ on a label map hashing wins for integers (1.3x at int8) but loses for floats (4.5x slower at
56
+ float32, 2.9x at float64). Segmentations travel through this library as float32 (augment_spatial
57
+ allocates seg_result as float32), so the float branch is the common one. Either way this is
58
+ milliseconds against one map_coordinates per label - consistency, not a speedup.
59
+
60
+ Order matters to the callers: it fixes the channel order of a one hot encoding, and
61
+ _resample_seg_by_argmax resolves a strict `>` chain to the first label attaining the maximum.
62
+ """
63
+ if arr.dtype.kind in 'iub':
64
+ return np.sort(pd.unique(arr.ravel()))
65
+ return np.unique(arr)
66
+
67
+
46
68
  def convert_seg_image_to_one_hot_encoding(image, classes=None):
47
69
  '''
48
70
  image must be either (x, y, z) or (x, y)
@@ -50,7 +72,7 @@ def convert_seg_image_to_one_hot_encoding(image, classes=None):
50
72
  Example (3D): if input is of shape (x, y, z), the output will ne of shape (n_classes, x, y, z)
51
73
  '''
52
74
  if classes is None:
53
- classes = np.unique(image)
75
+ classes = unique_labels(image)
54
76
  out_image = np.zeros([len(classes)]+list(image.shape), dtype=image.dtype)
55
77
  for i, c in enumerate(classes):
56
78
  out_image[i][image == c] = 1
@@ -62,7 +84,7 @@ def convert_seg_image_to_one_hot_encoding_batched(image, classes=None):
62
84
  same as convert_seg_image_to_one_hot_encoding, but expects image to be (b, x, y, z) or (b, x, y)
63
85
  '''
64
86
  if classes is None:
65
- classes = np.unique(image)
87
+ classes = unique_labels(image)
66
88
  output_shape = [image.shape[0]] + [len(classes)] + list(image.shape[1:])
67
89
  out_image = np.zeros(output_shape, dtype=image.dtype)
68
90
  for b in range(image.shape[0]):
@@ -142,14 +164,161 @@ def uncenter_coords(coords):
142
164
  return coords
143
165
 
144
166
 
145
- def interpolate_img(img, coords, order=3, mode='nearest', cval=0.0, is_seg=False):
167
+ SEG_TIEBREAKS = ('nearest', 'lowest', 'highest')
168
+
169
+
170
+ def _resample_seg_by_argmax(labels, score_fn, nearest_fn, out_shape, dtype, seg_tiebreak,
171
+ empty_value=None):
172
+ """
173
+ Resample a segmentation by interpolating each label's indicator and taking the per-voxel argmax over
174
+ labels. The argmax is accumulated incrementally, so the (n_labels, *out_shape) score stack is never
175
+ materialized.
176
+
177
+ This replaces the `interpolated >= 0.5` assignment these functions used to do, which had two problems:
178
+
179
+ * it can leave a voxel unwritten. Where more than two labels meet, no single indicator has to reach
180
+ 0.5 (four labels meeting at a sample point give 0.25 each), and for spline orders >= 2 skimage's
181
+ clip=True destroys the sum-to-one property outright - on random four-label 2d data at order=3,
182
+ 6.2 % of output voxels had no label at 0.5 or above. Those voxels kept whatever the result array
183
+ was initialized with (0), which need not be a label that occurs in the input at all.
184
+ * where several labels do pass 0.5 it assigned in ascending label order, so the largest label won by
185
+ being written last. That is a decision about label values, not about geometry.
186
+
187
+ seg_tiebreak decides the voxels where the top score is shared. That is not a rare case: whenever a
188
+ sample point falls exactly between two labels - which an even integer resize factor does at every
189
+ boundary voxel - the two indicators are exactly 0.5.
190
+
191
+ 'nearest' take the nearest neighbour label, if it is one of the tied labels. Symmetric in the labels,
192
+ and the only option that is a function of the geometry rather than of the label values.
193
+ Where three or more labels meet the nearest neighbour can be a label that lost; such a
194
+ voxel keeps the smallest of the tied labels, as with 'lowest'. Matches nnU-Net's
195
+ resample_torch seg_tiebreak='nearest'.
196
+ 'lowest' keep the smallest label, i.e. plain argmax semantics.
197
+ 'highest' keep the largest label. This is the direction the old code leaned, but it is NOT
198
+ bit-identical to it: the old code compared each label against 0.5, not against the other
199
+ labels, so at orders >= 2 the two disagree wherever clipping broke sum-to-one.
200
+
201
+ Only exact ties are resolved by seg_tiebreak; near ties go to the larger score, as they should.
202
+
203
+ empty_value, if given, is written where every label scores zero. That happens only outside the image,
204
+ and only for padding modes that put something constant there, so this is how the caller's cval keeps
205
+ reaching those voxels now that the result is no longer zero-initialized.
206
+ """
207
+ if seg_tiebreak not in SEG_TIEBREAKS:
208
+ raise ValueError('unknown seg_tiebreak: %s. Must be one of %s' % (seg_tiebreak, str(SEG_TIEBREAKS)))
209
+ out_shape = tuple(out_shape)
210
+ if len(labels) == 1 and empty_value is None:
211
+ # nothing to interpolate, and this also keeps the loop below from having to handle a missing `best`
212
+ return np.full(out_shape, labels[0], dtype=dtype)
213
+
214
+ # Memory: the scores are float64 (that is what the interpolators return, and ties are decided by exact
215
+ # equality on them), so `best` and the current `score` are the two large buffers and everything else is
216
+ # kept small. The running winner and the nearest neighbour are held as int8/int16 indices into `labels`
217
+ # and mapped to label values once at the end, the per-label masks are three reused bool buffers, and the
218
+ # previous score is released before the next one is allocated. Without that, peak memory was about twice
219
+ # the bytes per output voxel.
220
+ idx_dtype = _label_index_dtype(len(labels))
221
+ win = np.zeros(out_shape, dtype=idx_dtype)
222
+ best = None
223
+ better = np.empty(out_shape, dtype=bool)
224
+ # 'nearest': the nearest neighbour label settles a tie only if it is one of the tied labels. Where three
225
+ # or more labels meet it need not be - a 2x2x2 block [3,1,1,1,2,2,2,3] sampled at its centre scores
226
+ # 1 and 2 at 0.375 each and 3 at 0.25, and its nearest neighbour is a 3. So instead of marking tied
227
+ # voxels, track whether the nearest neighbour's label currently has the top score: a label that takes
228
+ # the lead strictly decides that afresh, one that draws level can only add to it. Where it holds at the
229
+ # end the nearest neighbour is the answer (it changes nothing where its label won outright), and where
230
+ # it does not the argmax winner stands.
231
+ nearest = seg_tiebreak == 'nearest'
232
+ if nearest:
233
+ nn_idx = _nearest_label_index(labels, nearest_fn(), idx_dtype)
234
+ nn_top = np.empty(out_shape, dtype=bool)
235
+ is_nn = np.empty(out_shape, dtype=bool)
236
+ level = np.empty(out_shape, dtype=bool)
237
+ for i, c in enumerate(labels):
238
+ score = score_fn(c)
239
+ if best is None:
240
+ # the first label is the running winner everywhere until something beats it. Inside the image the
241
+ # indicators sum to ~1, so this is only ever a starting point; outside it every label scores zero
242
+ # and empty_value (if the caller gave one) has the final say below.
243
+ best = score
244
+ if nearest:
245
+ np.equal(nn_idx, 0, out=nn_top)
246
+ np.greater(score, 0, out=level)
247
+ nn_top &= level
248
+ del score
249
+ continue
250
+ if nearest:
251
+ np.equal(nn_idx, i, out=is_nn)
252
+ # `score > 0` guard: two labels that both score zero at a voxel are not tied for the win there,
253
+ # they are simply both absent, and some other label will claim it. `better` is free until it is
254
+ # computed below, so it holds that guard meanwhile.
255
+ np.equal(score, best, out=level)
256
+ np.greater(score, 0, out=better)
257
+ level &= better
258
+ level &= is_nn
259
+ nn_top |= level
260
+ # '>=' lets the later (larger) label take ties, '>' leaves them with the earlier (smaller) one
261
+ (np.greater_equal if seg_tiebreak == 'highest' else np.greater)(score, best, out=better)
262
+ if nearest:
263
+ np.copyto(nn_top, is_nn, where=better)
264
+ np.copyto(win, i, where=better)
265
+ np.maximum(best, score, out=best)
266
+ del score
267
+
268
+ if nearest:
269
+ # copyto rather than win[nn_top] = nn_idx[nn_top]: nn_top holds nearly everywhere (also wherever the
270
+ # nearest neighbour's label won outright), and the indexed form copies all of those out first
271
+ np.copyto(win, nn_idx, where=nn_top)
272
+ del nn_idx, nn_top, is_nn, level
273
+ del better
274
+ empty = best <= 0 if empty_value is not None else None
275
+ del best
276
+ result = labels.astype(dtype, copy=False)[win]
277
+ if empty is not None:
278
+ result[empty] = empty_value
279
+ return result
280
+
281
+
282
+ def _label_index_dtype(n_labels):
283
+ # signed, so that -1 can mean 'not a label'
284
+ for t in (np.int8, np.int16, np.int32):
285
+ if n_labels <= np.iinfo(t).max:
286
+ return t
287
+ return np.int64
288
+
289
+
290
+ def _nearest_label_index(labels, nn, idx_dtype, chunk=1 << 22):
291
+ """
292
+ The index into `labels` of each voxel of the nearest neighbour sample `nn` (float64), or -1 where it is not
293
+ a label (cval outside the image). `labels` is sorted, so this is a searchsorted, verified with the same
294
+ `==` that compared `nn` with each label before. Chunked, so that its int64 temporaries stay small.
295
+ """
296
+ out = np.empty(nn.shape, dtype=idx_dtype)
297
+ flat_nn, flat_out = nn.reshape(-1), out.reshape(-1)
298
+ last = len(labels) - 1
299
+ for s in range(0, flat_nn.size, chunk):
300
+ v = flat_nn[s:s + chunk]
301
+ idx = np.minimum(np.searchsorted(labels, v), last)
302
+ idx[labels[idx] != v] = -1
303
+ flat_out[s:s + chunk] = idx
304
+ return out
305
+
306
+
307
+ def interpolate_img(img, coords, order=3, mode='nearest', cval=0.0, is_seg=False, *, seg_tiebreak='nearest'):
146
308
  if is_seg and order != 0:
147
- unique_labels = np.unique(img)
148
- result = np.zeros(coords.shape[1:], img.dtype)
149
- for i, c in enumerate(unique_labels):
150
- res_new = map_coordinates((img == c).astype(float), coords, order=order, mode=mode, cval=cval)
151
- result[res_new >= 0.5] = c
152
- return result
309
+ return _resample_seg_by_argmax(
310
+ unique_labels(img),
311
+ # the indicators are padded with 0, not cval: outside the image no label is present. Padding them
312
+ # with cval raised every label's score by the same amount there, so for cval > 0 a voxel entirely
313
+ # outside was a tie among all labels rather than empty, and empty_value never reached it.
314
+ lambda c: map_coordinates((img == c).astype(float), coords, order=order, mode=mode, cval=0.0),
315
+ lambda: map_coordinates(img.astype(float), coords, order=0, mode=mode, cval=cval),
316
+ coords.shape[1:], img.dtype, seg_tiebreak,
317
+ # outside the image the caller asked for cval, and with the old zero-initialized result that is
318
+ # what those voxels happened to get whenever cval was 0. Keep it, explicitly and for any cval.
319
+ # Converted the way order 0 converts it, so a cval the dtype cannot hold (-1 in uint8) comes out
320
+ # the same at every order instead of raising here.
321
+ empty_value=np.array(cval, dtype=float).astype(img.dtype) if mode == 'constant' else None)
153
322
  else:
154
323
  return map_coordinates(img.astype(float), coords, order=order, mode=mode, cval=cval).astype(img.dtype)
155
324
 
@@ -581,14 +750,16 @@ def transpose_channels(batch):
581
750
  raise ValueError("wrong dimensions in transpose_channel generator!")
582
751
 
583
752
 
584
- def resize_segmentation(segmentation, new_shape, order=3):
753
+ def resize_segmentation(segmentation, new_shape, order=3, *, seg_tiebreak='nearest'):
585
754
  '''
586
- Resizes a segmentation map. Supports all orders (see skimage documentation). Will transform segmentation map to one
587
- hot encoding which is resized and transformed back to a segmentation map.
588
- This prevents interpolation artifacts ([0, 0, 2] -> [0, 1, 2])
755
+ Resizes a segmentation map. Supports all orders (see skimage documentation). Each label's indicator is
756
+ resized on its own and the per-voxel winner is the label with the largest interpolated indicator, so no
757
+ intermediate label value can be produced ([0, 0, 2] -> [0, 1, 2]).
589
758
  :param segmentation:
590
759
  :param new_shape:
591
760
  :param order:
761
+ :param seg_tiebreak: how to settle voxels where two labels share the top score ('nearest', 'lowest' or
762
+ 'highest'). See _resample_seg_by_argmax. 'nearest' is the only geometric choice and is the default.
592
763
  :return:
593
764
  '''
594
765
  tpe = segmentation.dtype
@@ -596,14 +767,13 @@ def resize_segmentation(segmentation, new_shape, order=3):
596
767
  if order == 0:
597
768
  return resize(segmentation.astype(float), new_shape, order, mode="edge", clip=True, anti_aliasing=False).astype(tpe)
598
769
  else:
599
- reshaped = np.zeros(new_shape, dtype=segmentation.dtype)
600
-
601
- unique_labels = np.sort(pd.unique(segmentation.ravel()))
602
- for i, c in enumerate(unique_labels):
603
- mask = segmentation == c
604
- reshaped_multihot = resize(mask.astype(float), new_shape, order, mode="edge", clip=True, anti_aliasing=False)
605
- reshaped[reshaped_multihot >= 0.5] = c
606
- return reshaped
770
+ return _resample_seg_by_argmax(
771
+ unique_labels(segmentation),
772
+ lambda c: resize((segmentation == c).astype(float), new_shape, order, mode="edge", clip=True,
773
+ anti_aliasing=False),
774
+ lambda: resize(segmentation.astype(float), new_shape, 0, mode="edge", clip=True,
775
+ anti_aliasing=False),
776
+ new_shape, tpe, seg_tiebreak)
607
777
 
608
778
 
609
779
  def resize_multichannel_image(multichannel_image, new_shape, order=3):
@@ -616,11 +786,18 @@ def resize_multichannel_image(multichannel_image, new_shape, order=3):
616
786
  :return:
617
787
  '''
618
788
  tpe = multichannel_image.dtype
789
+ is_int = np.issubdtype(tpe, np.integer)
619
790
  new_shp = [multichannel_image.shape[0]] + list(new_shape)
620
- result = np.zeros(new_shp, dtype=multichannel_image.dtype)
791
+ # Integer input is accumulated in float and rounded at the end. Writing the interpolated float
792
+ # straight into an integer array truncates towards zero, which shifts every voxel that did not land
793
+ # exactly on a sample point down by up to one intensity step. Floating point input keeps its own
794
+ # dtype, so nothing about that path changes and the buffer does not grow.
795
+ result = np.zeros(new_shp, dtype=float if is_int else tpe)
621
796
  for i in range(multichannel_image.shape[0]):
622
797
  result[i] = resize(multichannel_image[i].astype(float), new_shape, order, clip=True, anti_aliasing=False)
623
- return result.astype(tpe)
798
+ if is_int:
799
+ np.rint(result, out=result)
800
+ return result.astype(tpe, copy=False)
624
801
 
625
802
 
626
803
  def get_range_val(value, rnd_type="uniform"):