diff --git a/README.md b/README.md index 44987ad0b..094306799 100644 --- a/README.md +++ b/README.md @@ -158,45 +158,44 @@ Pixel-level transforms will change just an input image and will leave any additi ### Spatial-level transforms Spatial-level transforms will simultaneously change both an input image as well as additional targets such as masks, bounding boxes, and keypoints. The following table shows which additional targets are supported by each transform. -| Transform | Image | Masks | BBoxes | Keypoints | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---: | :---: | :----: | :-------: | -| [CenterCrop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.CenterCrop) | ✓ | ✓ | ✓ | ✓ | -| [CoarseDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.CoarseDropout) | ✓ | ✓ | | | -| [Crop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Crop) | ✓ | ✓ | ✓ | ✓ | -| [CropNonEmptyMaskIfExists](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.CropNonEmptyMaskIfExists) | ✓ | ✓ | ✓ | ✓ | -| [ElasticTransform](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.ElasticTransform) | ✓ | ✓ | | | -| [Flip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Flip) | ✓ | ✓ | ✓ | ✓ | -| [GridDistortion](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.GridDistortion) | ✓ | ✓ | | | -| [GridDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.GridDropout) | ✓ | ✓ | | | -| [HorizontalFlip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.HorizontalFlip) | ✓ | ✓ | ✓ | ✓ | -| [IAAAffine](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAAffine) | ✓ | ✓ | ✓ | ✓ | -| [IAACropAndPad](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAACropAndPad) | ✓ | ✓ | ✓ | ✓ | -| [IAAFliplr](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAFliplr) | ✓ | ✓ | ✓ | ✓ | -| [IAAFlipud](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAFlipud) | ✓ | ✓ | ✓ | ✓ | -| [IAAPerspective](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAPerspective) | ✓ | ✓ | ✓ | ✓ | -| [IAAPiecewiseAffine](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAPiecewiseAffine) | ✓ | ✓ | ✓ | ✓ | -| [Lambda](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Lambda) | ✓ | ✓ | ✓ | ✓ | -| [LongestMaxSize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.LongestMaxSize) | ✓ | ✓ | ✓ | ✓ | -| [MaskDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.MaskDropout) | ✓ | ✓ | | | -| [NoOp](https://albumentations.ai/docs/api_reference/core/transforms_interface/#albumentations.core.transforms_interface.NoOp) | ✓ | ✓ | ✓ | ✓ | -| [OpticalDistortion](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.OpticalDistortion) | ✓ | ✓ | | | -| [PadIfNeeded](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.PadIfNeeded) | ✓ | ✓ | ✓ | ✓ | -| [Perspective](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.Perspective) | ✓ | ✓ | ✓ | ✓ | -| [RandomCrop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomCrop) | ✓ | ✓ | ✓ | ✓ | -| [RandomCropNearBBox](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomCropNearBBox) | ✓ | ✓ | ✓ | ✓ | -| [RandomGridShuffle](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomGridShuffle) | ✓ | ✓ | | | -| [RandomResizedCrop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomResizedCrop) | ✓ | ✓ | ✓ | ✓ | -| [RandomRotate90](https://albumentations.ai/docs/api_reference/augmentations/geometric/rotate/#albumentations.augmentations.geometric.rotate.RandomRotate90) | ✓ | ✓ | ✓ | ✓ | -| [RandomScale](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.RandomScale) | ✓ | ✓ | ✓ | ✓ | -| [RandomSizedBBoxSafeCrop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomSizedBBoxSafeCrop) | ✓ | ✓ | ✓ | | -| [RandomSizedCrop](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomSizedCrop) | ✓ | ✓ | ✓ | ✓ | -| [Resize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.Resize) | ✓ | ✓ | ✓ | ✓ | -| [Rotate](https://albumentations.ai/docs/api_reference/augmentations/geometric/rotate/#albumentations.augmentations.geometric.rotate.Rotate) | ✓ | ✓ | ✓ | ✓ | -| [ShiftScaleRotate](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.ShiftScaleRotate) | ✓ | ✓ | ✓ | ✓ | -| [SmallestMaxSize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.SmallestMaxSize) | ✓ | ✓ | ✓ | ✓ | -| [Transpose](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Transpose) | ✓ | ✓ | ✓ | ✓ | -| [VerticalFlip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.VerticalFlip) | ✓ | ✓ | ✓ | ✓ | - +| Transform | Image | Masks | BBoxes | Keypoints | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---: | :---: | :----: | :-------: | +| [CenterCrop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.CenterCrop) | ✓ | ✓ | ✓ | ✓ | +| [CoarseDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.CoarseDropout) | ✓ | ✓ | | | +| [Crop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.Crop) | ✓ | ✓ | ✓ | ✓ | +| [CropNonEmptyMaskIfExists](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.CropNonEmptyMaskIfExists) | ✓ | ✓ | ✓ | ✓ | +| [ElasticTransform](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.ElasticTransform) | ✓ | ✓ | | | +| [Flip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Flip) | ✓ | ✓ | ✓ | ✓ | +| [GridDistortion](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.GridDistortion) | ✓ | ✓ | | | +| [GridDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.GridDropout) | ✓ | ✓ | | | +| [HorizontalFlip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.HorizontalFlip) | ✓ | ✓ | ✓ | ✓ | +| [IAAAffine](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAAffine) | ✓ | ✓ | ✓ | ✓ | +| [IAACropAndPad](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAACropAndPad) | ✓ | ✓ | ✓ | ✓ | +| [IAAFliplr](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAFliplr) | ✓ | ✓ | ✓ | ✓ | +| [IAAFlipud](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAFlipud) | ✓ | ✓ | ✓ | ✓ | +| [IAAPerspective](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAPerspective) | ✓ | ✓ | ✓ | ✓ | +| [IAAPiecewiseAffine](https://albumentations.ai/docs/api_reference/imgaug/transforms/#albumentations.imgaug.transforms.IAAPiecewiseAffine) | ✓ | ✓ | ✓ | ✓ | +| [Lambda](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Lambda) | ✓ | ✓ | ✓ | ✓ | +| [LongestMaxSize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.LongestMaxSize) | ✓ | ✓ | ✓ | ✓ | +| [MaskDropout](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.MaskDropout) | ✓ | ✓ | | | +| [NoOp](https://albumentations.ai/docs/api_reference/core/transforms_interface/#albumentations.core.transforms_interface.NoOp) | ✓ | ✓ | ✓ | ✓ | +| [OpticalDistortion](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.OpticalDistortion) | ✓ | ✓ | | | +| [PadIfNeeded](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.PadIfNeeded) | ✓ | ✓ | ✓ | ✓ | +| [Perspective](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.Perspective) | ✓ | ✓ | ✓ | ✓ | +| [RandomCrop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.RandomCrop) | ✓ | ✓ | ✓ | ✓ | +| [RandomCropNearBBox](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.RandomCropNearBBox) | ✓ | ✓ | ✓ | ✓ | +| [RandomGridShuffle](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.RandomGridShuffle) | ✓ | ✓ | | | +| [RandomResizedCrop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.RandomResizedCrop) | ✓ | ✓ | ✓ | ✓ | +| [RandomRotate90](https://albumentations.ai/docs/api_reference/augmentations/geometric/rotate/#albumentations.augmentations.geometric.rotate.RandomRotate90) | ✓ | ✓ | ✓ | ✓ | +| [RandomScale](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.RandomScale) | ✓ | ✓ | ✓ | ✓ | +| [RandomSizedBBoxSafeCrop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.RandomSizedBBoxSafeCrop) | ✓ | ✓ | ✓ | | +| [RandomSizedCrop](https://albumentations.ai/docs/api_reference/augmentations/crops/transforms/#albumentations.augmentations.crops.transforms.RandomSizedCrop) | ✓ | ✓ | ✓ | ✓ | +| [Resize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.Resize) | ✓ | ✓ | ✓ | ✓ | +| [Rotate](https://albumentations.ai/docs/api_reference/augmentations/geometric/rotate/#albumentations.augmentations.geometric.rotate.Rotate) | ✓ | ✓ | ✓ | ✓ | +| [ShiftScaleRotate](https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.ShiftScaleRotate) | ✓ | ✓ | ✓ | ✓ | +| [SmallestMaxSize](https://albumentations.ai/docs/api_reference/augmentations/geometric/resize/#albumentations.augmentations.geometric.resize.SmallestMaxSize) | ✓ | ✓ | ✓ | ✓ | +| [Transpose](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.Transpose) | ✓ | ✓ | ✓ | ✓ | +| [VerticalFlip](https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.VerticalFlip) | ✓ | ✓ | ✓ | ✓ | ## A few more examples of augmentations ### Semantic segmentation on the Inria dataset diff --git a/albumentations/augmentations/__init__.py b/albumentations/augmentations/__init__.py index 8e68d2511..7eb38fad3 100644 --- a/albumentations/augmentations/__init__.py +++ b/albumentations/augmentations/__init__.py @@ -6,9 +6,13 @@ # New transformations goes to individual files listed below from .domain_adaptation import * + from .geometric.transforms import * from .geometric.functional import * from .geometric.resize import * from .geometric.rotate import * +from .crops.transforms import * +from .crops.functional import * + from .utils import * diff --git a/albumentations/augmentations/crops/__init__.py b/albumentations/augmentations/crops/__init__.py new file mode 100644 index 000000000..39b902595 --- /dev/null +++ b/albumentations/augmentations/crops/__init__.py @@ -0,0 +1,2 @@ +from .transforms import * +from .functional import * diff --git a/albumentations/augmentations/crops/functional.py b/albumentations/augmentations/crops/functional.py new file mode 100644 index 000000000..e508f7a56 --- /dev/null +++ b/albumentations/augmentations/crops/functional.py @@ -0,0 +1,203 @@ +import numpy as np + +from typing import List, Union, Tuple + +from ..bbox_utils import denormalize_bbox, normalize_bbox + +BboxType = Union[List[int], List[float], Tuple[int, ...], Tuple[float, ...], np.ndarray] +KeypointType = Union[List[int], List[float], Tuple[int, ...], Tuple[float, ...], np.ndarray] + + +def get_random_crop_coords(height: int, width: int, crop_height: int, crop_width: int, h_start: float, w_start: float): + y1 = int((height - crop_height) * h_start) + y2 = y1 + crop_height + x1 = int((width - crop_width) * w_start) + x2 = x1 + crop_width + return x1, y1, x2, y2 + + +def random_crop(img: np.ndarray, crop_height: int, crop_width: int, h_start: float, w_start: float): + height, width = img.shape[:2] + if height < crop_height or width < crop_width: + raise ValueError( + "Requested crop size ({crop_height}, {crop_width}) is " + "larger than the image size ({height}, {width})".format( + crop_height=crop_height, crop_width=crop_width, height=height, width=width + ) + ) + x1, y1, x2, y2 = get_random_crop_coords(height, width, crop_height, crop_width, h_start, w_start) + img = img[y1:y2, x1:x2] + return img + + +def crop_bbox_by_coords( + bbox: BboxType, crop_coords: Tuple[int, int, int, int], crop_height: int, crop_width: int, rows: int, cols: 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, x_max, y_max)`. + crop_coords (tuple): Crop coordinates `(x1, y1, x2, y2)`. + crop_height (int): + crop_width (int): + rows (int): Image rows. + cols (int): Image cols. + + Returns: + tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. + + """ + bbox = denormalize_bbox(bbox, rows, cols) + x_min, y_min, x_max, y_max = bbox[:4] + x1, y1, _, _ = crop_coords + cropped_bbox = x_min - x1, y_min - y1, x_max - x1, y_max - y1 + return normalize_bbox(cropped_bbox, crop_height, crop_width) + + +def bbox_random_crop( + bbox: BboxType, crop_height: int, crop_width: int, h_start: float, w_start: float, rows: int, cols: int +): + crop_coords = get_random_crop_coords(rows, cols, crop_height, crop_width, h_start, w_start) + return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) + + +def crop_keypoint_by_coords(keypoint: KeypointType, crop_coords: Tuple[int, int, int, int]): # skipcq: PYL-W0613 + """Crop a keypoint using the provided coordinates of bottom-left and top-right corners in pixels and the + required height and width of the crop. + + Args: + keypoint (tuple): A keypoint `(x, y, angle, scale)`. + crop_coords (tuple): Crop box coords `(x1, x2, y1, y2)`. + + Returns: + A keypoint `(x, y, angle, scale)`. + + """ + x, y, angle, scale = keypoint[:4] + x1, y1, _, _ = crop_coords + return x - x1, y - y1, angle, scale + + +def keypoint_random_crop( + keypoint: KeypointType, crop_height: int, crop_width: int, h_start: float, w_start: float, rows: int, cols: int +): + """Keypoint random crop. + + Args: + keypoint: (tuple): A keypoint `(x, y, angle, scale)`. + crop_height (int): Crop height. + crop_width (int): Crop width. + h_start (int): Crop height start. + w_start (int): Crop width start. + rows (int): Image height. + cols (int): Image width. + + Returns: + A keypoint `(x, y, angle, scale)`. + + """ + crop_coords = get_random_crop_coords(rows, cols, crop_height, crop_width, h_start, w_start) + return crop_keypoint_by_coords(keypoint, crop_coords) + + +def get_center_crop_coords(height: int, width: int, crop_height: int, crop_width: int): + y1 = (height - crop_height) // 2 + y2 = y1 + crop_height + x1 = (width - crop_width) // 2 + x2 = x1 + crop_width + return x1, y1, x2, y2 + + +def center_crop(img: np.ndarray, crop_height: int, crop_width: int): + height, width = img.shape[:2] + if height < crop_height or width < crop_width: + raise ValueError( + "Requested crop size ({crop_height}, {crop_width}) is " + "larger than the image size ({height}, {width})".format( + crop_height=crop_height, crop_width=crop_width, height=height, width=width + ) + ) + x1, y1, x2, y2 = get_center_crop_coords(height, width, crop_height, crop_width) + img = img[y1:y2, x1:x2] + return img + + +def bbox_center_crop(bbox: BboxType, crop_height: int, crop_width: int, rows: int, cols: int): + crop_coords = get_center_crop_coords(rows, cols, crop_height, crop_width) + return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) + + +def keypoint_center_crop(keypoint: KeypointType, crop_height: int, crop_width: int, rows: int, cols: int): + """Keypoint center crop. + + Args: + keypoint (tuple): A keypoint `(x, y, angle, scale)`. + crop_height (int): Crop height. + crop_width (int): Crop width. + rows (int): Image height. + cols (int): Image width. + + Returns: + tuple: A keypoint `(x, y, angle, scale)`. + + """ + crop_coords = get_center_crop_coords(rows, cols, crop_height, crop_width) + return crop_keypoint_by_coords(keypoint, crop_coords) + + +def crop(img: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int): + height, width = img.shape[:2] + if x_max <= x_min or y_max <= y_min: + raise ValueError( + "We should have x_min < x_max and y_min < y_max. But we got" + " (x_min = {x_min}, y_min = {y_min}, x_max = {x_max}, y_max = {y_max})".format( + x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max + ) + ) + + if x_min < 0 or x_max > width or y_min < 0 or y_max > height: + raise ValueError( + "Values for crop should be non negative and equal or smaller than image sizes" + "(x_min = {x_min}, y_min = {y_min}, x_max = {x_max}, y_max = {y_max}, " + "height = {height}, width = {width})".format( + x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, height=height, width=width + ) + ) + + return img[y_min:y_max, x_min:x_max] + + +def bbox_crop(bbox: BboxType, x_min: int, y_min: int, x_max: int, y_max: int, rows: int, cols: int): + """Crop a bounding box. + + Args: + bbox (tuple): A bounding box `(x_min, y_min, x_max, y_max)`. + x_min (int): + y_min (int): + x_max (int): + y_max (int): + rows (int): Image rows. + cols (int): Image cols. + + Returns: + tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. + + """ + crop_coords = x_min, y_min, x_max, y_max + crop_height = y_max - y_min + crop_width = x_max - x_min + return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) + + +def clamping_crop(img: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int): + h, w = img.shape[:2] + if x_min < 0: + x_min = 0 + if y_min < 0: + y_min = 0 + if y_max >= h: + y_max = h - 1 + if x_max >= w: + x_max = w - 1 + return img[int(y_min) : int(y_max), int(x_min) : int(x_max)] diff --git a/albumentations/augmentations/crops/transforms.py b/albumentations/augmentations/crops/transforms.py new file mode 100644 index 000000000..e97cf10a6 --- /dev/null +++ b/albumentations/augmentations/crops/transforms.py @@ -0,0 +1,501 @@ +import cv2 +import math +import random +import numpy as np + +from . import functional as F +from ..bbox_utils import union_of_bboxes +from ..geometric import functional as FGeometric +from ...core.transforms_interface import DualTransform + + +__all__ = [ + "RandomCrop", + "CenterCrop", + "Crop", + "CropNonEmptyMaskIfExists", + "RandomSizedCrop", + "RandomResizedCrop", + "RandomCropNearBBox", + "RandomSizedBBoxSafeCrop", +] + + +class RandomCrop(DualTransform): + """Crop a random part of the input. + + Args: + height (int): height of the crop. + width (int): width of the crop. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__(self, height, width, always_apply=False, p=1.0): + super().__init__(always_apply, p) + self.height = height + self.width = width + + def apply(self, img, h_start=0, w_start=0, **params): + return F.random_crop(img, self.height, self.width, h_start, w_start) + + def get_params(self): + return {"h_start": random.random(), "w_start": random.random()} + + def apply_to_bbox(self, bbox, **params): + return F.bbox_random_crop(bbox, self.height, self.width, **params) + + def apply_to_keypoint(self, keypoint, **params): + return F.keypoint_random_crop(keypoint, self.height, self.width, **params) + + def get_transform_init_args_names(self): + return ("height", "width") + + +class CenterCrop(DualTransform): + """Crop the central part of the input. + + Args: + height (int): height of the crop. + width (int): width of the crop. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + + Note: + It is recommended to use uint8 images as input. + Otherwise the operation will require internal conversion + float32 -> uint8 -> float32 that causes worse performance. + """ + + def __init__(self, height, width, always_apply=False, p=1.0): + super(CenterCrop, self).__init__(always_apply, p) + self.height = height + self.width = width + + def apply(self, img, **params): + return F.center_crop(img, self.height, self.width) + + def apply_to_bbox(self, bbox, **params): + return F.bbox_center_crop(bbox, self.height, self.width, **params) + + def apply_to_keypoint(self, keypoint, **params): + return F.keypoint_center_crop(keypoint, self.height, self.width, **params) + + def get_transform_init_args_names(self): + return ("height", "width") + + +class Crop(DualTransform): + """Crop region from image. + + Args: + x_min (int): Minimum upper left x coordinate. + y_min (int): Minimum upper left y coordinate. + x_max (int): Maximum lower right x coordinate. + y_max (int): Maximum lower right y coordinate. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__(self, x_min=0, y_min=0, x_max=1024, y_max=1024, always_apply=False, p=1.0): + super(Crop, self).__init__(always_apply, p) + self.x_min = x_min + self.y_min = y_min + self.x_max = x_max + self.y_max = y_max + + def apply(self, img, **params): + return F.crop(img, x_min=self.x_min, y_min=self.y_min, x_max=self.x_max, y_max=self.y_max) + + def apply_to_bbox(self, bbox, **params): + return F.bbox_crop(bbox, x_min=self.x_min, y_min=self.y_min, x_max=self.x_max, y_max=self.y_max, **params) + + def apply_to_keypoint(self, keypoint, **params): + return F.crop_keypoint_by_coords(keypoint, crop_coords=(self.x_min, self.y_min, self.x_max, self.y_max)) + + def get_transform_init_args_names(self): + return ("x_min", "y_min", "x_max", "y_max") + + +class CropNonEmptyMaskIfExists(DualTransform): + """Crop area with mask if mask is non-empty, else make random crop. + + Args: + height (int): vertical size of crop in pixels + width (int): horizontal size of crop in pixels + ignore_values (list of int): values to ignore in mask, `0` values are always ignored + (e.g. if background value is 5 set `ignore_values=[5]` to ignore) + ignore_channels (list of int): channels to ignore in mask + (e.g. if background is a first channel set `ignore_channels=[0]` to ignore) + p (float): probability of applying the transform. Default: 1.0. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__(self, height, width, ignore_values=None, ignore_channels=None, always_apply=False, p=1.0): + super(CropNonEmptyMaskIfExists, self).__init__(always_apply, p) + + if ignore_values is not None and not isinstance(ignore_values, list): + raise ValueError("Expected `ignore_values` of type `list`, got `{}`".format(type(ignore_values))) + if ignore_channels is not None and not isinstance(ignore_channels, list): + raise ValueError("Expected `ignore_channels` of type `list`, got `{}`".format(type(ignore_channels))) + + self.height = height + self.width = width + self.ignore_values = ignore_values + self.ignore_channels = ignore_channels + + def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params): + return F.crop(img, x_min, y_min, x_max, y_max) + + def apply_to_bbox(self, bbox, x_min=0, x_max=0, y_min=0, y_max=0, **params): + return F.bbox_crop( + bbox, x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, rows=params["rows"], cols=params["cols"] + ) + + def apply_to_keypoint(self, keypoint, x_min=0, x_max=0, y_min=0, y_max=0, **params): + return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) + + def _preprocess_mask(self, mask): + mask_height, mask_width = mask.shape[:2] + + if self.ignore_values is not None: + ignore_values_np = np.array(self.ignore_values) + mask = np.where(np.isin(mask, ignore_values_np), 0, mask) + + if mask.ndim == 3 and self.ignore_channels is not None: + target_channels = np.array([ch for ch in range(mask.shape[-1]) if ch not in self.ignore_channels]) + mask = np.take(mask, target_channels, axis=-1) + + if self.height > mask_height or self.width > mask_width: + raise ValueError( + "Crop size ({},{}) is larger than image ({},{})".format( + self.height, self.width, mask_height, mask_width + ) + ) + + return mask + + def update_params(self, params, **kwargs): + if "mask" in kwargs: + mask = self._preprocess_mask(kwargs["mask"]) + elif "masks" in kwargs and len(kwargs["masks"]): + masks = kwargs["masks"] + mask = self._preprocess_mask(masks[0]) + for m in masks[1:]: + mask |= self._preprocess_mask(m) + else: + raise RuntimeError("Can not find mask for CropNonEmptyMaskIfExists") + + mask_height, mask_width = mask.shape[:2] + + if mask.any(): + mask = mask.sum(axis=-1) if mask.ndim == 3 else mask + non_zero_yx = np.argwhere(mask) + y, x = random.choice(non_zero_yx) + x_min = x - random.randint(0, self.width - 1) + y_min = y - random.randint(0, self.height - 1) + x_min = np.clip(x_min, 0, mask_width - self.width) + y_min = np.clip(y_min, 0, mask_height - self.height) + else: + x_min = random.randint(0, mask_width - self.width) + y_min = random.randint(0, mask_height - self.height) + + x_max = x_min + self.width + y_max = y_min + self.height + + params.update({"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max}) + return params + + def get_transform_init_args_names(self): + return ("height", "width", "ignore_values", "ignore_channels") + + +class _BaseRandomSizedCrop(DualTransform): + # Base class for RandomSizedCrop and RandomResizedCrop + + def __init__(self, height, width, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0): + super(_BaseRandomSizedCrop, self).__init__(always_apply, p) + self.height = height + self.width = width + self.interpolation = interpolation + + def apply(self, img, crop_height=0, crop_width=0, h_start=0, w_start=0, interpolation=cv2.INTER_LINEAR, **params): + crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) + return FGeometric.resize(crop, self.height, self.width, interpolation) + + def apply_to_bbox(self, bbox, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): + return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) + + def apply_to_keypoint(self, keypoint, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): + keypoint = F.keypoint_random_crop(keypoint, crop_height, crop_width, h_start, w_start, rows, cols) + scale_x = self.width / crop_width + scale_y = self.height / crop_height + keypoint = FGeometric.keypoint_scale(keypoint, scale_x, scale_y) + return keypoint + + +class RandomSizedCrop(_BaseRandomSizedCrop): + """Crop a random part of the input and rescale it to some size. + + Args: + min_max_height ((int, int)): crop size limits. + height (int): height after crop and resize. + width (int): width after crop and resize. + w2h_ratio (float): aspect ratio of crop. + 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. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__( + self, min_max_height, height, width, w2h_ratio=1.0, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0 + ): + super(RandomSizedCrop, self).__init__( + height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p + ) + self.min_max_height = min_max_height + self.w2h_ratio = w2h_ratio + + def get_params(self): + crop_height = random.randint(self.min_max_height[0], self.min_max_height[1]) + return { + "h_start": random.random(), + "w_start": random.random(), + "crop_height": crop_height, + "crop_width": int(crop_height * self.w2h_ratio), + } + + def get_transform_init_args_names(self): + return "min_max_height", "height", "width", "w2h_ratio", "interpolation" + + +class RandomResizedCrop(_BaseRandomSizedCrop): + """Torchvision's variant of crop a random part of the input and rescale it to some size. + + Args: + height (int): height after crop and resize. + width (int): width after crop and resize. + scale ((float, float)): range of size of the origin size cropped + ratio ((float, float)): range of aspect ratio of the origin aspect ratio cropped + 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. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__( + self, + height, + width, + scale=(0.08, 1.0), + ratio=(0.75, 1.3333333333333333), + interpolation=cv2.INTER_LINEAR, + always_apply=False, + p=1.0, + ): + + super(RandomResizedCrop, self).__init__( + height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p + ) + self.scale = scale + self.ratio = ratio + + def get_params_dependent_on_targets(self, params): + img = params["image"] + area = img.shape[0] * img.shape[1] + + for _attempt in range(10): + target_area = random.uniform(*self.scale) * area + log_ratio = (math.log(self.ratio[0]), math.log(self.ratio[1])) + aspect_ratio = math.exp(random.uniform(*log_ratio)) + + w = int(round(math.sqrt(target_area * aspect_ratio))) # skipcq: PTC-W0028 + h = int(round(math.sqrt(target_area / aspect_ratio))) # skipcq: PTC-W0028 + + if 0 < w <= img.shape[1] and 0 < h <= img.shape[0]: + i = random.randint(0, img.shape[0] - h) + j = random.randint(0, img.shape[1] - w) + return { + "crop_height": h, + "crop_width": w, + "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), + "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), + } + + # Fallback to central crop + in_ratio = img.shape[1] / img.shape[0] + if in_ratio < min(self.ratio): + w = img.shape[1] + h = int(round(w / min(self.ratio))) + elif in_ratio > max(self.ratio): + h = img.shape[0] + w = int(round(h * max(self.ratio))) + else: # whole image + w = img.shape[1] + h = img.shape[0] + i = (img.shape[0] - h) // 2 + j = (img.shape[1] - w) // 2 + return { + "crop_height": h, + "crop_width": w, + "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), + "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), + } + + def get_params(self): + return {} + + @property + def targets_as_params(self): + return ["image"] + + def get_transform_init_args_names(self): + return "height", "width", "scale", "ratio", "interpolation" + + +class RandomCropNearBBox(DualTransform): + """Crop bbox from image with random shift by x,y coordinates + + Args: + max_part_shift (float): float value in (0.0, 1.0) range. Default 0.3 + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__(self, max_part_shift=0.3, always_apply=False, p=1.0): + super(RandomCropNearBBox, self).__init__(always_apply, p) + self.max_part_shift = max_part_shift + + def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params): + return F.clamping_crop(img, x_min, y_min, x_max, y_max) + + def get_params_dependent_on_targets(self, params): + bbox = params["cropping_bbox"] + h_max_shift = int((bbox[3] - bbox[1]) * self.max_part_shift) + w_max_shift = int((bbox[2] - bbox[0]) * self.max_part_shift) + + x_min = bbox[0] - random.randint(-w_max_shift, w_max_shift) + x_max = bbox[2] + random.randint(-w_max_shift, w_max_shift) + + y_min = bbox[1] - random.randint(-h_max_shift, h_max_shift) + y_max = bbox[3] + random.randint(-h_max_shift, h_max_shift) + + return {"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max} + + def apply_to_bbox(self, bbox, x_min=0, x_max=0, y_min=0, y_max=0, **params): + h_start = y_min + w_start = x_min + return F.bbox_crop(bbox, y_max - y_min, x_max - x_min, h_start, w_start, **params) + + def apply_to_keypoint(self, keypoint, x_min=0, x_max=0, y_min=0, y_max=0, **params): + return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) + + @property + def targets_as_params(self): + return ["cropping_bbox"] + + def get_transform_init_args_names(self): + return ("max_part_shift",) + + +class RandomSizedBBoxSafeCrop(DualTransform): + """Crop a random part of the input and rescale it to some size without loss of bboxes. + + Args: + height (int): height after crop and resize. + width (int): width after crop and resize. + erosion_rate (float): erosion rate applied on input image height before crop. + 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. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes + + Image types: + uint8, float32 + """ + + def __init__(self, height, width, erosion_rate=0.0, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0): + super(RandomSizedBBoxSafeCrop, self).__init__(always_apply, p) + self.height = height + self.width = width + self.interpolation = interpolation + self.erosion_rate = erosion_rate + + def apply(self, img, crop_height=0, crop_width=0, h_start=0, w_start=0, interpolation=cv2.INTER_LINEAR, **params): + crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) + return FGeometric.resize(crop, self.height, self.width, interpolation) + + def get_params_dependent_on_targets(self, params): + img_h, img_w = params["image"].shape[:2] + if len(params["bboxes"]) == 0: # less likely, this class is for use with bboxes. + erosive_h = int(img_h * (1.0 - self.erosion_rate)) + crop_height = img_h if erosive_h >= img_h else random.randint(erosive_h, img_h) + return { + "h_start": random.random(), + "w_start": random.random(), + "crop_height": crop_height, + "crop_width": int(crop_height * img_w / img_h), + } + # get union of all bboxes + x, y, x2, y2 = union_of_bboxes( + width=img_w, height=img_h, bboxes=params["bboxes"], erosion_rate=self.erosion_rate + ) + # find bigger region + bx, by = x * random.random(), y * random.random() + bx2, by2 = x2 + (1 - x2) * random.random(), y2 + (1 - y2) * random.random() + bw, bh = bx2 - bx, by2 - by + crop_height = img_h if bh >= 1.0 else int(img_h * bh) + crop_width = img_w if bw >= 1.0 else int(img_w * bw) + h_start = np.clip(0.0 if bh >= 1.0 else by / (1.0 - bh), 0.0, 1.0) + w_start = np.clip(0.0 if bw >= 1.0 else bx / (1.0 - bw), 0.0, 1.0) + return {"h_start": h_start, "w_start": w_start, "crop_height": crop_height, "crop_width": crop_width} + + def apply_to_bbox(self, bbox, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): + return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) + + @property + def targets_as_params(self): + return ["image", "bboxes"] + + def get_transform_init_args_names(self): + return ("height", "width", "erosion_rate", "interpolation") diff --git a/albumentations/augmentations/functional.py b/albumentations/augmentations/functional.py index 62785e5d5..f9f603279 100644 --- a/albumentations/augmentations/functional.py +++ b/albumentations/augmentations/functional.py @@ -7,7 +7,6 @@ import cv2 import numpy as np -from albumentations.augmentations.bbox_utils import denormalize_bbox, normalize_bbox from albumentations.augmentations.keypoints_utils import angle_to_2pi_range MAX_VALUES_BY_DTYPE = { @@ -190,85 +189,6 @@ def __process_fn(img): return __process_fn -def crop(img, x_min, y_min, x_max, y_max): - height, width = img.shape[:2] - if x_max <= x_min or y_max <= y_min: - raise ValueError( - "We should have x_min < x_max and y_min < y_max. But we got" - " (x_min = {x_min}, y_min = {y_min}, x_max = {x_max}, y_max = {y_max})".format( - x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max - ) - ) - - if x_min < 0 or x_max > width or y_min < 0 or y_max > height: - raise ValueError( - "Values for crop should be non negative and equal or smaller than image sizes" - "(x_min = {x_min}, y_min = {y_min}, x_max = {x_max}, y_max = {y_max}, " - "height = {height}, width = {width})".format( - x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, height=height, width=width - ) - ) - - return img[y_min:y_max, x_min:x_max] - - -def get_center_crop_coords(height, width, crop_height, crop_width): - y1 = (height - crop_height) // 2 - y2 = y1 + crop_height - x1 = (width - crop_width) // 2 - x2 = x1 + crop_width - return x1, y1, x2, y2 - - -def center_crop(img, crop_height, crop_width): - height, width = img.shape[:2] - if height < crop_height or width < crop_width: - raise ValueError( - "Requested crop size ({crop_height}, {crop_width}) is " - "larger than the image size ({height}, {width})".format( - crop_height=crop_height, crop_width=crop_width, height=height, width=width - ) - ) - x1, y1, x2, y2 = get_center_crop_coords(height, width, crop_height, crop_width) - img = img[y1:y2, x1:x2] - return img - - -def get_random_crop_coords(height, width, crop_height, crop_width, h_start, w_start): - y1 = int((height - crop_height) * h_start) - y2 = y1 + crop_height - x1 = int((width - crop_width) * w_start) - x2 = x1 + crop_width - return x1, y1, x2, y2 - - -def random_crop(img, crop_height, crop_width, h_start, w_start): - height, width = img.shape[:2] - if height < crop_height or width < crop_width: - raise ValueError( - "Requested crop size ({crop_height}, {crop_width}) is " - "larger than the image size ({height}, {width})".format( - crop_height=crop_height, crop_width=crop_width, height=height, width=width - ) - ) - x1, y1, x2, y2 = get_random_crop_coords(height, width, crop_height, crop_width, h_start, w_start) - img = img[y1:y2, x1:x2] - return img - - -def clamping_crop(img, x_min, y_min, x_max, y_max): - h, w = img.shape[:2] - if x_min < 0: - x_min = 0 - if y_min < 0: - y_min = 0 - if y_max >= h: - y_max = h - 1 - if x_max >= w: - x_max = w - 1 - return img[int(y_min) : int(y_max), int(x_min) : int(x_max)] - - def _shift_hsv_uint8(img, hue_shift, sat_shift, val_shift): dtype = img.dtype img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) @@ -1328,61 +1248,6 @@ def bbox_flip(bbox, d, rows, cols): return bbox -def crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols): - """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, x_max, y_max)`. - crop_coords (tuple): Crop coordinates `(x1, y1, x2, y2)`. - crop_height (int): - crop_width (int): - rows (int): Image rows. - cols (int): Image cols. - - Returns: - tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. - - """ - bbox = denormalize_bbox(bbox, rows, cols) - x_min, y_min, x_max, y_max = bbox[:4] - x1, y1, _, _ = crop_coords - cropped_bbox = x_min - x1, y_min - y1, x_max - x1, y_max - y1 - return normalize_bbox(cropped_bbox, crop_height, crop_width) - - -def bbox_crop(bbox, x_min, y_min, x_max, y_max, rows, cols): - """Crop a bounding box. - - Args: - bbox (tuple): A bounding box `(x_min, y_min, x_max, y_max)`. - x_min (int): - y_min (int): - x_max (int): - y_max (int): - rows (int): Image rows. - cols (int): Image cols. - - Returns: - tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. - - """ - crop_coords = x_min, y_min, x_max, y_max - crop_height = y_max - y_min - crop_width = x_max - x_min - return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) - - -def bbox_center_crop(bbox, crop_height, crop_width, rows, cols): - crop_coords = get_center_crop_coords(rows, cols, crop_height, crop_width) - return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) - - -def bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols): - crop_coords = get_random_crop_coords(rows, cols, crop_height, crop_width, h_start, w_start) - return crop_bbox_by_coords(bbox, crop_coords, crop_height, crop_width, rows, cols) - - def bbox_transpose(bbox, axis, rows, cols): # skipcq: PYL-W0613 """Transposes a bounding box along given axis. @@ -1476,67 +1341,6 @@ def keypoint_flip(keypoint, d, rows, cols): return keypoint -def crop_keypoint_by_coords(keypoint, crop_coords, crop_height, crop_width, rows, cols): # skipcq: PYL-W0613 - """Crop a keypoint using the provided coordinates of bottom-left and top-right corners in pixels and the - required height and width of the crop. - - Args: - keypoint (tuple): A keypoint `(x, y, angle, scale)`. - crop_coords (tuple): Crop box coords `(x1, x2, y1, y2)`. - crop height (int): Crop height. - crop_width (int): Crop width. - rows (int): Image height. - cols (int): Image width. - - Returns: - A keypoint `(x, y, angle, scale)`. - - """ - x, y, angle, scale = keypoint[:4] - x1, y1, _, _ = crop_coords - return x - x1, y - y1, angle, scale - - -def keypoint_random_crop(keypoint, crop_height, crop_width, h_start, w_start, rows, cols): - """Keypoint random crop. - - Args: - keypoint: (tuple): A keypoint `(x, y, angle, scale)`. - crop_height (int): Crop height. - crop_width (int): Crop width. - h_start (int): Crop height start. - w_start (int): Crop width start. - rows (int): Image height. - cols (int): Image width. - - Returns: - A keypoint `(x, y, angle, scale)`. - - """ - crop_coords = get_random_crop_coords(rows, cols, crop_height, crop_width, h_start, w_start) - return crop_keypoint_by_coords(keypoint, crop_coords, crop_height, crop_width, rows, cols) - - -def keypoint_center_crop(keypoint, crop_height, crop_width, rows, cols): - """Keypoint center crop. - - Args: - keypoint (tuple): A keypoint `(x, y, angle, scale)`. - crop_height (int): Crop height. - crop_width (int): Crop width. - h_start (int): Crop height start. - w_start (int): Crop width start. - rows (int): Image height. - cols (int): Image width. - - Returns: - tuple: A keypoint `(x, y, angle, scale)`. - - """ - crop_coords = get_center_crop_coords(rows, cols, crop_height, crop_width) - return crop_keypoint_by_coords(keypoint, crop_coords, crop_height, crop_width, rows, cols) - - def noop(input_obj, **params): # skipcq: PYL-W0613 return input_obj diff --git a/albumentations/augmentations/transforms.py b/albumentations/augmentations/transforms.py index e8d5cdd59..1455da12b 100644 --- a/albumentations/augmentations/transforms.py +++ b/albumentations/augmentations/transforms.py @@ -25,9 +25,7 @@ "Flip", "Normalize", "Transpose", - "RandomCrop", "RandomGamma", - "CenterCrop", "OpticalDistortion", "GridDistortion", "RandomGridShuffle", @@ -52,13 +50,7 @@ "CoarseDropout", "ToFloat", "FromFloat", - "Crop", - "CropNonEmptyMaskIfExists", - "RandomSizedCrop", - "RandomResizedCrop", "RandomBrightnessContrast", - "RandomCropNearBBox", - "RandomSizedBBoxSafeCrop", "RandomSnow", "RandomRain", "RandomFog", @@ -200,49 +192,6 @@ def get_transform_init_args_names(self): ) -class Crop(DualTransform): - """Crop region from image. - - Args: - x_min (int): Minimum upper left x coordinate. - y_min (int): Minimum upper left y coordinate. - x_max (int): Maximum lower right x coordinate. - y_max (int): Maximum lower right y coordinate. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__(self, x_min=0, y_min=0, x_max=1024, y_max=1024, always_apply=False, p=1.0): - super(Crop, self).__init__(always_apply, p) - self.x_min = x_min - self.y_min = y_min - self.x_max = x_max - self.y_max = y_max - - def apply(self, img, **params): - return F.crop(img, x_min=self.x_min, y_min=self.y_min, x_max=self.x_max, y_max=self.y_max) - - def apply_to_bbox(self, bbox, **params): - return F.bbox_crop(bbox, x_min=self.x_min, y_min=self.y_min, x_max=self.x_max, y_max=self.y_max, **params) - - def apply_to_keypoint(self, keypoint, **params): - return F.crop_keypoint_by_coords( - keypoint, - crop_coords=(self.x_min, self.y_min, self.x_max, self.y_max), - crop_height=self.y_max - self.y_min, - crop_width=self.x_max - self.x_min, - rows=params["rows"], - cols=params["cols"], - ) - - def get_transform_init_args_names(self): - return ("x_min", "y_min", "x_max", "y_max") - - class VerticalFlip(DualTransform): """Flip the input vertically around the x-axis. @@ -361,464 +310,6 @@ def get_transform_init_args_names(self): return () -class CenterCrop(DualTransform): - """Crop the central part of the input. - - Args: - height (int): height of the crop. - width (int): width of the crop. - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - - Note: - It is recommended to use uint8 images as input. - Otherwise the operation will require internal conversion - float32 -> uint8 -> float32 that causes worse performance. - """ - - def __init__(self, height, width, always_apply=False, p=1.0): - super(CenterCrop, self).__init__(always_apply, p) - self.height = height - self.width = width - - def apply(self, img, **params): - return F.center_crop(img, self.height, self.width) - - def apply_to_bbox(self, bbox, **params): - return F.bbox_center_crop(bbox, self.height, self.width, **params) - - def apply_to_keypoint(self, keypoint, **params): - return F.keypoint_center_crop(keypoint, self.height, self.width, **params) - - def get_transform_init_args_names(self): - return ("height", "width") - - -class RandomCrop(DualTransform): - """Crop a random part of the input. - - Args: - height (int): height of the crop. - width (int): width of the crop. - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__(self, height, width, always_apply=False, p=1.0): - super(RandomCrop, self).__init__(always_apply, p) - self.height = height - self.width = width - - def apply(self, img, h_start=0, w_start=0, **params): - return F.random_crop(img, self.height, self.width, h_start, w_start) - - def get_params(self): - return {"h_start": random.random(), "w_start": random.random()} - - def apply_to_bbox(self, bbox, **params): - return F.bbox_random_crop(bbox, self.height, self.width, **params) - - def apply_to_keypoint(self, keypoint, **params): - return F.keypoint_random_crop(keypoint, self.height, self.width, **params) - - def get_transform_init_args_names(self): - return ("height", "width") - - -class RandomCropNearBBox(DualTransform): - """Crop bbox from image with random shift by x,y coordinates - - Args: - max_part_shift (float): float value in (0.0, 1.0) range. Default 0.3 - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__(self, max_part_shift=0.3, always_apply=False, p=1.0): - super(RandomCropNearBBox, self).__init__(always_apply, p) - self.max_part_shift = max_part_shift - - def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params): - return F.clamping_crop(img, x_min, y_min, x_max, y_max) - - def get_params_dependent_on_targets(self, params): - bbox = params["cropping_bbox"] - h_max_shift = int((bbox[3] - bbox[1]) * self.max_part_shift) - w_max_shift = int((bbox[2] - bbox[0]) * self.max_part_shift) - - x_min = bbox[0] - random.randint(-w_max_shift, w_max_shift) - x_max = bbox[2] + random.randint(-w_max_shift, w_max_shift) - - y_min = bbox[1] - random.randint(-h_max_shift, h_max_shift) - y_max = bbox[3] + random.randint(-h_max_shift, h_max_shift) - - return {"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max} - - def apply_to_bbox(self, bbox, x_min=0, x_max=0, y_min=0, y_max=0, **params): - h_start = y_min - w_start = x_min - return F.bbox_crop(bbox, y_max - y_min, x_max - x_min, h_start, w_start, **params) - - def apply_to_keypoint(self, keypoint, x_min=0, x_max=0, y_min=0, y_max=0, **params): - return F.crop_keypoint_by_coords( - keypoint, - crop_coords=(x_min, y_min, x_max, y_max), - crop_height=y_max - y_min, - crop_width=x_max - x_min, - rows=params["rows"], - cols=params["cols"], - ) - - @property - def targets_as_params(self): - return ["cropping_bbox"] - - def get_transform_init_args_names(self): - return ("max_part_shift",) - - -class _BaseRandomSizedCrop(DualTransform): - # Base class for RandomSizedCrop and RandomResizedCrop - - def __init__(self, height, width, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0): - super(_BaseRandomSizedCrop, self).__init__(always_apply, p) - self.height = height - self.width = width - self.interpolation = interpolation - - def apply(self, img, crop_height=0, crop_width=0, h_start=0, w_start=0, interpolation=cv2.INTER_LINEAR, **params): - crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) - return FGeometric.resize(crop, self.height, self.width, interpolation) - - def apply_to_bbox(self, bbox, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): - return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) - - def apply_to_keypoint(self, keypoint, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): - keypoint = F.keypoint_random_crop(keypoint, crop_height, crop_width, h_start, w_start, rows, cols) - scale_x = self.width / crop_width - scale_y = self.height / crop_height - keypoint = FGeometric.keypoint_scale(keypoint, scale_x, scale_y) - return keypoint - - -class RandomSizedCrop(_BaseRandomSizedCrop): - """Crop a random part of the input and rescale it to some size. - - Args: - min_max_height ((int, int)): crop size limits. - height (int): height after crop and resize. - width (int): width after crop and resize. - w2h_ratio (float): aspect ratio of crop. - 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. - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__( - self, min_max_height, height, width, w2h_ratio=1.0, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0 - ): - super(RandomSizedCrop, self).__init__( - height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p - ) - self.min_max_height = min_max_height - self.w2h_ratio = w2h_ratio - - def get_params(self): - crop_height = random.randint(self.min_max_height[0], self.min_max_height[1]) - return { - "h_start": random.random(), - "w_start": random.random(), - "crop_height": crop_height, - "crop_width": int(crop_height * self.w2h_ratio), - } - - def get_transform_init_args_names(self): - return "min_max_height", "height", "width", "w2h_ratio", "interpolation" - - -class RandomResizedCrop(_BaseRandomSizedCrop): - """Torchvision's variant of crop a random part of the input and rescale it to some size. - - Args: - height (int): height after crop and resize. - width (int): width after crop and resize. - scale ((float, float)): range of size of the origin size cropped - ratio ((float, float)): range of aspect ratio of the origin aspect ratio cropped - 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. - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__( - self, - height, - width, - scale=(0.08, 1.0), - ratio=(0.75, 1.3333333333333333), - interpolation=cv2.INTER_LINEAR, - always_apply=False, - p=1.0, - ): - - super(RandomResizedCrop, self).__init__( - height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p - ) - self.scale = scale - self.ratio = ratio - - def get_params_dependent_on_targets(self, params): - img = params["image"] - area = img.shape[0] * img.shape[1] - - for _attempt in range(10): - target_area = random.uniform(*self.scale) * area - log_ratio = (math.log(self.ratio[0]), math.log(self.ratio[1])) - aspect_ratio = math.exp(random.uniform(*log_ratio)) - - w = int(round(math.sqrt(target_area * aspect_ratio))) # skipcq: PTC-W0028 - h = int(round(math.sqrt(target_area / aspect_ratio))) # skipcq: PTC-W0028 - - if 0 < w <= img.shape[1] and 0 < h <= img.shape[0]: - i = random.randint(0, img.shape[0] - h) - j = random.randint(0, img.shape[1] - w) - return { - "crop_height": h, - "crop_width": w, - "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), - "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), - } - - # Fallback to central crop - in_ratio = img.shape[1] / img.shape[0] - if in_ratio < min(self.ratio): - w = img.shape[1] - h = int(round(w / min(self.ratio))) - elif in_ratio > max(self.ratio): - h = img.shape[0] - w = int(round(h * max(self.ratio))) - else: # whole image - w = img.shape[1] - h = img.shape[0] - i = (img.shape[0] - h) // 2 - j = (img.shape[1] - w) // 2 - return { - "crop_height": h, - "crop_width": w, - "h_start": i * 1.0 / (img.shape[0] - h + 1e-10), - "w_start": j * 1.0 / (img.shape[1] - w + 1e-10), - } - - def get_params(self): - return {} - - @property - def targets_as_params(self): - return ["image"] - - def get_transform_init_args_names(self): - return "height", "width", "scale", "ratio", "interpolation" - - -class RandomSizedBBoxSafeCrop(DualTransform): - """Crop a random part of the input and rescale it to some size without loss of bboxes. - - Args: - height (int): height after crop and resize. - width (int): width after crop and resize. - erosion_rate (float): erosion rate applied on input image height before crop. - 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. - p (float): probability of applying the transform. Default: 1. - - Targets: - image, mask, bboxes - - Image types: - uint8, float32 - """ - - def __init__(self, height, width, erosion_rate=0.0, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0): - super(RandomSizedBBoxSafeCrop, self).__init__(always_apply, p) - self.height = height - self.width = width - self.interpolation = interpolation - self.erosion_rate = erosion_rate - - def apply(self, img, crop_height=0, crop_width=0, h_start=0, w_start=0, interpolation=cv2.INTER_LINEAR, **params): - crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) - return FGeometric.resize(crop, self.height, self.width, interpolation) - - def get_params_dependent_on_targets(self, params): - img_h, img_w = params["image"].shape[:2] - if len(params["bboxes"]) == 0: # less likely, this class is for use with bboxes. - erosive_h = int(img_h * (1.0 - self.erosion_rate)) - crop_height = img_h if erosive_h >= img_h else random.randint(erosive_h, img_h) - return { - "h_start": random.random(), - "w_start": random.random(), - "crop_height": crop_height, - "crop_width": int(crop_height * img_w / img_h), - } - # get union of all bboxes - x, y, x2, y2 = union_of_bboxes( - width=img_w, height=img_h, bboxes=params["bboxes"], erosion_rate=self.erosion_rate - ) - # find bigger region - bx, by = x * random.random(), y * random.random() - bx2, by2 = x2 + (1 - x2) * random.random(), y2 + (1 - y2) * random.random() - bw, bh = bx2 - bx, by2 - by - crop_height = img_h if bh >= 1.0 else int(img_h * bh) - crop_width = img_w if bw >= 1.0 else int(img_w * bw) - h_start = np.clip(0.0 if bh >= 1.0 else by / (1.0 - bh), 0.0, 1.0) - w_start = np.clip(0.0 if bw >= 1.0 else bx / (1.0 - bw), 0.0, 1.0) - return {"h_start": h_start, "w_start": w_start, "crop_height": crop_height, "crop_width": crop_width} - - def apply_to_bbox(self, bbox, crop_height=0, crop_width=0, h_start=0, w_start=0, rows=0, cols=0, **params): - return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) - - @property - def targets_as_params(self): - return ["image", "bboxes"] - - def get_transform_init_args_names(self): - return ("height", "width", "erosion_rate", "interpolation") - - -class CropNonEmptyMaskIfExists(DualTransform): - """Crop area with mask if mask is non-empty, else make random crop. - - Args: - height (int): vertical size of crop in pixels - width (int): horizontal size of crop in pixels - ignore_values (list of int): values to ignore in mask, `0` values are always ignored - (e.g. if background value is 5 set `ignore_values=[5]` to ignore) - ignore_channels (list of int): channels to ignore in mask - (e.g. if background is a first channel set `ignore_channels=[0]` to ignore) - p (float): probability of applying the transform. Default: 1.0. - - Targets: - image, mask, bboxes, keypoints - - Image types: - uint8, float32 - """ - - def __init__(self, height, width, ignore_values=None, ignore_channels=None, always_apply=False, p=1.0): - super(CropNonEmptyMaskIfExists, self).__init__(always_apply, p) - - if ignore_values is not None and not isinstance(ignore_values, list): - raise ValueError("Expected `ignore_values` of type `list`, got `{}`".format(type(ignore_values))) - if ignore_channels is not None and not isinstance(ignore_channels, list): - raise ValueError("Expected `ignore_channels` of type `list`, got `{}`".format(type(ignore_channels))) - - self.height = height - self.width = width - self.ignore_values = ignore_values - self.ignore_channels = ignore_channels - - def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params): - return F.crop(img, x_min, y_min, x_max, y_max) - - def apply_to_bbox(self, bbox, x_min=0, x_max=0, y_min=0, y_max=0, **params): - return F.bbox_crop( - bbox, x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, rows=params["rows"], cols=params["cols"] - ) - - def apply_to_keypoint(self, keypoint, x_min=0, x_max=0, y_min=0, y_max=0, **params): - return F.crop_keypoint_by_coords( - keypoint, - crop_coords=[x_min, y_min, x_max, y_max], - crop_height=y_max - y_min, - crop_width=x_max - x_min, - rows=params["rows"], - cols=params["cols"], - ) - - def _preprocess_mask(self, mask): - mask_height, mask_width = mask.shape[:2] - - if self.ignore_values is not None: - ignore_values_np = np.array(self.ignore_values) - mask = np.where(np.isin(mask, ignore_values_np), 0, mask) - - if mask.ndim == 3 and self.ignore_channels is not None: - target_channels = np.array([ch for ch in range(mask.shape[-1]) if ch not in self.ignore_channels]) - mask = np.take(mask, target_channels, axis=-1) - - if self.height > mask_height or self.width > mask_width: - raise ValueError( - "Crop size ({},{}) is larger than image ({},{})".format( - self.height, self.width, mask_height, mask_width - ) - ) - - return mask - - def update_params(self, params, **kwargs): - if "mask" in kwargs: - mask = self._preprocess_mask(kwargs["mask"]) - elif "masks" in kwargs and len(kwargs["masks"]): - masks = kwargs["masks"] - mask = self._preprocess_mask(masks[0]) - for m in masks[1:]: - mask |= self._preprocess_mask(m) - else: - raise RuntimeError("Can not find mask for CropNonEmptyMaskIfExists") - - mask_height, mask_width = mask.shape[:2] - - if mask.any(): - mask = mask.sum(axis=-1) if mask.ndim == 3 else mask - non_zero_yx = np.argwhere(mask) - y, x = random.choice(non_zero_yx) - x_min = x - random.randint(0, self.width - 1) - y_min = y - random.randint(0, self.height - 1) - x_min = np.clip(x_min, 0, mask_width - self.width) - y_min = np.clip(y_min, 0, mask_height - self.height) - else: - x_min = random.randint(0, mask_width - self.width) - y_min = random.randint(0, mask_height - self.height) - - x_max = x_min + self.width - y_max = y_min + self.height - - params.update({"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max}) - return params - - def get_transform_init_args_names(self): - return ("height", "width", "ignore_values", "ignore_channels") - - class OpticalDistortion(DualTransform): """ Args: diff --git a/tests/test_functional.py b/tests/test_functional.py index 9d19cc89b..48073ae6b 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -5,6 +5,7 @@ import pytest from numpy.testing import assert_array_almost_equal_nulp +import albumentations as A import albumentations.augmentations.functional as F import albumentations.augmentations.geometric.functional as FGeometric from albumentations.augmentations.bbox_utils import filter_bboxes @@ -126,7 +127,7 @@ def test_center_crop(target): img = np.array([[1, 1, 1, 1], [0, 1, 1, 1], [0, 0, 1, 1], [0, 0, 0, 1]], dtype=np.uint8) expected = np.array([[1, 1], [0, 1]], dtype=np.uint8) img, expected = convert_2d_to_target_format([img, expected], target=target) - cropped_img = F.center_crop(img, 2, 2) + cropped_img = A.center_crop(img, 2, 2) assert np.array_equal(cropped_img, expected) @@ -137,14 +138,14 @@ def test_center_crop_float(target): ) expected = np.array([[0.4, 0.4], [0.0, 0.4]], dtype=np.float32) img, expected = convert_2d_to_target_format([img, expected], target=target) - cropped_img = F.center_crop(img, 2, 2) + cropped_img = A.center_crop(img, 2, 2) assert_array_almost_equal_nulp(cropped_img, expected) def test_center_crop_with_incorrectly_large_crop_size(): img = np.ones((4, 4), dtype=np.uint8) with pytest.raises(ValueError) as exc_info: - F.center_crop(img, 8, 8) + A.center_crop(img, 8, 8) assert str(exc_info.value) == "Requested crop size (8, 8) is larger than the image size (4, 4)" @@ -153,7 +154,7 @@ def test_random_crop(target): img = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], dtype=np.uint8) expected = np.array([[5, 6], [9, 10]], dtype=np.uint8) img, expected = convert_2d_to_target_format([img, expected], target=target) - cropped_img = F.random_crop(img, crop_height=2, crop_width=2, h_start=0.5, w_start=0) + cropped_img = A.random_crop(img, crop_height=2, crop_width=2, h_start=0.5, w_start=0) assert np.array_equal(cropped_img, expected) @@ -165,14 +166,14 @@ def test_random_crop_float(target): ) expected = np.array([[0.05, 0.06], [0.09, 0.10]], dtype=np.float32) img, expected = convert_2d_to_target_format([img, expected], target=target) - cropped_img = F.random_crop(img, crop_height=2, crop_width=2, h_start=0.5, w_start=0) + cropped_img = A.random_crop(img, crop_height=2, crop_width=2, h_start=0.5, w_start=0) assert_array_almost_equal_nulp(cropped_img, expected) def test_random_crop_with_incorrectly_large_crop_size(): img = np.ones((4, 4), dtype=np.uint8) with pytest.raises(ValueError) as exc_info: - F.random_crop(img, crop_height=8, crop_width=8, h_start=0, w_start=0) + A.random_crop(img, crop_height=8, crop_width=8, h_start=0, w_start=0) assert str(exc_info.value) == "Requested crop size (8, 8) is larger than the image size (4, 4)" @@ -604,22 +605,22 @@ def test_bbox_flip(code, func): def test_crop_bbox_by_coords(): - cropped_bbox = F.crop_bbox_by_coords((0.5, 0.2, 0.9, 0.7), (18, 18, 82, 82), 64, 64, 100, 100) + cropped_bbox = A.crop_bbox_by_coords((0.5, 0.2, 0.9, 0.7), (18, 18, 82, 82), 64, 64, 100, 100) assert cropped_bbox == (0.5, 0.03125, 1.125, 0.8125) def test_bbox_center_crop(): - cropped_bbox = F.bbox_center_crop((0.5, 0.2, 0.9, 0.7), 64, 64, 100, 100) + cropped_bbox = A.bbox_center_crop((0.5, 0.2, 0.9, 0.7), 64, 64, 100, 100) assert cropped_bbox == (0.5, 0.03125, 1.125, 0.8125) def test_bbox_crop(): - cropped_bbox = F.bbox_crop([0.5, 0.2, 0.9, 0.7], 24, 24, 64, 64, 100, 100) + cropped_bbox = A.bbox_crop([0.5, 0.2, 0.9, 0.7], 24, 24, 64, 64, 100, 100) assert cropped_bbox == (0.65, -0.1, 1.65, 1.15) def test_bbox_random_crop(): - cropped_bbox = F.bbox_random_crop([0.5, 0.2, 0.9, 0.7], 80, 80, 0.2, 0.1, 100, 100) + cropped_bbox = A.bbox_random_crop([0.5, 0.2, 0.9, 0.7], 80, 80, 0.2, 0.1, 100, 100) assert cropped_bbox == (0.6, 0.2, 1.1, 0.825) diff --git a/tests/test_keypoint.py b/tests/test_keypoint.py index a7aad0cbd..3da182da8 100644 --- a/tests/test_keypoint.py +++ b/tests/test_keypoint.py @@ -3,9 +3,18 @@ import numpy as np import pytest -import albumentations.augmentations.functional as F import albumentations.augmentations.geometric.functional as FGeometric -from albumentations import HorizontalFlip, VerticalFlip, IAAFliplr, IAAFlipud, CenterCrop +from albumentations import ( + HorizontalFlip, + VerticalFlip, + IAAFliplr, + IAAFlipud, + CenterCrop, + RandomSizedCrop, + RandomResizedCrop, + Compose, + NoOp, +) from albumentations.augmentations.keypoints_utils import ( convert_keypoint_from_albumentations, convert_keypoints_from_albumentations, @@ -13,9 +22,6 @@ convert_keypoints_to_albumentations, angle_to_2pi_range, ) -from albumentations.augmentations.transforms import RandomSizedCrop, RandomResizedCrop -from albumentations.core.composition import Compose -from albumentations.core.transforms_interface import NoOp @pytest.mark.parametrize(