Spaces:
Configuration error
Configuration error
import math | |
import random | |
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union | |
import cv2 | |
import numpy as np | |
from ...core.transforms_interface import ( | |
BoxInternalType, | |
DualTransform, | |
FillValueType, | |
KeypointInternalType, | |
to_tuple, | |
) | |
from ..crops import functional as FCrops | |
from . import functional as F | |
__all__ = ["Rotate", "RandomRotate90", "SafeRotate"] | |
class RandomRotate90(DualTransform): | |
"""Randomly rotate the input by 90 degrees zero or more times. | |
Args: | |
p (float): probability of applying the transform. Default: 0.5. | |
Targets: | |
image, mask, bboxes, keypoints | |
Image types: | |
uint8, float32 | |
""" | |
def apply(self, img, factor=0, **params): | |
""" | |
Args: | |
factor (int): number of times the input will be rotated by 90 degrees. | |
""" | |
return np.ascontiguousarray(np.rot90(img, factor)) | |
def get_params(self): | |
# Random int in the range [0, 3] | |
return {"factor": random.randint(0, 3)} | |
def apply_to_bbox(self, bbox, factor=0, **params): | |
return F.bbox_rot90(bbox, factor, **params) | |
def apply_to_keypoint(self, keypoint, factor=0, **params): | |
return F.keypoint_rot90(keypoint, factor, **params) | |
def get_transform_init_args_names(self): | |
return () | |
class Rotate(DualTransform): | |
"""Rotate the input by an angle selected randomly from the uniform distribution. | |
Args: | |
limit ((int, int) or int): range from which a random angle is picked. If limit is a single int | |
an angle is picked from (-limit, limit). Default: (-90, 90) | |
interpolation (OpenCV flag): flag that is used to specify the interpolation algorithm. Should be one of: | |
cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_LANCZOS4. | |
Default: cv2.INTER_LINEAR. | |
border_mode (OpenCV flag): flag that is used to specify the pixel extrapolation method. Should be one of: | |
cv2.BORDER_CONSTANT, cv2.BORDER_REPLICATE, cv2.BORDER_REFLECT, cv2.BORDER_WRAP, cv2.BORDER_REFLECT_101. | |
Default: cv2.BORDER_REFLECT_101 | |
value (int, float, list of ints, list of float): padding value if border_mode is cv2.BORDER_CONSTANT. | |
mask_value (int, float, | |
list of ints, | |
list of float): padding value if border_mode is cv2.BORDER_CONSTANT applied for masks. | |
rotate_method (str): rotation method used for the bounding boxes. Should be one of "largest_box" or "ellipse". | |
Default: "largest_box" | |
crop_border (bool): If True would make a largest possible crop within rotated image | |
p (float): probability of applying the transform. Default: 0.5. | |
Targets: | |
image, mask, bboxes, keypoints | |
Image types: | |
uint8, float32 | |
""" | |
def __init__( | |
self, | |
limit=90, | |
interpolation=cv2.INTER_LINEAR, | |
border_mode=cv2.BORDER_REFLECT_101, | |
value=None, | |
mask_value=None, | |
rotate_method="largest_box", | |
crop_border=False, | |
always_apply=False, | |
p=0.5, | |
): | |
super(Rotate, self).__init__(always_apply, p) | |
self.limit = to_tuple(limit) | |
self.interpolation = interpolation | |
self.border_mode = border_mode | |
self.value = value | |
self.mask_value = mask_value | |
self.rotate_method = rotate_method | |
self.crop_border = crop_border | |
if rotate_method not in ["largest_box", "ellipse"]: | |
raise ValueError(f"Rotation method {self.rotate_method} is not valid.") | |
def apply( | |
self, img, angle=0, interpolation=cv2.INTER_LINEAR, x_min=None, x_max=None, y_min=None, y_max=None, **params | |
): | |
img_out = F.rotate(img, angle, interpolation, self.border_mode, self.value) | |
if self.crop_border: | |
img_out = FCrops.crop(img_out, x_min, y_min, x_max, y_max) | |
return img_out | |
def apply_to_mask(self, img, angle=0, x_min=None, x_max=None, y_min=None, y_max=None, **params): | |
img_out = F.rotate(img, angle, cv2.INTER_NEAREST, self.border_mode, self.mask_value) | |
if self.crop_border: | |
img_out = FCrops.crop(img_out, x_min, y_min, x_max, y_max) | |
return img_out | |
def apply_to_bbox(self, bbox, angle=0, x_min=None, x_max=None, y_min=None, y_max=None, cols=0, rows=0, **params): | |
bbox_out = F.bbox_rotate(bbox, angle, self.rotate_method, rows, cols) | |
if self.crop_border: | |
bbox_out = FCrops.bbox_crop(bbox_out, x_min, y_min, x_max, y_max, rows, cols) | |
return bbox_out | |
def apply_to_keypoint( | |
self, keypoint, angle=0, x_min=None, x_max=None, y_min=None, y_max=None, cols=0, rows=0, **params | |
): | |
keypoint_out = F.keypoint_rotate(keypoint, angle, rows, cols, **params) | |
if self.crop_border: | |
keypoint_out = FCrops.crop_keypoint_by_coords(keypoint_out, (x_min, y_min, x_max, y_max)) | |
return keypoint_out | |
def _rotated_rect_with_max_area(h, w, angle): | |
""" | |
Given a rectangle of size wxh that has been rotated by 'angle' (in | |
degrees), computes the width and height of the largest possible | |
axis-aligned rectangle (maximal area) within the rotated rectangle. | |
Code from: https://stackoverflow.com/questions/16702966/rotate-image-and-crop-out-black-borders | |
""" | |
angle = math.radians(angle) | |
width_is_longer = w >= h | |
side_long, side_short = (w, h) if width_is_longer else (h, w) | |
# since the solutions for angle, -angle and 180-angle are all the same, | |
# it is sufficient to look at the first quadrant and the absolute values of sin,cos: | |
sin_a, cos_a = abs(math.sin(angle)), abs(math.cos(angle)) | |
if side_short <= 2.0 * sin_a * cos_a * side_long or abs(sin_a - cos_a) < 1e-10: | |
# half constrained case: two crop corners touch the longer side, | |
# the other two corners are on the mid-line parallel to the longer line | |
x = 0.5 * side_short | |
wr, hr = (x / sin_a, x / cos_a) if width_is_longer else (x / cos_a, x / sin_a) | |
else: | |
# fully constrained case: crop touches all 4 sides | |
cos_2a = cos_a * cos_a - sin_a * sin_a | |
wr, hr = (w * cos_a - h * sin_a) / cos_2a, (h * cos_a - w * sin_a) / cos_2a | |
return dict( | |
x_min=max(0, int(w / 2 - wr / 2)), | |
x_max=min(w, int(w / 2 + wr / 2)), | |
y_min=max(0, int(h / 2 - hr / 2)), | |
y_max=min(h, int(h / 2 + hr / 2)), | |
) | |
def targets_as_params(self) -> List[str]: | |
return ["image"] | |
def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
out_params = {"angle": random.uniform(self.limit[0], self.limit[1])} | |
if self.crop_border: | |
h, w = params["image"].shape[:2] | |
out_params.update(self._rotated_rect_with_max_area(h, w, out_params["angle"])) | |
return out_params | |
def get_transform_init_args_names(self): | |
return ("limit", "interpolation", "border_mode", "value", "mask_value", "rotate_method", "crop_border") | |
class SafeRotate(DualTransform): | |
"""Rotate the input inside the input's frame by an angle selected randomly from the uniform distribution. | |
The resulting image may have artifacts in it. After rotation, the image may have a different aspect ratio, and | |
after resizing, it returns to its original shape with the original aspect ratio of the image. For these reason we | |
may see some artifacts. | |
Args: | |
limit ((int, int) or int): range from which a random angle is picked. If limit is a single int | |
an angle is picked from (-limit, limit). Default: (-90, 90) | |
interpolation (OpenCV flag): flag that is used to specify the interpolation algorithm. Should be one of: | |
cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_AREA, cv2.INTER_LANCZOS4. | |
Default: cv2.INTER_LINEAR. | |
border_mode (OpenCV flag): flag that is used to specify the pixel extrapolation method. Should be one of: | |
cv2.BORDER_CONSTANT, cv2.BORDER_REPLICATE, cv2.BORDER_REFLECT, cv2.BORDER_WRAP, cv2.BORDER_REFLECT_101. | |
Default: cv2.BORDER_REFLECT_101 | |
value (int, float, list of ints, list of float): padding value if border_mode is cv2.BORDER_CONSTANT. | |
mask_value (int, float, | |
list of ints, | |
list of float): padding value if border_mode is cv2.BORDER_CONSTANT applied for masks. | |
p (float): probability of applying the transform. Default: 0.5. | |
Targets: | |
image, mask, bboxes, keypoints | |
Image types: | |
uint8, float32 | |
""" | |
def __init__( | |
self, | |
limit: Union[float, Tuple[float, float]] = 90, | |
interpolation: int = cv2.INTER_LINEAR, | |
border_mode: int = cv2.BORDER_REFLECT_101, | |
value: FillValueType = None, | |
mask_value: Optional[Union[int, float, Sequence[int], Sequence[float]]] = None, | |
always_apply: bool = False, | |
p: float = 0.5, | |
): | |
super(SafeRotate, self).__init__(always_apply, p) | |
self.limit = to_tuple(limit) | |
self.interpolation = interpolation | |
self.border_mode = border_mode | |
self.value = value | |
self.mask_value = mask_value | |
def apply(self, img: np.ndarray, matrix: np.ndarray = np.array(None), **params) -> np.ndarray: | |
return F.safe_rotate(img, matrix, self.interpolation, self.value, self.border_mode) | |
def apply_to_mask(self, img: np.ndarray, matrix: np.ndarray = np.array(None), **params) -> np.ndarray: | |
return F.safe_rotate(img, matrix, cv2.INTER_NEAREST, self.mask_value, self.border_mode) | |
def apply_to_bbox(self, bbox: BoxInternalType, cols: int = 0, rows: int = 0, **params) -> BoxInternalType: | |
return F.bbox_safe_rotate(bbox, params["matrix"], cols, rows) | |
def apply_to_keypoint( | |
self, | |
keypoint: KeypointInternalType, | |
angle: float = 0, | |
scale_x: float = 0, | |
scale_y: float = 0, | |
cols: int = 0, | |
rows: int = 0, | |
**params | |
) -> KeypointInternalType: | |
return F.keypoint_safe_rotate(keypoint, params["matrix"], angle, scale_x, scale_y, cols, rows) | |
def targets_as_params(self) -> List[str]: | |
return ["image"] | |
def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
angle = random.uniform(self.limit[0], self.limit[1]) | |
image = params["image"] | |
h, w = image.shape[:2] | |
# https://stackoverflow.com/questions/43892506/opencv-python-rotate-image-without-cropping-sides | |
image_center = (w / 2, h / 2) | |
# Rotation Matrix | |
rotation_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) | |
# rotation calculates the cos and sin, taking absolutes of those. | |
abs_cos = abs(rotation_mat[0, 0]) | |
abs_sin = abs(rotation_mat[0, 1]) | |
# find the new width and height bounds | |
new_w = math.ceil(h * abs_sin + w * abs_cos) | |
new_h = math.ceil(h * abs_cos + w * abs_sin) | |
scale_x = w / new_w | |
scale_y = h / new_h | |
# Shift the image to create padding | |
rotation_mat[0, 2] += new_w / 2 - image_center[0] | |
rotation_mat[1, 2] += new_h / 2 - image_center[1] | |
# Rescale to original size | |
scale_mat = np.diag(np.ones(3)) | |
scale_mat[0, 0] *= scale_x | |
scale_mat[1, 1] *= scale_y | |
_tmp = np.diag(np.ones(3)) | |
_tmp[:2] = rotation_mat | |
_tmp = scale_mat @ _tmp | |
rotation_mat = _tmp[:2] | |
return {"matrix": rotation_mat, "angle": angle, "scale_x": scale_x, "scale_y": scale_y} | |
def get_transform_init_args_names(self) -> Tuple[str, str, str, str, str]: | |
return ("limit", "interpolation", "border_mode", "value", "mask_value") | |