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.
- acvl_utils-0.2/PKG-INFO +7 -0
- acvl_utils-0.2/acvl_utils/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/array_manipulation/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/array_manipulation/resampling.py +93 -0
- acvl_utils-0.2/acvl_utils/array_manipulation/slicer.py +66 -0
- acvl_utils-0.2/acvl_utils/cropping_and_padding/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/cropping_and_padding/bounding_boxes.py +102 -0
- acvl_utils-0.2/acvl_utils/cropping_and_padding/padding.py +105 -0
- acvl_utils-0.2/acvl_utils/instance_segmentation/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/instance_segmentation/instance_as_semantic_seg.py +520 -0
- acvl_utils-0.2/acvl_utils/instance_segmentation/instance_matching.py +126 -0
- acvl_utils-0.2/acvl_utils/miscellaneous/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/miscellaneous/ptqdm.py +46 -0
- acvl_utils-0.2/acvl_utils/miscellaneous/test_all_images_same.py +89 -0
- acvl_utils-0.2/acvl_utils/morphology/__init__.py +0 -0
- acvl_utils-0.2/acvl_utils/morphology/gpu_binary_morphology.py +145 -0
- acvl_utils-0.2/acvl_utils/morphology/morphology_helper.py +149 -0
- acvl_utils-0.2/acvl_utils/morphology/slice_interpolation.py +65 -0
- acvl_utils-0.2/acvl_utils.egg-info/PKG-INFO +7 -0
- acvl_utils-0.2/acvl_utils.egg-info/SOURCES.txt +23 -0
- acvl_utils-0.2/acvl_utils.egg-info/dependency_links.txt +1 -0
- acvl_utils-0.2/acvl_utils.egg-info/requires.txt +6 -0
- acvl_utils-0.2/acvl_utils.egg-info/top_level.txt +1 -0
- acvl_utils-0.2/setup.cfg +4 -0
- acvl_utils-0.2/setup.py +19 -0
acvl_utils-0.2/PKG-INFO
ADDED
|
@@ -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
|
|
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)
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|