batchgenerators 0.25.4__tar.gz → 0.25.6__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 (70) hide show
  1. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/PKG-INFO +2 -4
  2. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/color_augmentations.py +6 -17
  3. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/crop_and_pad_augmentations.py +1 -1
  4. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/spatial_transformations.py +11 -25
  5. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/utils.py +67 -33
  6. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/multi_threaded_augmenter.py +33 -12
  7. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/nondet_multi_threaded_augmenter.py +73 -9
  8. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/cifar10.py +20 -52
  9. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/spatial_transforms.py +33 -63
  10. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/PKG-INFO +2 -4
  11. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/SOURCES.txt +7 -1
  12. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/requires.txt +1 -3
  13. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/pyproject.toml +2 -4
  14. batchgenerators-0.25.6/tests/test_augmenter_failures.py +155 -0
  15. batchgenerators-0.25.6/tests/test_augmenter_shutdown.py +53 -0
  16. batchgenerators-0.25.6/tests/test_channel_translation.py +74 -0
  17. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_color_augmentations.py +53 -0
  18. batchgenerators-0.25.6/tests/test_random_crop.py +153 -0
  19. batchgenerators-0.25.6/tests/test_resize_transforms.py +88 -0
  20. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_seg_resampling.py +35 -0
  21. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_spatial_transformations.py +62 -0
  22. batchgenerators-0.25.6/tests/test_worker_seeding.py +68 -0
  23. batchgenerators-0.25.6/tests/test_worker_signals.py +38 -0
  24. batchgenerators-0.25.4/tests/test_random_crop.py +0 -76
  25. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/LICENSE +0 -0
  26. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/README.md +0 -0
  27. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/__init__.py +0 -0
  28. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/__init__.py +0 -0
  29. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/noise_augmentations.py +0 -0
  30. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/normalizations.py +0 -0
  31. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/resample_augmentations.py +0 -0
  32. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/__init__.py +0 -0
  33. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/data_loader.py +0 -0
  34. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/dataset.py +0 -0
  35. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/single_threaded_augmenter.py +0 -0
  36. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/datasets/__init__.py +0 -0
  37. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/datasets/cifar.py +0 -0
  38. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/__init__.py +0 -0
  39. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/__init__.py +0 -0
  40. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_dataloader_2D.py +0 -0
  41. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_dataloader_3D.py +0 -0
  42. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_preprocessing.py +0 -0
  43. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/config.py +0 -0
  44. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/multithreaded_dataloading.py +0 -0
  45. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/__init__.py +0 -0
  46. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/abstract_transforms.py +0 -0
  47. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/channel_selection_transforms.py +0 -0
  48. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/color_transforms.py +0 -0
  49. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/crop_and_pad_transforms.py +0 -0
  50. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/local_transforms.py +0 -0
  51. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/noise_transforms.py +0 -0
  52. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/resample_transforms.py +0 -0
  53. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/sample_normalization_transforms.py +0 -0
  54. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/utility_transforms.py +0 -0
  55. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/__init__.py +0 -0
  56. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/custom_types.py +0 -0
  57. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/data_splitting.py +0 -0
  58. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/file_and_folder_operations.py +0 -0
  59. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/dependency_links.txt +0 -0
  60. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/top_level.txt +0 -0
  61. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/setup.cfg +0 -0
  62. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/setup.py +0 -0
  63. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_DataLoader.py +0 -0
  64. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_augment_zoom.py +0 -0
  65. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_axis_mirroring.py +0 -0
  66. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_crop.py +0 -0
  67. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_multithreaded_augmenter.py +0 -0
  68. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_normalizations.py +0 -0
  69. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_resample_augmentations.py +0 -0
  70. {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_sanity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchgenerators
3
- Version: 0.25.4
3
+ Version: 0.25.6
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
@@ -210,13 +210,11 @@ Keywords: data augmentation,deep learning,image segmentation,image classificatio
210
210
  Description-Content-Type: text/markdown
211
211
  License-File: LICENSE
212
212
  Requires-Dist: pillow>=7.1.2
213
- Requires-Dist: numpy>=1.10.2
213
+ Requires-Dist: numpy>=1.17
214
214
  Requires-Dist: scipy
215
215
  Requires-Dist: scikit-image
216
216
  Requires-Dist: scikit-learn
217
- Requires-Dist: future
218
217
  Requires-Dist: pandas
219
- Requires-Dist: unittest2
220
218
  Requires-Dist: threadpoolctl
221
219
  Dynamic: license-file
222
220
 
@@ -17,7 +17,8 @@ from builtins import range
17
17
  from typing import Tuple, Union, Callable
18
18
 
19
19
  import numpy as np
20
- from batchgenerators.augmentations.utils import general_cc_var_num_channels, illumination_jitter
20
+ from batchgenerators.augmentations.utils import general_cc_var_num_channels, illumination_jitter, \
21
+ sample_factor_around_one
21
22
 
22
23
 
23
24
  def augment_contrast(data_sample: np.ndarray,
@@ -29,10 +30,7 @@ def augment_contrast(data_sample: np.ndarray,
29
30
  if callable(contrast_range):
30
31
  factor = contrast_range()
31
32
  else:
32
- if np.random.random() < 0.5 and contrast_range[0] < 1:
33
- factor = np.random.uniform(contrast_range[0], 1)
34
- else:
35
- factor = np.random.uniform(max(contrast_range[0], 1), contrast_range[1])
33
+ factor = sample_factor_around_one(contrast_range)
36
34
 
37
35
  for c in range(data_sample.shape[0]):
38
36
  if np.random.uniform() < p_per_channel:
@@ -52,10 +50,7 @@ def augment_contrast(data_sample: np.ndarray,
52
50
  if callable(contrast_range):
53
51
  factor = contrast_range()
54
52
  else:
55
- if np.random.random() < 0.5 and contrast_range[0] < 1:
56
- factor = np.random.uniform(contrast_range[0], 1)
57
- else:
58
- factor = np.random.uniform(max(contrast_range[0], 1), contrast_range[1])
53
+ factor = sample_factor_around_one(contrast_range)
59
54
 
60
55
  mn = data_sample[c].mean()
61
56
  if preserve_range:
@@ -114,10 +109,7 @@ def augment_gamma(data_sample, gamma_range=(0.5, 2), invert_image=False, epsilon
114
109
  if retain_stats_here:
115
110
  mn = data_sample.mean()
116
111
  sd = data_sample.std()
117
- if np.random.random() < 0.5 and gamma_range[0] < 1:
118
- gamma = np.random.uniform(gamma_range[0], 1)
119
- else:
120
- gamma = np.random.uniform(max(gamma_range[0], 1), gamma_range[1])
112
+ gamma = sample_factor_around_one(gamma_range)
121
113
  minm = data_sample.min()
122
114
  rnge = data_sample.max() - minm
123
115
  data_sample = np.power(((data_sample - minm) / float(rnge + epsilon)), gamma) * rnge + minm
@@ -131,10 +123,7 @@ def augment_gamma(data_sample, gamma_range=(0.5, 2), invert_image=False, epsilon
131
123
  if retain_stats_here:
132
124
  mn = data_sample[c].mean()
133
125
  sd = data_sample[c].std()
134
- if np.random.random() < 0.5 and gamma_range[0] < 1:
135
- gamma = np.random.uniform(gamma_range[0], 1)
136
- else:
137
- gamma = np.random.uniform(max(gamma_range[0], 1), gamma_range[1])
126
+ gamma = sample_factor_around_one(gamma_range)
138
127
  minm = data_sample[c].min()
139
128
  rnge = data_sample[c].max() - minm
140
129
  data_sample[c] = np.power(((data_sample[c] - minm) / float(rnge + epsilon)), gamma) * float(rnge + epsilon) + minm
@@ -33,7 +33,7 @@ def get_lbs_for_random_crop(crop_size, data_shape, margins):
33
33
  lbs = []
34
34
  for i in range(len(data_shape) - 2):
35
35
  if data_shape[i+2] - crop_size[i] - margins[i] > margins[i]:
36
- lbs.append(np.random.randint(margins[i], data_shape[i+2] - crop_size[i] - margins[i]))
36
+ lbs.append(np.random.randint(margins[i], data_shape[i+2] - crop_size[i] - margins[i] + 1))
37
37
  else:
38
38
  lbs.append((data_shape[i+2] - crop_size[i]) // 2)
39
39
  return lbs
@@ -21,7 +21,7 @@ from batchgenerators.augmentations.utils import create_zero_centered_coordinate_
21
21
  interpolate_img, \
22
22
  rotate_coords_2d, rotate_coords_3d, scale_coords, resize_segmentation, resize_multichannel_image, \
23
23
  elastic_deform_coordinates_2, \
24
- get_organ_gradient_field, ignore_anatomy
24
+ get_organ_gradient_field, ignore_anatomy, sample_factor_around_one
25
25
  from batchgenerators.augmentations.crop_and_pad_augmentations import random_crop as random_crop_aug
26
26
  from batchgenerators.augmentations.crop_and_pad_augmentations import center_crop as center_crop_aug
27
27
 
@@ -143,8 +143,8 @@ def augment_channel_translation(data, const_channel=0, max_shifts=None):
143
143
 
144
144
  shape = data.shape
145
145
 
146
- const_data = data[:, [const_channel]]
147
- trans_data = data[:, [i for i in range(shape[1]) if i != const_channel]]
146
+ trans_channels = [i for i in range(shape[1]) if i != const_channel]
147
+ trans_data = data[:, trans_channels]
148
148
 
149
149
  # iterate the batch dimension
150
150
  for j in range(shape[0]):
@@ -161,7 +161,7 @@ def augment_channel_translation(data, const_channel=0, max_shifts=None):
161
161
 
162
162
  # iterate the image dimensions, randomly draw shifts/translations
163
163
  for i, v in enumerate(dims):
164
- rand_shift = np.random.choice(list(range(-max_shifts[v], max_shifts[v], 1)))
164
+ rand_shift = np.random.choice(list(range(-max_shifts[v], max_shifts[v] + 1)))
165
165
 
166
166
  if rand_shift > 0:
167
167
  ixs[v] = {'lo': 0, 'hi': -rand_shift}
@@ -184,7 +184,9 @@ def augment_channel_translation(data, const_channel=0, max_shifts=None):
184
184
 
185
185
  trans_data[j] = slice
186
186
 
187
- data_return = np.concatenate([const_data, trans_data], axis=1)
187
+ # the constant channel keeps its position
188
+ data_return = data.copy()
189
+ data_return[:, trans_channels] = trans_data
188
190
  return data_return
189
191
 
190
192
 
@@ -249,17 +251,9 @@ def augment_spatial(data, seg, patch_size, patch_center_dist_from_border=30,
249
251
 
250
252
  if do_scale and np.random.uniform() < p_scale_per_sample:
251
253
  if independent_scale_for_each_axis and np.random.uniform() < p_independent_scale_per_axis:
252
- sc = []
253
- for _ in range(dim):
254
- if np.random.random() < 0.5 and scale[0] < 1:
255
- sc.append(np.random.uniform(scale[0], 1))
256
- else:
257
- sc.append(np.random.uniform(max(scale[0], 1), scale[1]))
254
+ sc = [sample_factor_around_one(scale) for _ in range(dim)]
258
255
  else:
259
- if np.random.random() < 0.5 and scale[0] < 1:
260
- sc = np.random.uniform(scale[0], 1)
261
- else:
262
- sc = np.random.uniform(max(scale[0], 1), scale[1])
256
+ sc = sample_factor_around_one(scale)
263
257
 
264
258
  coords = scale_coords(coords, sc)
265
259
  modified_coords = True
@@ -413,17 +407,9 @@ def augment_spatial_2(data, seg, patch_size, patch_center_dist_from_border=30,
413
407
 
414
408
  if do_scale and np.random.uniform() < p_scale_per_sample:
415
409
  if independent_scale_for_each_axis and np.random.uniform() < p_independent_scale_per_axis:
416
- sc = []
417
- for _ in range(dim):
418
- if np.random.random() < 0.5 and scale[0] < 1:
419
- sc.append(np.random.uniform(scale[0], 1))
420
- else:
421
- sc.append(np.random.uniform(max(scale[0], 1), scale[1]))
410
+ sc = [sample_factor_around_one(scale) for _ in range(dim)]
422
411
  else:
423
- if np.random.random() < 0.5 and scale[0] < 1:
424
- sc = np.random.uniform(scale[0], 1)
425
- else:
426
- sc = np.random.uniform(max(scale[0], 1), scale[1])
412
+ sc = sample_factor_around_one(scale)
427
413
 
428
414
  coords = scale_coords(coords, sc)
429
415
  modified_coords = True
@@ -17,6 +17,7 @@ import random
17
17
  import numpy as np
18
18
  from copy import deepcopy
19
19
  from scipy.ndimage import (
20
+ find_objects,
20
21
  fourier_gaussian,
21
22
  gaussian_filter,
22
23
  gaussian_gradient_magnitude,
@@ -156,6 +157,19 @@ def scale_coords(coords, scale):
156
157
  return coords
157
158
 
158
159
 
160
+ def sample_factor_around_one(factor_range):
161
+ """
162
+ Draws a factor (scale, contrast, gamma) from factor_range. If the range contains one, factors below and above one
163
+ are equally likely, however asymmetric the range is around one: (0.5, 2) shrinks as often as it enlarges. A range
164
+ that ends at or below one, or starts at or above one, is sampled uniformly.
165
+ """
166
+ if np.random.random() < 0.5 and factor_range[0] < 1:
167
+ return np.random.uniform(factor_range[0], min(1, factor_range[1]))
168
+ if factor_range[1] <= 1:
169
+ return np.random.uniform(factor_range[0], factor_range[1])
170
+ return np.random.uniform(max(factor_range[0], 1), factor_range[1])
171
+
172
+
159
173
  def uncenter_coords(coords):
160
174
  shp = coords.shape[1:]
161
175
  coords = deepcopy(coords)
@@ -207,6 +221,10 @@ def _resample_seg_by_argmax(labels, score_fn, nearest_fn, out_shape, dtype, seg_
207
221
  if seg_tiebreak not in SEG_TIEBREAKS:
208
222
  raise ValueError('unknown seg_tiebreak: %s. Must be one of %s' % (seg_tiebreak, str(SEG_TIEBREAKS)))
209
223
  out_shape = tuple(out_shape)
224
+ if len(labels) == 0:
225
+ # an empty input: there is nothing to sample, so every output voxel is outside the image. Returning here
226
+ # also keeps the nearest neighbour sample below from being asked of an empty array, which fails.
227
+ return np.full(out_shape, 0 if empty_value is None else empty_value, dtype=dtype)
210
228
  if len(labels) == 1 and empty_value is None:
211
229
  # nothing to interpolate, and this also keeps the loop below from having to handle a missing `best`
212
230
  return np.full(out_shape, labels[0], dtype=dtype)
@@ -223,15 +241,22 @@ def _resample_seg_by_argmax(labels, score_fn, nearest_fn, out_shape, dtype, seg_
223
241
  better = np.empty(out_shape, dtype=bool)
224
242
  # 'nearest': the nearest neighbour label settles a tie only if it is one of the tied labels. Where three
225
243
  # 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.
244
+ # 1 and 2 at 0.375 each and 3 at 0.25, and its nearest neighbour is a 3. So the nearest neighbour settles
245
+ # a voxel exactly where its own label ends up with the top score (it changes nothing where that label
246
+ # won outright), and elsewhere the argmax winner stands. That is decided in two halves:
247
+ # * at the nearest neighbour's own turn in the loop: does its label take the lead, or draw level with a
248
+ # positive score? -> nn_cand
249
+ # * after the loop: did no later label take the lead from it? The running winner only ever changes to a
250
+ # later label, and only on a strict improvement, so that is exactly win <= nn_idx.
251
+ # The first half only looks at voxels whose nearest neighbour is the current label, i.e. inside that
252
+ # label's bounding box in nn_idx, typically a few % of the volume, and nothing has to be tracked through
253
+ # the loop for the other labels.
231
254
  nearest = seg_tiebreak == 'nearest'
232
255
  if nearest:
233
256
  nn_idx = _nearest_label_index(labels, nearest_fn(), idx_dtype)
234
- nn_top = np.empty(out_shape, dtype=bool)
257
+ # boxes of the label indices >= 1 (find_objects does not report index 0, nor -1 = 'not a label')
258
+ nn_boxes = find_objects(nn_idx)
259
+ nn_cand = np.zeros(out_shape, dtype=bool)
235
260
  is_nn = np.empty(out_shape, dtype=bool)
236
261
  level = np.empty(out_shape, dtype=bool)
237
262
  for i, c in enumerate(labels):
@@ -242,34 +267,39 @@ def _resample_seg_by_argmax(labels, score_fn, nearest_fn, out_shape, dtype, seg_
242
267
  # and empty_value (if the caller gave one) has the final say below.
243
268
  best = score
244
269
  if nearest:
245
- np.equal(nn_idx, 0, out=nn_top)
246
- np.greater(score, 0, out=level)
247
- nn_top &= level
270
+ # `score > 0` guard: a label that scores zero at a voxel is absent there, not leading
271
+ np.equal(nn_idx, 0, out=is_nn)
272
+ np.greater(score, 0, out=nn_cand)
273
+ nn_cand &= is_nn
248
274
  del score
249
275
  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
276
  # '>=' lets the later (larger) label take ties, '>' leaves them with the earlier (smaller) one
261
277
  (np.greater_equal if seg_tiebreak == 'highest' else np.greater)(score, best, out=better)
262
278
  if nearest:
263
- np.copyto(nn_top, is_nn, where=better)
279
+ box = nn_boxes[i - 1] if i - 1 < len(nn_boxes) else None
280
+ if box is not None:
281
+ # nn_cand = is_nn and (takes the lead, or draws level with a positive score), against `best`
282
+ # before it is updated. `score > 0` guard on drawing level: two labels that both score zero at
283
+ # a voxel are not tied for the win there, they are simply both absent.
284
+ is_nn_b, level_b, score_b = is_nn[box], level[box], score[box]
285
+ np.greater(score_b, 0, out=level_b)
286
+ np.greater_equal(score_b, best[box], out=is_nn_b)
287
+ level_b &= is_nn_b
288
+ level_b |= better[box]
289
+ np.equal(nn_idx[box], i, out=is_nn_b)
290
+ level_b &= is_nn_b
291
+ nn_cand[box] |= level_b
264
292
  np.copyto(win, i, where=better)
265
293
  np.maximum(best, score, out=best)
266
294
  del score
267
295
 
268
296
  if nearest:
269
- # copyto rather than win[nn_top] = nn_idx[nn_top]: nn_top holds nearly everywhere (also wherever the
297
+ np.less_equal(win, nn_idx, out=level)
298
+ level &= nn_cand
299
+ # copyto rather than win[level] = nn_idx[level]: this holds nearly everywhere (also wherever the
270
300
  # 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
301
+ np.copyto(win, nn_idx, where=level)
302
+ del nn_idx, nn_cand, is_nn, level
273
303
  del better
274
304
  empty = best <= 0 if empty_value is not None else None
275
305
  del best
@@ -397,21 +427,21 @@ def random_crop_3D_image(img, crop_size):
397
427
  img.shape), "If you provide a list/tuple as center crop make sure it has the same len as your data has dims (3d)"
398
428
 
399
429
  if crop_size[0] < img.shape[0]:
400
- lb_x = np.random.randint(0, img.shape[0] - crop_size[0])
430
+ lb_x = np.random.randint(0, img.shape[0] - crop_size[0] + 1)
401
431
  elif crop_size[0] == img.shape[0]:
402
432
  lb_x = 0
403
433
  else:
404
434
  raise ValueError("crop_size[0] must be smaller or equal to the images x dimension")
405
435
 
406
436
  if crop_size[1] < img.shape[1]:
407
- lb_y = np.random.randint(0, img.shape[1] - crop_size[1])
437
+ lb_y = np.random.randint(0, img.shape[1] - crop_size[1] + 1)
408
438
  elif crop_size[1] == img.shape[1]:
409
439
  lb_y = 0
410
440
  else:
411
441
  raise ValueError("crop_size[1] must be smaller or equal to the images y dimension")
412
442
 
413
443
  if crop_size[2] < img.shape[2]:
414
- lb_z = np.random.randint(0, img.shape[2] - crop_size[2])
444
+ lb_z = np.random.randint(0, img.shape[2] - crop_size[2] + 1)
415
445
  elif crop_size[2] == img.shape[2]:
416
446
  lb_z = 0
417
447
  else:
@@ -428,21 +458,21 @@ def random_crop_3D_image_batched(img, crop_size):
428
458
  img.shape) - 2), "If you provide a list/tuple as center crop make sure it has the same len as your data has dims (3d)"
429
459
 
430
460
  if crop_size[0] < img.shape[2]:
431
- lb_x = np.random.randint(0, img.shape[2] - crop_size[0])
461
+ lb_x = np.random.randint(0, img.shape[2] - crop_size[0] + 1)
432
462
  elif crop_size[0] == img.shape[2]:
433
463
  lb_x = 0
434
464
  else:
435
465
  raise ValueError("crop_size[0] must be smaller or equal to the images x dimension")
436
466
 
437
467
  if crop_size[1] < img.shape[3]:
438
- lb_y = np.random.randint(0, img.shape[3] - crop_size[1])
468
+ lb_y = np.random.randint(0, img.shape[3] - crop_size[1] + 1)
439
469
  elif crop_size[1] == img.shape[3]:
440
470
  lb_y = 0
441
471
  else:
442
472
  raise ValueError("crop_size[1] must be smaller or equal to the images y dimension")
443
473
 
444
474
  if crop_size[2] < img.shape[4]:
445
- lb_z = np.random.randint(0, img.shape[4] - crop_size[2])
475
+ lb_z = np.random.randint(0, img.shape[4] - crop_size[2] + 1)
446
476
  elif crop_size[2] == img.shape[4]:
447
477
  lb_z = 0
448
478
  else:
@@ -459,14 +489,14 @@ def random_crop_2D_image(img, crop_size):
459
489
  img.shape), "If you provide a list/tuple as center crop make sure it has the same len as your data has dims (2d)"
460
490
 
461
491
  if crop_size[0] < img.shape[0]:
462
- lb_x = np.random.randint(0, img.shape[0] - crop_size[0])
492
+ lb_x = np.random.randint(0, img.shape[0] - crop_size[0] + 1)
463
493
  elif crop_size[0] == img.shape[0]:
464
494
  lb_x = 0
465
495
  else:
466
496
  raise ValueError("crop_size[0] must be smaller or equal to the images x dimension")
467
497
 
468
498
  if crop_size[1] < img.shape[1]:
469
- lb_y = np.random.randint(0, img.shape[1] - crop_size[1])
499
+ lb_y = np.random.randint(0, img.shape[1] - crop_size[1] + 1)
470
500
  elif crop_size[1] == img.shape[1]:
471
501
  lb_y = 0
472
502
  else:
@@ -483,14 +513,14 @@ def random_crop_2D_image_batched(img, crop_size):
483
513
  img.shape) - 2), "If you provide a list/tuple as center crop make sure it has the same len as your data has dims (2d)"
484
514
 
485
515
  if crop_size[0] < img.shape[2]:
486
- lb_x = np.random.randint(0, img.shape[2] - crop_size[0])
516
+ lb_x = np.random.randint(0, img.shape[2] - crop_size[0] + 1)
487
517
  elif crop_size[0] == img.shape[2]:
488
518
  lb_x = 0
489
519
  else:
490
520
  raise ValueError("crop_size[0] must be smaller or equal to the images x dimension")
491
521
 
492
522
  if crop_size[1] < img.shape[3]:
493
- lb_y = np.random.randint(0, img.shape[3] - crop_size[1])
523
+ lb_y = np.random.randint(0, img.shape[3] - crop_size[1] + 1)
494
524
  elif crop_size[1] == img.shape[3]:
495
525
  lb_y = 0
496
526
  else:
@@ -764,6 +794,10 @@ def resize_segmentation(segmentation, new_shape, order=3, *, seg_tiebreak='neare
764
794
  '''
765
795
  tpe = segmentation.dtype
766
796
  assert len(segmentation.shape) == len(new_shape), "new shape must have same dimensionality as segmentation"
797
+ if segmentation.size == 0:
798
+ # skimage cannot resize an empty array (scipy's zoom divides 0 by 0), at any order. Zeros of new_shape is
799
+ # what orders >= 1 returned before the argmax, from their zero-initialized result.
800
+ return np.zeros(new_shape, dtype=tpe)
767
801
  if order == 0:
768
802
  return resize(segmentation.astype(float), new_shape, order, mode="edge", clip=True, anti_aliasing=False).astype(tpe)
769
803
  else:
@@ -25,22 +25,32 @@ from multiprocessing import Event
25
25
  from time import sleep, time
26
26
  from threadpoolctl import threadpool_limits
27
27
 
28
- from batchgenerators.dataloading.nondet_multi_threaded_augmenter import pin_memory_of_all_eligible_items_in_dict
28
+ from batchgenerators.dataloading.nondet_multi_threaded_augmenter import pin_memory_of_all_eligible_items_in_dict, \
29
+ init_worker, ParentWatch
29
30
 
30
31
  try:
31
32
  import torch
32
33
  except ImportError:
33
34
  torch = None
34
35
 
36
+ logger = logging.getLogger(__name__)
37
+
35
38
 
36
39
  def producer(queue, data_loader, transform, thread_id, seed, abort_event,
37
40
  pause_event=None, wait_time: float = 0.02):
38
- np.random.seed(seed)
41
+ init_worker(seed)
39
42
  data_loader.set_thread_id(thread_id)
40
43
  item = None
44
+ parent = ParentWatch()
41
45
 
42
46
  try:
43
47
  while not abort_event.is_set():
48
+ # Nobody reads the queue once the parent is gone, so don't let the exit wait for the queue's feeder thread
49
+ # either.
50
+ if parent.parent_died():
51
+ queue.cancel_join_thread()
52
+ return
53
+
44
54
  # When paused, hold any in-flight item and stop interacting with the
45
55
  # queue. This guarantees no new bytes hit the pipe after pause is
46
56
  # observed, which is what _finish() relies on for safe shutdown.
@@ -106,6 +116,13 @@ def results_loop(in_queues: List[Queue], out_queue: thrQueue, abort_event: Event
106
116
  item = current_queue.get(timeout=wait_time)
107
117
  except Empty:
108
118
  continue # retry the same queue; abort_event re-checked at top of loop
119
+ except Exception:
120
+ # A worker can still put a batch during shutdown and exit right after. Torch tensors are unpickled
121
+ # by fetching their file descriptor from the sending process, so that batch may fail to load. It
122
+ # would be discarded anyway.
123
+ if abort_event.is_set():
124
+ return
125
+ raise
109
126
  # if we do pin memory, do it now, otherwise skip this. The isinstance(dict) guard keeps the 'end'
110
127
  # string sentinel from reaching the pinning logic.
111
128
  if do_pin_memory:
@@ -128,6 +145,10 @@ def results_loop(in_queues: List[Queue], out_queue: thrQueue, abort_event: Event
128
145
  except KeyboardInterrupt:
129
146
  abort_event.set()
130
147
  raise KeyboardInterrupt
148
+ except Exception:
149
+ # the main thread only watches abort_event, without it next() would wait for batches forever
150
+ abort_event.set()
151
+ raise
131
152
 
132
153
 
133
154
  class MultiThreadedAugmenter(object):
@@ -143,7 +164,7 @@ class MultiThreadedAugmenter(object):
143
164
  num_cached_per_queue (int): number of batches cached per process (each process has its own
144
165
  multiprocessing.Queue). We found 2 to be ideal.
145
166
  seeds (list of int): one seed for each worker. Must have len(num_processes).
146
- If None then seeds = range(num_processes)
167
+ If None, every worker seeds itself from the OS and runs are not reproducible
147
168
  pin_memory (bool): set to True if all torch tensors in data_dict are to be pinned. Pytorch only.
148
169
  timeout (int): How long do we wait for the background workers to do stuff? If timeout seconds have passed and
149
170
  self.__get_next_item still has not gotten an item from the workers we will perform a check whether all
@@ -186,8 +207,8 @@ class MultiThreadedAugmenter(object):
186
207
  while True:
187
208
  if self.abort_event.is_set():
188
209
  self._finish()
189
- raise RuntimeError("One or more background workers are no longer alive. Exiting. Please check the "
190
- "print statements above for the actual error message")
210
+ raise RuntimeError("A background worker or the thread collecting their batches failed. Exiting. Please "
211
+ "check the output above for the actual error message")
191
212
 
192
213
  try:
193
214
  return self.pin_memory_queue.get(timeout=self.wait_time)
@@ -206,7 +227,7 @@ class MultiThreadedAugmenter(object):
206
227
  if self._end_ctr == self.num_processes:
207
228
  self._end_ctr = 0
208
229
  self._queue_ctr = 0
209
- logging.debug("MultiThreadedGenerator: finished data generation")
230
+ logger.debug("MultiThreadedGenerator: finished data generation")
210
231
  raise StopIteration
211
232
 
212
233
  item = self.__get_next_item()
@@ -214,7 +235,7 @@ class MultiThreadedAugmenter(object):
214
235
  return item
215
236
 
216
237
  except KeyboardInterrupt:
217
- logging.error("MultiThreadedGenerator: caught exception: {}".format(sys.exc_info()))
238
+ logger.error("MultiThreadedGenerator: caught exception: {}".format(sys.exc_info()))
218
239
  self.abort_event.set()
219
240
  self._finish()
220
241
  raise KeyboardInterrupt
@@ -225,7 +246,7 @@ class MultiThreadedAugmenter(object):
225
246
  self.abort_event.clear()
226
247
  self.pause_event.clear()
227
248
 
228
- logging.debug("starting workers")
249
+ logger.debug("starting workers")
229
250
  self._queue_ctr = 0
230
251
  self._end_ctr = 0
231
252
 
@@ -258,8 +279,8 @@ class MultiThreadedAugmenter(object):
258
279
 
259
280
  self.was_initialized = True
260
281
  else:
261
- logging.debug("MultiThreadedGenerator Warning: start() has been called but it has already been "
262
- "initialized previously")
282
+ logger.debug("MultiThreadedGenerator Warning: start() has been called but it has already been "
283
+ "initialized previously")
263
284
 
264
285
  def _finish(self, timeout=10, force=False):
265
286
  """Shut down workers and the pin-memory thread.
@@ -303,7 +324,7 @@ class MultiThreadedAugmenter(object):
303
324
  # feeder threads need pipe space to flush before the worker
304
325
  # process can fully exit; draining concurrently unblocks them.
305
326
  if len(self._processes) > 0:
306
- logging.debug("MultiThreadedGenerator: shutting down workers...")
327
+ logger.debug("MultiThreadedGenerator: shutting down workers...")
307
328
  deadline = time() + timeout
308
329
  drain_tick = max(self.wait_time, 0.01)
309
330
  while time() < deadline and any(p.is_alive() for p in self._processes):
@@ -348,7 +369,7 @@ class MultiThreadedAugmenter(object):
348
369
  self._start()
349
370
 
350
371
  def __del__(self):
351
- logging.debug("MultiThreadedGenerator: destructor was called")
372
+ logger.debug("MultiThreadedGenerator: destructor was called")
352
373
  # Interpreter shutdown may have already torn down parts of
353
374
  # multiprocessing; take the fast path with a short timeout.
354
375
  self._finish(timeout=2, force=True)