Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/mypy-type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
tiatoolbox/__init__.py \
tiatoolbox/__main__.py \
tiatoolbox/typing.py \
tiatoolbox/tiatoolbox.py \
tiatoolbox/utils/__init__.py \
tiatoolbox/utils/env_detection.py \
tiatoolbox/utils/exceptions.py
tiatoolbox/utils/exceptions.py \
tiatoolbox/utils/magic.py \
tiatoolbox/utils/metrics.py \
tiatoolbox/utils/transforms.py
3 changes: 3 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def test_imresize_no_scale_factor() -> None:
resized_img = utils.transforms.imresize(img, output_size=(50, 100))
assert resized_img.shape == (100, 50, 3)

resized_img = utils.transforms.imresize(img, output_size=100)
assert resized_img.shape == (100, 100, 3)


def test_imresize_no_scale_factor_or_output_size() -> None:
"""Test imresize with no scale_factor or output_size."""
Expand Down
3 changes: 2 additions & 1 deletion tiatoolbox/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import numpy as np
from shapely.geometry import LineString, Point, Polygon # type: ignore[import-untyped]

# Proper type annotations for shapely is not yet available
# Proper type annotations for shapely is not yet available.


JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
NumPair = Tuple[SupportsFloat, SupportsFloat]
Expand Down
4 changes: 2 additions & 2 deletions tiatoolbox/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,11 +969,11 @@ def ppu2mpp(ppu: int, units: str | int) -> float:
return 1 / ppu * microns_per_unit[units]


