batchgenerators 0.25.2__tar.gz → 0.25.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/PKG-INFO +1 -1
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/spatial_transformations.py +16 -12
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/utils.py +204 -27
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/multi_threaded_augmenter.py +139 -79
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/nondet_multi_threaded_augmenter.py +131 -56
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/spatial_transforms.py +22 -10
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/PKG-INFO +1 -1
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/SOURCES.txt +1 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/dependency_links.txt +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/requires.txt +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators.egg-info/top_level.txt +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/pyproject.toml +1 -1
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_DataLoader.py +1 -1
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_axis_mirroring.py +1 -2
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_resample_augmentations.py +2 -2
- batchgenerators-0.25.4/tests/test_seg_resampling.py +247 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/LICENSE +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/README.md +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/color_augmentations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/crop_and_pad_augmentations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/noise_augmentations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/normalizations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/augmentations/resample_augmentations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/data_loader.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/dataset.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/dataloading/single_threaded_augmenter.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/datasets/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/datasets/cifar.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_dataloader_2D.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_dataloader_3D.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/brats2017_preprocessing.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/brats2017/config.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/cifar10.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/examples/multithreaded_dataloading.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/abstract_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/channel_selection_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/color_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/crop_and_pad_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/local_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/noise_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/resample_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/sample_normalization_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/transforms/utility_transforms.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/__init__.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/custom_types.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/data_splitting.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/batchgenerators/utilities/file_and_folder_operations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/setup.cfg +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/setup.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_augment_zoom.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_color_augmentations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_crop.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_multithreaded_augmenter.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_normalizations.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_random_crop.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_sanity.py +0 -0
- {batchgenerators-0.25.2 → batchgenerators-0.25.4}/tests/test_spatial_transformations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchgenerators
|
|
3
|
-
Version: 0.25.
|
|
3
|
+
Version: 0.25.4
|
|
4
4
|
Summary: Data augmentation toolkit
|
|
5
5
|
Author-email: "Division of Medical Image Computing, German Cancer Research Center AND Applied Computer Vision Lab, Helmholtz Imaging Platform" <f.isensee@dkfz-heidelberg.de>
|
|
6
6
|
License: Apache License
|
|
@@ -45,7 +45,7 @@ def augment_rot90(sample_data, sample_seg, num_rot=(1, 2, 3), axes=(0, 1, 2)):
|
|
|
45
45
|
return sample_data, sample_seg
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1):
|
|
48
|
+
def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1, *, seg_tiebreak='nearest'):
|
|
49
49
|
"""
|
|
50
50
|
Reshapes data (and seg) to target_size
|
|
51
51
|
:param sample_data: np.ndarray or list/tuple of np.ndarrays, must be (c, x, y(, z))) (if list/tuple then each entry
|
|
@@ -71,14 +71,15 @@ def augment_resize(sample_data, sample_seg, target_size, order=3, order_seg=1):
|
|
|
71
71
|
if sample_seg is not None:
|
|
72
72
|
target_seg = np.ones([sample_seg.shape[0]] + target_size_here)
|
|
73
73
|
for c in range(sample_seg.shape[0]):
|
|
74
|
-
target_seg[c] = resize_segmentation(sample_seg[c], target_size_here, order_seg
|
|
74
|
+
target_seg[c] = resize_segmentation(sample_seg[c], target_size_here, order_seg,
|
|
75
|
+
seg_tiebreak=seg_tiebreak)
|
|
75
76
|
else:
|
|
76
77
|
target_seg = None
|
|
77
78
|
|
|
78
79
|
return sample_data, target_seg
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1):
|
|
82
|
+
def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1, *, seg_tiebreak='nearest'):
|
|
82
83
|
"""
|
|
83
84
|
zooms data (and seg) by factor zoom_factors
|
|
84
85
|
:param sample_data: np.ndarray or list/tuple of np.ndarrays, must be (c, x, y(, z))) (if list/tuple then each entry
|
|
@@ -107,7 +108,8 @@ def augment_zoom(sample_data, sample_seg, zoom_factors, order=3, order_seg=1):
|
|
|
107
108
|
if sample_seg is not None:
|
|
108
109
|
target_seg = np.ones([sample_seg.shape[0]] + target_shape_here)
|
|
109
110
|
for c in range(sample_seg.shape[0]):
|
|
110
|
-
target_seg[c] = resize_segmentation(sample_seg[c], target_shape_here, order_seg
|
|
111
|
+
target_seg[c] = resize_segmentation(sample_seg[c], target_shape_here, order_seg,
|
|
112
|
+
seg_tiebreak=seg_tiebreak)
|
|
111
113
|
else:
|
|
112
114
|
target_seg = None
|
|
113
115
|
|
|
@@ -192,7 +194,8 @@ def augment_spatial(data, seg, patch_size, patch_center_dist_from_border=30,
|
|
|
192
194
|
do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
|
|
193
195
|
border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, p_el_per_sample=1,
|
|
194
196
|
p_scale_per_sample=1, p_rot_per_sample=1, independent_scale_for_each_axis=False,
|
|
195
|
-
p_rot_per_axis: float = 1, p_independent_scale_per_axis: int = 1
|
|
197
|
+
p_rot_per_axis: float = 1, p_independent_scale_per_axis: int = 1,
|
|
198
|
+
*, seg_tiebreak: str = 'nearest'):
|
|
196
199
|
dim = len(patch_size)
|
|
197
200
|
seg_result = None
|
|
198
201
|
if seg is not None:
|
|
@@ -277,7 +280,7 @@ def augment_spatial(data, seg, patch_size, patch_center_dist_from_border=30,
|
|
|
277
280
|
for channel_id in range(seg.shape[1]):
|
|
278
281
|
seg_result[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
|
|
279
282
|
border_mode_seg, cval=border_cval_seg,
|
|
280
|
-
is_seg=True)
|
|
283
|
+
is_seg=True, seg_tiebreak=seg_tiebreak)
|
|
281
284
|
else:
|
|
282
285
|
if seg is None:
|
|
283
286
|
s = None
|
|
@@ -300,7 +303,8 @@ def augment_spatial_2(data, seg, patch_size, patch_center_dist_from_border=30,
|
|
|
300
303
|
do_scale=True, scale=(0.75, 1.25), border_mode_data='nearest', border_cval_data=0, order_data=3,
|
|
301
304
|
border_mode_seg='constant', border_cval_seg=0, order_seg=0, random_crop=True, p_el_per_sample=1,
|
|
302
305
|
p_scale_per_sample=1, p_rot_per_sample=1, independent_scale_for_each_axis=False,
|
|
303
|
-
p_rot_per_axis: float = 1, p_independent_scale_per_axis: float = 1
|
|
306
|
+
p_rot_per_axis: float = 1, p_independent_scale_per_axis: float = 1,
|
|
307
|
+
*, seg_tiebreak: str = 'nearest'):
|
|
304
308
|
"""
|
|
305
309
|
|
|
306
310
|
:param data:
|
|
@@ -444,7 +448,7 @@ def augment_spatial_2(data, seg, patch_size, patch_center_dist_from_border=30,
|
|
|
444
448
|
for channel_id in range(seg.shape[1]):
|
|
445
449
|
seg_result[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
|
|
446
450
|
border_mode_seg, cval=border_cval_seg,
|
|
447
|
-
is_seg=True)
|
|
451
|
+
is_seg=True, seg_tiebreak=seg_tiebreak)
|
|
448
452
|
else:
|
|
449
453
|
if seg is None:
|
|
450
454
|
s = None
|
|
@@ -549,7 +553,7 @@ def augment_misalign(data, seg, data_size,
|
|
|
549
553
|
do_transl=False,
|
|
550
554
|
border_mode_data='constant', border_cval_data=0,
|
|
551
555
|
border_mode_seg='constant', border_cval_seg=0,
|
|
552
|
-
order_data=3, order_seg=0):
|
|
556
|
+
order_data=3, order_seg=0, *, seg_tiebreak='nearest'):
|
|
553
557
|
|
|
554
558
|
dim = len(data_size)
|
|
555
559
|
|
|
@@ -587,7 +591,7 @@ def augment_misalign(data, seg, data_size,
|
|
|
587
591
|
if channel_id in im_channels_2_misalign:
|
|
588
592
|
seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
|
|
589
593
|
border_mode_seg, cval=border_cval_seg,
|
|
590
|
-
is_seg=True)
|
|
594
|
+
is_seg=True, seg_tiebreak=seg_tiebreak)
|
|
591
595
|
|
|
592
596
|
if do_rotation and np.random.uniform() < p_rot_per_sample:
|
|
593
597
|
coords = create_zero_centered_coordinate_mesh(data_size)
|
|
@@ -620,7 +624,7 @@ def augment_misalign(data, seg, data_size,
|
|
|
620
624
|
if channel_id in im_channels_2_misalign:
|
|
621
625
|
seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
|
|
622
626
|
border_mode_seg, cval=border_cval_seg,
|
|
623
|
-
is_seg=True)
|
|
627
|
+
is_seg=True, seg_tiebreak=seg_tiebreak)
|
|
624
628
|
|
|
625
629
|
if do_transl and np.random.uniform() < p_transl_per_sample:
|
|
626
630
|
coords = create_zero_centered_coordinate_mesh(data_size)
|
|
@@ -654,5 +658,5 @@ def augment_misalign(data, seg, data_size,
|
|
|
654
658
|
if channel_id in label_channels_2_misalign:
|
|
655
659
|
seg[sample_id, channel_id] = interpolate_img(seg[sample_id, channel_id], coords, order_seg,
|
|
656
660
|
border_mode_seg, cval=border_cval_seg,
|
|
657
|
-
is_seg=True)
|
|
661
|
+
is_seg=True, seg_tiebreak=seg_tiebreak)
|
|
658
662
|
return data, seg
|
|
@@ -16,11 +16,15 @@
|
|
|
16
16
|
import random
|
|
17
17
|
import numpy as np
|
|
18
18
|
from copy import deepcopy
|
|
19
|
-
from scipy.ndimage import
|
|
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,161 @@ 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) == 1 and empty_value is None:
|
|
211
|
+
# nothing to interpolate, and this also keeps the loop below from having to handle a missing `best`
|
|
212
|
+
return np.full(out_shape, labels[0], dtype=dtype)
|
|
213
|
+
|
|
214
|
+
# Memory: the scores are float64 (that is what the interpolators return, and ties are decided by exact
|
|
215
|
+
# equality on them), so `best` and the current `score` are the two large buffers and everything else is
|
|
216
|
+
# kept small. The running winner and the nearest neighbour are held as int8/int16 indices into `labels`
|
|
217
|
+
# and mapped to label values once at the end, the per-label masks are three reused bool buffers, and the
|
|
218
|
+
# previous score is released before the next one is allocated. Without that, peak memory was about twice
|
|
219
|
+
# the bytes per output voxel.
|
|
220
|
+
idx_dtype = _label_index_dtype(len(labels))
|
|
221
|
+
win = np.zeros(out_shape, dtype=idx_dtype)
|
|
222
|
+
best = None
|
|
223
|
+
better = np.empty(out_shape, dtype=bool)
|
|
224
|
+
# 'nearest': the nearest neighbour label settles a tie only if it is one of the tied labels. Where three
|
|
225
|
+
# or more labels meet it need not be - a 2x2x2 block [3,1,1,1,2,2,2,3] sampled at its centre scores
|
|
226
|
+
# 1 and 2 at 0.375 each and 3 at 0.25, and its nearest neighbour is a 3. So instead of marking tied
|
|
227
|
+
# voxels, track whether the nearest neighbour's label currently has the top score: a label that takes
|
|
228
|
+
# the lead strictly decides that afresh, one that draws level can only add to it. Where it holds at the
|
|
229
|
+
# end the nearest neighbour is the answer (it changes nothing where its label won outright), and where
|
|
230
|
+
# it does not the argmax winner stands.
|
|
231
|
+
nearest = seg_tiebreak == 'nearest'
|
|
232
|
+
if nearest:
|
|
233
|
+
nn_idx = _nearest_label_index(labels, nearest_fn(), idx_dtype)
|
|
234
|
+
nn_top = np.empty(out_shape, dtype=bool)
|
|
235
|
+
is_nn = np.empty(out_shape, dtype=bool)
|
|
236
|
+
level = np.empty(out_shape, dtype=bool)
|
|
237
|
+
for i, c in enumerate(labels):
|
|
238
|
+
score = score_fn(c)
|
|
239
|
+
if best is None:
|
|
240
|
+
# the first label is the running winner everywhere until something beats it. Inside the image the
|
|
241
|
+
# indicators sum to ~1, so this is only ever a starting point; outside it every label scores zero
|
|
242
|
+
# and empty_value (if the caller gave one) has the final say below.
|
|
243
|
+
best = score
|
|
244
|
+
if nearest:
|
|
245
|
+
np.equal(nn_idx, 0, out=nn_top)
|
|
246
|
+
np.greater(score, 0, out=level)
|
|
247
|
+
nn_top &= level
|
|
248
|
+
del score
|
|
249
|
+
continue
|
|
250
|
+
if nearest:
|
|
251
|
+
np.equal(nn_idx, i, out=is_nn)
|
|
252
|
+
# `score > 0` guard: two labels that both score zero at a voxel are not tied for the win there,
|
|
253
|
+
# they are simply both absent, and some other label will claim it. `better` is free until it is
|
|
254
|
+
# computed below, so it holds that guard meanwhile.
|
|
255
|
+
np.equal(score, best, out=level)
|
|
256
|
+
np.greater(score, 0, out=better)
|
|
257
|
+
level &= better
|
|
258
|
+
level &= is_nn
|
|
259
|
+
nn_top |= level
|
|
260
|
+
# '>=' lets the later (larger) label take ties, '>' leaves them with the earlier (smaller) one
|
|
261
|
+
(np.greater_equal if seg_tiebreak == 'highest' else np.greater)(score, best, out=better)
|
|
262
|
+
if nearest:
|
|
263
|
+
np.copyto(nn_top, is_nn, where=better)
|
|
264
|
+
np.copyto(win, i, where=better)
|
|
265
|
+
np.maximum(best, score, out=best)
|
|
266
|
+
del score
|
|
267
|
+
|
|
268
|
+
if nearest:
|
|
269
|
+
# copyto rather than win[nn_top] = nn_idx[nn_top]: nn_top holds nearly everywhere (also wherever the
|
|
270
|
+
# nearest neighbour's label won outright), and the indexed form copies all of those out first
|
|
271
|
+
np.copyto(win, nn_idx, where=nn_top)
|
|
272
|
+
del nn_idx, nn_top, is_nn, level
|
|
273
|
+
del better
|
|
274
|
+
empty = best <= 0 if empty_value is not None else None
|
|
275
|
+
del best
|
|
276
|
+
result = labels.astype(dtype, copy=False)[win]
|
|
277
|
+
if empty is not None:
|
|
278
|
+
result[empty] = empty_value
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _label_index_dtype(n_labels):
|
|
283
|
+
# signed, so that -1 can mean 'not a label'
|
|
284
|
+
for t in (np.int8, np.int16, np.int32):
|
|
285
|
+
if n_labels <= np.iinfo(t).max:
|
|
286
|
+
return t
|
|
287
|
+
return np.int64
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _nearest_label_index(labels, nn, idx_dtype, chunk=1 << 22):
|
|
291
|
+
"""
|
|
292
|
+
The index into `labels` of each voxel of the nearest neighbour sample `nn` (float64), or -1 where it is not
|
|
293
|
+
a label (cval outside the image). `labels` is sorted, so this is a searchsorted, verified with the same
|
|
294
|
+
`==` that compared `nn` with each label before. Chunked, so that its int64 temporaries stay small.
|
|
295
|
+
"""
|
|
296
|
+
out = np.empty(nn.shape, dtype=idx_dtype)
|
|
297
|
+
flat_nn, flat_out = nn.reshape(-1), out.reshape(-1)
|
|
298
|
+
last = len(labels) - 1
|
|
299
|
+
for s in range(0, flat_nn.size, chunk):
|
|
300
|
+
v = flat_nn[s:s + chunk]
|
|
301
|
+
idx = np.minimum(np.searchsorted(labels, v), last)
|
|
302
|
+
idx[labels[idx] != v] = -1
|
|
303
|
+
flat_out[s:s + chunk] = idx
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def interpolate_img(img, coords, order=3, mode='nearest', cval=0.0, is_seg=False, *, seg_tiebreak='nearest'):
|
|
146
308
|
if is_seg and order != 0:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
309
|
+
return _resample_seg_by_argmax(
|
|
310
|
+
unique_labels(img),
|
|
311
|
+
# the indicators are padded with 0, not cval: outside the image no label is present. Padding them
|
|
312
|
+
# with cval raised every label's score by the same amount there, so for cval > 0 a voxel entirely
|
|
313
|
+
# outside was a tie among all labels rather than empty, and empty_value never reached it.
|
|
314
|
+
lambda c: map_coordinates((img == c).astype(float), coords, order=order, mode=mode, cval=0.0),
|
|
315
|
+
lambda: map_coordinates(img.astype(float), coords, order=0, mode=mode, cval=cval),
|
|
316
|
+
coords.shape[1:], img.dtype, seg_tiebreak,
|
|
317
|
+
# outside the image the caller asked for cval, and with the old zero-initialized result that is
|
|
318
|
+
# what those voxels happened to get whenever cval was 0. Keep it, explicitly and for any cval.
|
|
319
|
+
# Converted the way order 0 converts it, so a cval the dtype cannot hold (-1 in uint8) comes out
|
|
320
|
+
# the same at every order instead of raising here.
|
|
321
|
+
empty_value=np.array(cval, dtype=float).astype(img.dtype) if mode == 'constant' else None)
|
|
153
322
|
else:
|
|
154
323
|
return map_coordinates(img.astype(float), coords, order=order, mode=mode, cval=cval).astype(img.dtype)
|
|
155
324
|
|
|
@@ -581,14 +750,16 @@ def transpose_channels(batch):
|
|
|
581
750
|
raise ValueError("wrong dimensions in transpose_channel generator!")
|
|
582
751
|
|
|
583
752
|
|
|
584
|
-
def resize_segmentation(segmentation, new_shape, order=3):
|
|
753
|
+
def resize_segmentation(segmentation, new_shape, order=3, *, seg_tiebreak='nearest'):
|
|
585
754
|
'''
|
|
586
|
-
Resizes a segmentation map. Supports all orders (see skimage documentation).
|
|
587
|
-
|
|
588
|
-
|
|
755
|
+
Resizes a segmentation map. Supports all orders (see skimage documentation). Each label's indicator is
|
|
756
|
+
resized on its own and the per-voxel winner is the label with the largest interpolated indicator, so no
|
|
757
|
+
intermediate label value can be produced ([0, 0, 2] -> [0, 1, 2]).
|
|
589
758
|
:param segmentation:
|
|
590
759
|
:param new_shape:
|
|
591
760
|
:param order:
|
|
761
|
+
:param seg_tiebreak: how to settle voxels where two labels share the top score ('nearest', 'lowest' or
|
|
762
|
+
'highest'). See _resample_seg_by_argmax. 'nearest' is the only geometric choice and is the default.
|
|
592
763
|
:return:
|
|
593
764
|
'''
|
|
594
765
|
tpe = segmentation.dtype
|
|
@@ -596,14 +767,13 @@ def resize_segmentation(segmentation, new_shape, order=3):
|
|
|
596
767
|
if order == 0:
|
|
597
768
|
return resize(segmentation.astype(float), new_shape, order, mode="edge", clip=True, anti_aliasing=False).astype(tpe)
|
|
598
769
|
else:
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return reshaped
|
|
770
|
+
return _resample_seg_by_argmax(
|
|
771
|
+
unique_labels(segmentation),
|
|
772
|
+
lambda c: resize((segmentation == c).astype(float), new_shape, order, mode="edge", clip=True,
|
|
773
|
+
anti_aliasing=False),
|
|
774
|
+
lambda: resize(segmentation.astype(float), new_shape, 0, mode="edge", clip=True,
|
|
775
|
+
anti_aliasing=False),
|
|
776
|
+
new_shape, tpe, seg_tiebreak)
|
|
607
777
|
|
|
608
778
|
|
|
609
779
|
def resize_multichannel_image(multichannel_image, new_shape, order=3):
|
|
@@ -616,11 +786,18 @@ def resize_multichannel_image(multichannel_image, new_shape, order=3):
|
|
|
616
786
|
:return:
|
|
617
787
|
'''
|
|
618
788
|
tpe = multichannel_image.dtype
|
|
789
|
+
is_int = np.issubdtype(tpe, np.integer)
|
|
619
790
|
new_shp = [multichannel_image.shape[0]] + list(new_shape)
|
|
620
|
-
|
|
791
|
+
# Integer input is accumulated in float and rounded at the end. Writing the interpolated float
|
|
792
|
+
# straight into an integer array truncates towards zero, which shifts every voxel that did not land
|
|
793
|
+
# exactly on a sample point down by up to one intensity step. Floating point input keeps its own
|
|
794
|
+
# dtype, so nothing about that path changes and the buffer does not grow.
|
|
795
|
+
result = np.zeros(new_shp, dtype=float if is_int else tpe)
|
|
621
796
|
for i in range(multichannel_image.shape[0]):
|
|
622
797
|
result[i] = resize(multichannel_image[i].astype(float), new_shape, order, clip=True, anti_aliasing=False)
|
|
623
|
-
|
|
798
|
+
if is_int:
|
|
799
|
+
np.rint(result, out=result)
|
|
800
|
+
return result.astype(tpe, copy=False)
|
|
624
801
|
|
|
625
802
|
|
|
626
803
|
def get_range_val(value, rnd_type="uniform"):
|