acvl-utils 0.2.2__tar.gz → 0.2.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.
Files changed (33) hide show
  1. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/PKG-INFO +7 -2
  2. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/cropping_and_padding/bounding_boxes.py +135 -17
  3. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/morphology/morphology_helper.py +28 -4
  4. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils.egg-info/PKG-INFO +7 -2
  5. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils.egg-info/SOURCES.txt +1 -0
  6. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/setup.py +1 -1
  7. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_crop_and_pad.py +20 -7
  8. acvl_utils-0.2.4/tests/test_pad_bbox.py +46 -0
  9. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/__init__.py +0 -0
  10. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/array_manipulation/__init__.py +0 -0
  11. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/array_manipulation/resampling.py +0 -0
  12. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/array_manipulation/slicer.py +0 -0
  13. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/cropping_and_padding/__init__.py +0 -0
  14. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/cropping_and_padding/padding.py +0 -0
  15. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/instance_segmentation/__init__.py +0 -0
  16. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/instance_segmentation/instance_as_semantic_seg.py +0 -0
  17. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/instance_segmentation/instance_matching.py +0 -0
  18. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/miscellaneous/__init__.py +0 -0
  19. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/miscellaneous/ptqdm.py +0 -0
  20. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/miscellaneous/test_all_images_same.py +0 -0
  21. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/morphology/__init__.py +0 -0
  22. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/morphology/gpu_binary_morphology.py +0 -0
  23. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils/morphology/slice_interpolation.py +0 -0
  24. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils.egg-info/dependency_links.txt +0 -0
  25. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils.egg-info/requires.txt +0 -0
  26. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/acvl_utils.egg-info/top_level.txt +0 -0
  27. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/setup.cfg +0 -0
  28. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_component_removal.py +0 -0
  29. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_insert_crop.py +0 -0
  30. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_instance_matching.py +0 -0
  31. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_padding.py +0 -0
  32. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_ptqdm.py +0 -0
  33. {acvl_utils-0.2.2 → acvl_utils-0.2.4}/tests/test_slicer.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: acvl_utils
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -12,3 +12,8 @@ Requires-Dist: SimpleITK
12
12
  Requires-Dist: scikit-image
13
13
  Requires-Dist: connected-components-3d
14
14
  Requires-Dist: blosc2>=3.0.0b4
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: license
18
+ Dynamic: requires-dist
19
+ Dynamic: summary
@@ -11,7 +11,57 @@ import torch.nn.functional as F
11
11
  def pad_bbox(bounding_box: Union[List[List[int]], Tuple[Tuple[int, int]]], pad_amount: Union[int, List[int]],
12
12
  array_shape: Tuple[int, ...] = None) -> List[List[int]]:
13
13
  """
14
+ Pads a bounding box by a specified amount and optionally ensures that it stays within the bounds of an array.
14
15
 
16
+ Parameters:
17
+ -----------
18
+ bounding_box : Union[List[List[int]], Tuple[Tuple[int, int]]]
19
+ The input bounding box, represented as a list or tuple of coordinate pairs. Each pair corresponds to
20
+ one dimension and should have the form `[start, end]`, where `start` is inclusive and `end` is exclusive.
21
+
22
+ pad_amount : Union[int, List[int]]
23
+ The amount of padding to apply to each dimension of the bounding box.
24
+ - If an integer is provided, the same padding is applied to all dimensions.
25
+ - If a list is provided, each element specifies the padding for the corresponding dimension.
26
+
27
+ array_shape : Tuple[int, ...], optional
28
+ The shape of the array to which the bounding box is applied.
29
+ If provided, the padded bounding box will be clipped to stay within these bounds.
30
+
31
+ Returns:
32
+ --------
33
+ List[List[int]]
34
+ A new bounding box with the specified padding applied. Each dimension will be represented as a pair `[start, end]`.
35
+
36
+ Notes:
37
+ ------
38
+ - The input `bounding_box` is converted to a list to ensure it can be modified without affecting the original data.
39
+ - The padding is applied symmetrically, reducing the `start` coordinate and increasing the `end` coordinate.
40
+ - If `array_shape` is provided, the padded bounding box will not exceed the valid indices of the array.
41
+
42
+ Examples:
43
+ ---------
44
+ 1. Padding a bounding box without array shape constraints:
45
+ ```python
46
+ bbox = [[2, 10], [5, 15]]
47
+ padded_bbox = pad_bbox(bbox, pad_amount=2)
48
+ # Result: [[0, 12], [3, 17]]
49
+ ```
50
+
51
+ 2. Padding with array shape constraints:
52
+ ```python
53
+ bbox = [[2, 10], [5, 15]]
54
+ array_shape = (12, 20)
55
+ padded_bbox = pad_bbox(bbox, pad_amount=2, array_shape=array_shape)
56
+ # Result: [[0, 12], [3, 17]]
57
+ ```
58
+
59
+ 3. Using dimension-specific padding:
60
+ ```python
61
+ bbox = [[2, 10], [5, 15]]
62
+ padded_bbox = pad_bbox(bbox, pad_amount=[3, 1])
63
+ # Result: [[0, 13], [4, 16]]
64
+ ```
15
65
  """