def select_cv2_interpolation(scale_factor: float) -> str:
def select_cv2_interpolation(scale_factor: float | np.ndarray[float, float]) -> str:
"""Return appropriate interpolation method for opencv based image resize.

Args:
scale_factor (float):
scale_factor (float or np.ndarray[float, float]):
Image resize scale factor.

Returns:
Expand Down
85 changes: 62 additions & 23 deletions tiatoolbox/utils/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


def background_composite(
image: np.ndarray | Image,
image: np.ndarray | Image.Image,
fill: int = 255,
*,
alpha: bool,
Expand Down Expand Up @@ -57,6 +57,39 @@ def background_composite(
return np.asarray(composite)


def _convert_scalar_to_width_height(array: np.ndarray) -> np.ndarray:
"""Converts scalar numpy array to specify width and height."""
if array.size == 1:
return np.repeat(array, 2)

return array


def _get_scale_factor_array(
scale_factor: float | tuple[float, float] | None,
) -> np.ndarray | None:
"""Converts scale factor to appropriate format required by imresize."""
if scale_factor is not None:
scale_factor = np.array(scale_factor, dtype=float)
return _convert_scalar_to_width_height(scale_factor)
return scale_factor


def _get_output_size_array(
img: np.ndarray,
output_size: int | tuple[int, int] | None,
scale_factor_array: np.ndarray,
) -> np.ndarray:
"""Converts output size to appropriate format required by imresize."""
# Handle None arguments
if output_size is None:
width = int(img.shape[1] * scale_factor_array[0])
height = int(img.shape[0] * scale_factor_array[1])
return np.array((width, height))

return _convert_scalar_to_width_height(np.array(output_size))


def imresize(
img: np.ndarray,
scale_factor: float | tuple[float, float] | None = None,
Expand All @@ -68,7 +101,7 @@ def imresize(
Args:
img (:class:`numpy.ndarray`):
Input image, assumed to be in `HxWxC` or `HxW` format.
scale_factor (tuple(float)):
scale_factor (float or Tuple[float, float]):
Scaling factor to resize the input image.
output_size (tuple(int)):
Output image size, (width, height).
Expand All @@ -95,27 +128,24 @@ def imresize(
if scale_factor is None and output_size is None:
msg = "One of scale_factor and output_size must be not None."
raise TypeError(msg)
if scale_factor is not None:
scale_factor = np.array(scale_factor)
if scale_factor.size == 1:
scale_factor = np.repeat(scale_factor, 2)

# Handle None arguments
if output_size is None:
width = int(img.shape[1] * scale_factor[0])
height = int(img.shape[0] * scale_factor[1])
output_size = (width, height)
scale_factor_array = _get_scale_factor_array(scale_factor)
output_size_array = _get_output_size_array(
img=img,
output_size=output_size,
scale_factor_array=scale_factor_array,
)

if scale_factor is None:
scale_factor = img.shape[:2][::-1] / np.array(output_size)
scale_factor_array = img.shape[:2][::-1] / np.array(output_size_array)

# Return original if scale factor is 1
if np.all(scale_factor == 1.0): # noqa: PLR2004
if np.all(scale_factor_array == 1.0): # noqa: PLR2004
return img

# Get appropriate cv2 interpolation enum
if interpolation == "optimise":
interpolation = select_cv2_interpolation(scale_factor)
interpolation = select_cv2_interpolation(scale_factor_array)

# a list of (original type, converted type) tuple
# all `converted type` are np.dtypes that cv2.resize
Expand Down Expand Up @@ -147,24 +177,28 @@ def imresize(
converted_dtype = dtype_mapping[source_dtypes.index(original_dtype)][1]
img = img.astype(converted_dtype)

interpolation = parse_cv2_interpolaton(interpolation)
cv2_interpolation = parse_cv2_interpolaton(interpolation)

# Resize the image
# Handle case for 1x1 images which cv2 v4.5.4 no longer handles
if img.shape[0] == img.shape[1] == 1:
return img.repeat(output_size[1], 0).repeat(output_size[0], 1)
return img.repeat(output_size_array[1], 0).repeat(output_size_array[0], 1)

if len(img.shape) == 3 and img.shape[-1] > 4: # noqa: PLR2004
img_channels = [
cv2.resize(img[..., ch], tuple(output_size), interpolation=interpolation)[
cv2.resize(
src=img[..., ch],
dsize=output_size_array,
interpolation=cv2_interpolation,
)[
...,
None,
]
for ch in range(img.shape[-1])
]
return np.concatenate(img_channels, axis=-1)

return cv2.resize(img, tuple(output_size), interpolation=interpolation)
return cv2.resize(src=img, dsize=output_size_array, interpolation=cv2_interpolation)


def rgb2od(img: np.ndarray) -> np.ndarray:
Expand Down Expand Up @@ -220,7 +254,7 @@ def od2rgb(od: np.ndarray) -> np.ndarray:
def bounds2locsize(
bounds: tuple[int, int, int, int],
origin: str = "upper",
) -> np.ndarray:
) -> tuple[np.ndarray, np.ndarray]:
"""Calculate the size of a tuple of bounds.

Bounds are expected to be in the `(left, top, right, bottom)` or
Expand Down Expand Up @@ -297,7 +331,7 @@ def locsize2bounds(
def bounds2slices(
bounds: tuple[int, int, int, int],
stride: int | tuple[int, int, tuple[int, int]] = 1,
) -> tuple[slice]:
) -> tuple[slice, ...]:
"""Convert bounds to slices.

Create a tuple of slices for each start/stop pair in bounds.
Expand Down Expand Up @@ -327,13 +361,18 @@ def bounds2slices(
msg = "Invalid stride shape."
raise ValueError(msg)
if np.size(stride) == 1:
stride = np.tile(stride, 4)
stride_array = np.tile(stride, 4)
elif np.size(stride) == 2: # pragma: no cover # noqa: PLR2004
stride = np.tile(stride, 2)
stride_array = np.tile(stride, 2)

start, stop = np.reshape(bounds, (2, -1)).astype(int)
slice_array = np.stack([start[::-1], stop[::-1]], axis=1)
return tuple(slice(*x, s) for x, s in zip(slice_array, stride))

slices = []
for x, s in zip(slice_array, stride_array):
slices.append(slice(x[0], x[1], s))

return tuple(slices)


def pad_bounds(
Expand Down