acvl-utils 0.2__tar.gz → 0.2.2__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.
- {acvl_utils-0.2 → acvl_utils-0.2.2}/PKG-INFO +8 -1
- acvl_utils-0.2.2/acvl_utils/cropping_and_padding/bounding_boxes.py +267 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/instance_as_semantic_seg.py +6 -4
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/morphology_helper.py +25 -4
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/PKG-INFO +9 -2
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/SOURCES.txt +8 -1
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/requires.txt +1 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/setup.py +3 -2
- acvl_utils-0.2.2/tests/test_component_removal.py +107 -0
- acvl_utils-0.2.2/tests/test_crop_and_pad.py +345 -0
- acvl_utils-0.2.2/tests/test_insert_crop.py +112 -0
- acvl_utils-0.2.2/tests/test_instance_matching.py +66 -0
- acvl_utils-0.2.2/tests/test_padding.py +356 -0
- acvl_utils-0.2.2/tests/test_ptqdm.py +29 -0
- acvl_utils-0.2.2/tests/test_slicer.py +98 -0
- acvl_utils-0.2/acvl_utils/cropping_and_padding/bounding_boxes.py +0 -102
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/resampling.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/slicer.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/cropping_and_padding/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/cropping_and_padding/padding.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/instance_matching.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/ptqdm.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/test_all_images_same.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/__init__.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/gpu_binary_morphology.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/slice_interpolation.py +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/dependency_links.txt +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/top_level.txt +0 -0
- {acvl_utils-0.2 → acvl_utils-0.2.2}/setup.cfg +0 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: acvl_utils
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Super cool utilities that we just love to use
|
|
5
5
|
Author: Applied Computer Vision Lab, Helmholtz Imaging & Division of Medical Image Computing, German Cancer Research Center
|
|
6
6
|
Author-email: f.isensee@dkfz.de
|
|
7
7
|
License: Apache License Version 2.0, January 2004
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: batchgenerators
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: SimpleITK
|
|
12
|
+
Requires-Dist: scikit-image
|
|
13
|
+
Requires-Dist: connected-components-3d
|
|
14
|
+
Requires-Dist: blosc2>=3.0.0b4
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from typing import Union, List
|
|
4
|
+
|
|
5
|
+
import blosc2
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
import torch.nn.functional as F
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pad_bbox(bounding_box: Union[List[List[int]], Tuple[Tuple[int, int]]], pad_amount: Union[int, List[int]],
|
|
12
|
+
array_shape: Tuple[int, ...] = None) -> List[List[int]]:
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
if isinstance(bounding_box, tuple):
|
|
17
|
+
# convert to list
|
|
18
|
+
bounding_box = [list(i) for i in bounding_box]
|
|
19
|
+
else:
|
|
20
|
+
# needed because otherwise we overwrite input which could have unforseen consequences
|
|
21
|
+
bounding_box = deepcopy(bounding_box)
|
|
22
|
+
|
|
23
|
+
if isinstance(pad_amount, int):
|
|
24
|
+
pad_amount = [pad_amount] * len(bounding_box)
|
|
25
|
+
|
|
26
|
+
for i in range(len(bounding_box)):
|
|
27
|
+
new_values = [max(0, bounding_box[i][0] - pad_amount[i]), bounding_box[i][1] + pad_amount[i]]
|
|
28
|
+
if array_shape is not None:
|
|
29
|
+
new_values[1] = min(array_shape[i], new_values[1])
|
|
30
|
+
bounding_box[i] = new_values
|
|
31
|
+
|
|
32
|
+
return bounding_box
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def regionprops_bbox_to_proper_bbox(regionprops_bbox: Tuple[int, ...]) -> List[List[int]]:
|
|
36
|
+
"""
|
|
37
|
+
regionprops_bbox is what you get from `from skimage.measure import regionprops`
|
|
38
|
+
"""
|
|
39
|
+
dim = len(regionprops_bbox) // 2
|
|
40
|
+
return [[regionprops_bbox[i], regionprops_bbox[i + dim]] for i in range(dim)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def bounding_box_to_slice(bounding_box: List[List[int]]):
|
|
44
|
+
return tuple([slice(*i) for i in bounding_box])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def crop_to_bbox(array: np.ndarray, bounding_box: List[List[int]]):
|
|
48
|
+
assert len(bounding_box) == len(array.shape), f"Dimensionality of bbox and array do not match. bbox has length " \
|
|
49
|
+
f"{len(bounding_box)} while array has dimension {len(array.shape)}"
|
|
50
|
+
slicer = bounding_box_to_slice(bounding_box)
|
|
51
|
+
return array[slicer]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_bbox_from_mask(mask: np.ndarray) -> List[List[int]]:
|
|
55
|
+
"""
|
|
56
|
+
this implementation uses less ram than the np.where one and is faster as well IF we expect the bounding box to
|
|
57
|
+
be close to the image size. If it's not it's likely slower!
|
|
58
|
+
|
|
59
|
+
bbox is returned so that you can just do slice(minzidx, maxzidx) to retrieve the object of interest with nothing cut off
|
|
60
|
+
|
|
61
|
+
:param mask:
|
|
62
|
+
:param outside_value:
|
|
63
|
+
:return:
|
|
64
|
+
"""
|
|
65
|
+
Z, X, Y = mask.shape
|
|
66
|
+
minzidx, maxzidx, minxidx, maxxidx, minyidx, maxyidx = 0, Z, 0, X, 0, Y
|
|
67
|
+
zidx = list(range(Z))
|
|
68
|
+
for z in zidx:
|
|
69
|
+
if np.any(mask[z]):
|
|
70
|
+
minzidx = z
|
|
71
|
+
break
|
|
72
|
+
for z in zidx[::-1]:
|
|
73
|
+
if np.any(mask[z]):
|
|
74
|
+
maxzidx = z + 1
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
xidx = list(range(X))
|
|
78
|
+
for x in xidx:
|
|
79
|
+
if np.any(mask[:, x]):
|
|
80
|
+
minxidx = x
|
|
81
|
+
break
|
|
82
|
+
for x in xidx[::-1]:
|
|
83
|
+
if np.any(mask[:, x]):
|
|
84
|
+
maxxidx = x + 1
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
yidx = list(range(Y))
|
|
88
|
+
for y in yidx:
|
|
89
|
+
if np.any(mask[:, :, y]):
|
|
90
|
+
minyidx = y
|
|
91
|
+
break
|
|
92
|
+
for y in yidx[::-1]:
|
|
93
|
+
if np.any(mask[:, :, y]):
|
|
94
|
+
maxyidx = y + 1
|
|
95
|
+
break
|
|
96
|
+
return [[minzidx, maxzidx], [minxidx, maxxidx], [minyidx, maxyidx]]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_bbox_from_mask_npwhere(mask: np.ndarray) -> List[List[int]]:
|
|
100
|
+
where = np.array(np.where(mask))
|
|
101
|
+
mins = np.min(where, 1)
|
|
102
|
+
maxs = np.max(where, 1) + 1
|
|
103
|
+
return [[i, j] for i, j in zip(mins, maxs)]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def crop_and_pad_nd(
|
|
107
|
+
image: Union[torch.Tensor, np.ndarray, blosc2.ndarray.NDArray],
|
|
108
|
+
bbox: List[List[int]],
|
|
109
|
+
pad_value = 0
|
|
110
|
+
) -> Union[torch.Tensor, np.ndarray]:
|
|
111
|
+
"""
|
|
112
|
+
Crops a bounding box directly specified by bbox, excluding the upper bound.
|
|
113
|
+
If the bounding box extends beyond the image boundaries, the cropped area is padded
|
|
114
|
+
to maintain the desired size. Initial dimensions not included in bbox remain unaffected.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
- image: N-dimensional torch.Tensor or np.ndarray representing the image
|
|
118
|
+
- bbox: List of [[dim_min, dim_max], ...] defining the bounding box for the last dimensions.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
- Cropped and padded patch of the requested bounding box size, as the same type as `image`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Determine the number of dimensions to crop based on bbox
|
|
125
|
+
crop_dims = len(bbox)
|
|
126
|
+
img_shape = image.shape
|
|
127
|
+
num_dims = len(img_shape)
|
|
128
|
+
|
|
129
|
+
# Initialize the crop and pad specifications for each dimension
|
|
130
|
+
slices = []
|
|
131
|
+
padding = []
|
|
132
|
+
output_shape = list(img_shape[:num_dims - crop_dims]) # Initial dimensions remain as in the original image
|
|
133
|
+
target_shape = output_shape + [max_val - min_val for min_val, max_val in bbox]
|
|
134
|
+
|
|
135
|
+
# Iterate through dimensions, applying bbox to the last `crop_dims` dimensions
|
|
136
|
+
for i in range(num_dims):
|
|
137
|
+
if i < num_dims - crop_dims:
|
|
138
|
+
# For initial dimensions not covered by bbox, include the entire dimension
|
|
139
|
+
slices.append(slice(None))
|
|
140
|
+
padding.append([0, 0])
|
|
141
|
+
output_shape.append(img_shape[i]) # Keep the initial dimensions as they are
|
|
142
|
+
else:
|
|
143
|
+
# For dimensions specified in bbox, directly use the min and max bounds
|
|
144
|
+
dim_idx = i - (num_dims - crop_dims) # Index within bbox
|
|
145
|
+
|
|
146
|
+
min_val = bbox[dim_idx][0]
|
|
147
|
+
max_val = bbox[dim_idx][1]
|
|
148
|
+
|
|
149
|
+
# Check if the bounding box is completely outside the image bounds
|
|
150
|
+
if max_val <= 0 or min_val >= img_shape[i]:
|
|
151
|
+
# If outside bounds, return an empty array or tensor of the target shape
|
|
152
|
+
if isinstance(image, torch.Tensor):
|
|
153
|
+
return torch.zeros(target_shape, dtype=image.dtype, device=image.device)
|
|
154
|
+
elif isinstance(image, (np.ndarray, blosc2.ndarray.NDArray)):
|
|
155
|
+
return np.zeros(target_shape, dtype=image.dtype)
|
|
156
|
+
|
|
157
|
+
# Calculate valid cropping ranges within image bounds, excluding the upper bound
|
|
158
|
+
valid_min = max(min_val, 0)
|
|
159
|
+
valid_max = min(max_val, img_shape[i]) # Exclude upper bound by using max_val directly
|
|
160
|
+
slices.append(slice(valid_min, valid_max))
|
|
161
|
+
|
|
162
|
+
# Calculate padding needed for this dimension
|
|
163
|
+
pad_before = max(0, -min_val)
|
|
164
|
+
pad_after = max(0, max_val - img_shape[i])
|
|
165
|
+
padding.append([pad_before, pad_after])
|
|
166
|
+
|
|
167
|
+
# Define the shape based on the bbox range in this dimension
|
|
168
|
+
output_shape.append(max_val - min_val)
|
|
169
|
+
|
|
170
|
+
# Crop the valid part of the bounding box
|
|
171
|
+
cropped = image[tuple(slices)]
|
|
172
|
+
|
|
173
|
+
# Apply padding to the cropped patch
|
|
174
|
+
if isinstance(image, torch.Tensor):
|
|
175
|
+
flattened_padding = [p for sublist in reversed(padding) for p in sublist] # Flatten in reverse order for PyTorch
|
|
176
|
+
padded_cropped = F.pad(cropped, flattened_padding, mode="constant", value=pad_value)
|
|
177
|
+
elif isinstance(image, (np.ndarray, blosc2.ndarray.NDArray)):
|
|
178
|
+
pad_width = [(p[0], p[1]) for p in padding]
|
|
179
|
+
padded_cropped = np.pad(cropped, pad_width=pad_width, mode='constant', constant_values=pad_value)
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError(f'Unsupported image type {type(image)}')
|
|
182
|
+
|
|
183
|
+
return padded_cropped
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def insert_crop_into_image(
|
|
187
|
+
image: Union[torch.Tensor, np.ndarray],
|
|
188
|
+
crop: Union[torch.Tensor, np.ndarray],
|
|
189
|
+
bbox: List[List[int]]
|
|
190
|
+
) -> Union[torch.Tensor, np.ndarray]:
|
|
191
|
+
"""
|
|
192
|
+
Inserts a cropped patch back into the original image at the position specified by bbox.
|
|
193
|
+
If the bounding box extends beyond the image boundaries, only the valid portions are inserted.
|
|
194
|
+
If the bounding box lies entirely outside the image, the original image is returned.
|
|
195
|
+
|
|
196
|
+
Parameters:
|
|
197
|
+
- image: Original N-dimensional torch.Tensor or np.ndarray to which the crop will be inserted.
|
|
198
|
+
- crop: Cropped patch of the image to be reinserted. May have additional dimensions compared to bbox.
|
|
199
|
+
- bbox: List of [[dim_min, dim_max], ...] defining the bounding box for the last dimensions of the crop in the original image.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
- image: The original image with the crop reinserted at the specified location (modified in-place).
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
# Ensure that bbox only applies to the last len(bbox) dimensions of crop and image
|
|
206
|
+
num_dims = len(image.shape)
|
|
207
|
+
crop_dims = len(crop.shape)
|
|
208
|
+
bbox_dims = len(bbox)
|
|
209
|
+
|
|
210
|
+
if crop_dims < bbox_dims:
|
|
211
|
+
raise ValueError("Bounding box dimensions cannot exceed crop dimensions.")
|
|
212
|
+
|
|
213
|
+
# Validate that non-cropped leading dimensions match between image and crop
|
|
214
|
+
leading_dims = num_dims - bbox_dims
|
|
215
|
+
if image.shape[:leading_dims] != crop.shape[:leading_dims]:
|
|
216
|
+
raise ValueError("Leading dimensions of crop and image must match.")
|
|
217
|
+
|
|
218
|
+
# Check if the bounding box lies completely outside the image bounds for each cropped dimension
|
|
219
|
+
for i in range(bbox_dims):
|
|
220
|
+
min_val, max_val = bbox[i]
|
|
221
|
+
dim_idx = leading_dims + i # Corresponding dimension in the image
|
|
222
|
+
|
|
223
|
+
if max_val <= 0 or min_val >= image.shape[dim_idx]:
|
|
224
|
+
# If completely out of bounds in any dimension, return the original image
|
|
225
|
+
return image
|
|
226
|
+
|
|
227
|
+
# Prepare slices for inserting the crop into the original image
|
|
228
|
+
image_slices = []
|
|
229
|
+
crop_slices = []
|
|
230
|
+
|
|
231
|
+
# Iterate over all dimensions, applying bbox only to the last len(bbox) dimensions
|
|
232
|
+
for i in range(num_dims):
|
|
233
|
+
if i < leading_dims:
|
|
234
|
+
# For leading dimensions, use entire dimension (slice(None)) and validate shape
|
|
235
|
+
image_slices.append(slice(None))
|
|
236
|
+
crop_slices.append(slice(None))
|
|
237
|
+
else:
|
|
238
|
+
# For dimensions specified by bbox, calculate the intersection with image bounds
|
|
239
|
+
dim_idx = i - leading_dims
|
|
240
|
+
min_val, max_val = bbox[dim_idx]
|
|
241
|
+
|
|
242
|
+
crop_start = max(0, -min_val) # Start of the crop within the valid area
|
|
243
|
+
image_start = max(0, min_val) # Start of the image where the crop will be inserted
|
|
244
|
+
image_end = min(max_val, image.shape[i]) # Exclude upper bound by using max_val directly
|
|
245
|
+
|
|
246
|
+
# Adjusted range for insertion
|
|
247
|
+
crop_end = crop_start + (image_end - image_start)
|
|
248
|
+
|
|
249
|
+
# Append slices for both image and crop insertion ranges
|
|
250
|
+
image_slices.append(slice(image_start, image_end))
|
|
251
|
+
crop_slices.append(slice(crop_start, crop_end))
|
|
252
|
+
|
|
253
|
+
# Insert the valid part of the crop back into the original image
|
|
254
|
+
if isinstance(image, torch.Tensor):
|
|
255
|
+
image[tuple(image_slices)] = crop[tuple(crop_slices)]
|
|
256
|
+
elif isinstance(image, np.ndarray):
|
|
257
|
+
image[tuple(image_slices)] = crop[tuple(crop_slices)]
|
|
258
|
+
else:
|
|
259
|
+
raise ValueError(f"Unsupported image type {type(image)}")
|
|
260
|
+
|
|
261
|
+
return image
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == '__main__':
|
|
265
|
+
bbox = [[32, 64], [21, 46]]
|
|
266
|
+
bbox_padded = pad_bbox(bbox, 3)
|
|
267
|
+
slicer = bounding_box_to_slice(bbox_padded)
|
{acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/instance_as_semantic_seg.py
RENAMED
|
@@ -2,6 +2,7 @@ from time import time
|
|
|
2
2
|
from multiprocessing import Pool
|
|
3
3
|
from typing import Union, Tuple, List
|
|
4
4
|
|
|
5
|
+
import SimpleITK
|
|
5
6
|
import numpy as np
|
|
6
7
|
|
|
7
8
|
from acvl_utils.cropping_and_padding.bounding_boxes import pad_bbox, bounding_box_to_slice
|
|
@@ -79,11 +80,11 @@ def convert_semantic_to_instanceseg_mp(arr: np.ndarray,
|
|
|
79
80
|
# we first identify centers that are too small and set them to be border. This should remove false positive instances
|
|
80
81
|
labeled_image, component_sizes = label_with_component_sizes(arr == CENTER_LABEL, connectivity=1)
|
|
81
82
|
remove = np.array([i for i, j in component_sizes.items() if j <= small_center_threshold_in_pixels])
|
|
82
|
-
remove = np.
|
|
83
|
+
remove = np.isin(labeled_image.ravel(), remove).reshape(labeled_image.shape)
|
|
83
84
|
arr[remove] = BORDER_LABEL
|
|
84
85
|
|
|
85
86
|
# recompute core labels
|
|
86
|
-
core_instances = label(arr == CENTER_LABEL)
|
|
87
|
+
core_instances = label(arr == CENTER_LABEL, connectivity=1)
|
|
87
88
|
|
|
88
89
|
# prepare empty array for results. uint32 for allowing HEAPS of instances. Otherwise we would cap at 65535
|
|
89
90
|
final = np.zeros_like(core_instances, dtype=np.uint32)
|
|
@@ -165,11 +166,11 @@ def convert_semantic_to_instanceseg(arr: np.ndarray,
|
|
|
165
166
|
# we first identify centers that are too small and set them to be border. This should remove false positive instances
|
|
166
167
|
labeled_image, component_sizes = label_with_component_sizes(arr == CENTER_LABEL, connectivity=1)
|
|
167
168
|
remove = np.array([i for i, j in component_sizes.items() if j <= small_center_threshold_in_pixels])
|
|
168
|
-
remove = np.
|
|
169
|
+
remove = np.isin(labeled_image.ravel(), remove).reshape(labeled_image.shape)
|
|
169
170
|
arr[remove] = BORDER_LABEL
|
|
170
171
|
|
|
171
172
|
# recompute core labels
|
|
172
|
-
core_instances = label(arr == CENTER_LABEL)
|
|
173
|
+
core_instances = label(arr == CENTER_LABEL, connectivity=1)
|
|
173
174
|
|
|
174
175
|
# prepare empty array for results
|
|
175
176
|
final = np.zeros_like(core_instances, dtype=np.uint32)
|
|
@@ -518,3 +519,4 @@ def main_sem_to_instance():
|
|
|
518
519
|
|
|
519
520
|
if __name__ == '__main__':
|
|
520
521
|
main_sem_to_instance()
|
|
522
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Union, Tuple, List, Callable
|
|
1
|
+
from typing import Union, Tuple, List, Callable, Dict
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from skimage.measure import label
|
|
@@ -30,6 +30,27 @@ def label_with_component_sizes(binary_image: np.ndarray, connectivity: int = Non
|
|
|
30
30
|
return labeled_image, component_sizes
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def cc3d_label_with_component_sizes(binary_image: np.ndarray, connectivity: int = 6) -> Tuple[np.ndarray, Dict[int, int]]:
|
|
34
|
+
"""
|
|
35
|
+
Uses cc3d to label connected components in a binary image and returns the labeled image along with
|
|
36
|
+
a dictionary of component sizes.
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
binary_image (np.ndarray): The input binary image where connected components should be labeled.
|
|
40
|
+
connectivity (int): Connectivity of components (default 6 for 3D images).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple[np.ndarray, dict]: A tuple with the labeled image and a dictionary of component sizes.
|
|
44
|
+
"""
|
|
45
|
+
# Ensure the image is binary
|
|
46
|
+
labeled_image = cc3d.connected_components(binary_image, connectivity=connectivity)
|
|
47
|
+
|
|
48
|
+
# Get the sizes of each component (ignoring background, i.e., label 0)
|
|
49
|
+
component_sizes = {i: j for i, j in enumerate(np.bincount(labeled_image.ravel())[1:], start=1)}
|
|
50
|
+
|
|
51
|
+
return labeled_image, component_sizes
|
|
52
|
+
|
|
53
|
+
|
|
33
54
|
def remove_all_but_largest_component(binary_image: np.ndarray, connectivity: int = None) -> np.ndarray:
|
|
34
55
|
"""
|
|
35
56
|
Removes all but the largest component in binary_image. Replaces pixels that don't belong to it with background_label
|
|
@@ -50,7 +71,7 @@ def generic_filter_components(binary_image: np.ndarray, filter_fn: Callable[[Lis
|
|
|
50
71
|
component_ids = list(component_sizes.keys())
|
|
51
72
|
component_sizes = list(component_sizes.values())
|
|
52
73
|
keep = filter_fn(component_ids, component_sizes)
|
|
53
|
-
return np.
|
|
74
|
+
return np.isin(labeled_image.ravel(), keep).reshape(labeled_image.shape)
|
|
54
75
|
|
|
55
76
|
|
|
56
77
|
def remove_components(binary_image: np.ndarray, threshold_size_in_pixels: int, threshold_type: str = 'min',
|
|
@@ -86,7 +107,7 @@ def remove_components(binary_image: np.ndarray, threshold_size_in_pixels: int, t
|
|
|
86
107
|
print(f'{len(keep)} objects are smaller than the maximum size of {threshold_size_in_pixels}. '
|
|
87
108
|
f'Removing {len(component_sizes) - len(keep)} large objects...')
|
|
88
109
|
|
|
89
|
-
keep = np.
|
|
110
|
+
keep = np.isin(labeled_image, keep).astype(binary_image.dtype).reshape(binary_image.shape)
|
|
90
111
|
return keep
|
|
91
112
|
|
|
92
113
|
|
|
@@ -139,7 +160,7 @@ def remove_components_cc3d(binary_image: np.ndarray, threshold_size_in_pixels: i
|
|
|
139
160
|
f'Removing {num_components - len(labels_to_keep)} large objects...')
|
|
140
161
|
|
|
141
162
|
# in the component mask set every label that should be removed to 0
|
|
142
|
-
keep = np.
|
|
163
|
+
keep = np.isin(components, labels_to_keep).astype(binary_image.dtype).reshape(binary_image.shape)
|
|
143
164
|
|
|
144
165
|
return keep
|
|
145
166
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version: 0.2
|
|
2
|
+
Name: acvl_utils
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Super cool utilities that we just love to use
|
|
5
5
|
Author: Applied Computer Vision Lab, Helmholtz Imaging & Division of Medical Image Computing, German Cancer Research Center
|
|
6
6
|
Author-email: f.isensee@dkfz.de
|
|
7
7
|
License: Apache License Version 2.0, January 2004
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: batchgenerators
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: SimpleITK
|
|
12
|
+
Requires-Dist: scikit-image
|
|
13
|
+
Requires-Dist: connected-components-3d
|
|
14
|
+
Requires-Dist: blosc2>=3.0.0b4
|
|
@@ -20,4 +20,11 @@ acvl_utils/miscellaneous/test_all_images_same.py
|
|
|
20
20
|
acvl_utils/morphology/__init__.py
|
|
21
21
|
acvl_utils/morphology/gpu_binary_morphology.py
|
|
22
22
|
acvl_utils/morphology/morphology_helper.py
|
|
23
|
-
acvl_utils/morphology/slice_interpolation.py
|
|
23
|
+
acvl_utils/morphology/slice_interpolation.py
|
|
24
|
+
tests/test_component_removal.py
|
|
25
|
+
tests/test_crop_and_pad.py
|
|
26
|
+
tests/test_insert_crop.py
|
|
27
|
+
tests/test_instance_matching.py
|
|
28
|
+
tests/test_padding.py
|
|
29
|
+
tests/test_ptqdm.py
|
|
30
|
+
tests/test_slicer.py
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_namespace_packages
|
|
|
2
2
|
|
|
3
3
|
setup(name='acvl_utils',
|
|
4
4
|
packages=find_namespace_packages(include=["acvl_utils", "acvl_utils.*"]),
|
|
5
|
-
version='0.2',
|
|
5
|
+
version='0.2.2',
|
|
6
6
|
description='Super cool utilities that we just love to use',
|
|
7
7
|
# url='',
|
|
8
8
|
author='Applied Computer Vision Lab, Helmholtz Imaging & Division of Medical Image Computing, German Cancer Research Center',
|
|
@@ -14,6 +14,7 @@ setup(name='acvl_utils',
|
|
|
14
14
|
"torch",
|
|
15
15
|
"SimpleITK",
|
|
16
16
|
"scikit-image",
|
|
17
|
-
"connected-components-3d"
|
|
17
|
+
"connected-components-3d",
|
|
18
|
+
"blosc2>=3.0.0b4"
|
|
18
19
|
],
|
|
19
20
|
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from acvl_utils.morphology.morphology_helper import remove_components, remove_components_cc3d
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestComponentRemoval(unittest.TestCase):
|
|
7
|
+
def setUp(self) -> None:
|
|
8
|
+
self.binary_image_2D = np.array([[1, 0, 1, 1, 0, 0, 1],
|
|
9
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
10
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
11
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
12
|
+
[1, 1, 0, 1, 0, 0, 1]]).astype(bool)
|
|
13
|
+
|
|
14
|
+
self.binary_image_3D = np.array([[[1, 0, 1, 1, 0, 0, 1],
|
|
15
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
16
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
17
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
18
|
+
[1, 1, 0, 1, 0, 0, 1]],
|
|
19
|
+
[[1, 0, 1, 1, 0, 0, 1],
|
|
20
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
21
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
22
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
23
|
+
[1, 1, 0, 1, 0, 0, 1]],
|
|
24
|
+
[[1, 0, 1, 1, 0, 0, 1],
|
|
25
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
26
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
27
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
28
|
+
[1, 1, 0, 1, 0, 0, 1]]
|
|
29
|
+
]).astype(bool)
|
|
30
|
+
|
|
31
|
+
def test_remove_small_comp_2D(self):
|
|
32
|
+
skimage_res = remove_components(self.binary_image_2D, 3, 'min', verbose=False)
|
|
33
|
+
cc3d_res = remove_components_cc3d(self.binary_image_2D, 3, 'min', verbose=False, connectivity=8)
|
|
34
|
+
|
|
35
|
+
self.assertTrue((skimage_res == cc3d_res).all())
|
|
36
|
+
|
|
37
|
+
expected_output = np.array([[1, 0, 1, 1, 0, 0, 1],
|
|
38
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
39
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
40
|
+
[1, 0, 0, 0, 0, 0, 0],
|
|
41
|
+
[1, 1, 0, 0, 0, 0, 0]]).astype(bool)
|
|
42
|
+
|
|
43
|
+
self.assertTrue((skimage_res == expected_output).all())
|
|
44
|
+
|
|
45
|
+
def test_remove_large_comp_2D(self):
|
|
46
|
+
skimage_res = remove_components(self.binary_image_2D, 4, 'max', verbose=False)
|
|
47
|
+
cc3d_res = remove_components_cc3d(self.binary_image_2D, 4, 'max', verbose=False, connectivity=8)
|
|
48
|
+
|
|
49
|
+
self.assertTrue((skimage_res == cc3d_res).all())
|
|
50
|
+
|
|
51
|
+
expected_output = np.array([[0, 0, 0, 0, 0, 0, 1],
|
|
52
|
+
[0, 0, 0, 0, 0, 0, 1],
|
|
53
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
54
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
55
|
+
[1, 1, 0, 1, 0, 0, 1]]).astype(bool)
|
|
56
|
+
|
|
57
|
+
self.assertTrue((skimage_res == expected_output).all())
|
|
58
|
+
|
|
59
|
+
def test_remove_small_comp_3D(self):
|
|
60
|
+
skimage_res = remove_components(self.binary_image_3D, 9, 'min', verbose=False)
|
|
61
|
+
cc3d_res = remove_components_cc3d(self.binary_image_3D, 9, 'min', verbose=False, connectivity=26)
|
|
62
|
+
|
|
63
|
+
self.assertTrue((skimage_res == cc3d_res).all())
|
|
64
|
+
|
|
65
|
+
expected_output = np.array([[[1, 0, 1, 1, 0, 0, 1],
|
|
66
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
67
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
68
|
+
[1, 0, 0, 0, 0, 0, 0],
|
|
69
|
+
[1, 1, 0, 0, 0, 0, 0]],
|
|
70
|
+
[[1, 0, 1, 1, 0, 0, 1],
|
|
71
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
72
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
73
|
+
[1, 0, 0, 0, 0, 0, 0],
|
|
74
|
+
[1, 1, 0, 0, 0, 0, 0]],
|
|
75
|
+
[[1, 0, 1, 1, 0, 0, 1],
|
|
76
|
+
[0, 1, 1, 1, 0, 0, 1],
|
|
77
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
78
|
+
[1, 0, 0, 0, 0, 0, 0],
|
|
79
|
+
[1, 1, 0, 0, 0, 0, 0]],
|
|
80
|
+
]).astype(bool)
|
|
81
|
+
|
|
82
|
+
self.assertTrue((skimage_res == expected_output).all())
|
|
83
|
+
|
|
84
|
+
def test_remove_large_comp_3D(self):
|
|
85
|
+
skimage_res = remove_components(self.binary_image_3D, 12, 'max', verbose=False)
|
|
86
|
+
cc3d_res = remove_components_cc3d(self.binary_image_3D, 12, 'max', verbose=False, connectivity=26)
|
|
87
|
+
|
|
88
|
+
self.assertTrue((skimage_res == cc3d_res).all())
|
|
89
|
+
|
|
90
|
+
expected_output = np.array([[[0, 0, 0, 0, 0, 0, 1],
|
|
91
|
+
[0, 0, 0, 0, 0, 0, 1],
|
|
92
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
93
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
94
|
+
[1, 1, 0, 1, 0, 0, 1]],
|
|
95
|
+
[[0, 0, 0, 0, 0, 0, 1],
|
|
96
|
+
[0, 0, 0, 0, 0, 0, 1],
|
|
97
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
98
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
99
|
+
[1, 1, 0, 1, 0, 0, 1]],
|
|
100
|
+
[[0, 0, 0, 0, 0, 0, 1],
|
|
101
|
+
[0, 0, 0, 0, 0, 0, 1],
|
|
102
|
+
[0, 0, 0, 0, 0, 1, 0],
|
|
103
|
+
[1, 0, 0, 1, 0, 0, 0],
|
|
104
|
+
[1, 1, 0, 1, 0, 0, 1]],
|
|
105
|
+
]).astype(bool)
|
|
106
|
+
|
|
107
|
+
self.assertTrue((skimage_res == expected_output).all())
|