16
66
  if isinstance(bounding_box, tuple):
17
67
  # convert to list
@@ -41,10 +91,26 @@ def regionprops_bbox_to_proper_bbox(regionprops_bbox: Tuple[int, ...]) -> List[L
41
91
 
42
92
 
43
93
  def bounding_box_to_slice(bounding_box: List[List[int]]):
94
+ """
95
+ ALL bounding boxes in acvl_utils and nnU-Netv2 are half open interval [start, end)!
96
+ - Alignment with Python Slicing
97
+ - Ease of Subdivision
98
+ - Consistency in Multi-Dimensional Arrays
99
+ - Precedent in Computer Graphics
100
+ https://chatgpt.com/share/679203ec-3fbc-8013-a003-13a7adfb1e73
101
+ """
44
102
  return tuple([slice(*i) for i in bounding_box])
45
103
 
46
104
 
47
105
  def crop_to_bbox(array: np.ndarray, bounding_box: List[List[int]]):
106
+ """
107
+ ALL bounding boxes in acvl_utils and nnU-Netv2 are half open interval [start, end)!
108
+ - Alignment with Python Slicing
109
+ - Ease of Subdivision
110
+ - Consistency in Multi-Dimensional Arrays
111
+ - Precedent in Computer Graphics
112
+ https://chatgpt.com/share/679203ec-3fbc-8013-a003-13a7adfb1e73
113
+ """
48
114
  assert len(bounding_box) == len(array.shape), f"Dimensionality of bbox and array do not match. bbox has length " \
49
115
  f"{len(bounding_box)} while array has dimension {len(array.shape)}"
50
116
  slicer = bounding_box_to_slice(bounding_box)
@@ -53,10 +119,15 @@ def crop_to_bbox(array: np.ndarray, bounding_box: List[List[int]]):
53
119
 
54
120
  def get_bbox_from_mask(mask: np.ndarray) -> List[List[int]]:
55
121
  """
122
+ ALL bounding boxes in acvl_utils and nnU-Netv2 are half open interval [start, end)!
123
+ - Alignment with Python Slicing
124
+ - Ease of Subdivision
125
+ - Consistency in Multi-Dimensional Arrays
126
+ - Precedent in Computer Graphics
127
+ https://chatgpt.com/share/679203ec-3fbc-8013-a003-13a7adfb1e73
128
+
56
129
  this implementation uses less ram than the np.where one and is faster as well IF we expect the bounding box to
57
130
  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
131
 
61
132
  :param mask:
62
133
  :param outside_value:
@@ -96,7 +167,22 @@ def get_bbox_from_mask(mask: np.ndarray) -> List[List[int]]:
96
167
  return [[minzidx, maxzidx], [minxidx, maxxidx], [minyidx, maxyidx]]
97
168
 
98
169
 
170
+ def int_bbox(bbox):
171
+ bbox2 = [[int(i) for i in row] for row in bbox] # Convert all values to int
172
+ if bbox2 != bbox: # Directly compare the original and casted versions
173
+ raise RuntimeError(f"Invalid bbox encountered. Cannot be safely cast to int: {bbox}. Casting result: {bbox2}")
174
+ return bbox2
175
+
176
+
99
177
  def get_bbox_from_mask_npwhere(mask: np.ndarray) -> List[List[int]]:
178
+ """
179
+ ALL bounding boxes in acvl_utils and nnU-Netv2 are half open interval [start, end)!
180
+ - Alignment with Python Slicing
181
+ - Ease of Subdivision
182
+ - Consistency in Multi-Dimensional Arrays
183
+ - Precedent in Computer Graphics
184
+ https://chatgpt.com/share/679203ec-3fbc-8013-a003-13a7adfb1e73
185
+ """
100
186
  where = np.array(np.where(mask))
101
187
  mins = np.min(where, 1)
102
188
  maxs = np.max(where, 1) + 1
@@ -106,21 +192,37 @@ def get_bbox_from_mask_npwhere(mask: np.ndarray) -> List[List[int]]:
106
192
  def crop_and_pad_nd(
107
193
  image: Union[torch.Tensor, np.ndarray, blosc2.ndarray.NDArray],
108
194
  bbox: List[List[int]],
109
- pad_value = 0
195
+ pad_value=0,
196
+ pad_mode: str = 'constant'
110
197
  ) -> Union[torch.Tensor, np.ndarray]:
111
198
  """
112
- Crops a bounding box directly specified by bbox, excluding the upper bound.
199
+ Crops a bounding box directly specified by bbox, adhering to the half-open interval [start, end).
113
200
  If the bounding box extends beyond the image boundaries, the cropped area is padded
114
201
  to maintain the desired size. Initial dimensions not included in bbox remain unaffected.
115
202
 
203
+ ALL bounding boxes in acvl_utils and nnU-Netv2 are half open interval [start, end)!
204
+ - Alignment with Python Slicing
205
+ - Ease of Subdivision
206
+ - Consistency in Multi-Dimensional Arrays
207
+ - Precedent in Computer Graphics
208
+ https://chatgpt.com/share/679203ec-3fbc-8013-a003-13a7adfb1e73
209
+
116
210
  Parameters:
117
- - image: N-dimensional torch.Tensor or np.ndarray representing the image
211
+ - image: N-dimensional torch.Tensor, np.ndarray, or blosc2.ndarray.NDArray representing the image.
118
212
  - bbox: List of [[dim_min, dim_max], ...] defining the bounding box for the last dimensions.
213
+ Each dimension follows the half-open interval [start, end).
214
+ - pad_value: Value used for padding when bbox extends beyond image boundaries.
215
+ - pad_mode: Padding mode, one of 'constant', 'reflect', or 'replicate' (alias for 'edge').
119
216
 
120
217
  Returns:
121
218
  - Cropped and padded patch of the requested bounding box size, as the same type as `image`.
122
219
  """
123
220
 
221
+ assert pad_mode in ['constant', 'reflect', 'replicate', 'edge'], "Unsupported pad_mode."
222
+
223
+ # make sure bounding boxes are int and not uint. Otherwise we may get underflow
224
+ bbox = int_bbox(bbox)
225
+
124
226
  # Determine the number of dimensions to crop based on bbox
125
227
  crop_dims = len(bbox)
126
228
  img_shape = image.shape
@@ -130,7 +232,7 @@ def crop_and_pad_nd(
130
232
  slices = []
131
233
  padding = []
132
234
  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]
235
+ target_shape = output_shape + [max_val - min_val for min_val, max_val in bbox] # Half-open interval
134
236
 
135
237
  # Iterate through dimensions, applying bbox to the last `crop_dims` dimensions
136
238
  for i in range(num_dims):
@@ -138,13 +240,13 @@ def crop_and_pad_nd(
138
240
  # For initial dimensions not covered by bbox, include the entire dimension
139
241
  slices.append(slice(None))
140
242
  padding.append([0, 0])
141
- output_shape.append(img_shape[i]) # Keep the initial dimensions as they are
243
+ output_shape.append(img_shape[i])
142
244
  else:
143
245
  # For dimensions specified in bbox, directly use the min and max bounds
144
246
  dim_idx = i - (num_dims - crop_dims) # Index within bbox
145
247
 
146
248
  min_val = bbox[dim_idx][0]
147
- max_val = bbox[dim_idx][1]
249
+ max_val = bbox[dim_idx][1] # This is exclusive by definition
148
250
 
149
251
  # Check if the bounding box is completely outside the image bounds
150
252
  if max_val <= 0 or min_val >= img_shape[i]:
@@ -154,9 +256,9 @@ def crop_and_pad_nd(
154
256
  elif isinstance(image, (np.ndarray, blosc2.ndarray.NDArray)):
155
257
  return np.zeros(target_shape, dtype=image.dtype)
156
258
 
157
- # Calculate valid cropping ranges within image bounds, excluding the upper bound
259
+ # Calculate valid cropping ranges within image bounds (half-open interval)
158
260
  valid_min = max(min_val, 0)
159
- valid_max = min(max_val, img_shape[i]) # Exclude upper bound by using max_val directly
261
+ valid_max = min(max_val, img_shape[i]) # Exclusive upper bound
160
262
  slices.append(slice(valid_min, valid_max))
161
263
 
162
264
  # Calculate padding needed for this dimension
@@ -171,14 +273,28 @@ def crop_and_pad_nd(
171
273
  cropped = image[tuple(slices)]
172
274
 
173
275
  # 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)
276
+ if np.any(padding):
277
+ if isinstance(image, torch.Tensor):
278
+ if pad_mode == 'edge':
279
+ pad_mode = 'replicate'
280
+ cropped = cropped[None, None] # Adjust shape for PyTorch padding requirements
281
+ flattened_padding = [p for sublist in reversed(padding) for p in sublist] # Flatten in reverse order for PyTorch
282
+ padded_cropped = F.pad(cropped, flattened_padding, mode=pad_mode, value=pad_value)
283
+ if pad_mode == 'replicate':
284
+ padded_cropped = padded_cropped[0, 0]
285
+ elif isinstance(image, (np.ndarray, blosc2.ndarray.NDArray)):
286
+ if pad_mode == 'replicate':
287
+ pad_mode = 'edge'
288
+ if pad_mode == 'edge' or pad_mode == 'reflect':
289
+ kwargs = {}
290
+ else:
291
+ kwargs = {'constant_values': pad_value}
292
+ pad_width = [(p[0], p[1]) for p in padding]
293
+ padded_cropped = np.pad(cropped, pad_width=pad_width, mode=pad_mode, **kwargs)
294
+ else:
295
+ raise ValueError(f'Unsupported image type {type(image)}')
180
296
  else:
181
- raise ValueError(f'Unsupported image type {type(image)}')
297
+ padded_cropped = cropped
182
298
 
183
299
  return padded_cropped
184
300
 
@@ -201,6 +317,8 @@ def insert_crop_into_image(
201
317
  Returns:
202
318
  - image: The original image with the crop reinserted at the specified location (modified in-place).
203
319
  """
320
+ # make sure bounding boxes are int and not uint. Otherwise we may get underflow
321
+ bbox = int_bbox(bbox)
204
322
 
205
323
  # Ensure that bbox only applies to the last len(bbox) dimensions of crop and image
206
324
  num_dims = len(image.shape)
@@ -2,20 +2,24 @@ from typing import Union, Tuple, List, Callable, Dict
2
2
 
3
3
  import numpy as np
4
4
  from skimage.measure import label
5
- from skimage.morphology import ball
5
+ from skimage.morphology import ball, disk
6
6
  from skimage.transform import resize
7
7
  import cc3d
8
8
 
9
9
 
10
- def generate_ball(radius: Union[Tuple, List], spacing: Union[Tuple, List] = (1, 1, 1), dtype=np.uint8) -> np.ndarray:
10
+ def generate_ball(radius: Union[Tuple, List], spacing: Union[Tuple, List] = None, dtype=np.uint8) -> np.ndarray:
11
11
  """
12
12
  Returns a ball/ellipsoid corresponding to the specified size (radius = list/tuple of len 3 with one radius per axis)
13
13
  If you use spacing, both radius and spacing will be interpreted relative to each other, so a radius of 10 with a
14
14
  spacing of 5 will result in a ball with radius 2 pixels.
15
15
  """
16
- radius_in_voxels = np.array([round(i) for i in radius / np.array(spacing)])
16
+ if spacing is None:
17
+ spacing = [1] * len(radius)
18
+ radius_in_voxels = np.array([round(i) for i in radius]) / np.array(spacing)
19
+ if all([i == radius_in_voxels[0] for i in radius_in_voxels[1:]]):
20
+ return ball(radius_in_voxels[0]) if len(radius) == 3 else disk(radius_in_voxels[0])
17
21
  n = 2 * radius_in_voxels + 1
18
- ball_iso = ball(max(n) * 2, dtype=np.float64)
22
+ ball_iso = ball(max(n) * 2, dtype=np.float64) if len(radius) == 3 else disk(max(n) * 2, dtype=np.float64)
19
23
  ball_resampled = resize(ball_iso, n, 1, 'constant', 0, clip=True, anti_aliasing=False, preserve_range=True)
20
24
  ball_resampled[ball_resampled > 0.5] = 1
21
25
  ball_resampled[ball_resampled <= 0.5] = 0
@@ -59,6 +63,14 @@ def remove_all_but_largest_component(binary_image: np.ndarray, connectivity: int
59
63
  return generic_filter_components(binary_image, filter_fn, connectivity)
60
64
 
61
65
 
66
+ def remove_all_but_largest_component_cc3d(binary_image: np.ndarray, connectivity: int = None) -> np.ndarray:
67
+ """
68
+ Removes all but the largest component in binary_image. Replaces pixels that don't belong to it with background_label
69
+ """
70
+ filter_fn = lambda x, y: [i for i, j in zip(x, y) if j == max(y)]
71
+ return generic_filter_components_cc3d(binary_image, filter_fn, connectivity)
72
+
73
+
62
74
  def generic_filter_components(binary_image: np.ndarray, filter_fn: Callable[[List[int], List[int]], List[int]],
63
75
  connectivity: int = None):
64
76
  """
@@ -74,6 +86,17 @@ def generic_filter_components(binary_image: np.ndarray, filter_fn: Callable[[Lis
74
86
  return np.isin(labeled_image.ravel(), keep).reshape(labeled_image.shape)
75
87
 
76
88
 
89
+ def generic_filter_components_cc3d(binary_image: np.ndarray, filter_fn: Callable[[List[int], List[int]], List[int]],
90
+ connectivity: int = None):
91
+ """
92
+ """
93
+ labeled_image, component_sizes = cc3d_label_with_component_sizes(binary_image, connectivity)
94
+ component_ids = list(component_sizes.keys())
95
+ component_sizes = list(component_sizes.values())
96
+ keep = filter_fn(component_ids, component_sizes)
97
+ return np.isin(labeled_image.ravel(), keep).reshape(labeled_image.shape)
98
+
99
+
77
100
  def remove_components(binary_image: np.ndarray, threshold_size_in_pixels: int, threshold_type: str = 'min',
78
101
  connectivity=None, verbose: bool = False):
79
102
  """
@@ -165,6 +188,7 @@ def remove_components_cc3d(binary_image: np.ndarray, threshold_size_in_pixels: i
165
188
  return keep
166
189
 
167
190
 
191
+
168
192
  if __name__ == '__main__':
169
193
  print(generate_ball((10, 10, 10), (5, 5, 5)).shape)
170
194
  print(generate_ball((10, 5, 15), (1, 1, 1)).shape)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: acvl_utils
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -12,3 +12,8 @@ Requires-Dist: SimpleITK
12
12
  Requires-Dist: scikit-image
13
13
  Requires-Dist: connected-components-3d
14
14
  Requires-Dist: blosc2>=3.0.0b4
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: license
18
+ Dynamic: requires-dist
19
+ Dynamic: summary
@@ -25,6 +25,7 @@ tests/test_component_removal.py
25
25
  tests/test_crop_and_pad.py
26
26
  tests/test_insert_crop.py
27
27
  tests/test_instance_matching.py
28
+ tests/test_pad_bbox.py
28
29
  tests/test_padding.py
29
30
  tests/test_ptqdm.py
30
31
  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.2',
5
+ version='0.2.4',
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',
@@ -264,7 +264,6 @@ class TestCropAndPadND(unittest.TestCase):
264
264
  self.assertEqual(cropped_patch.shape, (5, 10, 10, 5))
265
265
  np.testing.assert_array_equal(cropped_patch, self.image_np[:, :, :, 3:8])
266
266
 
267
-
268
267
  def test_two_dimensions_crop_np(self):
269
268
  """Test cropping the last two dimensions of a 4D np.ndarray."""
270
269
  bbox = [[3, 8], [1, 7]]
@@ -274,7 +273,6 @@ class TestCropAndPadND(unittest.TestCase):
274
273
  self.assertEqual(cropped_patch.shape, (5, 10, 5, 6))
275
274
  np.testing.assert_array_equal(cropped_patch, self.image_np[:, :, 3:8, 1:7])
276
275
 
277
-
278
276
  def test_three_dimensions_crop_np(self):
279
277
  """Test cropping the last three dimensions of a 4D np.ndarray."""
280
278
  bbox = [[1, 6], [2, 9], [3, 8]]
@@ -284,7 +282,6 @@ class TestCropAndPadND(unittest.TestCase):
284
282
  self.assertEqual(cropped_patch.shape, (5, 5, 7, 5))
285
283
  np.testing.assert_array_equal(cropped_patch, self.image_np[:, 1:6, 2:9, 3:8])
286
284
 
287
-
288
285
  def test_all_dimensions_crop_np(self):
289
286
  """Test cropping all dimensions of a 4D np.ndarray."""
290
287
  bbox = [[1, 4], [2, 7], [0, 6], [4, 9]]
@@ -294,7 +291,6 @@ class TestCropAndPadND(unittest.TestCase):
294
291
  self.assertEqual(cropped_patch.shape, (3, 5, 6, 5))
295
292
  np.testing.assert_array_equal(cropped_patch, self.image_np[1:4, 2:7, 0:6, 4:9])
296
293
 
297
-
298
294
  def test_single_dimension_crop_torch(self):
299
295
  """Test cropping only the last dimension of a 4D torch.Tensor."""
300
296
  bbox = [[3, 8]]
@@ -304,7 +300,6 @@ class TestCropAndPadND(unittest.TestCase):
304
300
  self.assertEqual(cropped_patch.shape, (5, 10, 10, 5))
305
301
  torch.testing.assert_close(cropped_patch, self.image_torch[:, :, :, 3:8])
306
302
 
307
-
308
303
  def test_two_dimensions_crop_torch(self):
309
304
  """Test cropping the last two dimensions of a 4D torch.Tensor."""
310
305
  bbox = [[3, 8], [1, 7]]
@@ -314,7 +309,6 @@ class TestCropAndPadND(unittest.TestCase):
314
309
  self.assertEqual(cropped_patch.shape, (5, 10, 5, 6))
315
310
  torch.testing.assert_close(cropped_patch, self.image_torch[:, :, 3:8, 1:7])
316
311
 
317
-
318
312
  def test_three_dimensions_crop_torch(self):
319
313
  """Test cropping the last three dimensions of a 4D torch.Tensor."""
320
314
  bbox = [[1, 6], [2, 9], [3, 8]]
@@ -324,7 +318,6 @@ class TestCropAndPadND(unittest.TestCase):
324
318
  self.assertEqual(cropped_patch.shape, (5, 5, 7, 5))
325
319
  torch.testing.assert_close(cropped_patch, self.image_torch[:, 1:6, 2:9, 3:8])
326
320
 
327
-
328
321
  def test_all_dimensions_crop_torch(self):
329
322
  """Test cropping all dimensions of a 4D torch.Tensor."""
330
323
  bbox = [[1, 4], [2, 7], [0, 6], [4, 9]]
@@ -334,6 +327,26 @@ class TestCropAndPadND(unittest.TestCase):
334
327
  self.assertEqual(cropped_patch.shape, (3, 5, 6, 5))
335
328
  torch.testing.assert_close(cropped_patch, self.image_torch[1:4, 2:7, 0:6, 4:9])
336
329
 
330
+ def test_pad_all_dims(self):
331
+ seg = np.ones((128, 128))
332
+ bbox = [[-5, 133], [-5, 133]]
333
+ out = crop_and_pad_nd(seg, bbox)
334
+ self.assertEqual(out.shape, (138, 138))
335
+ bbox_reverse = [[5, 133], [5, 133]]
336
+ out2 = crop_and_pad_nd(out, bbox_reverse)
337
+ self.assertEqual(out2.shape, (128, 128))
338
+ self.assertTrue(np.all(out2==1))
339
+
340
+ def test_pad_all_dims_torch(self):
341
+ seg = torch.ones((128, 128))
342
+ bbox = [[-5, 133], [-5, 133]]
343
+ out = crop_and_pad_nd(seg, bbox)
344
+ self.assertEqual(out.shape, (138, 138))
345
+ bbox_reverse = [[5, 133], [5, 133]]
346
+ out2 = crop_and_pad_nd(out, bbox_reverse)
347
+ self.assertEqual(out2.shape, (128, 128))
348
+ self.assertTrue(torch.all(out2==1))
349
+
337
350
 
338
351
  if __name__ == "__main__":
339
352
  unittest.main()
@@ -0,0 +1,46 @@
1
+ import unittest
2
+
3
+ from acvl_utils.cropping_and_padding.bounding_boxes import pad_bbox
4
+
5
+
6
+ class TestPadBBox(unittest.TestCase):
7
+
8
+ def test_no_array_shape(self):
9
+ bbox = [[2, 10], [5, 15]]
10
+ pad_amount = 2
11
+ expected = [[0, 12], [3, 17]]
12
+ result = pad_bbox(bbox, pad_amount)
13
+ self.assertEqual(result, expected)
14
+
15
+ def test_with_array_shape(self):
16
+ bbox = [[2, 10], [5, 15]]
17
+ pad_amount = 2
18
+ array_shape = (12, 20)
19
+ expected = [[0, 12], [3, 17]]
20
+ result = pad_bbox(bbox, pad_amount, array_shape)
21
+ self.assertEqual(result, expected)
22
+
23
+ def test_dimension_specific_padding(self):
24
+ bbox = [[2, 10], [5, 15]]
25
+ pad_amount = [3, 1]
26
+ expected = [[0, 13], [4, 16]]
27
+ result = pad_bbox(bbox, pad_amount)
28
+ self.assertEqual(result, expected)
29
+
30
+ def test_clipping_with_array_shape(self):
31
+ bbox = [[2, 10], [5, 15]]
32
+ pad_amount = 5
33
+ array_shape = (12, 18)
34
+ expected = [[0, 12], [0, 18]]
35
+ result = pad_bbox(bbox, pad_amount, array_shape)
36
+ self.assertEqual(result, expected)
37
+
38
+ def test_no_padding(self):
39
+ bbox = [[2, 10], [5, 15]]
40
+ pad_amount = 0
41
+ expected = [[2, 10], [5, 15]]
42
+ result = pad_bbox(bbox, pad_amount)
43
+ self.assertEqual(result, expected)
44
+
45
+ if __name__ == "__main__":
46
+ unittest.main()
File without changes