from typing import Optional, Sequence, Tuple
import cv2
import numpy as np
from dicaugment.augmentations.utils import (
_maybe_process_in_chunks,
preserve_channel_dim,
)
from ...core.bbox_utils import denormalize_bbox, normalize_bbox
from ...core.transforms_interface import BoxInternalType, KeypointInternalType
from ..geometric import functional as FGeometric
__all__ = [
"get_random_crop_coords",
"random_crop",
"crop_bbox_by_coords",
"bbox_random_crop",
"crop_keypoint_by_coords",
"keypoint_random_crop",
"get_center_crop_coords",
"center_crop",
"bbox_center_crop",
"keypoint_center_crop",
"crop",
"bbox_crop",
"clamping_crop",
"crop_and_pad",
"crop_and_pad_bbox",
"crop_and_pad_keypoint",
]
[docs]
def get_random_crop_coords(
height: int,
width: int,
depth: int,
crop_height: int,
crop_width: int,
crop_depth: int,
h_start: float,
w_start: float,
d_start: float,
):
"""
Get cropping coordinates from crop dimensions and starting location
Args:
height (int): Image height.
width (int): Image width.
depth (int): Image depth.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
h_start (int): Crop height start.
w_start (int): Crop width start.
d_start (int): Crop depth start.
Returns:
Cropping coodinates `(x1, y1, z1, x2, y2, z2)`
"""
# h_start is [0, 1) and should map to [0, (height - crop_height)] (note inclusive)
# This is conceptually equivalent to mapping onto `range(0, (height - crop_height + 1))`
y1 = int((height - crop_height + 1) * h_start)
y2 = y1 + crop_height
x1 = int((width - crop_width + 1) * w_start)
x2 = x1 + crop_width
z1 = int((depth - crop_depth + 1) * d_start)
z2 = z1 + crop_depth
return x1, y1, z1, x2, y2, z2
[docs]
def random_crop(
img: np.ndarray,
crop_height: int,
crop_width: int,
crop_depth: int,
h_start: float,
w_start: float,
d_start: float,
):
"""
Performs crop on image
Args:
img (np.ndarray): An image
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
h_start (int): Crop height start.
w_start (int): Crop width start.
d_start (int): Crop depth start.
Returns:
A cropped image
"""
height, width, depth = img.shape[:3]
if height < crop_height or width < crop_width or depth < crop_depth:
raise ValueError(
"Requested crop size ({crop_height}, {crop_width}, {crop_depth}) is "
"larger than the image size ({height}, {width}, {depth})".format(
crop_height=crop_height,
crop_width=crop_width,
crop_depth=crop_depth,
height=height,
width=width,
depth=depth,
)
)
x1, y1, z1, x2, y2, z2 = get_random_crop_coords(
height,
width,
depth,
crop_height,
crop_width,
crop_depth,
h_start,
w_start,
d_start,
)
img = img[y1:y2, x1:x2, z1:z2]
return img
[docs]
def crop_bbox_by_coords(
bbox: BoxInternalType,
crop_coords: Tuple[int, int, int, int, int, int],
crop_height: int,
crop_width: int,
crop_depth: int,
rows: int,
cols: int,
slices: int,
):
"""Crop a bounding box using the provided coordinates of bottom-left and top-right corners in pixels and the
required height and width of the crop.
Args:
bbox (tuple): A cropped box `(x_min, y_min, z_min, x_max, y_max, z_max)`.
crop_coords (tuple): Crop coordinates `(x1, y1, z1, x2, y2, z2)`.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
rows (int): Image rows.
cols (int): Image cols.
slices (int): Image slices.
Returns:
A cropped bounding box `(x_min, y_min, x_max, y_max, z_min, z_max)`.
"""
bbox = denormalize_bbox(bbox, rows, cols, slices)
x_min, y_min, z_min, x_max, y_max, z_max = bbox[:6]
x1, y1, z1, x2, y2, z2 = crop_coords
cropped_bbox = (
x_min - x1,
y_min - y1,
z_min - z1,
x_max - x1,
y_max - y1,
z_max - z1,
)
return normalize_bbox(cropped_bbox, crop_height, crop_width, crop_depth)
[docs]
def bbox_random_crop(
bbox: BoxInternalType,
crop_height: int,
crop_width: int,
crop_depth: int,
h_start: float,
w_start: float,
d_start: float,
rows: int,
cols: int,
slices: int,
):
"""Crop a bounding box using the crop dimensions and starting locations.
Args:
bbox (tuple): A cropped box `(x_min, y_min, z_min, x_max, y_max, z_max)`.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
h_start (float): Crop height start.
w_start (float): Crop width start.
d_start (float): Crop depth start.
rows (int): Image rows.
cols (int): Image cols.
slices (int): Image slices.
Returns:
A cropped bounding box `(x_min, y_min, x_max, y_max, z_min, z_max)`.
"""
crop_coords = get_random_crop_coords(
rows,
cols,
slices,
crop_height,
crop_width,
crop_depth,
h_start,
w_start,
d_start,
)
return crop_bbox_by_coords(
bbox, crop_coords, crop_height, crop_width, crop_depth, rows, cols, slices
)
[docs]
def crop_keypoint_by_coords(
keypoint: KeypointInternalType, crop_coords: Tuple[int, int, int, int, int, int]
): # skipcq: PYL-W0613
"""Crop a keypoint using the provided coordinates of closest-top-left and furthest-bottom-right corners in pixels and the
required height, width, and depth of the crop.
Args:
keypoint (tuple): A keypoint `(x, y, z, angle, scale)`.
crop_coords (tuple): Crop box coords `(x1, y1, z1, x2, y2, z2)`.
Returns:
A keypoint `(x, y, z, angle, scale)`.
"""
x, y, z, angle, scale = keypoint[:5]
x1, y1, z1, x2, y2, z2 = crop_coords
return x - x1, y - y1, z - z1, angle, scale
[docs]
def keypoint_random_crop(
keypoint: KeypointInternalType,
crop_height: int,
crop_width: int,
crop_depth: int,
h_start: float,
w_start: float,
d_start: float,
rows: int,
cols: int,
slices: int,
):
"""Keypoint random crop.
Args:
keypoint: (tuple): A keypoint `(x, y, angle, scale)`.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
h_start (int): Crop height start.
w_start (int): Crop width start.
d_start (int): Crop depth start.
rows (int): Image height.
cols (int): Image width.
slices (int): Image depth
Returns:
A keypoint `(x, y, z, angle, scale)`.
"""
crop_coords = get_random_crop_coords(
rows,
cols,
slices,
crop_height,
crop_width,
crop_depth,
h_start,
w_start,
d_start,
)
return crop_keypoint_by_coords(keypoint, crop_coords)
[docs]
def get_center_crop_coords(
height: int,
width: int,
depth: int,
crop_height: int,
crop_width: int,
crop_depth: int,
):
"""
Calculate center crop coordinates from crop dimensions and image dimensions
Args:
height (int): Image height.
width (int): Image width.
depth (int): Image depth.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
Returns:
Cropping coodinates `(x1, y1, z1, x2, y2, z2)`
"""
y1 = (height - crop_height) // 2
y2 = y1 + crop_height
x1 = (width - crop_width) // 2
x2 = x1 + crop_width
z1 = (depth - crop_depth) // 2
z2 = z1 + crop_depth
return x1, y1, z1, x2, y2, z2
[docs]
def center_crop(img: np.ndarray, crop_height: int, crop_width: int, crop_depth: int):
"""
Crops an image around the center.
Args:
img (np.ndarray): an image
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
Returns:
A center cropped image
"""
height, width, depth = img.shape[:3]
if height < crop_height or width < crop_width or depth < crop_depth:
raise ValueError(
"Requested crop size ({crop_height}, {crop_width}, {crop_depth}) is "
"larger than the image size ({height}, {width}, {depth})".format(
crop_height=crop_height,
crop_width=crop_width,
crop_depth=crop_depth,
height=height,
width=width,
depth=depth,
)
)
x1, y1, z1, x2, y2, z2 = get_center_crop_coords(
height, width, depth, crop_height, crop_width, crop_depth
)
img = img[y1:y2, x1:x2, z1:z2]
return img
[docs]
def bbox_center_crop(
bbox: BoxInternalType,
crop_height: int,
crop_width: int,
crop_depth: int,
rows: int,
cols: int,
slices: int,
):
"""Crop a bounding box around the center of the image
Args:
bbox (tuple): A cropped box `(x_min, y_min, z_min, x_max, y_max, z_max)`.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
rows (int): Image rows.
cols (int): Image cols.
slices (int): Image slices.
Returns:
A cropped bounding box `(x_min, y_min, x_max, y_max, z_min, z_max)`.
"""
crop_coords = get_center_crop_coords(
rows, cols, slices, crop_height, crop_width, crop_depth
)
return crop_bbox_by_coords(
bbox, crop_coords, crop_height, crop_width, crop_depth, rows, cols, slices
)
[docs]
def keypoint_center_crop(
keypoint: KeypointInternalType,
crop_height: int,
crop_width: int,
crop_depth: int,
rows: int,
cols: int,
slices: int,
):
"""Keypoint center crop.
Args:
keypoint (tuple): A keypoint `(x, y, z, angle, scale)`.
crop_height (int): Crop height.
crop_width (int): Crop width.
crop_depth (int): Crop depth.
rows (int): Image height.
cols (int): Image width.
slices (int): Image depths.
Returns:
A keypoint `(x, y, z, angle, scale)`.
"""
crop_coords = get_center_crop_coords(
rows, cols, slices, crop_height, crop_width, crop_depth
)
return crop_keypoint_by_coords(keypoint, crop_coords)
[docs]
def crop(
img: np.ndarray,
x_min: int,
y_min: int,
z_min: int,
x_max: int,
y_max: int,
z_max: int,
):
"""Crop an image.
Args:
img (np.ndarray): An image
x_min (int): Minimum closest upper left x coordinate.
y_min (int): Minimum closest upper left y coordinate.
z_min (int): Minimum closest upper left z coordinate.
x_max (int): Maximum furthest lower right x coordinate.
y_max (int): Maximum furthest lower right y coordinate.
z_max (int): Maximum furthest lower right z coordinate.
Returns:
np.ndarray: A cropped image
"""
height, width, depth = img.shape[:3]
if x_max <= x_min or y_max <= y_min or z_max <= z_min:
raise ValueError(
"Expected x_min < x_max, y_min < y_max, and z_min < z_max. Got"
" (x_min = {x_min}, y_min = {y_min}, z_min = {z_min}, x_max = {x_max}, y_max = {y_max}, z_max = {z_max})".format(
x_min=x_min,
x_max=x_max,
y_min=y_min,
y_max=y_max,
z_min=z_min,
z_max=z_max,
)
)
if (
x_min < 0
or x_max > width
or y_min < 0
or y_max > height
or z_min < 0
or z_max > depth
):
raise ValueError(
"Values for crop should be non negative and equal or smaller than image sizes. Got "
"(x_min = {x_min}, y_min = {y_min}, z_min = {z_min}, x_max = {x_max}, y_max = {y_max}, z_max = {z_max}, "
"height = {height}, width = {width}, depth = {depth})".format(
x_min=x_min,
x_max=x_max,
y_min=y_min,
y_max=y_max,
z_min=z_min,
z_max=z_max,
height=height,
width=width,
depth=depth,
)
)
return img[y_min:y_max, x_min:x_max, z_min:z_max]
[docs]
def bbox_crop(
bbox: BoxInternalType,
x_min: int,
y_min: int,
z_min: int,
x_max: int,
y_max: int,
z_max: int,
rows: int,
cols: int,
slices: int,
):
"""Crop a bounding box.
Args:
bbox (tuple): A bounding box `(x_min, y_min, z_min, x_max, y_max, z_max)`.
x_min (int): Minimum closest upper left x coordinate.
y_min (int): Minimum closest upper left y coordinate.
z_min (int): Minimum closest upper left z coordinate.
x_max (int): Maximum furthest lower right x coordinate.
y_max (int): Maximum furthest lower right y coordinate.
z_max (int): Maximum furthest lower right z coordinate.
rows (int): Image width.
cols (int): Image height.
slices (int): Image depth.
Returns:
tuple: A cropped bounding box `(x_min, y_min, z_min, x_max, y_max, z_max)`.
"""
crop_coords = x_min, y_min, z_min, x_max, y_max, z_max
crop_height = y_max - y_min
crop_width = x_max - x_min
crop_depth = z_max - z_min
return crop_bbox_by_coords(
bbox, crop_coords, crop_height, crop_width, crop_depth, rows, cols, slices
)
[docs]
def clamping_crop(
img: np.ndarray,
x_min: int,
y_min: int,
z_min: int,
x_max: int,
y_max: int,
z_max: int,
):
"""Crop an image, with safeguard that clips cropping coordinates to fit within image
Args:
img (np.ndarray): An image
x_min (int): Minimum closest upper left x coordinate.
y_min (int): Minimum closest upper left y coordinate.
z_min (int): Minimum closest upper left z coordinate.
x_max (int): Maximum furthest lower right x coordinate.
y_max (int): Maximum furthest lower right y coordinate.
z_max (int): Maximum furthest lower right z coordinate.
Returns:
A cropped image
"""
h, w, d = img.shape[:3]
x_min = max(x_min, 0)
y_min = max(y_min, 0)
z_min = max(z_min, 0)
x_max = min(x_max, w - 1)
y_max = min(y_max, h - 1)
z_max = min(z_max, d - 1)
return img[
int(y_min) : int(y_max), int(x_min) : int(x_max), int(z_min) : int(z_max)
]
[docs]
@preserve_channel_dim
def crop_and_pad(
img: np.ndarray,
crop_params: Optional[Sequence[int]],
pad_params: Optional[Sequence[int]],
pad_value: Optional[float],
rows: int,
cols: int,
slices: int,
interpolation: int,
pad_mode: int,
keep_size: bool,
) -> np.ndarray:
"""
Performs cropping and padding operations on an image
Args:
img (np.ndarray): an image.
crop_params (Sequence of ints): Cropping coodinates `(x1, y1, z1, x2, y2, z2)`
pad_params (Sequence of ints): Padding parameters `(top, bottom, left, right, close, far)`
pad_value (float): The constant value to use if pad_mode is `constant`
rows (int): Image width.
cols (int): Image height.
slices (int): Image depth.
interpolation: scipy interpolation method (e.g. dicaugment.INTER_NEAREST).
pad_mode (str): scipy parameter to determine how the input image is extended during convolution to maintain image shape. Must be one of the following:
- `reflect` (d c b a | a b c d | d c b a): The input is extended by reflecting about the edge of the last pixel. This mode is also sometimes referred to as half-sample symmetric.
- `constant` (k k k k | a b c d | k k k k): The input is extended by filling all values beyond the edge with the same constant value, defined by the cval parameter.
- `nearest` (a a a a | a b c d | d d d d): The input is extended by replicating the last pixel.
- `mirror` (d c b | a b c d | c b a): The input is extended by reflecting about the center of the last pixel. This mode is also sometimes referred to as whole-sample symmetric.
- `wrap` (a b c d | a b c d | a b c d): The input is extended by wrapping around to the opposite edge.
Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.median_filter.html
Default: `constant`
keep_size (bool):
After cropping and padding, the result image will usually have a
different height/width compared to the original input image. If this
parameter is set to ``True``, then the cropped/padded image will be
resized to the input image's size, i.e. the output shape is always identical to the input shape.
"""
if crop_params is not None and any(i != 0 for i in crop_params):
img = crop(img, *crop_params)
if pad_params is not None and any(i != 0 for i in pad_params):
img = FGeometric.pad_with_params(
img, *pad_params[:6], border_mode=pad_mode, value=pad_value
)
if keep_size:
img = FGeometric.resize(
img, height=cols, width=rows, depth=slices, interpolation=interpolation
)
return img
[docs]
def crop_and_pad_bbox(
bbox: BoxInternalType,
crop_params: Optional[Sequence[int]],
pad_params: Optional[Sequence[int]],
rows,
cols,
slices,
result_rows,
result_cols,
result_slices,
) -> BoxInternalType:
"""
Performs cropping and padding operations on a bbox
Args:
bbox (BoxInternalType): a bounding box `(x_min, y_min, x_max, y_max, z_min, z_max)`.
crop_params (Sequence of ints): Cropping coodinates `(x1, y1, z1, x2, y2, z2)`
pad_params (Sequence of ints): Padding parameters `(top, bottom, left, right, close, far)`
rows (int): Image width.
cols (int): Image height.
slices (int): Image depth.
result_rows (int): Expected image width after transform.
result_cols (int): Expected image height after transform.
result_slices (int): Expected image depth after transform.
Returns:
A bounding box `(x_min, y_min, x_max, y_max, z_min, z_max)`.
"""
x1, y1, z1, x2, y2, z2 = denormalize_bbox(bbox, rows, cols, slices)[:6]
if crop_params is not None:
crop_x, _, crop_y, _, crop_z, _ = crop_params
x1, y1, z1, x2, y2, z2 = (
x1 - crop_x,
y1 - crop_y,
z1 - crop_z,
x2 - crop_x,
y2 - crop_y,
z2 - crop_z,
)
if pad_params is not None:
top, bottom, left, right, close, far = pad_params
x1, y1, z1, x2, y2, z2 = (
x1 + left,
y1 + top,
z1 + close,
x2 + left,
y2 + top,
z2 + close,
)
return normalize_bbox(
(x1, y1, z1, x2, y2, z2), result_rows, result_cols, result_slices
)
[docs]
def crop_and_pad_keypoint(
keypoint: KeypointInternalType,
crop_params: Optional[Sequence[int]],
pad_params: Optional[Sequence[int]],
rows: int,
cols: int,
slices: int,
result_rows: int,
result_cols: int,
result_slices: int,
keep_size: bool,
) -> KeypointInternalType:
"""
Performs cropping and padding operations on a keypoint
Args:
keypoint KeypointInternalType): A keypoint `(x, y, z, angle, scale)`.
crop_params (Sequence of ints): Cropping coodinates `(x1, y1, z1, x2, y2, z2)`
pad_params (Sequence of ints): Padding parameters `(top, bottom, left, right, close, far)`
rows (int): Image width.
cols (int): Image height.
slices (int): Image depth.
result_rows (int): Expected image width after transform.
result_cols (int): Expected image height after transform.
result_slices (int): Expected image depth after transform.
keep_size (bool):
After cropping and padding, the result image will usually have a
different height/width compared to the original input image. If this
parameter is set to ``True``, then the cropped/padded image will be
resized to the input image's size, i.e. the output shape is always identical to the input shape.
Returns:
A keypoint `(x, y, z, angle, scale)`.
"""
x, y, z, angle, scale = keypoint[:5]
if crop_params is not None:
crop_x, _, crop_y, _, crop_z, _ = crop_params
x, y, z = x - crop_x, y - crop_y, z - crop_z
if pad_params is not None:
top, bottom, left, right, close, far = pad_params
x, y, z = x + left, y + top, z + close
if keep_size and (
result_cols != cols or result_rows != rows or result_cols != slices
):
scale_x = cols / result_cols
scale_y = rows / result_rows
scale_z = slices / result_slices
return FGeometric.keypoint_scale(
(x, y, z, angle, scale), scale_x, scale_y, scale_z
)
return x, y, z, angle, scale