import numpy as np
from scipy.ndimage import affine_transform, map_coordinates
from dipy.testing.decorators import warning_for_keywords
[docs]
@warning_for_keywords()
def normalize(image, *, min_v=None, max_v=None, new_min=-1, new_max=1):
"""
normalization function
Parameters
----------
image : np.ndarray
Image to be normalized.
min_v : int or float, optional
minimum value range for normalization
intensities below min_v will be clipped
if None it is set to min value of image
max_v : int or float, optional
maximum value range for normalization
intensities above max_v will be clipped
if None it is set to max value of image
new_min : int or float, optional
new minimum value after normalization
new_max : int or float, optional
new maximum value after normalization
Returns
-------
np.ndarray
Normalized image from range new_min to new_max
"""
if min_v is None:
min_v = np.min(image)
if max_v is None:
max_v = np.max(image)
return np.interp(image, (min_v, max_v), (new_min, new_max))
[docs]
def unnormalize(image, norm_min, norm_max, min_v, max_v):
"""
unnormalization function
Parameters
----------
image : np.ndarray
norm_min : int or float
minimum value of normalized image
norm_max : int or float
maximum value of normalized image
min_v : int or float
minimum value of unnormalized image
max_v : int or float
maximum value of unnormalized image
Returns
-------
np.ndarray
unnormalized image from range min_v to max_v
"""
return (image - norm_min) / (norm_max - norm_min) * (max_v - min_v) + min_v
def set_logger_level(log_level, logger):
"""Change the logger to one of the following:
DEBUG, INFO, WARNING, CRITICAL, ERROR
Parameters
----------
log_level : str
Log level for the logger
"""
logger.setLevel(level=log_level)
[docs]
@warning_for_keywords()
def get_bounds(shape, affine, *, considered_points="corners"):
"""
Function to get the bounds of an image after applying affine
Parameters
----------
shape : tuple (3,)
Shape of the image
affine : np.ndarray
Affine matrix provided by the image file.
considered_points : str, optional
Considered points when calculating the transformed shape.
\"corners\" will consider only corners of the image shape.
\"all\" will consider all voxels.
Might be needed when shearing is applied.
Returns
-------
min_bounds : np.ndarray (3,)
Minimum bounds of the transformed image
max_bounds : np.ndarray (3,)
Maximum bounds of the transformed image
"""
if considered_points == "corners":
points = np.array(
[
[0, 0, 0, 1],
[shape[0] - 1, 0, 0, 1],
[0, shape[1] - 1, 0, 1],
[0, 0, shape[2] - 1, 1],
[shape[0] - 1, shape[1] - 1, shape[2] - 1, 1],
[shape[0] - 1, 0, shape[2] - 1, 1],
[0, shape[1] - 1, shape[2] - 1, 1],
[shape[0] - 1, shape[1] - 1, 0, 1],
],
dtype=np.float64,
)
elif considered_points == "all":
temp1 = np.arange(shape[0])
temp2 = np.arange(shape[1])
temp3 = np.arange(shape[2])
grid1, grid2, grid3 = np.meshgrid(temp1, temp2, temp3)
points = np.vstack([grid1.ravel(), grid2.ravel(), grid3.ravel()]).T
points = np.hstack([points, np.full((points.shape[0], 1), 1)])
points = points.astype(np.float64)
else:
ValueError('considered points should be "corners" or "all"')
transformed_corners = (affine @ points.T).T
min_bounds = transformed_corners.min(axis=0)[:3]
max_bounds = transformed_corners.max(axis=0)[:3]
return min_bounds, max_bounds
[docs]
def recover_img(image, params, *, order=3):
"""
Function to recover image from transform_img
Parameters
----------
image : np.ndarray
Image to recover.
params : dict
Parameters for recover_img function.
Returned from transform_img.
order : int, optional
The order of the spline interpolation.
The order has to be in the range 0-5.
If recovering an int image, order 0 is recommended.
Returns
-------
recovered : np.ndarray
Recovered image
"""
expected = {
"init_shape",
"ori_affine",
"target_voxsize",
"new_origin_world",
"pad_value",
"crop_value",
}
missing = expected - set(params.keys())
if missing:
raise ValueError(f"params missing: {missing}")
init_shape = tuple(int(x) for x in params["init_shape"])
affine = np.asarray(params["ori_affine"], dtype=float)
target_voxsize = np.asarray(params["target_voxsize"], dtype=float)
new_origin_world = np.asarray(params["new_origin_world"], dtype=float)
pad_vs = params["pad_value"]
crop_vs = params["crop_value"]
if crop_vs is not None and pad_vs is not None:
new_image = inv_pad_crop(image, crop_vs, pad_vs)
else:
new_image = image
A = affine
D = np.diag(target_voxsize)
coords_idx = np.indices(init_shape, dtype=float)
coords_idx = coords_idx.reshape((len(init_shape), -1))
world_coords = (A[:3, :3] @ coords_idx) + A[:3, 3][:, None]
coords_new = np.linalg.inv(D) @ (world_coords - new_origin_world[:, None])
coords_for_map = coords_new.reshape((len(init_shape),) + tuple(init_shape))
recovered = map_coordinates(new_image, coords_for_map, order=order)
return recovered
[docs]
def pad_crop(image, target_shape):
"""
Function to figure out pad and crop range
to fit the target shape with the image
Parameters
----------
image : np.ndarray
Target image
target_shape : (3,)
Target shape
Returns
-------
image : np.ndarray
Padded/cropped image
pad_vs : np.ndarray (3,2)
Pad range used
crop_vs : np.ndarray (3,2)
Crop range used
"""
crop_vs = np.zeros((3, 2)).astype(int)
pad_vs = np.zeros((3, 2)).astype(int)
if np.all(np.array(image.shape) == target_shape):
return image, pad_vs, crop_vs
pad_crop_v = np.array(target_shape) - np.array(image.shape)
for d in range(3):
if pad_crop_v[d] < 0:
crop_vs[d, 0] = -pad_crop_v[d] // 2
crop_vs[d, 1] = -pad_crop_v[d] - crop_vs[d, 0]
elif pad_crop_v[d] > 0:
pad_vs[d, 0] = pad_crop_v[d] // 2
pad_vs[d, 1] = pad_crop_v[d] - pad_vs[d, 0]
image = np.pad(
image,
(
(pad_vs[0, 0], pad_vs[0, 1]),
(pad_vs[1, 0], pad_vs[1, 1]),
(pad_vs[2, 0], pad_vs[2, 1]),
),
)
image = image[
crop_vs[0, 0] : image.shape[0] - crop_vs[0, 1],
crop_vs[1, 0] : image.shape[1] - crop_vs[1, 1],
crop_vs[2, 0] : image.shape[2] - crop_vs[2, 1],
]
return image, pad_vs, crop_vs
[docs]
def inv_pad_crop(image, crop_vs, pad_vs):
"""
Function to figure out pad and crop range
to fit the target shape with the image
Parameters
----------
image : np.ndarray
Target image
crop_vs : np.ndarray (3,2)
Crop range used when padding/cropping
pad_vs : np.ndarray (3,2)
Pad range used when padding/cropping
Returns
-------
image : np.ndarray
Recovered image
"""
crop_vs = crop_vs.astype(int)
pad_vs = pad_vs.astype(int)
image = np.pad(
image,
(
(crop_vs[0, 0], crop_vs[0, 1]),
(crop_vs[1, 0], crop_vs[1, 1]),
(crop_vs[2, 0], crop_vs[2, 1]),
),
)
image = image[
pad_vs[0, 0] : image.shape[0] - pad_vs[0, 1],
pad_vs[1, 0] : image.shape[1] - pad_vs[1, 1],
pad_vs[2, 0] : image.shape[2] - pad_vs[2, 1],
]
return image