"""Module containing wrappers to resize routines from skimage and cv2.
Access is given through objects. Also contains utility routine which equalizes voxel
size lengths.
"""
from __future__ import annotations
from typing import Optional, Union, overload
import cv2
import numpy as np
import darsia
[docs]
class Resize:
"""Object for resizing 2d arrays.
Example:
import darsia
options = {
"example resize x": 0.2,
"example resize y": 0.5,
"example interpolation": "inter_nearest",
}
resizer = darsia.Resize(key = "example ", **options)
img_small = resizer(img_large)
"""
def __init__(
self,
ref_image: Optional[darsia.Image] = None,
shape: Optional[tuple[int]] = None,
fx: Optional[float] = None,
fy: Optional[float] = None,
interpolation: Optional[str] = None,
dtype=None,
key: str = "",
**kwargs,
) -> None:
"""
Args:
ref_image (Image, optional): image whose shape is desired
shape (tuple of int, optional): desired shape (in matrix indexing)
fx (float, optional): resize factor in x-dimension.
fy (float, optional): resize factor in y-dimension.
interpolation (str, optional): interpolation method, default: None, invoking
the default option in cv2.resize.
dtype: conversion dtype before resizing; noting happens if None
"""
# Cache parameters
self.shape = kwargs.get(key + "resize shape", None) if shape is None else shape
general_f = kwargs.get(key + "resize", None)
self.fx = kwargs.get(key + "resize x", general_f) if fx is None else fx
self.fy = kwargs.get(key + "resize y", general_f) if fy is None else fy
self.dtype = kwargs.get(key + "resize dtype", None) if dtype is None else dtype
# Check if reference image is provided
if ref_image is not None:
assert (
self.shape is None
), "Provide only reference image or shape (not both)."
self.shape = ref_image.num_voxels
# Safety checks - double check resize options
if self.shape is None:
self.fx = 1 if self.fx is None else self.fx
self.fy = 1 if self.fy is None else self.fy
self.dsize = None
else:
# cv2 expects a flipped order
self.dsize = tuple(reversed(self.shape))
# Convert to CV2 format
# NOTE: The default value for interpolation in cv2 is depending on whether
# the image is shrinked or enlarged. In case of shrinking, cv2.INTER_NEAREST
# is chosen, as opposed to cv2.INTER_LINEAR in the case of enlarging.
interpolation_pre = (
kwargs.get(key + "resize interpolation", None)
if interpolation is None
else interpolation
)
if interpolation_pre is None:
self.interpolation = None
elif interpolation_pre == "inter_area":
self.interpolation = cv2.INTER_AREA
elif interpolation_pre == "inter_linear":
self.interpolation = cv2.INTER_LINEAR
elif interpolation_pre == "inter_nearest":
self.interpolation = cv2.INTER_NEAREST
else:
raise NotImplementedError(
f"Interpolation option {interpolation_pre} is not implemented. Pick from:\n"
f" - inter_area\n"
f" - inter_linear\n"
f" - inter_nearest\n"
)
# Check for conservative rescaling
self.is_conservative = kwargs.get(key + "resize conservative", False)
def __str__(self) -> str:
"""String representation of the resize correction."""
return "resize"
@overload
def __call__(self, img: np.ndarray, overwrite: bool = False) -> np.ndarray: ...
@overload
def __call__(self, img: darsia.Image, overwrite: bool = False) -> darsia.Image: ...
def __call__(
self, img: Union[np.ndarray, darsia.Image], overwrite: bool = False
) -> Union[np.ndarray, darsia.Image]:
"""
Wrapper to cv2.resize.
Args:
img (np.ndarray, or darsia.Image): image
Returns:
np.ndarray, or darsia.Image: resized image, same format as input
"""
input_is_image = isinstance(img, darsia.Image)
if input_is_image:
assert img.space_dim == 2
# Extract original image
img_array = img.img.copy() if input_is_image else img.copy()
if self.dtype is not None:
img_array = img_array.astype(self.dtype)
# Treat all indices > 2 as channels
original_shape = img_array.shape
multi_channel_img_array = np.reshape(img_array, (*original_shape[:2], -1))
# Split possibly multi-channel image in single channels
img_channels: tuple[np.ndarray] = cv2.split(multi_channel_img_array)
# Apply resizing to each channel separately
resized_channels = []
for channel in img_channels:
if self.interpolation is None:
resized_channels.append(
cv2.resize(
channel,
dsize=self.dsize,
fx=self.fx,
fy=self.fy,
)
)
else:
resized_channels.append(
cv2.resize(
channel,
dsize=self.dsize,
fx=self.fx,
fy=self.fy,
interpolation=self.interpolation,
)
)
# Merge channels again and create resized image array of original shape (data-wise)
resized_multi_channel_img_array = cv2.merge(resized_channels)
resized_shape = *resized_multi_channel_img_array.shape[:2], *original_shape[2:]
resized_img_array = np.reshape(resized_multi_channel_img_array, resized_shape)
# Conserve the (weighted) sum
if self.is_conservative:
resized_img_array *= np.prod(img_array.shape[:2]) / np.prod(
resized_img_array.shape[:2]
)
# Return resized image
if input_is_image:
meta = img.metadata()
if overwrite:
img = type(img)(resized_img_array, **meta)
return img
else:
return type(img)(resized_img_array, **meta)
else:
if overwrite:
img = resized_img_array
return img
else:
# Return resized array
return resized_img_array
[docs]
def save(self, path: str) -> None:
"""Save parameters to npz file.
Args:
path (str): path to save the parameters to
"""
np.savez(
path,
class_name=type(self).__name__,
shape=self.shape,
dsize=self.dsize,
fx=self.fx,
fy=self.fy,
interpolation=self.interpolation,
dtype=self.dtype,
is_conservative=self.is_conservative,
)
print(f"Resize object saved to {path}.")
[docs]
def load(self, path: str) -> None:
"""Load parameters from file.
Args:
path (str): path to load the parameters from
"""
data = np.load(path, allow_pickle=True)
self.shape = tuple(data["shape"]) if "shape" in data else None
self.dsize = tuple(data["dsize"]) if "dsize" in data else None
if self.dsize is None and self.shape is not None:
self.dsize = tuple(reversed(self.shape))
if self.shape is None and self.dsize is not None:
self.shape = tuple(reversed(self.dsize))
self.fx = data["fx"].item() if "fx" in data else None
self.fy = data["fy"].item() if "fy" in data else None
self.interpolation = (
data["interpolation"].item() if "interpolation" in data else None
)
self.dtype = data["dtype"].item() if "dtype" in data else None
self.is_conservative = (
data["is_conservative"].item() if "is_conservative" in data else None
)
[docs]
def resize(
image: darsia.Image,
ref_image: Optional[darsia.Image] = None,
shape: Optional[tuple[int]] = None,
fx: Optional[float] = None,
fy: Optional[float] = None,
interpolation: Optional[str] = None,
dtype=None,
) -> darsia.Image:
"""Function wrapper to Resize object.
Args:
image (darsia.Image): image to be resized
ref_image (Image, optional): reference image whose shape is desired
shape (tuple of int, optional): desired shape (in matrix indexing)
fx (float, optional): resize factor in x-dimension.
fy (float, optional): resize factor in y-dimension.
interpolation (str, optional): interpolation method, default: None, invoking
the default option in cv2.resize.
dtype: conversion dtype before resizing; noting happens if None
"""
# Define Resize object
resizer = Resize(
ref_image=ref_image,
shape=shape,
fx=fx,
fy=fy,
interpolation=interpolation,
dtype=dtype,
)
# Return resized image
return resizer(image)
[docs]
def equalize_voxel_size(
image: darsia.Image, voxel_size: Optional[float] = None, **kwargs
) -> darsia.Image:
"""Resize routine which keeps physical dimensions, but unifies the voxel length.
Args:
image (darsia.Image): image to be resized
voxel_size (float, optional): side length, min of the voxel side of the image
if None.
keyword arguments:
interpolation (str): interpolation type used for resize
Returns:
darsia.Image: resized image
"""
# Fetch dimensions to be kept
dimensions = image.dimensions
# Determine resulting shape of the resized image
if voxel_size is None:
voxel_size = min(image.voxel_size)
shape = tuple(int(d / voxel_size) for d in dimensions)
# Perform resize
interpolation = kwargs.get("interpolation")
resize = Resize(shape=shape, interpolation=interpolation)
return resize(image)