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.
Files changed (33) hide show
  1. {acvl_utils-0.2 → acvl_utils-0.2.2}/PKG-INFO +8 -1
  2. acvl_utils-0.2.2/acvl_utils/cropping_and_padding/bounding_boxes.py +267 -0
  3. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/instance_as_semantic_seg.py +6 -4
  4. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/morphology_helper.py +25 -4
  5. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/PKG-INFO +9 -2
  6. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/SOURCES.txt +8 -1
  7. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/requires.txt +1 -0
  8. {acvl_utils-0.2 → acvl_utils-0.2.2}/setup.py +3 -2
  9. acvl_utils-0.2.2/tests/test_component_removal.py +107 -0
  10. acvl_utils-0.2.2/tests/test_crop_and_pad.py +345 -0
  11. acvl_utils-0.2.2/tests/test_insert_crop.py +112 -0
  12. acvl_utils-0.2.2/tests/test_instance_matching.py +66 -0
  13. acvl_utils-0.2.2/tests/test_padding.py +356 -0
  14. acvl_utils-0.2.2/tests/test_ptqdm.py +29 -0
  15. acvl_utils-0.2.2/tests/test_slicer.py +98 -0
  16. acvl_utils-0.2/acvl_utils/cropping_and_padding/bounding_boxes.py +0 -102
  17. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/__init__.py +0 -0
  18. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/__init__.py +0 -0
  19. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/resampling.py +0 -0
  20. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/array_manipulation/slicer.py +0 -0
  21. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/cropping_and_padding/__init__.py +0 -0
  22. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/cropping_and_padding/padding.py +0 -0
  23. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/__init__.py +0 -0
  24. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/instance_segmentation/instance_matching.py +0 -0
  25. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/__init__.py +0 -0
  26. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/ptqdm.py +0 -0
  27. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/miscellaneous/test_all_images_same.py +0 -0
  28. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/__init__.py +0 -0
  29. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/gpu_binary_morphology.py +0 -0
  30. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils/morphology/slice_interpolation.py +0 -0
  31. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/dependency_links.txt +0 -0
  32. {acvl_utils-0.2 → acvl_utils-0.2.2}/acvl_utils.egg-info/top_level.txt +0 -0
  33. {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)
@@ -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.in1d(labeled_image.ravel(), remove).reshape(labeled_image.shape)
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.in1d(labeled_image.ravel(), remove).reshape(labeled_image.shape)
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.in1d(labeled_image.ravel(), keep).reshape(labeled_image.shape)
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.in1d(labeled_image, keep).astype(binary_image.dtype).reshape(binary_image.shape)
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.in1d(components, labels_to_keep).astype(binary_image.dtype).reshape(binary_image.shape)
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: acvl-utils
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
@@ -4,3 +4,4 @@ torch
4
4
  SimpleITK
5
5
  scikit-image
6
6
  connected-components-3d
7
+ blosc2>=3.0.0b4
@@ -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())