batchgenerators 0.25.3__tar.gz → 0.25.5__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.3 → batchgenerators-0.25.5}/PKG-INFO +1 -1
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/spatial_transformations.py +16 -12
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/utils.py +212 -27
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/spatial_transforms.py +22 -10
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/PKG-INFO +1 -1
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/SOURCES.txt +1 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/pyproject.toml +1 -1
- batchgenerators-0.25.5/tests/test_seg_resampling.py +282 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/LICENSE +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/README.md +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/color_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/crop_and_pad_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/noise_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/normalizations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/resample_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/data_loader.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/dataset.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/multi_threaded_augmenter.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/nondet_multi_threaded_augmenter.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/single_threaded_augmenter.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/datasets/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/datasets/cifar.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/brats2017_dataloader_2D.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/brats2017_dataloader_3D.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/brats2017_preprocessing.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/config.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/cifar10.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/multithreaded_dataloading.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/abstract_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/channel_selection_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/color_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/crop_and_pad_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/local_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/noise_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/resample_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/sample_normalization_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/utility_transforms.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/utilities/__init__.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/utilities/custom_types.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/utilities/data_splitting.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/utilities/file_and_folder_operations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/dependency_links.txt +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/requires.txt +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/top_level.txt +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/setup.cfg +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/setup.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_DataLoader.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_augment_zoom.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_axis_mirroring.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_color_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_crop.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_multithreaded_augmenter.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_normalizations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_random_crop.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_resample_augmentations.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/tests/test_sanity.py +0 -0
- {batchgenerators-0.25.3 → batchgenerators-0.25.5}/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.
|
|
3
|
+
Version: 0.25.5
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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 =
|
|
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 =
|
|
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,165 @@ def uncenter_coords(coords):
|
|
|
142
164
|
return coords
|
|
143
165
|
|
|
144
166
|
|
|
145
|
-
|
|
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) == 0:
|
|
211
|
+
# an empty input: there is nothing to sample, so every output voxel is outside the image. Returning here
|
|
212
|
+
# also keeps the nearest neighbour sample below from being asked of an empty array, which fails.
|
|
213
|
+
return np.full(out_shape, 0 if empty_value is None else empty_value, dtype=dtype)
|
|
214
|
+
if len(labels) == 1 and empty_value is None:
|
|
215
|
+
# nothing to interpolate, and this also keeps the loop below from having to handle a missing `best`
|
|
216
|
+
return np.full(out_shape, labels[0], dtype=dtype)
|
|
217
|
+
|
|
218
|
+
# Memory: the scores are float64 (that is what the interpolators return, and ties are decided by exact
|
|
219
|
+
# equality on them), so `best` and the current `score` are the two large buffers and everything else is
|
|
220
|
+
# kept small. The running winner and the nearest neighbour are held as int8/int16 indices into `labels`
|
|
221
|
+
# and mapped to label values once at the end, the per-label masks are three reused bool buffers, and the
|
|
222
|
+
# previous score is released before the next one is allocated. Without that, peak memory was about twice
|
|
223
|
+
# the bytes per output voxel.
|
|
224
|
+
idx_dtype = _label_index_dtype(len(labels))
|
|
225
|
+
win = np.zeros(out_shape, dtype=idx_dtype)
|
|
226
|
+
best = None
|
|
227
|
+
better = np.empty(out_shape, dtype=bool)
|
|
228
|
+
# 'nearest': the nearest neighbour label settles a tie only if it is one of the tied labels. Where three
|
|
229
|
+
# or more labels meet it need not be - a 2x2x2 block [3,1,1,1,2,2,2,3] sampled at its centre scores
|
|
230
|
+
# 1 and 2 at 0.375 each and 3 at 0.25, and its nearest neighbour is a 3. So instead of marking tied
|
|
231
|
+
# voxels, track whether the nearest neighbour's label currently has the top score: a label that takes
|
|
232
|
+
# the lead strictly decides that afresh, one that draws level can only add to it. Where it holds at the
|
|
233
|
+
# end the nearest neighbour is the answer (it changes nothing where its label won outright), and where
|
|
234
|
+
# it does not the argmax winner stands.
|
|
235
|
+
nearest = seg_tiebreak == 'nearest'
|
|
236
|
+
if nearest:
|
|
237
|
+
nn_idx = _nearest_label_index(labels, nearest_fn(), idx_dtype)
|
|
238
|
+
nn_top = np.empty(out_shape, dtype=bool)
|
|
239
|
+
is_nn = np.empty(out_shape, dtype=bool)
|
|
240
|
+
level = np.empty(out_shape, dtype=bool)
|
|
241
|
+
for i, c in enumerate(labels):
|
|
242
|
+
score = score_fn(c)
|
|
243
|
+
if best is None:
|
|
244
|
+
# the first label is the running winner everywhere until something beats it. Inside the image the
|
|
245
|
+
# indicators sum to ~1, so this is only ever a starting point; outside it every label scores zero
|
|
246
|
+
# and empty_value (if the caller gave one) has the final say below.
|
|
247
|
+
best = score
|
|
248
|
+
if nearest:
|
|
249
|
+
np.equal(nn_idx, 0, out=nn_top)
|
|
250
|
+
np.greater(score, 0, out=level)
|
|
251
|
+
nn_top &= level
|
|
252
|
+
del score
|
|
253
|
+
continue
|
|
254
|
+
if nearest:
|
|
255
|
+
np.equal(nn_idx, i, out=is_nn)
|
|
256
|
+
# `score > 0` guard: two labels that both score zero at a voxel are not tied for the win there,
|
|
257
|
+
# they are simply both absent, and some other label will claim it. `better` is free until it is
|
|
258
|
+
# computed below, so it holds that guard meanwhile.
|
|
259
|
+
np.equal(score, best, out=level)
|
|
260
|
+
np.greater(score, 0, out=better)
|
|
261
|
+
level &= better
|
|
262
|
+
level &= is_nn
|
|
263
|
+
nn_top |= level
|
|
264
|
+
# '>=' lets the later (larger) label take ties, '>' leaves them with the earlier (smaller) one
|
|
265
|
+
(np.greater_equal if seg_tiebreak == 'highest' else np.greater)(score, best, out=better)
|
|
266
|
+
if nearest:
|
|
267
|
+
np.copyto(nn_top, is_nn, where=better)
|
|
268
|
+
np.copyto(win, i, where=better)
|
|
269
|
+
np.maximum(best, score, out=best)
|
|
270
|
+
del score
|
|
271
|
+
|
|
272
|
+
if nearest:
|
|
273
|
+
# copyto rather than win[nn_top] = nn_idx[nn_top]: nn_top holds nearly everywhere (also wherever the
|
|
274
|
+
# nearest neighbour's label won outright), and the indexed form copies all of those out first
|
|
275
|
+
np.copyto(win, nn_idx, where=nn_top)
|
|
276
|
+
del nn_idx, nn_top, is_nn, level
|
|
277
|
+
del better
|
|
278
|
+
empty = best <= 0 if empty_value is not None else None
|
|
279
|
+
del best
|
|
280
|
+
result = labels.astype(dtype, copy=False)[win]
|
|
281
|
+
if empty is not None:
|
|
282
|
+
result[empty] = empty_value
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _label_index_dtype(n_labels):
|
|
287
|
+
# signed, so that -1 can mean 'not a label'
|
|
288
|
+
for t in (np.int8, np.int16, np.int32):
|
|
289
|
+
if n_labels <= np.iinfo(t).max:
|
|
290
|
+
return t
|
|
291
|
+
return np.int64
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _nearest_label_index(labels, nn, idx_dtype, chunk=1 << 22):
|
|
295
|
+
"""
|
|
296
|
+
The index into `labels` of each voxel of the nearest neighbour sample `nn` (float64), or -1 where it is not
|
|
297
|
+
a label (cval outside the image). `labels` is sorted, so this is a searchsorted, verified with the same
|
|
298
|
+
`==` that compared `nn` with each label before. Chunked, so that its int64 temporaries stay small.
|
|
299
|
+
"""
|
|
300
|
+
out = np.empty(nn.shape, dtype=idx_dtype)
|
|
301
|
+
flat_nn, flat_out = nn.reshape(-1), out.reshape(-1)
|
|
302
|
+
last = len(labels) - 1
|
|
303
|
+
for s in range(0, flat_nn.size, chunk):
|
|
304
|
+
v = flat_nn[s:s + chunk]
|
|
305
|
+
idx = np.minimum(np.searchsorted(labels, v), last)
|
|
306
|
+
idx[labels[idx] != v] = -1
|
|
307
|
+
flat_out[s:s + chunk] = idx
|
|
308
|
+
return out
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def interpolate_img(img, coords, order=3, mode='nearest', cval=0.0, is_seg=False, *, seg_tiebreak='nearest'):
|
|
146
312
|
if is_seg and order != 0:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
313
|
+
return _resample_seg_by_argmax(
|
|
314
|
+
unique_labels(img),
|
|
315
|
+
# the indicators are padded with 0, not cval: outside the image no label is present. Padding them
|
|
316
|
+
# with cval raised every label's score by the same amount there, so for cval > 0 a voxel entirely
|
|
317
|
+
# outside was a tie among all labels rather than empty, and empty_value never reached it.
|
|
318
|
+
lambda c: map_coordinates((img == c).astype(float), coords, order=order, mode=mode, cval=0.0),
|
|
319
|
+
lambda: map_coordinates(img.astype(float), coords, order=0, mode=mode, cval=cval),
|
|
320
|
+
coords.shape[1:], img.dtype, seg_tiebreak,
|
|
321
|
+
# outside the image the caller asked for cval, and with the old zero-initialized result that is
|
|
322
|
+
# what those voxels happened to get whenever cval was 0. Keep it, explicitly and for any cval.
|
|
323
|
+
# Converted the way order 0 converts it, so a cval the dtype cannot hold (-1 in uint8) comes out
|
|
324
|
+
# the same at every order instead of raising here.
|
|
325
|
+
empty_value=np.array(cval, dtype=float).astype(img.dtype) if mode == 'constant' else None)
|
|
153
326
|
else:
|
|
154
327
|
return map_coordinates(img.astype(float), coords, order=order, mode=mode, cval=cval).astype(img.dtype)
|
|
155
328
|
|
|
@@ -581,29 +754,34 @@ def transpose_channels(batch):
|
|
|
581
754
|
raise ValueError("wrong dimensions in transpose_channel generator!")
|
|
582
755
|
|
|
583
756
|
|
|
584
|
-
def resize_segmentation(segmentation, new_shape, order=3):
|
|
757
|
+
def resize_segmentation(segmentation, new_shape, order=3, *, seg_tiebreak='nearest'):
|
|
585
758
|
'''
|
|
586
|
-
Resizes a segmentation map. Supports all orders (see skimage documentation).
|
|
587
|
-
|
|
588
|
-
|
|
759
|
+
Resizes a segmentation map. Supports all orders (see skimage documentation). Each label's indicator is
|
|
760
|
+
resized on its own and the per-voxel winner is the label with the largest interpolated indicator, so no
|
|
761
|
+
intermediate label value can be produced ([0, 0, 2] -> [0, 1, 2]).
|
|
589
762
|
:param segmentation:
|
|
590
763
|
:param new_shape:
|
|
591
764
|
:param order:
|
|
765
|
+
:param seg_tiebreak: how to settle voxels where two labels share the top score ('nearest', 'lowest' or
|
|
766
|
+
'highest'). See _resample_seg_by_argmax. 'nearest' is the only geometric choice and is the default.
|
|
592
767
|
:return:
|
|
593
768
|
'''
|
|
594
769
|
tpe = segmentation.dtype
|
|
595
770
|
assert len(segmentation.shape) == len(new_shape), "new shape must have same dimensionality as segmentation"
|
|
771
|
+
if segmentation.size == 0:
|
|
772
|
+
# skimage cannot resize an empty array (scipy's zoom divides 0 by 0), at any order. Zeros of new_shape is
|
|
773
|
+
# what orders >= 1 returned before the argmax, from their zero-initialized result.
|
|
774
|
+
return np.zeros(new_shape, dtype=tpe)
|
|
596
775
|
if order == 0:
|
|
597
776
|
return resize(segmentation.astype(float), new_shape, order, mode="edge", clip=True, anti_aliasing=False).astype(tpe)
|
|
598
777
|
else:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return reshaped
|
|
778
|
+
return _resample_seg_by_argmax(
|
|
779
|
+
unique_labels(segmentation),
|
|
780
|
+
lambda c: resize((segmentation == c).astype(float), new_shape, order, mode="edge", clip=True,
|
|
781
|
+
anti_aliasing=False),
|
|
782
|
+
lambda: resize(segmentation.astype(float), new_shape, 0, mode="edge", clip=True,
|
|
783
|
+
anti_aliasing=False),
|
|
784
|
+
new_shape, tpe, seg_tiebreak)
|
|
607
785
|
|
|
608
786
|
|
|
609
787
|
def resize_multichannel_image(multichannel_image, new_shape, order=3):
|
|
@@ -616,11 +794,18 @@ def resize_multichannel_image(multichannel_image, new_shape, order=3):
|
|
|
616
794
|
:return:
|
|
617
795
|
'''
|
|
618
796
|
tpe = multichannel_image.dtype
|
|
797
|
+
is_int = np.issubdtype(tpe, np.integer)
|
|
619
798
|
new_shp = [multichannel_image.shape[0]] + list(new_shape)
|
|
620
|
-
|
|
799
|
+
# Integer input is accumulated in float and rounded at the end. Writing the interpolated float
|
|
800
|
+
# straight into an integer array truncates towards zero, which shifts every voxel that did not land
|
|
801
|
+
# exactly on a sample point down by up to one intensity step. Floating point input keeps its own
|
|
802
|
+
# dtype, so nothing about that path changes and the buffer does not grow.
|
|
803
|
+
result = np.zeros(new_shp, dtype=float if is_int else tpe)
|
|
621
804
|
for i in range(multichannel_image.shape[0]):
|
|
622
805
|
result[i] = resize(multichannel_image[i].astype(float), new_shape, order, clip=True, anti_aliasing=False)
|
|
623
|
-
|
|
806
|
+
if is_int:
|
|
807
|
+
np.rint(result, out=result)
|
|
808
|
+
return result.astype(tpe, copy=False)
|
|
624
809
|
|
|
625
810
|
|
|
626
811
|
def get_range_val(value, rnd_type="uniform"):
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/spatial_transforms.py
RENAMED
|
@@ -61,7 +61,7 @@ class Rot90Transform(AbstractTransform):
|
|
|
61
61
|
|
|
62
62
|
class ZoomTransform(AbstractTransform):
|
|
63
63
|
def __init__(self, zoom_factors=1, order=3, order_seg=1, concatenate_list=False, data_key="data",
|
|
64
|
-
label_key="seg"):
|
|
64
|
+
label_key="seg", *, seg_tiebreak='nearest'):
|
|
65
65
|
"""
|
|
66
66
|
Zooms 'data' (and 'seg') by zoom_factors
|
|
67
67
|
:param zoom_factors: int or list/tuple of int
|
|
@@ -81,6 +81,7 @@ class ZoomTransform(AbstractTransform):
|
|
|
81
81
|
self.data_key = data_key
|
|
82
82
|
self.label_key = label_key
|
|
83
83
|
self.order = order
|
|
84
|
+
self.seg_tiebreak = seg_tiebreak
|
|
84
85
|
self.zoom_factors = zoom_factors
|
|
85
86
|
|
|
86
87
|
def __call__(self, **data_dict):
|
|
@@ -105,7 +106,8 @@ class ZoomTransform(AbstractTransform):
|
|
|
105
106
|
sample_seg = None
|
|
106
107
|
if seg is not None:
|
|
107
108
|
sample_seg = seg[b]
|
|
108
|
-
res_data, res_seg = augment_zoom(data[b], sample_seg, self.zoom_factors, self.order, self.order_seg
|
|
109
|
+
res_data, res_seg = augment_zoom(data[b], sample_seg, self.zoom_factors, self.order, self.order_seg,
|
|
110
|
+
seg_tiebreak=self.seg_tiebreak)
|
|
109
111
|
results.append((res_data, res_seg))
|
|
110
112
|
|
|
111
113
|
if concatenate:
|
|
@@ -123,7 +125,7 @@ class ZoomTransform(AbstractTransform):
|
|
|
123
125
|
class ResizeTransform(AbstractTransform):
|
|
124
126
|
|
|
125
127
|
def __init__(self, target_size, order=3, order_seg=1, concatenate_list=False, data_key="data",
|
|
126
|
-
label_key="seg"):
|
|
128
|
+
label_key="seg", *, seg_tiebreak='nearest'):
|
|
127
129
|
"""
|
|
128
130
|
Reshapes 'data' (and 'seg') to target_size
|
|
129
131
|
:param target_size: int or list/tuple of int
|
|
@@ -143,6 +145,7 @@ class ResizeTransform(AbstractTransform):
|
|
|
143
145
|
self.data_key = data_key
|
|
144
146
|
self.label_key = label_key
|
|
145
147
|
self.order = order
|
|
148
|
+
self.seg_tiebreak = seg_tiebreak
|
|
146
149
|
self.target_size = target_size
|
|
147
150
|
|
|
148
151
|
def __call__(self, **data_dict):
|
|
@@ -167,7 +170,8 @@ class ResizeTransform(AbstractTransform):
|
|
|
167
170
|
sample_seg = None
|
|
168
171
|
if seg is not None:
|
|
169
172
|
sample_seg = seg[b]
|
|
170
|
-
res_data, res_seg = augment_resize(data[b], sample_seg, self.target_size, self.order, self.order_seg
|
|
173
|
+
res_data, res_seg = augment_resize(data[b], sample_seg, self.target_size, self.order, self.order_seg,
|
|
174
|
+
seg_tiebreak=self.seg_tiebreak)
|
|
171
175
|
results.append((res_data, res_seg))
|
|
172
176
|
|
|
173
177
|
if concatenate:
|
|
@@ -303,7 +307,8 @@ class SpatialTransform(AbstractTransform):
|
|
|
303
307
|
do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
|
|
304
308
|
border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, data_key="data",
|
|
305
309
|
label_key="seg", p_el_per_sample=1, p_scale_per_sample=1, p_rot_per_sample=1,
|
|
306
|
-
independent_scale_for_each_axis=False, p_rot_per_axis:float=1, p_independent_scale_per_axis: int=1
|
|
310
|
+
independent_scale_for_each_axis=False, p_rot_per_axis:float=1, p_independent_scale_per_axis: int=1,
|
|
311
|
+
*, seg_tiebreak: str='nearest'):
|
|
307
312
|
self.independent_scale_for_each_axis = independent_scale_for_each_axis
|
|
308
313
|
self.p_rot_per_sample = p_rot_per_sample
|
|
309
314
|
self.p_scale_per_sample = p_scale_per_sample
|
|
@@ -330,6 +335,7 @@ class SpatialTransform(AbstractTransform):
|
|
|
330
335
|
self.random_crop = random_crop
|
|
331
336
|
self.p_rot_per_axis = p_rot_per_axis
|
|
332
337
|
self.p_independent_scale_per_axis = p_independent_scale_per_axis
|
|
338
|
+
self.seg_tiebreak = seg_tiebreak
|
|
333
339
|
|
|
334
340
|
def __call__(self, **data_dict):
|
|
335
341
|
data = data_dict.get(self.data_key)
|
|
@@ -358,7 +364,8 @@ class SpatialTransform(AbstractTransform):
|
|
|
358
364
|
p_rot_per_sample=self.p_rot_per_sample,
|
|
359
365
|
independent_scale_for_each_axis=self.independent_scale_for_each_axis,
|
|
360
366
|
p_rot_per_axis=self.p_rot_per_axis,
|
|
361
|
-
p_independent_scale_per_axis=self.p_independent_scale_per_axis
|
|
367
|
+
p_independent_scale_per_axis=self.p_independent_scale_per_axis,
|
|
368
|
+
seg_tiebreak=self.seg_tiebreak)
|
|
362
369
|
data_dict[self.data_key] = ret_val[0]
|
|
363
370
|
if seg is not None:
|
|
364
371
|
data_dict[self.label_key] = ret_val[1]
|
|
@@ -419,7 +426,8 @@ class SpatialTransform_2(AbstractTransform):
|
|
|
419
426
|
do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
|
|
420
427
|
border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, data_key="data",
|
|
421
428
|
label_key="seg", p_el_per_sample=1, p_scale_per_sample=1, p_rot_per_sample=1,
|
|
422
|
-
independent_scale_for_each_axis=False, p_rot_per_axis:float=1, p_independent_scale_per_axis: float=1
|
|
429
|
+
independent_scale_for_each_axis=False, p_rot_per_axis:float=1, p_independent_scale_per_axis: float=1,
|
|
430
|
+
*, seg_tiebreak: str='nearest'):
|
|
423
431
|
self.p_rot_per_sample = p_rot_per_sample
|
|
424
432
|
self.p_scale_per_sample = p_scale_per_sample
|
|
425
433
|
self.p_el_per_sample = p_el_per_sample
|
|
@@ -445,6 +453,7 @@ class SpatialTransform_2(AbstractTransform):
|
|
|
445
453
|
self.p_independent_scale_per_axis = p_independent_scale_per_axis
|
|
446
454
|
self.independent_scale_for_each_axis = independent_scale_for_each_axis
|
|
447
455
|
self.p_rot_per_axis = p_rot_per_axis
|
|
456
|
+
self.seg_tiebreak = seg_tiebreak
|
|
448
457
|
|
|
449
458
|
def __call__(self, **data_dict):
|
|
450
459
|
data = data_dict.get(self.data_key)
|
|
@@ -473,7 +482,8 @@ class SpatialTransform_2(AbstractTransform):
|
|
|
473
482
|
p_rot_per_sample=self.p_rot_per_sample,
|
|
474
483
|
independent_scale_for_each_axis=self.independent_scale_for_each_axis,
|
|
475
484
|
p_rot_per_axis=self.p_rot_per_axis,
|
|
476
|
-
p_independent_scale_per_axis=self.p_independent_scale_per_axis
|
|
485
|
+
p_independent_scale_per_axis=self.p_independent_scale_per_axis,
|
|
486
|
+
seg_tiebreak=self.seg_tiebreak)
|
|
477
487
|
|
|
478
488
|
data_dict[self.data_key] = ret_val[0]
|
|
479
489
|
if seg is not None:
|
|
@@ -633,8 +643,9 @@ class MisalignTransform(AbstractTransform):
|
|
|
633
643
|
p_transl_per_sample=0.1, p_transl_per_dir=1.0,
|
|
634
644
|
border_mode_data='constant', border_cval_data=0,
|
|
635
645
|
border_mode_seg='constant', border_cval_seg=0,
|
|
636
|
-
order_data=3, order_seg=0):
|
|
646
|
+
order_data=3, order_seg=0, *, seg_tiebreak='nearest'):
|
|
637
647
|
|
|
648
|
+
self.seg_tiebreak = seg_tiebreak
|
|
638
649
|
self.data_key = data_key
|
|
639
650
|
self.label_key = label_key
|
|
640
651
|
|
|
@@ -709,7 +720,8 @@ class MisalignTransform(AbstractTransform):
|
|
|
709
720
|
border_cval_data=self.border_cval_data,
|
|
710
721
|
order_seg=self.order_seg,
|
|
711
722
|
border_mode_seg=self.border_mode_seg,
|
|
712
|
-
border_cval_seg=self.border_cval_seg
|
|
723
|
+
border_cval_seg=self.border_cval_seg,
|
|
724
|
+
seg_tiebreak=self.seg_tiebreak)
|
|
713
725
|
|
|
714
726
|
data_dict[self.data_key] = ret_val[0]
|
|
715
727
|
if seg is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchgenerators
|
|
3
|
-
Version: 0.25.
|
|
3
|
+
Version: 0.25.5
|
|
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
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from skimage.transform import resize
|
|
5
|
+
|
|
6
|
+
from batchgenerators.augmentations.utils import (interpolate_img, resize_multichannel_image,
|
|
7
|
+
resize_segmentation)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def nearest(seg, new_shape):
|
|
11
|
+
return resize(seg.astype(float), new_shape, 0, mode="edge", clip=True, anti_aliasing=False).astype(seg.dtype)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def label_scores(seg, new_shape, order):
|
|
15
|
+
labels = np.sort(np.unique(seg))
|
|
16
|
+
return labels, np.stack([resize((seg == c).astype(float), new_shape, order, mode="edge", clip=True,
|
|
17
|
+
anti_aliasing=False) for c in labels])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestResizeSegmentation(unittest.TestCase):
|
|
21
|
+
"""
|
|
22
|
+
resize_segmentation and interpolate_img resize a segmentation by interpolating each label's indicator and
|
|
23
|
+
picking a winner per voxel. They used to assign with `interpolated >= 0.5` into a zero-initialized result,
|
|
24
|
+
which could leave a voxel unwritten (it then kept a 0 that need not be a label of the input) and, where
|
|
25
|
+
several labels passed 0.5, handed the voxel to whichever label was written last, i.e. the largest.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def test_no_label_is_invented(self):
|
|
29
|
+
"""Four labels meeting at an output centre score 0.25 each, so the old rule wrote nothing there."""
|
|
30
|
+
seg = np.array([[1, 2, 1, 1],
|
|
31
|
+
[3, 4, 1, 1],
|
|
32
|
+
[1, 1, 1, 1],
|
|
33
|
+
[1, 1, 1, 1]], dtype=np.uint8)
|
|
34
|
+
labels, scores = label_scores(seg, (2, 2), 1)
|
|
35
|
+
self.assertLess(scores[:, 0, 0].max(), 0.5, 'this test is only meaningful if no label reaches 0.5')
|
|
36
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
37
|
+
out = resize_segmentation(seg, (2, 2), order=1, seg_tiebreak=tiebreak)
|
|
38
|
+
self.assertTrue(set(np.unique(out).tolist()) <= set(np.unique(seg).tolist()),
|
|
39
|
+
f'{np.unique(out)} contains a label that is not in the input ({tiebreak})')
|
|
40
|
+
|
|
41
|
+
def test_no_label_is_invented_randomized(self):
|
|
42
|
+
"""At order 3 skimage's clip=True breaks sum-to-one outright, so the old rule missed whole voxels."""
|
|
43
|
+
rng = np.random.RandomState(3)
|
|
44
|
+
missed_by_the_old_rule = 0
|
|
45
|
+
for _ in range(60):
|
|
46
|
+
shape = tuple(rng.randint(6, 14) for _ in range(2))
|
|
47
|
+
seg = rng.randint(1, 5, shape).astype(np.uint8) # labels 1..4, the input never contains 0
|
|
48
|
+
new_shape = tuple(max(int(round(s / f)), 2)
|
|
49
|
+
for s, f in zip(shape, rng.choice([1.5, 2., 2.5, 3.], 2)))
|
|
50
|
+
labels, scores = label_scores(seg, new_shape, 3)
|
|
51
|
+
missed_by_the_old_rule += int(((scores >= 0.5).sum(0) == 0).sum())
|
|
52
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
53
|
+
out = resize_segmentation(seg, new_shape, order=3, seg_tiebreak=tiebreak)
|
|
54
|
+
self.assertTrue(np.isin(out, labels).all(),
|
|
55
|
+
f'{np.unique(out)} vs input labels {labels} ({tiebreak})')
|
|
56
|
+
self.assertGreater(missed_by_the_old_rule, 0, 'the old rule should have left voxels unwritten here')
|
|
57
|
+
|
|
58
|
+
def test_nearest_tiebreak_is_symmetric_in_the_label_values(self):
|
|
59
|
+
"""
|
|
60
|
+
An even integer factor puts an output centre exactly between two labels at every boundary voxel, and
|
|
61
|
+
an exact tie carries no information about which label belongs there. 'nearest' answers it from the
|
|
62
|
+
geometry, so relabelling moves the same decision with the labels; 'lowest' and 'highest' answer it
|
|
63
|
+
from the label values, so they do not.
|
|
64
|
+
"""
|
|
65
|
+
for lo, hi in ((1, 2), (2, 7), (3, 4)):
|
|
66
|
+
forward = np.full((8, 2), lo, dtype=np.uint8)
|
|
67
|
+
forward[5:] = hi # boundary lands on the output centre x = 4.5
|
|
68
|
+
swapped = np.full((8, 2), hi, dtype=np.uint8)
|
|
69
|
+
swapped[5:] = lo
|
|
70
|
+
remap = {lo: hi, hi: lo}
|
|
71
|
+
|
|
72
|
+
a = resize_segmentation(forward, (4, 2), order=1, seg_tiebreak='nearest')
|
|
73
|
+
b = resize_segmentation(swapped, (4, 2), order=1, seg_tiebreak='nearest')
|
|
74
|
+
self.assertTrue(np.array_equal(np.vectorize(remap.get)(a), b),
|
|
75
|
+
f"'nearest' is not symmetric for ({lo}, {hi}): {a[:, 0]} vs {b[:, 0]}")
|
|
76
|
+
|
|
77
|
+
# the tie is at output index 2; 'lowest' and 'highest' take it by label value
|
|
78
|
+
self.assertEqual(resize_segmentation(forward, (4, 2), 1, seg_tiebreak='lowest')[2, 0], lo)
|
|
79
|
+
self.assertEqual(resize_segmentation(forward, (4, 2), 1, seg_tiebreak='highest')[2, 0], hi)
|
|
80
|
+
|
|
81
|
+
def test_nearest_tiebreak_matches_a_nearest_neighbour_resize(self):
|
|
82
|
+
seg = np.full((8, 2), 1, dtype=np.uint8)
|
|
83
|
+
seg[5:] = 2
|
|
84
|
+
self.assertTrue(np.array_equal(resize_segmentation(seg, (4, 2), order=1, seg_tiebreak='nearest'),
|
|
85
|
+
nearest(seg, (4, 2))))
|
|
86
|
+
|
|
87
|
+
def test_nearest_tiebreak_only_picks_among_the_tied_labels(self):
|
|
88
|
+
"""
|
|
89
|
+
Where three or more labels meet, the nearest neighbour can be a label that lost: halving this 2x2x2
|
|
90
|
+
block scores 1 and 2 at 0.375 each and 3 at 0.25, and the nearest neighbour of the centre is a 3.
|
|
91
|
+
"""
|
|
92
|
+
block = np.array([3, 1, 1, 1, 2, 2, 2, 3], dtype=np.int16).reshape(2, 2, 2)
|
|
93
|
+
seg = np.tile(block, (4, 4, 4))
|
|
94
|
+
labels, scores = label_scores(seg, (4, 4, 4), 1)
|
|
95
|
+
self.assertTrue(np.allclose(scores[:, 0, 0, 0], [0.375, 0.375, 0.25]))
|
|
96
|
+
self.assertEqual(int(nearest(seg, (4, 4, 4))[0, 0, 0]), 3, 'the nearest neighbour has to be the loser')
|
|
97
|
+
for tiebreak, expected in (('nearest', 1), ('lowest', 1), ('highest', 2)):
|
|
98
|
+
out = resize_segmentation(seg, (4, 4, 4), order=1, seg_tiebreak=tiebreak)
|
|
99
|
+
self.assertTrue((out == expected).all(), f'{np.unique(out)} ({tiebreak})')
|
|
100
|
+
|
|
101
|
+
def test_winner_has_the_top_score_randomized(self):
|
|
102
|
+
"""whatever settles a tie, the label a voxel gets must be one that attains the maximum score there"""
|
|
103
|
+
rng = np.random.RandomState(4)
|
|
104
|
+
for _ in range(40):
|
|
105
|
+
shape = tuple(rng.randint(4, 9) for _ in range(3))
|
|
106
|
+
seg = rng.randint(0, rng.randint(2, 6), shape).astype(np.int16)
|
|
107
|
+
new_shape = tuple(max(s // 2, 1) for s in shape)
|
|
108
|
+
labels, scores = label_scores(seg, new_shape, 1)
|
|
109
|
+
top = scores.max(0)
|
|
110
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
111
|
+
out = resize_segmentation(seg, new_shape, order=1, seg_tiebreak=tiebreak)
|
|
112
|
+
own = np.take_along_axis(scores, np.searchsorted(labels, out)[None], 0)[0]
|
|
113
|
+
self.assertTrue((own == top).all(), f'{int((own != top).sum())} voxels ({tiebreak})')
|
|
114
|
+
|
|
115
|
+
def test_block_aligned_roundtrip_is_exact(self):
|
|
116
|
+
"""00001111 -> 0011 -> 00001111: down and up share the block grid, so nothing may move."""
|
|
117
|
+
seg = np.array([0, 0, 0, 0, 1, 1, 1, 1], dtype=np.uint8)[:, None].repeat(2, axis=1)
|
|
118
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
119
|
+
down = resize_segmentation(seg, (4, 2), order=1, seg_tiebreak=tiebreak)
|
|
120
|
+
back = resize_segmentation(down, (8, 2), order=1, seg_tiebreak=tiebreak)
|
|
121
|
+
self.assertTrue(np.array_equal(back, seg), f'tiebreak={tiebreak}')
|
|
122
|
+
|
|
123
|
+
def test_single_label_is_passed_through(self):
|
|
124
|
+
seg = np.full((8, 6), 3, dtype=np.uint8)
|
|
125
|
+
out = resize_segmentation(seg, (4, 3), order=3)
|
|
126
|
+
self.assertTrue(np.array_equal(out, np.full((4, 3), 3, dtype=np.uint8)))
|
|
127
|
+
|
|
128
|
+
def test_order_zero_is_untouched(self):
|
|
129
|
+
rng = np.random.RandomState(0)
|
|
130
|
+
seg = rng.randint(0, 5, (9, 7)).astype(np.uint8)
|
|
131
|
+
self.assertTrue(np.array_equal(resize_segmentation(seg, (5, 4), order=0), nearest(seg, (5, 4))))
|
|
132
|
+
|
|
133
|
+
def test_dtype_is_preserved(self):
|
|
134
|
+
for dtype in (np.uint8, np.int16, np.int32):
|
|
135
|
+
seg = np.array([1, 1, 1, 1, 2, 2, 2, 2], dtype=dtype)[:, None].repeat(2, axis=1)
|
|
136
|
+
self.assertEqual(resize_segmentation(seg, (4, 2), order=1).dtype, dtype)
|
|
137
|
+
|
|
138
|
+
def test_unknown_tiebreak_is_rejected(self):
|
|
139
|
+
seg = np.zeros((8, 8), dtype=np.uint8)
|
|
140
|
+
seg[4:] = 1
|
|
141
|
+
with self.assertRaises(ValueError):
|
|
142
|
+
resize_segmentation(seg, (4, 4), order=1, seg_tiebreak='bogus')
|
|
143
|
+
|
|
144
|
+
def test_empty_input_gives_zeros(self):
|
|
145
|
+
"""
|
|
146
|
+
An empty segmentation came back as zeros of new_shape (the zero-initialized result) at orders >= 1. The
|
|
147
|
+
argmax broke that: it takes the nearest neighbour sample up front, which skimage cannot compute for an
|
|
148
|
+
empty array (scipy's zoom divides 0 by 0), and 'lowest'/'highest' indexed an empty label list. Order 0
|
|
149
|
+
failed the same way even before.
|
|
150
|
+
"""
|
|
151
|
+
for order in (0, 1, 3):
|
|
152
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
153
|
+
for in_shape, out_shape in (((0, 4, 4), (0, 8, 8)), ((0, 4, 4), (2, 8, 8)), ((0, 5), (0, 10))):
|
|
154
|
+
out = resize_segmentation(np.zeros(in_shape, dtype=np.int16), out_shape, order=order,
|
|
155
|
+
seg_tiebreak=tiebreak)
|
|
156
|
+
msg = f'{in_shape} -> {out_shape}, order {order}, {tiebreak}'
|
|
157
|
+
self.assertEqual(out.shape, out_shape, msg)
|
|
158
|
+
self.assertEqual(out.dtype, np.int16, msg)
|
|
159
|
+
self.assertFalse(out.any(), msg)
|
|
160
|
+
out = resize_segmentation(np.ones((4, 4, 4), dtype=np.int16), (0, 8, 8), order=order,
|
|
161
|
+
seg_tiebreak=tiebreak)
|
|
162
|
+
self.assertEqual(out.shape, (0, 8, 8))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestInterpolateImg(unittest.TestCase):
|
|
166
|
+
"""interpolate_img is the same rule on the augment_spatial path, and had the same two problems."""
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _coords(xs, ys):
|
|
170
|
+
return np.stack(np.meshgrid(np.array(xs, dtype=float), np.array(ys, dtype=float), indexing='ij'))
|
|
171
|
+
|
|
172
|
+
def test_no_label_is_invented(self):
|
|
173
|
+
img = np.array([[1, 2, 1, 1],
|
|
174
|
+
[3, 4, 1, 1],
|
|
175
|
+
[1, 1, 1, 1],
|
|
176
|
+
[1, 1, 1, 1]], dtype=np.uint8)
|
|
177
|
+
coords = self._coords([0.5, 2.5], [0.5, 2.5])
|
|
178
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
179
|
+
out = interpolate_img(img, coords, order=1, is_seg=True, seg_tiebreak=tiebreak)
|
|
180
|
+
self.assertTrue(set(np.unique(out).tolist()) <= set(np.unique(img).tolist()),
|
|
181
|
+
f'{np.unique(out)} contains a label that is not in the input ({tiebreak})')
|
|
182
|
+
|
|
183
|
+
def test_nearest_tiebreak_is_symmetric_in_the_label_values(self):
|
|
184
|
+
coords = self._coords([4.5], [0.0])
|
|
185
|
+
for lo, hi in ((1, 2), (2, 7)):
|
|
186
|
+
forward = np.full((8, 2), lo, dtype=np.uint8)
|
|
187
|
+
forward[5:] = hi
|
|
188
|
+
swapped = np.full((8, 2), hi, dtype=np.uint8)
|
|
189
|
+
swapped[5:] = lo
|
|
190
|
+
a = interpolate_img(forward, coords, order=1, is_seg=True, seg_tiebreak='nearest')
|
|
191
|
+
b = interpolate_img(swapped, coords, order=1, is_seg=True, seg_tiebreak='nearest')
|
|
192
|
+
self.assertEqual({lo: hi, hi: lo}[int(a.ravel()[0])], int(b.ravel()[0]))
|
|
193
|
+
self.assertEqual(int(interpolate_img(forward, coords, 1, is_seg=True, seg_tiebreak='lowest').ravel()[0]), lo)
|
|
194
|
+
self.assertEqual(int(interpolate_img(forward, coords, 1, is_seg=True, seg_tiebreak='highest').ravel()[0]), hi)
|
|
195
|
+
|
|
196
|
+
def test_constant_padding_still_uses_cval_outside_the_image(self):
|
|
197
|
+
"""
|
|
198
|
+
The old code zero-initialized the result, so voxels sampled outside the image happened to come back
|
|
199
|
+
as 0 - right whenever cval was 0, and only then. Now that the result carries a real label from the
|
|
200
|
+
start, cval has to be written there deliberately.
|
|
201
|
+
"""
|
|
202
|
+
img = np.array([[1, 1, 2, 2]] * 4, dtype=np.uint8) # the input does not contain 0
|
|
203
|
+
coords = self._coords([-5., 1.5, 9.], [2.5]) # outside, inside (clear of the 1/2 tie), outside
|
|
204
|
+
for cval in (0.0, 7.0):
|
|
205
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
206
|
+
out = interpolate_img(img, coords, order=1, mode='constant', cval=cval, is_seg=True,
|
|
207
|
+
seg_tiebreak=tiebreak).ravel()
|
|
208
|
+
self.assertEqual(int(out[0]), int(cval), tiebreak)
|
|
209
|
+
self.assertEqual(int(out[2]), int(cval), tiebreak)
|
|
210
|
+
self.assertEqual(int(out[1]), 2, tiebreak)
|
|
211
|
+
# a cval the dtype cannot hold comes out as order 0 converts it, instead of raising
|
|
212
|
+
for order in (0, 1, 3):
|
|
213
|
+
out = interpolate_img(img, coords, order=order, mode='constant', cval=-1, is_seg=True).ravel()
|
|
214
|
+
self.assertEqual(int(out[0]), int(np.array(-1.).astype(np.uint8)), f'order {order}')
|
|
215
|
+
# padding modes with no 'outside' must be left alone
|
|
216
|
+
self.assertTrue((interpolate_img(img, coords, order=1, mode='nearest', is_seg=True) == 2).all())
|
|
217
|
+
|
|
218
|
+
def test_empty_input_gives_the_outside_value(self):
|
|
219
|
+
"""With no image to sample from every point is outside it: cval for 'constant', 0 otherwise."""
|
|
220
|
+
img = np.zeros((0, 4), dtype=np.uint8)
|
|
221
|
+
coords = self._coords([0.5, 1.5], [0.5, 2.5])
|
|
222
|
+
for order in (1, 3):
|
|
223
|
+
for tiebreak in ('nearest', 'lowest', 'highest'):
|
|
224
|
+
out = interpolate_img(img, coords, order=order, mode='nearest', is_seg=True, seg_tiebreak=tiebreak)
|
|
225
|
+
self.assertEqual(out.shape, (2, 2))
|
|
226
|
+
self.assertEqual(out.dtype, np.uint8)
|
|
227
|
+
self.assertFalse(out.any(), f'order {order}, {tiebreak}')
|
|
228
|
+
for cval in (0., 7.):
|
|
229
|
+
out = interpolate_img(img, coords, order=order, mode='constant', cval=cval, is_seg=True,
|
|
230
|
+
seg_tiebreak=tiebreak)
|
|
231
|
+
self.assertTrue((out == cval).all(), f'order {order}, {tiebreak}, cval {cval}')
|
|
232
|
+
|
|
233
|
+
def test_order_zero_and_non_seg_are_untouched(self):
|
|
234
|
+
img = np.arange(16, dtype=np.uint8).reshape(4, 4)
|
|
235
|
+
coords = self._coords([0.5, 2.5], [0.5, 2.5])
|
|
236
|
+
self.assertTrue(np.array_equal(interpolate_img(img, coords, order=0, is_seg=True),
|
|
237
|
+
interpolate_img(img, coords, order=0, is_seg=False)))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestResizeMultichannelImage(unittest.TestCase):
|
|
241
|
+
"""
|
|
242
|
+
The image counterpart in the same module. It has no per-label reduction and therefore none of the
|
|
243
|
+
tie-break problem, but it used to allocate its output with the *input* dtype and assign the
|
|
244
|
+
interpolated float into it, which truncates towards zero for integer input.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def test_integer_input_is_rounded_not_truncated(self):
|
|
248
|
+
img = (np.arange(8, dtype=np.uint8) * 30)[None, :, None].repeat(2, axis=2)
|
|
249
|
+
exact = resize(img[0].astype(float), (4, 2), 3, clip=True, anti_aliasing=False)
|
|
250
|
+
got = resize_multichannel_image(img, (4, 2), 3)
|
|
251
|
+
self.assertTrue(np.array_equal(got[0], np.rint(exact).astype(np.uint8)))
|
|
252
|
+
self.assertFalse(np.array_equal(got[0], np.trunc(exact).astype(np.uint8)),
|
|
253
|
+
'this test needs a case where rounding and truncation disagree')
|
|
254
|
+
|
|
255
|
+
def test_integer_input_has_no_downward_bias(self):
|
|
256
|
+
"""truncation costs about half an intensity step per voxel, everywhere, in one direction"""
|
|
257
|
+
rng = np.random.RandomState(0)
|
|
258
|
+
bias = []
|
|
259
|
+
for _ in range(50):
|
|
260
|
+
a = rng.randint(0, 256, (1, 16, 16)).astype(np.uint8)
|
|
261
|
+
exact = resize(a[0].astype(float), (9, 9), 3, clip=True, anti_aliasing=False)
|
|
262
|
+
bias.append((resize_multichannel_image(a, (9, 9), 3)[0].astype(float) - exact).mean())
|
|
263
|
+
self.assertLess(abs(float(np.mean(bias))), 0.02)
|
|
264
|
+
|
|
265
|
+
def test_float_input_is_unchanged(self):
|
|
266
|
+
"""the only path our pipelines use, and it must stay bit-identical"""
|
|
267
|
+
rng = np.random.RandomState(0)
|
|
268
|
+
for dtype in (np.float32, np.float64):
|
|
269
|
+
a = rng.rand(2, 16, 16).astype(dtype)
|
|
270
|
+
expected = np.stack([resize(a[i].astype(float), (9, 9), 3, clip=True,
|
|
271
|
+
anti_aliasing=False).astype(dtype) for i in range(2)])
|
|
272
|
+
self.assertTrue(np.array_equal(resize_multichannel_image(a, (9, 9), 3), expected))
|
|
273
|
+
|
|
274
|
+
def test_dtype_is_preserved(self):
|
|
275
|
+
rng = np.random.RandomState(0)
|
|
276
|
+
for dtype in (np.uint8, np.int16, np.int32, np.float32, np.float64):
|
|
277
|
+
a = (rng.rand(1, 8, 8) * 100).astype(dtype)
|
|
278
|
+
self.assertEqual(resize_multichannel_image(a, (4, 4), 3).dtype, dtype)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == '__main__':
|
|
282
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/augmentations/normalizations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/dataloading/data_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/examples/brats2017/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/abstract_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/color_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/local_transforms.py
RENAMED
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/noise_transforms.py
RENAMED
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/resample_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/transforms/utility_transforms.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators/utilities/data_splitting.py
RENAMED
|
File without changes
|
|
File without changes
|
{batchgenerators-0.25.3 → batchgenerators-0.25.5}/batchgenerators.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|