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.
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/PKG-INFO +2 -4
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/color_augmentations.py +6 -17
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/crop_and_pad_augmentations.py +1 -1
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/spatial_transformations.py +11 -25
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/utils.py +67 -33
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/multi_threaded_augmenter.py +33 -12
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/nondet_multi_threaded_augmenter.py +73 -9
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/cifar10.py +20 -52
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/spatial_transforms.py +33 -63
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/PKG-INFO +2 -4
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/SOURCES.txt +7 -1
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/requires.txt +1 -3
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/pyproject.toml +2 -4
- batchgenerators-0.25.6/tests/test_augmenter_failures.py +155 -0
- batchgenerators-0.25.6/tests/test_augmenter_shutdown.py +53 -0
- batchgenerators-0.25.6/tests/test_channel_translation.py +74 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_color_augmentations.py +53 -0
- batchgenerators-0.25.6/tests/test_random_crop.py +153 -0
- batchgenerators-0.25.6/tests/test_resize_transforms.py +88 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_seg_resampling.py +35 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_spatial_transformations.py +62 -0
- batchgenerators-0.25.6/tests/test_worker_seeding.py +68 -0
- batchgenerators-0.25.6/tests/test_worker_signals.py +38 -0
- batchgenerators-0.25.4/tests/test_random_crop.py +0 -76
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/LICENSE +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/README.md +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/noise_augmentations.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/normalizations.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/augmentations/resample_augmentations.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/data_loader.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/dataset.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/dataloading/single_threaded_augmenter.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/datasets/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/datasets/cifar.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_dataloader_2D.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_dataloader_3D.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/brats2017_preprocessing.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/brats2017/config.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/examples/multithreaded_dataloading.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/abstract_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/channel_selection_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/color_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/crop_and_pad_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/local_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/noise_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/resample_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/sample_normalization_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/transforms/utility_transforms.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/__init__.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/custom_types.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/data_splitting.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators/utilities/file_and_folder_operations.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/dependency_links.txt +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/batchgenerators.egg-info/top_level.txt +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/setup.cfg +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/setup.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_DataLoader.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_augment_zoom.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_axis_mirroring.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_crop.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_multithreaded_augmenter.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_normalizations.py +0 -0
- {batchgenerators-0.25.4 → batchgenerators-0.25.6}/tests/test_resample_augmentations.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
trans_data = data[:,
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
np.
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
272
|
-
del nn_idx,
|
|
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
|
-
|
|
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
|
|
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("
|
|
190
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|