acvl-utils 0.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 (25) hide show
  1. acvl_utils-0.2/PKG-INFO +7 -0
  2. acvl_utils-0.2/acvl_utils/__init__.py +0 -0
  3. acvl_utils-0.2/acvl_utils/array_manipulation/__init__.py +0 -0
  4. acvl_utils-0.2/acvl_utils/array_manipulation/resampling.py +93 -0
  5. acvl_utils-0.2/acvl_utils/array_manipulation/slicer.py +66 -0
  6. acvl_utils-0.2/acvl_utils/cropping_and_padding/__init__.py +0 -0
  7. acvl_utils-0.2/acvl_utils/cropping_and_padding/bounding_boxes.py +102 -0
  8. acvl_utils-0.2/acvl_utils/cropping_and_padding/padding.py +105 -0
  9. acvl_utils-0.2/acvl_utils/instance_segmentation/__init__.py +0 -0
  10. acvl_utils-0.2/acvl_utils/instance_segmentation/instance_as_semantic_seg.py +520 -0
  11. acvl_utils-0.2/acvl_utils/instance_segmentation/instance_matching.py +126 -0
  12. acvl_utils-0.2/acvl_utils/miscellaneous/__init__.py +0 -0
  13. acvl_utils-0.2/acvl_utils/miscellaneous/ptqdm.py +46 -0
  14. acvl_utils-0.2/acvl_utils/miscellaneous/test_all_images_same.py +89 -0
  15. acvl_utils-0.2/acvl_utils/morphology/__init__.py +0 -0
  16. acvl_utils-0.2/acvl_utils/morphology/gpu_binary_morphology.py +145 -0
  17. acvl_utils-0.2/acvl_utils/morphology/morphology_helper.py +149 -0
  18. acvl_utils-0.2/acvl_utils/morphology/slice_interpolation.py +65 -0
  19. acvl_utils-0.2/acvl_utils.egg-info/PKG-INFO +7 -0
  20. acvl_utils-0.2/acvl_utils.egg-info/SOURCES.txt +23 -0
  21. acvl_utils-0.2/acvl_utils.egg-info/dependency_links.txt +1 -0
  22. acvl_utils-0.2/acvl_utils.egg-info/requires.txt +6 -0
  23. acvl_utils-0.2/acvl_utils.egg-info/top_level.txt +1 -0
  24. acvl_utils-0.2/setup.cfg +4 -0
  25. acvl_utils-0.2/setup.py +19 -0
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: acvl_utils
3
+ Version: 0.2
4
+ Summary: Super cool utilities that we just love to use
5
+ Author: Applied Computer Vision Lab, Helmholtz Imaging & Division of Medical Image Computing, German Cancer Research Center
6
+ Author-email: f.isensee@dkfz.de
7
+ License: Apache License Version 2.0, January 2004
File without changes
@@ -0,0 +1,93 @@
1
+ from typing import Tuple
2
+
3
+ import torch
4
+ from torch.nn import functional as F
5
+
6
+
7
+ def maybe_resample_on_gpu(data: torch.Tensor,
8
+ target_shape: Tuple[int, ...],
9
+ return_type=torch.half,
10
+ compute_device: str = 'cuda:0', result_device: str = 'cuda:0',
11
+ compute_precision=torch.half,
12
+ fallback_compute_device: str = 'cpu', fallback_result_device: str = 'cpu',
13
+ fallback_compute_precision=torch.float,
14
+ verbose: bool = True) -> torch.Tensor:
15
+ """
16
+ channel-wise resampling function. Most useful for softmax resampling. Tries three things:
17
+ - resample the entire array at once on compute_device (fastest but uses the most memory). returned tensor will be on result_device
18
+ - failing that it will resample channel-wise on compute_device. returned tensor will be on fallback_result_device
19
+ - failing that it will resample channel-wise on fallback_device. returned tensor will be on fallback_result_device
20
+
21
+ data must be a 4d tensor of shape (c, x, y, z). It will be resampled to shape (tx, ty, tz), so the returned tensor
22
+ will have shape (c, tx, ty, tz)
23
+
24
+ returned tensor will be on result_device/fallback_result_device
25
+ returned tensor will have dtype return_type. half recommended if memory is scarce
26
+
27
+ fallback_compute_precision must be torch.float for CPU (half not implemented as of 11/2022)
28
+
29
+ this function does not use with torch.no_grad()!! You need to do this externally
30
+
31
+ Defaults for compute_device, result_device, compute_precision, fallback_compute_device, fallback_result_device,
32
+ fallback_compute_precision are good and should only be deviated from by experts.
33
+ """
34
+
35
+ def _maybe_empty_cache():
36
+ if torch.cuda.is_available(): torch.cuda.empty_cache()
37
+
38
+ def _allocate_result_tensor(shape, dtype, device, fallback_device) -> torch.Tensor:
39
+ try:
40
+ softmax_resampled = torch.zeros(shape, dtype=dtype, device=device)
41
+ except RuntimeError:
42
+ if verbose:
43
+ print(f'could not allocate result tensor of shape {(data.shape[0], *target_shape)} on device '
44
+ f'{result_device}. Using fallback {fallback_device}')
45
+ assert fallback_device != device, 'Allocation on device failed and fallback_device=device so there is no ' \
46
+ 'point attempting this. Duh.'
47
+ softmax_resampled = torch.zeros(shape, dtype=dtype, device=fallback_device)
48
+ finally:
49
+ _maybe_empty_cache()
50
+ return softmax_resampled
51
+
52
+ _maybe_empty_cache()
53
+
54
+ if all([i == j for i, j in zip(data.shape[1:], target_shape)]):
55
+ try:
56
+ return data.to(result_device)
57
+ except RuntimeError:
58
+ return data.to(fallback_result_device)
59
+
60
+ try:
61
+ # try all in one
62
+ result_tensor = \
63
+ F.interpolate(data.to(compute_device).type(compute_precision)[None], target_shape, mode='trilinear')[0].type(
64
+ return_type).to(result_device)
65
+ except RuntimeError:
66
+ if verbose:
67
+ print(f'Resampling all in one failed, attempting channel-wise on compute_device {compute_device}')
68
+ try:
69
+ _maybe_empty_cache()
70
+ # allocate result tensor
71
+ result_tensor = _allocate_result_tensor((data.shape[0], *target_shape), return_type, result_device,
72
+ fallback_result_device)
73
+ for c in range(data.shape[0]):
74
+ result_tensor[c] = F.interpolate(data[c][None, None].to(compute_device).type(compute_precision),
75
+ size=target_shape,
76
+ mode='trilinear')[0, 0].to(result_tensor.device)
77
+ except RuntimeError:
78
+ if verbose:
79
+ print(f'Resampling channel-wise on compute_device {compute_device} failed, attempting chanel-wise '
80
+ f'on fallback {fallback_compute_device}')
81
+ _maybe_empty_cache()
82
+ # allocate result tensor
83
+ result_tensor = _allocate_result_tensor((data.shape[0], *target_shape), return_type, fallback_result_device,
84
+ fallback_result_device)
85
+ for c in range(data.shape[0]):
86
+ result_tensor[c] = F.interpolate(data[c][None, None].to(fallback_compute_device).type(fallback_compute_precision),
87
+ size=target_shape,
88
+ mode='trilinear')[0, 0].to(result_tensor.device)
89
+ finally:
90
+ _maybe_empty_cache()
91
+ finally:
92
+ _maybe_empty_cache()
93
+ return result_tensor
@@ -0,0 +1,66 @@
1
+ import numbers
2
+
3
+
4
+ def slicer(arr, slices):
5
+ """
6
+ A dynamic N-dimensional array slicer that returns a tuple that can be used for slicing an N-dimensional array. Works exactly as Python and Numpy slicing, only with different syntax.
7
+ The conventional slicing method has the drawback that one must know the dimensionality of the array beforehand. By contrast, this slicer can be adapted dynamically at runtime.
8
+ :param arr: The array that should be sliced.
9
+ :param slices: A list of slices that obeys the syntax described below.
10
+ :return A tuple that can be used for slicing the array.
11
+
12
+ Use case example - Center crop augmentation for an N-dimensional image:
13
+ Imaging you have an N-dimensional array. You don't know what dimension it has. You want to crop each dimension to the center:
14
+ image = ...
15
+ print(image.shape) # (32, 32) (48, 32, 32) or ...
16
+ indices = comp_indices(image, width=8)
17
+ # crop = image[???] # Not possible to define a crop with arbitrary dimensions with regular slicing. Instead ...
18
+ if len(image.shape) == 2:
19
+ crop = image[indices[0][0]:indices[0][1], indices[1][0]:indices[1][1]] # image[10:22, 10:22])
20
+ elif len(image.shape) == 3:
21
+ crop = image[indices[0][0]:indices[0][1], indices[1][0]:indices[1][1], indices[2][0]:indices[2][1]] # image[18:30, 10:22, 10:22]
22
+ elif ...
23
+ As you see, this cannot be done with regular slicing as the slicing syntax needs to be hardcoded.
24
+ You would need to create an if case for each possible dimension.
25
+ However, this slicer function enables you to do just that dynamically for data with arbitrary dimensions:
26
+ image = ...
27
+ print(image.shape) # (32, 32) (48, 32, 32) or ...
28
+ indices = comp_indices(image, width=8)
29
+ crop = data[slicer(data, indices)] # Center crop the data
30
+
31
+ Syntax:
32
+ - ... -> None
33
+ - : -> [None]
34
+ - i -> i
35
+ - i:j -> [i, j]
36
+ - i: -> [i, None]
37
+ - :j -> [None, j]
38
+
39
+ Syntax examples:
40
+ - sub_arr = arr[5] -> sub_arr = arr[slicer(arr, [5])]
41
+ - sub_arr = arr[...] -> sub_arr = arr[slicer(arr, [None])]
42
+ - sub_arr = arr[..., 5] -> sub_arr = arr[slicer(arr, [None, 5])]
43
+ - sub_arr = arr[5:9] -> sub_arr = arr[slicer(arr, [[5, 9]])]
44
+ - sub_arr = arr[14:30, 12:17] -> sub_arr = arr[slicer(arr, [[14, 30], [12, 17]])]
45
+ - sub_arr = arr[14:30, 12:17, ...] -> sub_arr = arr[slicer(arr, [[14, 30], [12, 17], None])]
46
+ - sub_arr = arr[14:30, 12:17, :] -> sub_arr = arr[slicer(arr, [[14, 30], [12, 17], [None]])]
47
+ - sub_arr = arr[14:30, :-17, :] -> sub_arr = arr[slicer(arr, [[14, 30], [None, -17], [None]])]
48
+ - arr[5] = 7 -> arr[slicer(arr, [5])] = 7
49
+ - arr[15:30, 12:19] = np.zeros((15, 7)) -> arr[slicer(arr, [[15, 30], [12, 19]])] = np.zeros((15, 7))
50
+
51
+ This function was inspired by the following stackoverflow thread: https://stackoverflow.com/questions/24398708/slicing-a-numpy-array-along-a-dynamically-specified-axis/37729566#37729566
52
+ """
53
+ slc = [slice(None)] * len(arr.shape)
54
+ axis = 0
55
+ for i in range(len(slices)):
56
+ if slices[i] is None: # Ellipsis: Take all values from multiple axes: array[..., ???]
57
+ axis = len(arr.shape) - (len(slices) - 1)
58
+ elif isinstance(slices[i], numbers.Number): # Take single value from a single axis: array[???, i, ???]
59
+ slc[axis] = slices[i]
60
+ axis += 1
61
+ elif len(slices[i]) == 1 and slices[i][0] is None: # Colon: Take all values from a single axis: array[???, :, ???]
62
+ axis += 1
63
+ else: # Take all values from the range i to j on a single axis: array[???, i:j, ???]. i or j can also be None
64
+ slc[axis] = slice(slices[i][0], slices[i][1])
65
+ axis += 1
66
+ return tuple(slc)
@@ -0,0 +1,102 @@
1
+ import numpy as np
2
+ from copy import deepcopy
3
+ from typing import List, Tuple, Union
4
+
5
+
6
+ def pad_bbox(bounding_box: Union[List[List[int]], Tuple[Tuple[int, int]]], pad_amount: Union[int, List[int]],
7
+ array_shape: Tuple[int, ...] = None) -> List[List[int]]:
8
+ """
9
+
10
+ """
11
+ if isinstance(bounding_box, tuple):
12
+ # convert to list
13
+ bounding_box = [list(i) for i in bounding_box]
14
+ else:
15
+ # needed because otherwise we overwrite input which could have unforseen consequences
16
+ bounding_box = deepcopy(bounding_box)
17
+
18
+ if isinstance(pad_amount, int):
19
+ pad_amount = [pad_amount] * len(bounding_box)
20
+
21
+ for i in range(len(bounding_box)):
22
+ new_values = [max(0, bounding_box[i][0] - pad_amount[i]), bounding_box[i][1] + pad_amount[i]]
23
+ if array_shape is not None:
24
+ new_values[1] = min(array_shape[i], new_values[1])
25
+ bounding_box[i] = new_values
26
+
27
+ return bounding_box
28
+
29
+
30
+ def regionprops_bbox_to_proper_bbox(regionprops_bbox: Tuple[int, ...]) -> List[List[int]]:
31
+ """
32
+ regionprops_bbox is what you get from `from skimage.measure import regionprops`
33
+ """
34
+ dim = len(regionprops_bbox) // 2
35
+ return [[regionprops_bbox[i], regionprops_bbox[i + dim]] for i in range(dim)]
36
+
37
+
38
+ def bounding_box_to_slice(bounding_box: List[List[int]]):
39
+ return tuple([slice(*i) for i in bounding_box])
40
+
41
+
42
+ def crop_to_bbox(array: np.ndarray, bounding_box: List[List[int]]):
43
+ assert len(bounding_box) == len(array.shape), f"Dimensionality of bbox and array do not match. bbox has length " \
44
+ f"{len(bounding_box)} while array has dimension {len(array.shape)}"
45
+ slicer = bounding_box_to_slice(bounding_box)
46
+ return array[slicer]
47
+
48
+
49
+ def get_bbox_from_mask(mask: np.ndarray) -> List[List[int]]:
50
+ """
51
+ this implementation uses less ram than the np.where one and is faster as well IF we expect the bounding box to
52
+ be close to the image size. If it's not it's likely slower!
53
+
54
+ :param mask:
55
+ :param outside_value:
56
+ :return:
57
+ """
58
+ Z, X, Y = mask.shape
59
+ minzidx, maxzidx, minxidx, maxxidx, minyidx, maxyidx = 0, Z, 0, X, 0, Y
60
+ zidx = list(range(Z))
61
+ for z in zidx:
62
+ if np.any(mask[z]):
63
+ minzidx = z
64
+ break
65
+ for z in zidx[::-1]:
66
+ if np.any(mask[z]):
67
+ maxzidx = z + 1
68
+ break
69
+
70
+ xidx = list(range(X))
71
+ for x in xidx:
72
+ if np.any(mask[:, x]):
73
+ minxidx = x
74
+ break
75
+ for x in xidx[::-1]:
76
+ if np.any(mask[:, x]):
77
+ maxxidx = x + 1
78
+ break
79
+
80
+ yidx = list(range(Y))
81
+ for y in yidx:
82
+ if np.any(mask[:, :, y]):
83
+ minyidx = y
84
+ break
85
+ for y in yidx[::-1]:
86
+ if np.any(mask[:, :, y]):
87
+ maxyidx = y + 1
88
+ break
89
+ return [[minzidx, maxzidx], [minxidx, maxxidx], [minyidx, maxyidx]]
90
+
91
+
92
+ def get_bbox_from_mask_npwhere(mask: np.ndarray) -> List[List[int]]:
93
+ where = np.array(np.where(mask))
94
+ mins = np.min(where, 1)
95
+ maxs = np.max(where, 1) + 1
96
+ return [[i, j] for i, j in zip(mins, maxs)]
97
+
98
+
99
+ if __name__ == '__main__':
100
+ bbox = [[32, 64], [21, 46]]
101
+ bbox_padded = pad_bbox(bbox, 3)
102
+ slicer = bounding_box_to_slice(bbox_padded)
@@ -0,0 +1,105 @@
1
+ from typing import Union, List, Tuple
2
+
3
+ import numpy as np
4
+ import torch
5
+ from torch.nn import functional as F
6
+
7
+
8
+ def pad_nd_image(image: Union[torch.Tensor, np.ndarray], new_shape: Tuple[int, ...] = None,
9
+ mode: str = "constant", kwargs: dict = None, return_slicer: bool = False,
10
+ shape_must_be_divisible_by: Union[int, Tuple[int, ...], List[int]] = None) -> \
11
+ Union[Union[torch.Tensor, np.ndarray], Tuple[Union[torch.Tensor, np.ndarray], Tuple]]:
12
+ """
13
+ One padder to pad them all. Documentation? Well okay. A little bit
14
+
15
+ Padding is done such that the original content will be at the center of the padded image. If the amount of padding
16
+ needed it odd, the padding 'above' the content is larger,
17
+ Example:
18
+ old shape: [ 3 34 55 3]
19
+ new_shape: [3, 34, 96, 64]
20
+ amount of padding (low, high for each axis): [[0, 0], [0, 0], [20, 21], [30, 31]]
21
+
22
+ :param image: can either be a numpy array or a torch.Tensor. pad_nd_image uses np.pad for the former and
23
+ torch.nn.functional.pad for the latter
24
+ :param new_shape: what shape do you want? new_shape does not have to have the same dimensionality as image. If
25
+ len(new_shape) < len(image.shape) then the last axes of image will be padded. If new_shape < image.shape in
26
+ any of the axes then we will not pad that axis, but also not crop! (interpret new_shape as new_min_shape)
27
+
28
+ Example:
29
+ image.shape = (10, 1, 512, 512); new_shape = (768, 768) -> result: (10, 1, 768, 768). Cool, huh?
30
+ image.shape = (10, 1, 512, 512); new_shape = (364, 768) -> result: (10, 1, 512, 768).
31
+
32
+ :param mode: will be passed to either np.pad or torch.nn.functional.pad depending on what the image is. Read the
33
+ respective documentation!
34
+ :param return_slicer: if True then this function will also return a tuple of python slice objects that you can use
35
+ to crop back to the original image (reverse padding)
36
+ :param shape_must_be_divisible_by: for network prediction. After applying new_shape, make sure the new shape is
37
+ divisibly by that number (can also be a list with an entry for each axis). Whatever is missing to match
38
+ that will be padded (so the result may be larger than new_shape if shape_must_be_divisible_by is not None)
39
+ :param kwargs: see np.pad for documentation (numpy) or torch.nn.functional.pad (torch)
40
+
41
+ :returns: if return_slicer=False, this function returns the padded numpy array / torch Tensor. If
42
+ return_slicer=True it will also return a tuple of slice objects that you can use to revert the padding:
43
+ output, slicer = pad_nd_image(input_array, new_shape=XXX, return_slicer=True)
44
+ reversed_padding = output[slicer] ## this is now the same as input_array, padding was reversed
45
+ """
46
+ if kwargs is None:
47
+ kwargs = {}
48
+
49
+ old_shape = np.array(image.shape)
50
+
51
+ if shape_must_be_divisible_by is not None:
52
+ assert isinstance(shape_must_be_divisible_by, (int, list, tuple, np.ndarray))
53
+ if isinstance(shape_must_be_divisible_by, int):
54
+ shape_must_be_divisible_by = [shape_must_be_divisible_by] * len(image.shape)
55
+ else:
56
+ if len(shape_must_be_divisible_by) < len(image.shape):
57
+ shape_must_be_divisible_by = [1] * (len(image.shape) - len(shape_must_be_divisible_by)) + \
58
+ list(shape_must_be_divisible_by)
59
+
60
+ if new_shape is None:
61
+ assert shape_must_be_divisible_by is not None
62
+ new_shape = image.shape
63
+
64
+ if len(new_shape) < len(image.shape):
65
+ new_shape = list(image.shape[:len(image.shape) - len(new_shape)]) + list(new_shape)
66
+
67
+ new_shape = [max(new_shape[i], old_shape[i]) for i in range(len(new_shape))]
68
+
69
+ if shape_must_be_divisible_by is not None:
70
+ if not isinstance(shape_must_be_divisible_by, (list, tuple, np.ndarray)):
71
+ shape_must_be_divisible_by = [shape_must_be_divisible_by] * len(new_shape)
72
+
73
+ if len(shape_must_be_divisible_by) < len(new_shape):
74
+ shape_must_be_divisible_by = [1] * (len(new_shape) - len(shape_must_be_divisible_by)) + \
75
+ list(shape_must_be_divisible_by)
76
+
77
+ for i in range(len(new_shape)):
78
+ if new_shape[i] % shape_must_be_divisible_by[i] == 0:
79
+ new_shape[i] -= shape_must_be_divisible_by[i]
80
+
81
+ new_shape = np.array([new_shape[i] + shape_must_be_divisible_by[i] - new_shape[i] %
82
+ shape_must_be_divisible_by[i] for i in range(len(new_shape))])
83
+
84
+ difference = new_shape - old_shape
85
+ pad_below = difference // 2
86
+ pad_above = difference // 2 + difference % 2
87
+ pad_list = [list(i) for i in zip(pad_below, pad_above)]
88
+
89
+ if not ((all([i == 0 for i in pad_below])) and (all([i == 0 for i in pad_above]))):
90
+ if isinstance(image, np.ndarray):
91
+ res = np.pad(image, pad_list, mode, **kwargs)
92
+ elif isinstance(image, torch.Tensor):
93
+ # torch padding has the weirdest interface ever. Like wtf? Y u no read numpy documentation? So much easier
94
+ torch_pad_list = [i for j in pad_list for i in j[::-1]][::-1]
95
+ res = F.pad(image, torch_pad_list, mode, **kwargs)
96
+ else:
97
+ res = image
98
+
99
+ if not return_slicer:
100
+ return res
101
+ else:
102
+ pad_list = np.array(pad_list)
103
+ pad_list[:, 1] = np.array(res.shape) - pad_list[:, 1]
104
+ slicer = tuple(slice(*i) for i in pad_list)
105
+ return res, slicer