diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..6a6724e4d --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,17 @@ +version = 1 + +test_patterns = [ + 'tests/**' +] + +exclude_patterns = [ + +] + +[[analyzers]] +name = 'python' +enabled = true +runtime_version = '3.x.x' + + [analyzers.meta] + max_line_length = 119 diff --git a/.gitignore b/.gitignore index c4a9db8fe..6efd32c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,5 @@ fabric.properties .idea conda_build/ + +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a1061418..77eca7ba8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,80 @@ +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + autofix_prs: true + autoupdate_branch: '' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: weekly + skip: [ ] + submodules: false + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: requirements-txt-fixer - - id: check-json - - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-merge-conflict - id: check-toml - id: check-xml + - id: debug-statements + - id: detect-private-key + - id: forbid-new-submodules + - id: forbid-submodules + - id: mixed-line-ending + - id: name-tests-test + - id: destroyed-symlinks + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: name-tests-test + - id: check-docstring-first + - id: pretty-format-json + - id: check-json + - id: check-merge-conflict + - id: check-yaml + args: [ --unsafe ] + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + - id: requirements-txt-fixer + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: ["--py38-plus"] - repo: https://github.com/pycqa/isort - rev: 5.11.5 + rev: 5.13.2 hooks: - id: isort args: [ "--profile", "black" ] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 24.2.0 hooks: - id: black - args: [ --config=black.toml ] + args: [ --config=pyproject.toml ] + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-mock-methods + - id: python-use-type-annotations + - id: python-check-blanket-noqa + - id: python-use-type-annotations + - id: text-unicode-replacement-char + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + exclude: ^setup.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: [ types-PyYAML, types-pkg-resources, types-setuptools ] @@ -29,9 +83,5 @@ repos: --ignore-missing-imports, --warn-no-return, --warn-redundant-casts, + --disallow-incomplete-defs, ] - - repo: https://github.com/PyCQA/flake8 - rev: 21d3c70d676007470908d39b73f0521d39b3b997 - hooks: - - id: flake8 - additional_dependencies: [ flake8-docstrings==1.6.0 ] diff --git a/README.md b/README.md index 0d9168613..788dfa684 100644 --- a/README.md +++ b/README.md @@ -17,24 +17,32 @@ Here is an example of how you can apply some [pixel-level](#pixel-level-transfor - The library is [**widely used**](#who-is-using-albumentations) in industry, deep learning research, machine learning competitions, and open source projects. ## Table of contents -- [Authors](#authors) -- [Installation](#installation) -- [Documentation](#documentation) -- [A simple example](#a-simple-example) -- [Getting started](#getting-started) - - [I am new to image augmentation](#i-am-new-to-image-augmentation) - - [I want to use Albumentations for the specific task such as classification or segmentation](#i-want-to-use-albumentations-for-the-specific-task-such-as-classification-or-segmentation) - - [I want to know how to use Albumentations with deep learning frameworks](#i-want-to-know-how-to-use-albumentations-with-deep-learning-frameworks) - - [I want to explore augmentations and see Albumentations in action](#i-want-to-explore-augmentations-and-see-albumentations-in-action) -- [Who is using Albumentations](#who-is-using-albumentations) -- [List of augmentations](#list-of-augmentations) - - [Pixel-level transforms](#pixel-level-transforms) - - [Spatial-level transforms](#spatial-level-transforms) -- [A few more examples of augmentations](#a-few-more-examples-of-augmentations) -- [Benchmarking results](#benchmarking-results) -- [Contributing](#contributing) -- [Comments](#comments) -- [Citing](#citing) +- [Albumentations](#albumentations) + - [Why Albumentations](#why-albumentations) + - [Table of contents](#table-of-contents) + - [Authors](#authors) + - [Installation](#installation) + - [Documentation](#documentation) + - [A simple example](#a-simple-example) + - [Getting started](#getting-started) + - [I am new to image augmentation](#i-am-new-to-image-augmentation) + - [I want to use Albumentations for the specific task such as classification or segmentation](#i-want-to-use-albumentations-for-the-specific-task-such-as-classification-or-segmentation) + - [I want to know how to use Albumentations with deep learning frameworks](#i-want-to-know-how-to-use-albumentations-with-deep-learning-frameworks) + - [I want to explore augmentations and see Albumentations in action](#i-want-to-explore-augmentations-and-see-albumentations-in-action) + - [Who is using Albumentations](#who-is-using-albumentations) + - [See also:](#see-also) + - [List of augmentations](#list-of-augmentations) + - [Pixel-level transforms](#pixel-level-transforms) + - [Spatial-level transforms](#spatial-level-transforms) + - [A few more examples of augmentations](#a-few-more-examples-of-augmentations) + - [Semantic segmentation on the Inria dataset](#semantic-segmentation-on-the-inria-dataset) + - [Medical imaging](#medical-imaging) + - [Object detection and semantic segmentation on the Mapillary Vistas dataset](#object-detection-and-semantic-segmentation-on-the-mapillary-vistas-dataset) + - [Keypoints augmentation](#keypoints-augmentation) + - [Benchmarking results](#benchmarking-results) + - [Contributing](#contributing) + - [Comments](#comments) + - [Citing](#citing) ## Authors [**Alexander Buslaev** — Computer Vision Engineer at Mapbox](https://www.linkedin.com/in/al-buslaev/) | [Kaggle Master](https://www.kaggle.com/albuslaev) diff --git a/albumentations/__init__.py b/albumentations/__init__.py index 7e9d0b803..760626a07 100644 --- a/albumentations/__init__.py +++ b/albumentations/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - __version__ = "1.3.1" from .augmentations import * diff --git a/albumentations/augmentations/blur/transforms.py b/albumentations/augmentations/blur/transforms.py index fa15808ba..ba32cec90 100644 --- a/albumentations/augmentations/blur/transforms.py +++ b/albumentations/augmentations/blur/transforms.py @@ -1,6 +1,6 @@ import random import warnings -from typing import Any, Dict, List, Sequence, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple, cast import cv2 import numpy as np @@ -8,12 +8,8 @@ from albumentations import random_utils from albumentations.augmentations import functional as FMain from albumentations.augmentations.blur import functional as F -from albumentations.core.transforms_interface import ( - ImageOnlyTransform, - ScaleFloatType, - ScaleIntType, - to_tuple, -) +from albumentations.core.transforms_interface import ImageOnlyTransform, to_tuple +from albumentations.core.types import ScaleFloatType, ScaleIntType __all__ = ["Blur", "MotionBlur", "GaussianBlur", "GlassBlur", "AdvancedBlur", "MedianBlur", "Defocus", "ZoomBlur"] @@ -22,9 +18,9 @@ class Blur(ImageOnlyTransform): """Blur the input image using a random-sized kernel. Args: - blur_limit (int, (int, int)): maximum kernel size for blurring the input image. + blur_limit: maximum kernel size for blurring the input image. Should be in range [3, inf). Default: (3, 7). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -35,9 +31,9 @@ class Blur(ImageOnlyTransform): def __init__(self, blur_limit: ScaleIntType = 7, always_apply: bool = False, p: float = 0.5): super().__init__(always_apply, p) - self.blur_limit = to_tuple(blur_limit, 3) + self.blur_limit = cast(Tuple[int, int], to_tuple(blur_limit, 3)) - def apply(self, img: np.ndarray, ksize: int = 3, **params) -> np.ndarray: + def apply(self, img: np.ndarray, ksize: int = 3, **params: Any) -> np.ndarray: return F.blur(img, ksize) def get_params(self) -> Dict[str, Any]: @@ -80,13 +76,13 @@ def __init__( def get_transform_init_args_names(self) -> Tuple[str, ...]: return super().get_transform_init_args_names() + ("allow_shifted",) - def apply(self, img: np.ndarray, kernel: np.ndarray = None, **params) -> np.ndarray: # type: ignore - return FMain.convolve(img, kernel=kernel) + def apply(self, img: np.ndarray, ksize: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + return FMain.convolve(img, kernel=ksize) def get_params(self) -> Dict[str, Any]: ksize = random.choice(list(range(self.blur_limit[0], self.blur_limit[1] + 1, 2))) if ksize <= 2: - raise ValueError("ksize must be > 2. Got: {}".format(ksize)) + raise ValueError(f"ksize must be > 2. Got: {ksize}") kernel = np.zeros((ksize, ksize), dtype=np.uint8) x1, x2 = random.randint(0, ksize - 1), random.randint(0, ksize - 1) if x1 == x2: @@ -94,7 +90,7 @@ def get_params(self) -> Dict[str, Any]: else: y1, y2 = random.randint(0, ksize - 1), random.randint(0, ksize - 1) - def make_odd_val(v1, v2): + def make_odd_val(v1: int, v2: int) -> Tuple[int, int]: len_v = abs(v1 - v2) + 1 if len_v % 2 != 1: if v2 > v1: @@ -113,8 +109,8 @@ def make_odd_val(v1, v2): center = ksize / 2 - 0.5 dx = xc - center dy = yc - center - x1, x2 = [int(i - dx) for i in [x1, x2]] - y1, y2 = [int(i - dy) for i in [y1, y2]] + x1, x2 = (int(i - dx) for i in [x1, x2]) + y1, y2 = (int(i - dy) for i in [y1, y2]) cv2.line(kernel, (x1, y1), (x2, y2), 1, thickness=1) @@ -143,7 +139,7 @@ def __init__(self, blur_limit: ScaleIntType = 7, always_apply: bool = False, p: if self.blur_limit[0] % 2 != 1 or self.blur_limit[1] % 2 != 1: raise ValueError("MedianBlur supports only odd blur limits.") - def apply(self, img: np.ndarray, ksize: int = 3, **params) -> np.ndarray: + def apply(self, img: np.ndarray, ksize: int = 3, **params: Any) -> np.ndarray: return F.median_blur(img, ksize) @@ -176,7 +172,7 @@ def __init__( p: float = 0.5, ): super().__init__(always_apply, p) - self.blur_limit = to_tuple(blur_limit, 0) + self.blur_limit = cast(Tuple[int, int], to_tuple(blur_limit, 0)) self.sigma_limit = to_tuple(sigma_limit if sigma_limit is not None else 0, 0) if self.blur_limit[0] == 0 and self.sigma_limit[0] == 0: @@ -191,7 +187,7 @@ def __init__( ): raise ValueError("GaussianBlur supports only odd blur limits.") - def apply(self, img: np.ndarray, ksize: int = 3, sigma: float = 0, **params) -> np.ndarray: + def apply(self, img: np.ndarray, ksize: int = 3, sigma: float = 0, **params: Any) -> np.ndarray: return F.gaussian_blur(img, ksize, sigma=sigma) def get_params(self) -> Dict[str, float]: @@ -258,7 +254,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, n # generate array containing all necessary values for transformations width_pixels = img.shape[0] - self.max_delta * 2 height_pixels = img.shape[1] - self.max_delta * 2 - total_pixels = width_pixels * height_pixels + total_pixels = int(width_pixels * height_pixels) dxy = random_utils.randint(-self.max_delta, self.max_delta, size=(total_pixels, self.iterations, 2)) return {"dxy": dxy} @@ -315,7 +311,7 @@ def __init__( p: float = 0.5, ): super().__init__(always_apply, p) - self.blur_limit = to_tuple(blur_limit, 3) + self.blur_limit = cast(Tuple[int, int], to_tuple(blur_limit, 3)) self.sigmaX_limit = self.__check_values(to_tuple(sigmaX_limit, 0.0), name="sigmaX_limit") self.sigmaY_limit = self.__check_values(to_tuple(sigmaY_limit, 0.0), name="sigmaY_limit") self.rotate_limit = to_tuple(rotate_limit) @@ -341,7 +337,7 @@ def __check_values( raise ValueError(f"{name} values should be between {bounds}") return value - def apply(self, img: np.ndarray, kernel: np.ndarray = np.array(None), **params) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: np.ndarray = np.array(None), **params: Any) -> np.ndarray: return FMain.convolve(img, kernel=kernel) def get_params(self) -> Dict[str, np.ndarray]: @@ -372,7 +368,7 @@ def get_params(self) -> Dict[str, np.ndarray]: # Described in "Parameter Estimation For Multivariate Generalized Gaussian Distributions" kernel = np.exp(-0.5 * np.power(np.sum(np.dot(grid, inverse_sigma) * grid, 2), beta)) # Add noise - kernel = kernel * noise_matrix + kernel *= noise_matrix # Normalize kernel kernel = kernel.astype(np.float32) / np.sum(kernel) @@ -424,7 +420,7 @@ def __init__( if self.alias_blur[0] < 0: raise ValueError("Parameter alias_blur must be non-negative") - def apply(self, img: np.ndarray, radius: int = 3, alias_blur: float = 0.5, **params) -> np.ndarray: + def apply(self, img: np.ndarray, radius: int = 3, alias_blur: float = 0.5, **params: Any) -> np.ndarray: return F.defocus(img, radius, alias_blur) def get_params(self) -> Dict[str, Any]: @@ -473,7 +469,7 @@ def __init__( if self.step_factor[0] <= 0: raise ValueError("Step factor must be positive") - def apply(self, img: np.ndarray, zoom_factors: np.ndarray = np.array(None), **params) -> np.ndarray: + def apply(self, img: np.ndarray, zoom_factors: np.ndarray = np.array(None), **params: Any) -> np.ndarray: assert zoom_factors is not None return F.zoom_blur(img, zoom_factors) diff --git a/albumentations/augmentations/crops/functional.py b/albumentations/augmentations/crops/functional.py index 18a43d949..249bc7bae 100644 --- a/albumentations/augmentations/crops/functional.py +++ b/albumentations/augmentations/crops/functional.py @@ -1,4 +1,4 @@ -from typing import Optional, Sequence, Tuple +from typing import Optional, Sequence, Tuple, cast import cv2 import numpy as np @@ -9,7 +9,7 @@ ) from ...core.bbox_utils import denormalize_bbox, normalize_bbox -from ...core.transforms_interface import BoxInternalType, KeypointInternalType +from ...core.types import BoxInternalType, KeypointInternalType from ..geometric import functional as FGeometric __all__ = [ @@ -32,7 +32,9 @@ ] -def get_random_crop_coords(height: int, width: int, crop_height: int, crop_width: int, h_start: float, w_start: float): +def get_random_crop_coords( + height: int, width: int, crop_height: int, crop_width: int, h_start: float, w_start: float +) -> Tuple[int, int, int, int]: # 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))` # See: https://github.com/albumentations-team/albumentations/pull/1080 @@ -43,7 +45,7 @@ def get_random_crop_coords(height: int, width: int, crop_height: int, 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): +def random_crop(img: np.ndarray, crop_height: int, crop_width: int, h_start: float, w_start: float) -> np.ndarray: height, width = img.shape[:2] if height < crop_height or width < crop_width: raise ValueError( @@ -53,8 +55,7 @@ def random_crop(img: np.ndarray, crop_height: int, crop_width: int, h_start: flo ) ) 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 + return img[y1:y2, x1:x2] def crop_bbox_by_coords( @@ -64,39 +65,39 @@ def crop_bbox_by_coords( crop_width: int, rows: int, cols: int, -): +) -> BoxInternalType: """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. + bbox: A cropped box `(x_min, y_min, x_max, y_max)`. + crop_coords: Crop coordinates `(x1, y1, x2, y2)`. + crop_height: + crop_width: + rows: Image rows. + cols: Image cols. Returns: - tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. + 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 + normalized_bbox = denormalize_bbox(bbox, rows, cols) + x_min, y_min, x_max, y_max = normalized_bbox[:4] + x1, y1 = crop_coords[:2] cropped_bbox = x_min - x1, y_min - y1, x_max - x1, y_max - y1 - return normalize_bbox(cropped_bbox, crop_height, crop_width) + return cast(BoxInternalType, normalize_bbox(cropped_bbox, crop_height, crop_width)) def bbox_random_crop( bbox: BoxInternalType, crop_height: int, crop_width: int, h_start: float, w_start: float, rows: int, cols: int -): +) -> BoxInternalType: 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: KeypointInternalType, crop_coords: Tuple[int, int, int, int] -): # skipcq: PYL-W0613 +) -> KeypointInternalType: """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. @@ -121,7 +122,7 @@ def keypoint_random_crop( w_start: float, rows: int, cols: int, -): +) -> KeypointInternalType: """Keypoint random crop. Args: @@ -141,7 +142,7 @@ def keypoint_random_crop( return crop_keypoint_by_coords(keypoint, crop_coords) -def get_center_crop_coords(height: int, width: int, crop_height: int, crop_width: int): +def get_center_crop_coords(height: int, width: int, crop_height: int, crop_width: int) -> Tuple[int, int, int, int]: y1 = (height - crop_height) // 2 y2 = y1 + crop_height x1 = (width - crop_width) // 2 @@ -149,7 +150,7 @@ def get_center_crop_coords(height: int, width: int, crop_height: int, crop_width return x1, y1, x2, y2 -def center_crop(img: np.ndarray, crop_height: int, crop_width: int): +def center_crop(img: np.ndarray, crop_height: int, crop_width: int) -> np.ndarray: height, width = img.shape[:2] if height < crop_height or width < crop_width: raise ValueError( @@ -159,34 +160,35 @@ def center_crop(img: np.ndarray, crop_height: int, crop_width: int): ) ) x1, y1, x2, y2 = get_center_crop_coords(height, width, crop_height, crop_width) - img = img[y1:y2, x1:x2] - return img + return img[y1:y2, x1:x2] -def bbox_center_crop(bbox: BoxInternalType, crop_height: int, crop_width: int, rows: int, cols: int): +def bbox_center_crop(bbox: BoxInternalType, crop_height: int, crop_width: int, rows: int, cols: int) -> BoxInternalType: 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: KeypointInternalType, crop_height: int, crop_width: int, rows: int, cols: int): +def keypoint_center_crop( + keypoint: KeypointInternalType, crop_height: int, crop_width: int, rows: int, cols: int +) -> KeypointInternalType: """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. + keypoint: A keypoint `(x, y, angle, scale)`. + crop_height: Crop height. + crop_width: Crop width. + rows: Image height. + cols: Image width. Returns: - tuple: A keypoint `(x, y, angle, scale)`. + 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): +def crop(img: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int) -> np.ndarray: height, width = img.shape[:2] if x_max <= x_min or y_max <= y_min: raise ValueError( @@ -208,20 +210,22 @@ def crop(img: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int): return img[y_min:y_max, x_min:x_max] -def bbox_crop(bbox: BoxInternalType, x_min: int, y_min: int, x_max: int, y_max: int, rows: int, cols: int): +def bbox_crop( + bbox: BoxInternalType, x_min: int, y_min: int, x_max: int, y_max: int, rows: int, cols: int +) -> BoxInternalType: """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. + bbox: A bounding box `(x_min, y_min, x_max, y_max)`. + x_min: + y_min: + x_max: + y_max: + rows: Image rows. + cols: Image cols. Returns: - tuple: A cropped bounding box `(x_min, y_min, x_max, y_max)`. + A cropped bounding box `(x_min, y_min, x_max, y_max)`. """ crop_coords = x_min, y_min, x_max, y_max @@ -230,7 +234,7 @@ def bbox_crop(bbox: BoxInternalType, x_min: int, y_min: int, x_max: int, y_max: 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): +def clamping_crop(img: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int) -> np.ndarray: h, w = img.shape[:2] if x_min < 0: x_min = 0 @@ -264,7 +268,7 @@ def crop_and_pad( if keep_size: resize_fn = _maybe_process_in_chunks(cv2.resize, dsize=(cols, rows), interpolation=interpolation) - img = resize_fn(img) + return resize_fn(img) return img @@ -273,21 +277,31 @@ def crop_and_pad_bbox( bbox: BoxInternalType, crop_params: Optional[Sequence[int]], pad_params: Optional[Sequence[int]], - rows, - cols, - result_rows, - result_cols, + rows: int, + cols: int, + result_rows: int, + result_cols: int, ) -> BoxInternalType: x1, y1, x2, y2 = denormalize_bbox(bbox, rows, cols)[:4] if crop_params is not None: crop_x, crop_y = crop_params[:2] - x1, y1, x2, y2 = x1 - crop_x, y1 - crop_y, x2 - crop_x, y2 - crop_y + + x1 -= crop_x + y1 -= crop_y + x2 -= crop_x + y2 -= crop_y + if pad_params is not None: - top, bottom, left, right = pad_params - x1, y1, x2, y2 = x1 + left, y1 + top, x2 + left, y2 + top + top = pad_params[0] + left = pad_params[2] - return normalize_bbox((x1, y1, x2, y2), result_rows, result_cols) + x1 += left + y1 += top + x2 += left + y2 += top + + return cast(BoxInternalType, normalize_bbox((x1, y1, x2, y2), result_rows, result_cols)) def crop_and_pad_keypoint( @@ -303,11 +317,14 @@ def crop_and_pad_keypoint( x, y, angle, scale = keypoint[:4] if crop_params is not None: - crop_x1, crop_y1, crop_x2, crop_y2 = crop_params + crop_x1, crop_y1 = crop_params[:2] x, y = x - crop_x1, y - crop_y1 if pad_params is not None: - top, bottom, left, right = pad_params - x, y = x + left, y + top + top = pad_params[0] + left = pad_params[2] + + x += left + y += top if keep_size and (result_cols != cols or result_rows != rows): scale_x = cols / result_cols diff --git a/albumentations/augmentations/crops/transforms.py b/albumentations/augmentations/crops/transforms.py index c3b3f33bd..566c75c67 100644 --- a/albumentations/augmentations/crops/transforms.py +++ b/albumentations/augmentations/crops/transforms.py @@ -7,11 +7,12 @@ from albumentations.core.bbox_utils import union_of_bboxes -from ...core.transforms_interface import ( +from ...core.transforms_interface import DualTransform, to_tuple +from ...core.types import ( BoxInternalType, - DualTransform, KeypointInternalType, - to_tuple, + ScaleFloatType, + ScaleIntType, ) from ..geometric import functional as FGeometric from . import functional as F @@ -35,9 +36,9 @@ 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. + height: height of the crop. + width: width of the crop. + p: probability of applying the transform. Default: 1. Targets: image, mask, bboxes, keypoints @@ -46,24 +47,24 @@ class RandomCrop(DualTransform): uint8, float32 """ - def __init__(self, height, width, always_apply=False, p=1.0): + def __init__(self, height: int, width: int, always_apply: bool = False, p: float = 1.0): super().__init__(always_apply, p) self.height = height self.width = width - def apply(self, img, h_start=0, w_start=0, **params): + def apply(self, img: np.ndarray, h_start: int = 0, w_start: int = 0, **params: Any) -> np.ndarray: return F.random_crop(img, self.height, self.width, h_start, w_start) - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"h_start": random.random(), "w_start": random.random()} - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_random_crop(bbox, self.height, self.width, **params) - def apply_to_keypoint(self, keypoint, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_random_crop(keypoint, self.height, self.width, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("height", "width") @@ -71,9 +72,9 @@ 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. + height: height of the crop. + width: width of the crop. + p: probability of applying the transform. Default: 1. Targets: image, mask, bboxes, keypoints @@ -87,21 +88,21 @@ class CenterCrop(DualTransform): 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) + def __init__(self, height: int, width: int, always_apply: bool = False, p: float = 1.0): + super().__init__(always_apply, p) self.height = height self.width = width - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.center_crop(img, self.height, self.width) - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_center_crop(bbox, self.height, self.width, **params) - def apply_to_keypoint(self, keypoint, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_center_crop(keypoint, self.height, self.width, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("height", "width") @@ -109,10 +110,10 @@ 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. + x_min: Minimum upper left x coordinate. + y_min: Minimum upper left y coordinate. + x_max: Maximum lower right x coordinate. + y_max: Maximum lower right y coordinate. Targets: image, mask, bboxes, keypoints @@ -121,23 +122,31 @@ class Crop(DualTransform): 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) + def __init__( + self, + x_min: int = 0, + y_min: int = 0, + x_max: int = 1024, + y_max: int = 1024, + always_apply: bool = False, + p: float = 1.0, + ): + super().__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): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: 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): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: 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): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: 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): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: return ("x_min", "y_min", "x_max", "y_max") @@ -145,13 +154,13 @@ 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 + height: vertical size of crop in pixels + width: 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. + p: probability of applying the transform. Default: 1.0. Targets: image, mask, bboxes, keypoints @@ -160,31 +169,51 @@ class CropNonEmptyMaskIfExists(DualTransform): 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) + def __init__( + self, + height: int, + width: int, + ignore_values: Optional[List[int]] = None, + ignore_channels: Optional[List[int]] = None, + always_apply: bool = False, + p: float = 1.0, + ): + super().__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))) + raise ValueError(f"Expected `ignore_values` of type `list`, got `{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))) + raise ValueError(f"Expected `ignore_channels` of type `list`, got `{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): + def apply( + self, img: np.ndarray, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any + ) -> np.ndarray: 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): + def apply_to_bbox( + self, bbox: BoxInternalType, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any + ) -> BoxInternalType: 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): + def apply_to_keypoint( + self, + keypoint: KeypointInternalType, + x_min: int = 0, + x_max: int = 0, + y_min: int = 0, + y_max: int = 0, + **params: Any, + ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) - def _preprocess_mask(self, mask): + def _preprocess_mask(self, mask: np.ndarray) -> np.ndarray: mask_height, mask_width = mask.shape[:2] if self.ignore_values is not None: @@ -204,7 +233,7 @@ def _preprocess_mask(self, mask): return mask - def update_params(self, params, **kwargs): + def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: super().update_params(params, **kwargs) if "mask" in kwargs: mask = self._preprocess_mask(kwargs["mask"]) @@ -236,32 +265,62 @@ def update_params(self, params, **kwargs): 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): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: 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) + def __init__( + self, height: int, width: int, interpolation: int = cv2.INTER_LINEAR, always_apply: bool = False, p: float = 1.0 + ): + super().__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): + def apply( + self, + img: np.ndarray, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + interpolation: int = cv2.INTER_LINEAR, + **params: Any, + ) -> np.ndarray: 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): + def apply_to_bbox( + self, + bbox: BoxInternalType, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> BoxInternalType: 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): + def apply_to_keypoint( + self, + keypoint: KeypointInternalType, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> KeypointInternalType: 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 + return FGeometric.keypoint_scale(keypoint, scale_x, scale_y) class RandomSizedCrop(_BaseRandomSizedCrop): @@ -285,15 +344,20 @@ class RandomSizedCrop(_BaseRandomSizedCrop): """ def __init__( - self, min_max_height, height, width, w2h_ratio=1.0, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1.0 + self, + min_max_height: Tuple[int, int], + height: int, + width: int, + w2h_ratio: float = 1.0, + interpolation: int = cv2.INTER_LINEAR, + always_apply: bool = False, + p: float = 1.0, ): - super(RandomSizedCrop, self).__init__( - height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p - ) + super().__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): + def get_params(self) -> Dict[str, Union[int, float]]: crop_height = random.randint(self.min_max_height[0], self.min_max_height[1]) return { "h_start": random.random(), @@ -302,7 +366,7 @@ def get_params(self): "crop_width": int(crop_height * self.w2h_ratio), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str, str]: return "min_max_height", "height", "width", "w2h_ratio", "interpolation" @@ -328,70 +392,68 @@ class RandomResizedCrop(_BaseRandomSizedCrop): 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, + height: int, + width: int, + scale: Tuple[float, float] = (0.08, 1.0), + ratio: Tuple[float, float] = (0.75, 1.3333333333333333), + interpolation: int = cv2.INTER_LINEAR, + always_apply: bool = False, + p: float = 1.0, ): - super(RandomResizedCrop, self).__init__( - height=height, width=width, interpolation=interpolation, always_apply=always_apply, p=p - ) + super().__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): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Union[int, float]]: img = params["image"] area = img.shape[0] * img.shape[1] - for _attempt in range(10): + for _ 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 + width = int(round(math.sqrt(target_area * aspect_ratio))) + height = int(round(math.sqrt(target_area / aspect_ratio))) - 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) + if 0 < width <= img.shape[1] and 0 < height <= img.shape[0]: + i = random.randint(0, img.shape[0] - height) + j = random.randint(0, img.shape[1] - width) 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), + "crop_height": height, + "crop_width": width, + "h_start": i * 1.0 / (img.shape[0] - height + 1e-10), + "w_start": j * 1.0 / (img.shape[1] - width + 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))) + width = img.shape[1] + height = int(round(width / min(self.ratio))) elif in_ratio > max(self.ratio): - h = img.shape[0] - w = int(round(h * max(self.ratio))) + height = img.shape[0] + width = int(round(height * 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 + width = img.shape[1] + height = img.shape[0] + i = (img.shape[0] - height) // 2 + j = (img.shape[1] - width) // 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), + "crop_height": height, + "crop_width": width, + "h_start": i * 1.0 / (img.shape[0] - height + 1e-10), + "w_start": j * 1.0 / (img.shape[1] - width + 1e-10), } - def get_params(self): + def get_params(self) -> Dict[str, Any]: return {} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str, str]: return "height", "width", "scale", "ratio", "interpolation" @@ -421,20 +483,20 @@ class RandomCropNearBBox(DualTransform): def __init__( self, - max_part_shift: Union[float, Tuple[float, float]] = (0.3, 0.3), + max_part_shift: ScaleFloatType = (0.3, 0.3), cropping_box_key: str = "cropping_bbox", always_apply: bool = False, p: float = 1.0, ): - super(RandomCropNearBBox, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.max_part_shift = to_tuple(max_part_shift, low=max_part_shift) self.cropping_bbox_key = cropping_box_key if min(self.max_part_shift) < 0 or max(self.max_part_shift) > 1: - raise ValueError("Invalid max_part_shift. Got: {}".format(max_part_shift)) + raise ValueError(f"Invalid max_part_shift. Got: {max_part_shift}") def apply( - self, img: np.ndarray, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params + self, img: np.ndarray, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any ) -> np.ndarray: return F.clamping_crop(img, x_min, y_min, x_max, y_max) @@ -454,18 +516,18 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, i return {"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max} - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_crop(bbox, **params) def apply_to_keypoint( self, - keypoint: Tuple[float, float, float, float], + keypoint: KeypointInternalType, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, - **params - ) -> Tuple[float, float, float, float]: + **params: Any, + ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) @property @@ -479,22 +541,30 @@ def get_transform_init_args_names(self) -> Tuple[str]: class BBoxSafeRandomCrop(DualTransform): """Crop a random part of the input without loss of bboxes. Args: - erosion_rate (float): erosion rate applied on input image height before crop. - p (float): probability of applying the transform. Default: 1. + erosion_rate: erosion rate applied on input image height before crop. + p: probability of applying the transform. Default: 1. Targets: image, mask, bboxes Image types: uint8, float32 """ - def __init__(self, erosion_rate=0.0, always_apply=False, p=1.0): - super(BBoxSafeRandomCrop, self).__init__(always_apply, p) + def __init__(self, erosion_rate: float = 0.0, always_apply: bool = False, p: float = 1.0): + super().__init__(always_apply, p) self.erosion_rate = erosion_rate - def apply(self, img, crop_height=0, crop_width=0, h_start=0, w_start=0, **params): + def apply( + self, + img: np.ndarray, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + **params: Any, + ) -> np.ndarray: return F.random_crop(img, crop_height, crop_width, h_start, w_start) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Union[int, float]]: 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)) @@ -519,23 +589,33 @@ def get_params_dependent_on_targets(self, params): 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): + def apply_to_bbox( + self, + bbox: BoxInternalType, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> BoxInternalType: return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image", "bboxes"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("erosion_rate",) class RandomSizedBBoxSafeCrop(BBoxSafeRandomCrop): """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. + height: height after crop and resize. + width: width after crop and resize. + erosion_rate: 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. @@ -546,17 +626,34 @@ class RandomSizedBBoxSafeCrop(BBoxSafeRandomCrop): 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__(erosion_rate, always_apply, p) + def __init__( + self, + height: int, + width: int, + erosion_rate: float = 0.0, + interpolation: int = cv2.INTER_LINEAR, + always_apply: bool = False, + p: float = 1.0, + ): + super().__init__(erosion_rate, 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): + def apply( + self, + img: np.ndarray, + crop_height: int = 0, + crop_width: int = 0, + h_start: int = 0, + w_start: int = 0, + interpolation: int = cv2.INTER_LINEAR, + **params: Any, + ) -> np.ndarray: crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) return FGeometric.resize(crop, self.height, self.width, interpolation) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return super().get_transform_init_args_names() + ("height", "width", "interpolation") @@ -649,8 +746,8 @@ class CropAndPad(DualTransform): def __init__( self, - px: Optional[Union[int, Sequence[float], Sequence[Tuple]]] = None, - percent: Optional[Union[float, Sequence[float], Sequence[Tuple]]] = None, + px: Optional[Union[int, List[int]]] = None, + percent: Optional[Union[float, List[float]]] = None, pad_mode: int = cv2.BORDER_CONSTANT, pad_cval: Union[float, Sequence[float]] = 0, pad_cval_mask: Union[float, Sequence[float]] = 0, @@ -688,7 +785,7 @@ def apply( rows: int = 0, cols: int = 0, interpolation: int = cv2.INTER_LINEAR, - **params + **params: Any, ) -> np.ndarray: return F.crop_and_pad( img, crop_params, pad_params, pad_value, rows, cols, interpolation, self.pad_mode, self.keep_size @@ -696,17 +793,17 @@ def apply( def apply_to_mask( self, - img: np.ndarray, + mask: np.ndarray, crop_params: Optional[Sequence[int]] = None, pad_params: Optional[Sequence[int]] = None, pad_value_mask: Optional[float] = None, rows: int = 0, cols: int = 0, interpolation: int = cv2.INTER_NEAREST, - **params + **params: Any, ) -> np.ndarray: return F.crop_and_pad( - img, crop_params, pad_params, pad_value_mask, rows, cols, interpolation, self.pad_mode, self.keep_size + mask, crop_params, pad_params, pad_value_mask, rows, cols, interpolation, self.pad_mode, self.keep_size ) def apply_to_bbox( @@ -718,7 +815,7 @@ def apply_to_bbox( cols: int = 0, result_rows: int = 0, result_cols: int = 0, - **params + **params: Any, ) -> BoxInternalType: return F.crop_and_pad_bbox(bbox, crop_params, pad_params, rows, cols, result_rows, result_cols) @@ -731,7 +828,7 @@ def apply_to_keypoint( cols: int = 0, result_rows: int = 0, result_cols: int = 0, - **params + **params: Any, ) -> KeypointInternalType: return F.crop_and_pad_keypoint( keypoint, crop_params, pad_params, rows, cols, result_rows, result_cols, self.keep_size @@ -764,7 +861,7 @@ def __prevent_zero(val1: int, val2: int, max_val: int) -> Tuple[int, int]: return val1, val2 @staticmethod - def _prevent_zero(crop_params: List[int], height: int, width: int) -> Sequence[int]: + def _prevent_zero(crop_params: List[int], height: int, width: int) -> List[int]: top, right, bottom, left = crop_params remaining_height = height - (top + bottom) @@ -777,21 +874,23 @@ def _prevent_zero(crop_params: List[int], height: int, width: int) -> Sequence[i return [max(top, 0), max(right, 0), max(bottom, 0), max(left, 0)] - def get_params_dependent_on_targets(self, params) -> dict: + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: height, width = params["image"].shape[:2] if self.px is not None: - params = self._get_px_params() + new_params = self._get_px_params() else: - params = self._get_percent_params() - params[0] = int(params[0] * height) - params[1] = int(params[1] * width) - params[2] = int(params[2] * height) - params[3] = int(params[3] * width) + percent_params = self._get_percent_params() + new_params = [ + int(percent_params[0] * height), + int(percent_params[1] * width), + int(percent_params[2] * height), + int(percent_params[3] * width), + ] - pad_params = [max(i, 0) for i in params] + pad_params = [max(i, 0) for i in new_params] - crop_params = self._prevent_zero([-min(i, 0) for i in params], height, width) + crop_params = self._prevent_zero([-min(i, 0) for i in new_params], height, width) top, right, bottom, left = crop_params crop_params = [left, top, width - right, height - bottom] @@ -830,9 +929,11 @@ def _get_px_params(self) -> List[int]: px = random.randrange(*self.px) params = [px] * 4 else: - params = [i if isinstance(i, int) else random.randrange(*i) for i in self.px] # type: ignore + if isinstance(self.px[0], int): + return self.px + return [random.randrange(*i) for i in self.px] - return params # [top, right, bottom, left] + return params def _get_percent_params(self) -> List[float]: if self.percent is None: @@ -847,13 +948,15 @@ def _get_percent_params(self) -> List[float]: px = random.uniform(*self.percent) params = [px] * 4 else: - params = [i if isinstance(i, (int, float)) else random.uniform(*i) for i in self.percent] + if isinstance(self.percent[0], (int, float)): + return self.percent + return [random.uniform(*i) for i in self.percent] return params # params = [top, right, bottom, left] @staticmethod def _get_pad_value(pad_value: Union[float, Sequence[float]]) -> Union[int, float]: - if isinstance(pad_value, (int, float)): + if isinstance(pad_value, float): return pad_value if len(pad_value) == 2: @@ -901,20 +1004,20 @@ class RandomCropFromBorders(DualTransform): def __init__( self, - crop_left=0.1, - crop_right=0.1, - crop_top=0.1, - crop_bottom=0.1, - always_apply=False, - p=1.0, + crop_left: float = 0.1, + crop_right: float = 0.1, + crop_top: float = 0.1, + crop_bottom: float = 0.1, + always_apply: bool = False, + p: float = 1.0, ): - super(RandomCropFromBorders, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.crop_left = crop_left self.crop_right = crop_right self.crop_top = crop_top self.crop_bottom = crop_bottom - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, int]: img = params["image"] x_min = random.randint(0, int(self.crop_left * img.shape[1])) x_max = random.randint(max(x_min + 1, int((1 - self.crop_right) * img.shape[1])), img.shape[1]) @@ -922,22 +1025,36 @@ def get_params_dependent_on_targets(self, params): y_max = random.randint(max(y_min + 1, int((1 - self.crop_bottom) * img.shape[0])), img.shape[0]) return {"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max} - def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params): + def apply( + self, img: np.ndarray, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any + ) -> np.ndarray: return F.clamping_crop(img, x_min, y_min, x_max, y_max) - def apply_to_mask(self, mask, x_min=0, x_max=0, y_min=0, y_max=0, **params): + def apply_to_mask( + self, mask: np.ndarray, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any + ) -> np.ndarray: return F.clamping_crop(mask, 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): + def apply_to_bbox( + self, bbox: BoxInternalType, x_min: int = 0, x_max: int = 0, y_min: int = 0, y_max: int = 0, **params: Any + ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] return F.bbox_crop(bbox, x_min, y_min, x_max, y_max, rows, cols) - def apply_to_keypoint(self, keypoint, x_min=0, x_max=0, y_min=0, y_max=0, **params): + def apply_to_keypoint( + self, + keypoint: KeypointInternalType, + x_min: int = 0, + x_max: int = 0, + y_min: int = 0, + y_max: int = 0, + **params: Any, + ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return "crop_left", "crop_right", "crop_top", "crop_bottom" diff --git a/albumentations/augmentations/domain_adaptation.py b/albumentations/augmentations/domain_adaptation.py index 3b77b3d59..c38a58495 100644 --- a/albumentations/augmentations/domain_adaptation.py +++ b/albumentations/augmentations/domain_adaptation.py @@ -1,5 +1,5 @@ import random -from typing import Any, Callable, Literal, Sequence, Tuple +from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple import cv2 import numpy as np @@ -17,7 +17,8 @@ read_rgb_image, ) -from ..core.transforms_interface import ImageOnlyTransform, ScaleFloatType, to_tuple +from ..core.transforms_interface import ImageOnlyTransform, to_tuple +from ..core.types import ScaleFloatType __all__ = [ "HistogramMatching", @@ -97,7 +98,7 @@ def apply_histogram(img: np.ndarray, reference_image: np.ndarray, blend_ratio: f matched = match_histograms(img, reference_image, channel_axis=2 if len(img.shape) == 3 else None) except TypeError: matched = match_histograms(img, reference_image, multichannel=True) # case for scikit-image<0.19.1 - img = cv2.addWeighted( + return cv2.addWeighted( matched, blend_ratio, img, @@ -105,7 +106,6 @@ def apply_histogram(img: np.ndarray, reference_image: np.ndarray, blend_ratio: f 0, dtype=get_opencv_dtype_from_numpy(img.dtype), ) - return img @preserve_shape @@ -162,19 +162,25 @@ def __init__( self.read_fn = read_fn self.blend_ratio = blend_ratio - def apply(self, img, reference_image=None, blend_ratio=0.5, **params): + def apply( + self: np.ndarray, + img: np.ndarray, + reference_image: Optional[np.ndarray] = None, + blend_ratio: float = 0.5, + **params: Any, + ) -> np.ndarray: return apply_histogram(img, reference_image, blend_ratio) - def get_params(self): + def get_params(self) -> Dict[str, np.ndarray]: return { "reference_image": self.read_fn(random.choice(self.reference_images)), "blend_ratio": random.uniform(self.blend_ratio[0], self.blend_ratio[1]), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("reference_images", "blend_ratio", "read_fn") - def _to_dict(self): + def _to_dict(self) -> Dict[str, Any]: raise NotImplementedError("HistogramMatching can not be serialized.") @@ -212,38 +218,40 @@ class FDA(ImageOnlyTransform): def __init__( self, - reference_images: Sequence[Any], + reference_images: Sequence[np.ndarray], beta_limit: ScaleFloatType = 0.1, read_fn: Callable[[Any], np.ndarray] = read_rgb_image, always_apply: bool = False, p: float = 0.5, ): - super(FDA, self).__init__(always_apply=always_apply, p=p) + super().__init__(always_apply=always_apply, p=p) self.reference_images = reference_images self.read_fn = read_fn self.beta_limit = to_tuple(beta_limit, low=0) - def apply(self, img, target_image=None, beta=0.1, **params): - return fourier_domain_adaptation(img=img, target_img=target_image, beta=beta) + def apply( + self, img: np.ndarray, target_image: Optional[np.ndarray] = None, beta: float = 0.1, **params: Any + ) -> np.ndarray: + return fourier_domain_adaptation(img, target_image, beta) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, np.ndarray]: img = params["image"] target_img = self.read_fn(random.choice(self.reference_images)) target_img = cv2.resize(target_img, dsize=(img.shape[1], img.shape[0])) return {"target_image": target_img} - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"beta": random.uniform(self.beta_limit[0], self.beta_limit[1])} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): - return ("reference_images", "beta_limit", "read_fn") + def get_transform_init_args_names(self) -> Tuple[str, str, str]: + return "reference_images", "beta_limit", "read_fn" - def _to_dict(self): + def _to_dict(self) -> Dict[str, Any]: raise NotImplementedError("FDA can not be serialized.") @@ -291,7 +299,7 @@ def __init__( self.transform_type = transform_type @staticmethod - def _validate_shape(img: np.ndarray): + def _validate_shape(img: np.ndarray) -> None: if is_grayscale_image(img) or is_multispectral_image(img): raise ValueError( f"Unexpected image shape: expected 3 dimensions, got {len(img.shape)}." @@ -309,13 +317,13 @@ def ensure_uint8(self, img: np.ndarray) -> Tuple[np.ndarray, bool]: return (img * 255).astype("uint8"), True return img, False - def apply(self, img, reference_image, blend_ratio, **params): + def apply(self, img: np.ndarray, reference_image: np.ndarray, blend_ratio: float, **params: Any) -> np.ndarray: self._validate_shape(img) reference_image, _ = self.ensure_uint8(reference_image) img, needs_reconvert = self.ensure_uint8(img) adapted = adapt_pixel_distribution( - img=img, + img, ref=reference_image, weight=blend_ratio, transform_type=self.transform_type, @@ -324,14 +332,14 @@ def apply(self, img, reference_image, blend_ratio, **params): adapted = adapted.astype("float32") * (1 / 255) return adapted - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "reference_image": self.read_fn(random.choice(self.reference_images)), "blend_ratio": random.uniform(self.blend_ratio[0], self.blend_ratio[1]), } - def get_transform_init_args_names(self): - return ("reference_images", "blend_ratio", "read_fn", "transform_type") + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: + return "reference_images", "blend_ratio", "read_fn", "transform_type" - def _to_dict(self): + def _to_dict(self) -> Dict[str, Any]: raise NotImplementedError("PixelDistributionAdaptation can not be serialized.") diff --git a/albumentations/augmentations/dropout/channel_dropout.py b/albumentations/augmentations/dropout/channel_dropout.py index 48b4707ce..9861ac860 100644 --- a/albumentations/augmentations/dropout/channel_dropout.py +++ b/albumentations/augmentations/dropout/channel_dropout.py @@ -1,5 +1,5 @@ import random -from typing import Any, Mapping, Tuple, Union +from typing import Any, Dict, List, Mapping, Tuple, Union import numpy as np @@ -32,7 +32,7 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(ChannelDropout, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.channel_drop_range = channel_drop_range @@ -40,14 +40,14 @@ def __init__( self.max_channels = channel_drop_range[1] if not 1 <= self.min_channels <= self.max_channels: - raise ValueError("Invalid channel_drop_range. Got: {}".format(channel_drop_range)) + raise ValueError(f"Invalid channel_drop_range. Got: {channel_drop_range}") self.fill_value = fill_value - def apply(self, img: np.ndarray, channels_to_drop: Tuple[int, ...] = (0,), **params) -> np.ndarray: + def apply(self, img: np.ndarray, channels_to_drop: Tuple[int, ...] = (0,), **params: Any) -> np.ndarray: return channel_dropout(img, channels_to_drop, self.fill_value) - def get_params_dependent_on_targets(self, params: Mapping[str, Any]): + def get_params_dependent_on_targets(self, params: Mapping[str, Any]) -> Dict[str, Any]: img = params["image"] num_channels = img.shape[-1] @@ -68,5 +68,5 @@ def get_transform_init_args_names(self) -> Tuple[str, ...]: return "channel_drop_range", "fill_value" @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] diff --git a/albumentations/augmentations/dropout/coarse_dropout.py b/albumentations/augmentations/dropout/coarse_dropout.py index 64017d70c..6a5f5c48b 100644 --- a/albumentations/augmentations/dropout/coarse_dropout.py +++ b/albumentations/augmentations/dropout/coarse_dropout.py @@ -1,9 +1,10 @@ import random -from typing import Iterable, List, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple import numpy as np -from ...core.transforms_interface import DualTransform, KeypointType +from ...core.transforms_interface import DualTransform +from ...core.types import KeypointType, ScalarType from .functional import cutout __all__ = ["CoarseDropout"] @@ -56,7 +57,7 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(CoarseDropout, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.max_holes = max_holes self.max_height = max_height self.max_width = max_width @@ -66,7 +67,7 @@ def __init__( self.fill_value = fill_value self.mask_fill_value = mask_fill_value if not 0 < self.min_holes <= self.max_holes: - raise ValueError("Invalid combination of min_holes and max_holes. Got: {}".format([min_holes, max_holes])) + raise ValueError(f"Invalid combination of min_holes and max_holes. Got: {[min_holes, max_holes]}") self.check_range(self.max_height) self.check_range(self.min_height) @@ -74,39 +75,35 @@ def __init__( self.check_range(self.min_width) if not 0 < self.min_height <= self.max_height: - raise ValueError( - "Invalid combination of min_height and max_height. Got: {}".format([min_height, max_height]) - ) + raise ValueError(f"Invalid combination of min_height and max_height. Got: {[min_height, max_height]}") if not 0 < self.min_width <= self.max_width: - raise ValueError("Invalid combination of min_width and max_width. Got: {}".format([min_width, max_width])) + raise ValueError(f"Invalid combination of min_width and max_width. Got: {[min_width, max_width]}") - def check_range(self, dimension): + def check_range(self, dimension: ScalarType) -> None: if isinstance(dimension, float) and not 0 <= dimension < 1.0: - raise ValueError( - "Invalid value {}. If using floats, the value should be in the range [0.0, 1.0)".format(dimension) - ) + raise ValueError(f"Invalid value {dimension}. If using floats, the value should be in the range [0.0, 1.0)") def apply( self, img: np.ndarray, - fill_value: Union[int, float] = 0, + fill_value: ScalarType = 0, holes: Iterable[Tuple[int, int, int, int]] = (), - **params + **params: Any, ) -> np.ndarray: return cutout(img, holes, fill_value) def apply_to_mask( self, - img: np.ndarray, - mask_fill_value: Union[int, float] = 0, + mask: np.ndarray, + mask_fill_value: ScalarType = 0, holes: Iterable[Tuple[int, int, int, int]] = (), - **params + **params: Any, ) -> np.ndarray: if mask_fill_value is None: - return img - return cutout(img, holes, mask_fill_value) + return mask + return cutout(mask, holes, mask_fill_value) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] height, width = img.shape[:2] @@ -156,7 +153,7 @@ def get_params_dependent_on_targets(self, params): return {"holes": holes} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] def _keypoint_in_hole(self, keypoint: KeypointType, hole: Tuple[int, int, int, int]) -> bool: @@ -165,7 +162,7 @@ def _keypoint_in_hole(self, keypoint: KeypointType, hole: Tuple[int, int, int, i return x1 <= x < x2 and y1 <= y < y2 def apply_to_keypoints( - self, keypoints: Sequence[KeypointType], holes: Iterable[Tuple[int, int, int, int]] = (), **params + self, keypoints: Sequence[KeypointType], holes: Iterable[Tuple[int, int, int, int]] = (), **params: Any ) -> List[KeypointType]: result = set(keypoints) for hole in holes: @@ -174,7 +171,7 @@ def apply_to_keypoints( result.discard(kp) return list(result) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "max_holes", "max_height", diff --git a/albumentations/augmentations/dropout/cutout.py b/albumentations/augmentations/dropout/cutout.py index c781c97bb..332b8b8f7 100644 --- a/albumentations/augmentations/dropout/cutout.py +++ b/albumentations/augmentations/dropout/cutout.py @@ -1,6 +1,6 @@ import random import warnings -from typing import Any, Dict, Tuple, Union +from typing import Any, Dict, Iterable, List, Tuple, Union import numpy as np @@ -41,7 +41,7 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(Cutout, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.num_holes = num_holes self.max_h_size = max_h_size self.max_w_size = max_w_size @@ -51,7 +51,13 @@ def __init__( FutureWarning, ) - def apply(self, img: np.ndarray, fill_value: Union[int, float] = 0, holes=(), **params): + def apply( + self, + img: np.ndarray, + fill_value: Union[int, float] = 0, + holes: Iterable[Tuple[int, int, int, int]] = (), + **params: Any, + ) -> np.ndarray: return cutout(img, holes, fill_value) def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -72,8 +78,8 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A return {"holes": holes} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] def get_transform_init_args_names(self) -> Tuple[str, ...]: - return ("num_holes", "max_h_size", "max_w_size") + return "num_holes", "max_h_size", "max_w_size" diff --git a/albumentations/augmentations/dropout/grid_dropout.py b/albumentations/augmentations/dropout/grid_dropout.py index 094b989aa..431ffef37 100644 --- a/albumentations/augmentations/dropout/grid_dropout.py +++ b/albumentations/augmentations/dropout/grid_dropout.py @@ -1,9 +1,10 @@ import random -from typing import Iterable, Optional, Tuple +from typing import Any, Dict, Iterable, List, Optional, Tuple import numpy as np from ...core.transforms_interface import DualTransform +from ...core.types import ScalarType from . import functional as F __all__ = ["GridDropout"] @@ -13,7 +14,7 @@ class GridDropout(DualTransform): """GridDropout, drops out rectangular regions of an image and the corresponding mask in a grid fashion. Args: - ratio (float): the ratio of the mask holes to the unit_size (same for horizontal and vertical directions). + ratio: the ratio of the mask holes to the unit_size (same for horizontal and vertical directions). Must be between 0 and 1. Default: 0.5. unit_size_min (int): minimum size of the grid unit. Must be between 2 and the image shorter edge. If 'None', holes_number_x and holes_number_y are used to setup the grid. Default: `None`. @@ -55,11 +56,11 @@ def __init__( shift_y: int = 0, random_offset: bool = False, fill_value: int = 0, - mask_fill_value: Optional[int] = None, + mask_fill_value: Optional[ScalarType] = None, always_apply: bool = False, p: float = 0.5, ): - super(GridDropout, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.ratio = ratio self.unit_size_min = unit_size_min self.unit_size_max = unit_size_max @@ -73,16 +74,18 @@ def __init__( if not 0 < self.ratio <= 1: raise ValueError("ratio must be between 0 and 1.") - def apply(self, img: np.ndarray, holes: Iterable[Tuple[int, int, int, int]] = (), **params) -> np.ndarray: + def apply(self, img: np.ndarray, holes: Iterable[Tuple[int, int, int, int]] = (), **params: Any) -> np.ndarray: return F.cutout(img, holes, self.fill_value) - def apply_to_mask(self, img: np.ndarray, holes: Iterable[Tuple[int, int, int, int]] = (), **params) -> np.ndarray: + def apply_to_mask( + self, mask: np.ndarray, holes: Iterable[Tuple[int, int, int, int]] = (), **params: Any + ) -> np.ndarray: if self.mask_fill_value is None: - return img + return mask - return F.cutout(img, holes, self.mask_fill_value) + return F.cutout(mask, holes, self.mask_fill_value) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] height, width = img.shape[:2] # set grid using unit size limits @@ -137,10 +140,10 @@ def get_params_dependent_on_targets(self, params): return {"holes": holes} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "ratio", "unit_size_min", diff --git a/albumentations/augmentations/dropout/mask_dropout.py b/albumentations/augmentations/dropout/mask_dropout.py index fe67a6540..2596da2b8 100644 --- a/albumentations/augmentations/dropout/mask_dropout.py +++ b/albumentations/augmentations/dropout/mask_dropout.py @@ -1,11 +1,12 @@ import random -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import cv2 import numpy as np from skimage.measure import label from ...core.transforms_interface import DualTransform, to_tuple +from ...core.types import ScalarType __all__ = ["MaskDropout"] @@ -37,20 +38,20 @@ def __init__( self, max_objects: int = 1, image_fill_value: Union[int, float, str] = 0, - mask_fill_value: Union[int, float] = 0, + mask_fill_value: ScalarType = 0, always_apply: bool = False, p: float = 0.5, ): - super(MaskDropout, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.max_objects = to_tuple(max_objects, 1) self.image_fill_value = image_fill_value self.mask_fill_value = mask_fill_value @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["mask"] - def get_params_dependent_on_targets(self, params) -> Dict[str, Any]: + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: mask = params["mask"] label_image, num_labels = label(mask, return_num=True) @@ -72,7 +73,7 @@ def get_params_dependent_on_targets(self, params) -> Dict[str, Any]: params.update({"dropout_mask": dropout_mask}) return params - def apply(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params) -> np.ndarray: + def apply(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: if dropout_mask is None: return img @@ -87,13 +88,13 @@ def apply(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **pa return img - def apply_to_mask(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: if dropout_mask is None: - return img + return mask - img = img.copy() - img[dropout_mask] = self.mask_fill_value - return img + mask = mask.copy() + mask[dropout_mask] = self.mask_fill_value + return mask def get_transform_init_args_names(self) -> Tuple[str, ...]: return "max_objects", "image_fill_value", "mask_fill_value" diff --git a/albumentations/augmentations/functional.py b/albumentations/augmentations/functional.py index 54dd43e2c..45f66231d 100644 --- a/albumentations/augmentations/functional.py +++ b/albumentations/augmentations/functional.py @@ -1,6 +1,4 @@ -from __future__ import division - -from typing import Optional, Sequence, Union +from typing import Any, List, Optional, Sequence, Tuple, Union from warnings import warn import cv2 @@ -21,6 +19,8 @@ preserve_shape, ) +from ..core.types import ColorType, ScalarType + __all__ = [ "add_fog", "add_rain", @@ -61,10 +61,11 @@ "to_gray", "gray_to_rgb", "unsharp_mask", + "MAX_VALUES_BY_DTYPE", ] -def normalize_cv2(img, mean, denominator): +def normalize_cv2(img: np.ndarray, mean: np.ndarray, denominator: np.ndarray) -> np.ndarray: if mean.shape and len(mean) != 4 and mean.shape != img.shape: mean = np.array(mean.tolist() + [0] * (4 - len(mean)), dtype=np.float64) if not denominator.shape: @@ -78,14 +79,14 @@ def normalize_cv2(img, mean, denominator): return img -def normalize_numpy(img, mean, denominator): +def normalize_numpy(img: np.ndarray, mean: np.ndarray, denominator: np.ndarray) -> np.ndarray: img = img.astype(np.float32) img -= mean img *= denominator return img -def normalize(img, mean, std, max_pixel_value=255.0): +def normalize(img: np.ndarray, mean: np.ndarray, std: np.ndarray, max_pixel_value: float = 255.0) -> np.ndarray: mean = np.array(mean, dtype=np.float32) mean *= max_pixel_value @@ -99,7 +100,9 @@ def normalize(img, mean, std, max_pixel_value=255.0): return normalize_numpy(img, mean, denominator) -def _shift_hsv_uint8(img, hue_shift, sat_shift, val_shift): +def _shift_hsv_uint8( + img: np.ndarray, hue_shift: np.ndarray, sat_shift: np.ndarray, val_shift: np.ndarray +) -> np.ndarray: dtype = img.dtype img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) hue, sat, val = cv2.split(img) @@ -124,7 +127,9 @@ def _shift_hsv_uint8(img, hue_shift, sat_shift, val_shift): return img -def _shift_hsv_non_uint8(img, hue_shift, sat_shift, val_shift): +def _shift_hsv_non_uint8( + img: np.ndarray, hue_shift: np.ndarray, sat_shift: np.ndarray, val_shift: np.ndarray +) -> np.ndarray: dtype = img.dtype img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) hue, sat, val = cv2.split(img) @@ -140,12 +145,11 @@ def _shift_hsv_non_uint8(img, hue_shift, sat_shift, val_shift): val = clip(cv2.add(val, val_shift), dtype, 1.0) img = cv2.merge((hue, sat, val)) - img = cv2.cvtColor(img, cv2.COLOR_HSV2RGB) - return img + return cv2.cvtColor(img, cv2.COLOR_HSV2RGB) @preserve_shape -def shift_hsv(img, hue_shift, sat_shift, val_shift): +def shift_hsv(img: np.ndarray, hue_shift: np.ndarray, sat_shift: np.ndarray, val_shift: np.ndarray) -> np.ndarray: if hue_shift == 0 and sat_shift == 0 and val_shift == 0: return img @@ -171,22 +175,22 @@ def shift_hsv(img, hue_shift, sat_shift, val_shift): return img -def solarize(img, threshold=128): +def solarize(img: np.ndarray, threshold: int = 128) -> np.ndarray: """Invert all pixel values above a threshold. Args: - img (numpy.ndarray): The image to solarize. - threshold (int): All pixels above this greyscale level are inverted. + img: The image to solarize. + threshold: All pixels above this grayscale level are inverted. Returns: - numpy.ndarray: Solarized image. + Solarized image. """ dtype = img.dtype max_val = MAX_VALUES_BY_DTYPE[dtype] if dtype == np.dtype("uint8"): - lut = [(i if i < threshold else max_val - i) for i in range(max_val + 1)] + lut = [(i if i < threshold else max_val - i) for i in range(int(max_val) + 1)] prev_shape = img.shape img = cv2.LUT(img, np.array(lut, dtype=dtype)) @@ -202,32 +206,32 @@ def solarize(img, threshold=128): @preserve_shape -def posterize(img, bits): +def posterize(img: np.ndarray, bits: int) -> np.ndarray: """Reduce the number of bits for each color channel. Args: - img (numpy.ndarray): image to posterize. - bits (int): number of high bits. Must be in range [0, 8] + img: image to posterize. + bits: number of high bits. Must be in range [0, 8] Returns: - numpy.ndarray: Image with reduced color channels. + Image with reduced color channels. """ - bits = np.uint8(bits) + bits_array = np.uint8(bits) if img.dtype != np.uint8: raise TypeError("Image must have uint8 channel type") - if np.any((bits < 0) | (bits > 8)): + if np.any((bits_array < 0) | (bits_array > 8)): raise ValueError("bits must be in range [0, 8]") - if not bits.shape or len(bits) == 1: - if bits == 0: + if not bits_array.shape or len(bits_array) == 1: + if bits_array == 0: return np.zeros_like(img) - if bits == 8: + if bits_array == 8: return img.copy() lut = np.arange(0, 256, dtype=np.uint8) - mask = ~np.uint8(2 ** (8 - bits) - 1) + mask = ~np.uint8(2 ** (8 - bits_array) - 1) lut &= mask return cv2.LUT(img, lut) @@ -236,7 +240,7 @@ def posterize(img, bits): raise TypeError("If bits is iterable image must be RGB") result_img = np.empty_like(img) - for i, channel_bits in enumerate(bits): + for i, channel_bits in enumerate(bits_array): if channel_bits == 0: result_img[..., i] = np.zeros_like(img[..., i]) elif channel_bits == 8: @@ -251,7 +255,7 @@ def posterize(img, bits): return result_img -def _equalize_pil(img, mask=None): +def _equalize_pil(img: np.ndarray, mask: Optional[np.ndarray] = None) -> np.ndarray: histogram = cv2.calcHist([img], [0], mask, [256], (0, 256)).ravel() h = [_f for _f in histogram if _f] @@ -271,7 +275,7 @@ def _equalize_pil(img, mask=None): return cv2.LUT(img, np.array(lut)) -def _equalize_cv(img, mask=None): +def _equalize_cv(img: np.ndarray, mask: Optional[np.ndarray] = None) -> np.ndarray: if mask is None: return cv2.equalizeHist(img) @@ -300,19 +304,21 @@ def _equalize_cv(img, mask=None): @preserve_channel_dim -def equalize(img, mask=None, mode="cv", by_channels=True): +def equalize( + img: np.ndarray, mask: Optional[np.ndarray] = None, mode: str = "cv", by_channels: bool = True +) -> np.ndarray: """Equalize the image histogram. Args: - img (numpy.ndarray): RGB or grayscale image. - mask (numpy.ndarray): An optional mask. If given, only the pixels selected by + img: RGB or grayscale image. + mask: An optional mask. If given, only the pixels selected by the mask are included in the analysis. Maybe 1 channel or 3 channel array. - mode (str): {'cv', 'pil'}. Use OpenCV or Pillow equalization method. - by_channels (bool): If True, use equalization by channels separately, + mode: {'cv', 'pil'}. Use OpenCV or Pillow equalization method. + by_channels: If True, use equalization by channels separately, else convert image to YCbCr representation and use equalization by `Y` channel. Returns: - numpy.ndarray: Equalized image. + Equalized image. """ if img.dtype != np.uint8: @@ -361,42 +367,41 @@ def equalize(img, mask=None, mode="cv", by_channels=True): @preserve_shape -def move_tone_curve(img, low_y, high_y): +def move_tone_curve(img: np.ndarray, low_y: float, high_y: float) -> np.ndarray: """Rescales the relationship between bright and dark areas of the image by manipulating its tone curve. Args: - img (numpy.ndarray): RGB or grayscale image. - low_y (float): y-position of a Bezier control point used + img: RGB or grayscale image. + low_y: y-position of a Bezier control point used to adjust the tone curve, must be in range [0, 1] - high_y (float): y-position of a Bezier control point used + high_y: y-position of a Bezier control point used to adjust image tone curve, must be in range [0, 1] """ input_dtype = img.dtype - if low_y < 0 or low_y > 1: + if not 0 < low_y < 1: raise ValueError("low_shift must be in range [0, 1]") - if high_y < 0 or high_y > 1: + if not 0 < high_y < 1: raise ValueError("high_shift must be in range [0, 1]") if input_dtype != np.uint8: - raise ValueError("Unsupported image type {}".format(input_dtype)) + raise ValueError(f"Unsupported image type {input_dtype}") t = np.linspace(0.0, 1.0, 256) - # Defines responze of a four-point bezier curve - def evaluate_bez(t): + # Defines response of a four-point Bezier curve + def evaluate_bez(t: np.ndarray) -> np.ndarray: return 3 * (1 - t) ** 2 * t * low_y + 3 * (1 - t) * t**2 * high_y + t**3 evaluate_bez = np.vectorize(evaluate_bez) remapping = np.rint(evaluate_bez(t) * 255).astype(np.uint8) lut_fn = _maybe_process_in_chunks(cv2.LUT, lut=remapping) - img = lut_fn(img) - return img + return lut_fn(img) @clipped -def _shift_rgb_non_uint8(img, r_shift, g_shift, b_shift): +def _shift_rgb_non_uint8(img: np.ndarray, r_shift: float, g_shift: float, b_shift: float) -> np.ndarray: if r_shift == g_shift == b_shift: return img + r_shift @@ -408,7 +413,7 @@ def _shift_rgb_non_uint8(img, r_shift, g_shift, b_shift): return result_img -def _shift_image_uint8(img, value): +def _shift_image_uint8(img: np.ndarray, value: np.ndarray) -> np.ndarray: max_value = MAX_VALUES_BY_DTYPE[img.dtype] lut = np.arange(0, max_value + 1).astype("float32") @@ -419,10 +424,10 @@ def _shift_image_uint8(img, value): @preserve_shape -def _shift_rgb_uint8(img, r_shift, g_shift, b_shift): +def _shift_rgb_uint8(img: np.ndarray, r_shift: ScalarType, g_shift: ScalarType, b_shift: ScalarType) -> np.ndarray: if r_shift == g_shift == b_shift: - h, w, c = img.shape - img = img.reshape([h, w * c]) + height, width, channels = img.shape + img = img.reshape([height, width * channels]) return _shift_image_uint8(img, r_shift) @@ -434,7 +439,7 @@ def _shift_rgb_uint8(img, r_shift, g_shift, b_shift): return result_img -def shift_rgb(img, r_shift, g_shift, b_shift): +def shift_rgb(img: np.ndarray, r_shift: ScalarType, g_shift: ScalarType, b_shift: ScalarType) -> np.ndarray: if img.dtype == np.uint8: return _shift_rgb_uint8(img, r_shift, g_shift, b_shift) @@ -442,37 +447,33 @@ def shift_rgb(img, r_shift, g_shift, b_shift): @clipped -def linear_transformation_rgb(img, transformation_matrix): - result_img = cv2.transform(img, transformation_matrix) - - return result_img +def linear_transformation_rgb(img: np.ndarray, transformation_matrix: np.ndarray) -> np.ndarray: + return cv2.transform(img, transformation_matrix) @preserve_channel_dim -def clahe(img, clip_limit=2.0, tile_grid_size=(8, 8)): +def clahe(img: np.ndarray, clip_limit: float = 2.0, tile_grid_size: Tuple[int, int] = (8, 8)) -> np.ndarray: if img.dtype != np.uint8: raise TypeError("clahe supports only uint8 inputs") clahe_mat = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size) if len(img.shape) == 2 or img.shape[2] == 1: - img = clahe_mat.apply(img) - else: - img = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) - img[:, :, 0] = clahe_mat.apply(img[:, :, 0]) - img = cv2.cvtColor(img, cv2.COLOR_LAB2RGB) + return clahe_mat.apply(img) - return img + img = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) + img[:, :, 0] = clahe_mat.apply(img[:, :, 0]) + return cv2.cvtColor(img, cv2.COLOR_LAB2RGB) @preserve_shape -def convolve(img, kernel): +def convolve(img: np.ndarray, kernel: np.ndarray) -> np.ndarray: conv_fn = _maybe_process_in_chunks(cv2.filter2D, ddepth=-1, kernel=kernel) return conv_fn(img) @preserve_shape -def image_compression(img, quality, image_type): +def image_compression(img: np.ndarray, quality: int, image_type: np.dtype) -> np.ndarray: if image_type in [".jpeg", ".jpg"]: quality_flag = cv2.IMWRITE_JPEG_QUALITY elif image_type == ".webp": @@ -493,7 +494,7 @@ def image_compression(img, quality, image_type): img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for image augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for image augmentation") _, encoded_img = cv2.imencode(image_type, img, (int(quality_flag), quality)) img = cv2.imdecode(encoded_img, cv2.IMREAD_UNCHANGED) @@ -504,18 +505,18 @@ def image_compression(img, quality, image_type): @preserve_shape -def add_snow(img, snow_point, brightness_coeff): +def add_snow(img: np.ndarray, snow_point: float, brightness_coeff: float) -> np.ndarray: """Bleaches out pixels, imitation snow. From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - img (numpy.ndarray): Image. + img: Image. snow_point: Number of show points. brightness_coeff: Brightness coefficient. Returns: - numpy.ndarray: Image. + Image. """ non_rgb_warning(img) @@ -530,7 +531,7 @@ def add_snow(img, snow_point, brightness_coeff): img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for RandomSnow augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for RandomSnow augmentation") image_HLS = cv2.cvtColor(img, cv2.COLOR_RGB2HLS) image_HLS = np.array(image_HLS, dtype=np.float32) @@ -551,31 +552,31 @@ def add_snow(img, snow_point, brightness_coeff): @preserve_shape def add_rain( - img, - slant, - drop_length, - drop_width, - drop_color, - blur_value, - brightness_coefficient, - rain_drops, -): + img: np.ndarray, + slant: int, + drop_length: int, + drop_width: int, + drop_color: Tuple[int, int, int], + blur_value: int, + brightness_coefficient: float, + rain_drops: List[Tuple[int, int]], +) -> np.ndarray: """ From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - img (numpy.ndarray): Image. - slant (int): + img: Image. + slant: drop_length: drop_width: drop_color: - blur_value (int): Rainy view are blurry. - brightness_coefficient (float): Rainy days are usually shady. + blur_value: Rainy view are blurry. + brightness_coefficient: Rainy days are usually shady. rain_drops: Returns: - numpy.ndarray: Image. + Image. """ non_rgb_warning(img) @@ -587,7 +588,7 @@ def add_rain( img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for RandomRain augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for RandomRain augmentation") image = img.copy() @@ -616,19 +617,19 @@ def add_rain( @preserve_shape -def add_fog(img, fog_coef, alpha_coef, haze_list): +def add_fog(img: np.ndarray, fog_coef: float, alpha_coef: float, haze_list: List[Tuple[int, int]]) -> np.ndarray: """Add fog to the image. From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - img (numpy.ndarray): Image. - fog_coef (float): Fog coefficient. - alpha_coef (float): Alpha coefficient. - haze_list (list): + img: Image. + fog_coef: Fog coefficient. + alpha_coef: Alpha coefficient. + haze_list: Returns: - numpy.ndarray: Image. + Image. """ non_rgb_warning(img) @@ -640,7 +641,7 @@ def add_fog(img, fog_coef, alpha_coef, haze_list): img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for RandomFog augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for RandomFog augmentation") width = img.shape[1] @@ -667,7 +668,14 @@ def add_fog(img, fog_coef, alpha_coef, haze_list): @preserve_shape -def add_sun_flare(img, flare_center_x, flare_center_y, src_radius, src_color, circles): +def add_sun_flare( + img: np.ndarray, + flare_center_x: float, + flare_center_y: float, + src_radius: int, + src_color: ColorType, + circles: List[Any], +) -> np.ndarray: """Add sun flare. From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library @@ -693,7 +701,7 @@ def add_sun_flare(img, flare_center_x, flare_center_y, src_radius, src_color, ci img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for RandomSunFlareaugmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for RandomSunFlareaugmentation") overlay = img.copy() output = img.copy() @@ -724,7 +732,7 @@ def add_sun_flare(img, flare_center_x, flare_center_y, src_radius, src_color, ci @ensure_contiguous @preserve_shape -def add_shadow(img, vertices_list): +def add_shadow(img: np.ndarray, vertices_list: List[List[Tuple[int, int]]]) -> np.ndarray: """Add shadows to the image. From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library @@ -745,7 +753,7 @@ def add_shadow(img, vertices_list): img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for RandomShadow augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for RandomShadow augmentation") image_hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS) mask = np.zeros_like(img) @@ -761,14 +769,14 @@ def add_shadow(img, vertices_list): image_rgb = cv2.cvtColor(image_hls, cv2.COLOR_HLS2RGB) if needs_float: - image_rgb = to_float(image_rgb, max_value=255) + return to_float(image_rgb, max_value=255) return image_rgb @ensure_contiguous @preserve_shape -def add_gravel(img: np.ndarray, gravels: list): +def add_gravel(img: np.ndarray, gravels: List[Any]) -> np.ndarray: """Add gravel to the image. From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library @@ -789,7 +797,7 @@ def add_gravel(img: np.ndarray, gravels: list): img = from_float(img, dtype=np.dtype("uint8")) needs_float = True elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for AddGravel augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for AddGravel augmentation") image_hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS) @@ -811,30 +819,28 @@ def invert(img: np.ndarray) -> np.ndarray: return MAX_VALUES_BY_DTYPE[img.dtype] - img -def channel_shuffle(img, channels_shuffled): - img = img[..., channels_shuffled] - return img +def channel_shuffle(img: np.ndarray, channels_shuffled: np.ndarray) -> np.ndarray: + return img[..., channels_shuffled] @preserve_shape -def gamma_transform(img, gamma): +def gamma_transform(img: np.ndarray, gamma: float) -> np.ndarray: if img.dtype == np.uint8: table = (np.arange(0, 256.0 / 255, 1.0 / 255) ** gamma) * 255 - img = cv2.LUT(img, table.astype(np.uint8)) - else: - img = np.power(img, gamma) - - return img + return cv2.LUT(img, table.astype(np.uint8)) + return np.power(img, gamma) @clipped -def gauss_noise(image, gauss): +def gauss_noise(image: np.ndarray, gauss: np.ndarray) -> np.ndarray: image = image.astype("float32") return image + gauss @clipped -def _brightness_contrast_adjust_non_uint(img, alpha=1, beta=0, beta_by_max=False): +def _brightness_contrast_adjust_non_uint( + img: np.ndarray, alpha: float = 1, beta: float = 0, beta_by_max: bool = False +) -> np.ndarray: dtype = img.dtype img = img.astype("float32") @@ -850,7 +856,9 @@ def _brightness_contrast_adjust_non_uint(img, alpha=1, beta=0, beta_by_max=False @preserve_shape -def _brightness_contrast_adjust_uint(img, alpha=1, beta=0, beta_by_max=False): +def _brightness_contrast_adjust_uint( + img: np.ndarray, alpha: float = 1, beta: float = 0, beta_by_max: bool = False +) -> np.ndarray: dtype = np.dtype("uint8") max_value = MAX_VALUES_BY_DTYPE[dtype] @@ -866,11 +874,12 @@ def _brightness_contrast_adjust_uint(img, alpha=1, beta=0, beta_by_max=False): lut += (alpha * beta) * np.mean(img) lut = np.clip(lut, 0, max_value).astype(dtype) - img = cv2.LUT(img, lut) - return img + return cv2.LUT(img, lut) -def brightness_contrast_adjust(img, alpha=1, beta=0, beta_by_max=False): +def brightness_contrast_adjust( + img: np.ndarray, alpha: float = 1, beta: float = 0, beta_by_max: bool = False +) -> np.ndarray: if img.dtype == np.uint8: return _brightness_contrast_adjust_uint(img, alpha, beta, beta_by_max) @@ -878,7 +887,13 @@ def brightness_contrast_adjust(img, alpha=1, beta=0, beta_by_max=False): @clipped -def iso_noise(image, color_shift=0.05, intensity=0.5, random_state=None, **kwargs): +def iso_noise( + image: np.ndarray, + color_shift: float = 0.05, + intensity: float = 0.5, + random_state: Optional[int] = None, + **kwargs: Any, +) -> np.ndarray: """ Apply poisson noise to image to simulate camera sensor noise. @@ -919,18 +934,20 @@ def iso_noise(image, color_shift=0.05, intensity=0.5, random_state=None, **kwarg return image.astype(np.uint8) -def to_gray(img): +def to_gray(img: np.ndarray) -> np.ndarray: gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) return cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB) -def gray_to_rgb(img): +def gray_to_rgb(img: np.ndarray) -> np.ndarray: return cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) @preserve_shape -def downscale(img, scale, down_interpolation=cv2.INTER_AREA, up_interpolation=cv2.INTER_LINEAR): - h, w = img.shape[:2] +def downscale( + img: np.ndarray, scale: float, down_interpolation: int = cv2.INTER_AREA, up_interpolation: int = cv2.INTER_LINEAR +) -> np.ndarray: + height, width = img.shape[:2] need_cast = ( up_interpolation != cv2.INTER_NEAREST or down_interpolation != cv2.INTER_NEAREST @@ -938,13 +955,13 @@ def downscale(img, scale, down_interpolation=cv2.INTER_AREA, up_interpolation=cv if need_cast: img = to_float(img) downscaled = cv2.resize(img, None, fx=scale, fy=scale, interpolation=down_interpolation) - upscaled = cv2.resize(downscaled, (w, h), interpolation=up_interpolation) + upscaled = cv2.resize(downscaled, (width, height), interpolation=up_interpolation) if need_cast: - upscaled = from_float(np.clip(upscaled, 0, 1), dtype=np.dtype("uint8")) + return from_float(np.clip(upscaled, 0, 1), dtype=np.dtype("uint8")) return upscaled -def to_float(img, max_value=None): +def to_float(img: np.ndarray, max_value: Optional[float] = None) -> np.ndarray: if max_value is None: try: max_value = MAX_VALUES_BY_DTYPE[img.dtype] @@ -956,7 +973,7 @@ def to_float(img, max_value=None): return img.astype("float32") / max_value -def from_float(img, dtype, max_value=None): +def from_float(img: np.ndarray, dtype: np.dtype, max_value: Optional[float] = None) -> np.ndarray: if max_value is None: try: max_value = MAX_VALUES_BY_DTYPE[dtype] @@ -968,11 +985,11 @@ def from_float(img, dtype, max_value=None): return (img * max_value).astype(dtype) -def noop(input_obj, **params): # skipcq: PYL-W0613 +def noop(input_obj: Any, **params: Any) -> Any: return input_obj -def swap_tiles_on_image(image, tiles): +def swap_tiles_on_image(image: np.ndarray, tiles: np.ndarray) -> np.ndarray: """ Swap tiles on image. @@ -998,13 +1015,13 @@ def swap_tiles_on_image(image, tiles): @clipped -def _multiply_uint8(img, multiplier): +def _multiply_uint8(img: np.ndarray, multiplier: np.ndarray) -> np.ndarray: img = img.astype(np.float32) return np.multiply(img, multiplier) @preserve_shape -def _multiply_uint8_optimized(img, multiplier): +def _multiply_uint8_optimized(img: np.ndarray, multiplier: np.ndarray) -> np.ndarray: if is_grayscale_image(img) or len(multiplier) == 1: multiplier = multiplier[0] lut = np.arange(0, 256, dtype=np.float32) @@ -1028,18 +1045,18 @@ def _multiply_uint8_optimized(img, multiplier): @clipped -def _multiply_non_uint8(img, multiplier): +def _multiply_non_uint8(img: np.ndarray, multiplier: np.ndarray) -> np.ndarray: return img * multiplier -def multiply(img, multiplier): +def multiply(img: np.ndarray, multiplier: np.ndarray) -> np.ndarray: """ Args: - img (numpy.ndarray): Image. - multiplier (numpy.ndarray): Multiplier coefficient. + img: Image. + multiplier: Multiplier coefficient. Returns: - numpy.ndarray: Image multiplied by `multiplier` coefficient. + Image multiplied by `multiplier` coefficient. """ if img.dtype == np.uint8: @@ -1051,7 +1068,7 @@ def multiply(img, multiplier): return _multiply_non_uint8(img, multiplier) -def bbox_from_mask(mask): +def bbox_from_mask(mask: np.ndarray) -> Tuple[int, int, int, int]: """Create bounding box from binary mask (fast version) Args: @@ -1070,15 +1087,15 @@ def bbox_from_mask(mask): return x_min, y_min, x_max + 1, y_max + 1 -def mask_from_bbox(img, bbox): +def mask_from_bbox(img: np.ndarray, bbox: Tuple[int, int, int, int]) -> np.ndarray: """Create binary mask from bounding box Args: - img (numpy.ndarray): input image + img: input image bbox: A bounding box tuple `(x_min, y_min, x_max, y_max)` Returns: - mask (numpy.ndarray): binary mask + mask: binary mask """ @@ -1088,17 +1105,17 @@ def mask_from_bbox(img, bbox): return mask -def fancy_pca(img, alpha=0.1): +def fancy_pca(img: np.ndarray, alpha: float = 0.1) -> np.ndarray: """Perform 'Fancy PCA' augmentation from: http://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf Args: - img (numpy.ndarray): numpy array with (h, w, rgb) shape, as ints between 0-255 - alpha (float): how much to perturb/scale the eigen vecs and vals + img: numpy array with (h, w, rgb) shape, as ints between 0-255 + alpha: how much to perturb/scale the eigen vecs and vals the paper used std=0.1 Returns: - numpy.ndarray: numpy image-like array as uint8 range(0, 255) + numpy image-like array as uint8 range(0, 255) """ if not is_rgb_image(img) or img.dtype != np.uint8: @@ -1127,7 +1144,7 @@ def fancy_pca(img, alpha=0.1): eig_vecs = eig_vecs[:, sort_perm] # get [p1, p2, p3] - m1 = np.column_stack((eig_vecs)) + m1 = np.column_stack(eig_vecs) # get 3x1 matrix of eigen values multiplied by random variable draw from normal # distribution with mean of 0 and standard deviation of 0.1 @@ -1155,14 +1172,14 @@ def fancy_pca(img, alpha=0.1): return orig_img -def _adjust_brightness_torchvision_uint8(img, factor): +def _adjust_brightness_torchvision_uint8(img: np.ndarray, factor: float) -> np.ndarray: lut = np.arange(0, 256) * factor lut = np.clip(lut, 0, 255).astype(np.uint8) return cv2.LUT(img, lut) @preserve_shape -def adjust_brightness_torchvision(img, factor): +def adjust_brightness_torchvision(img: np.ndarray, factor: np.ndarray) -> np: if factor == 0: return np.zeros_like(img) elif factor == 1: @@ -1174,7 +1191,7 @@ def adjust_brightness_torchvision(img, factor): return clip(img * factor, img.dtype, MAX_VALUES_BY_DTYPE[img.dtype]) -def _adjust_contrast_torchvision_uint8(img, factor, mean): +def _adjust_contrast_torchvision_uint8(img: np.ndarray, factor: float, mean: np.ndarray) -> np.ndarray: lut = np.arange(0, 256) * factor lut = lut + mean * (1 - factor) lut = clip(lut, img.dtype, 255) @@ -1183,7 +1200,7 @@ def _adjust_contrast_torchvision_uint8(img, factor, mean): @preserve_shape -def adjust_contrast_torchvision(img, factor): +def adjust_contrast_torchvision(img: np.ndarray, factor: float) -> np.ndarray: if factor == 1: return img @@ -1208,7 +1225,7 @@ def adjust_contrast_torchvision(img, factor): @preserve_shape -def adjust_saturation_torchvision(img, factor, gamma=0): +def adjust_saturation_torchvision(img: np.ndarray, factor: float, gamma: float = 0) -> np.ndarray: if factor == 1: return img @@ -1230,7 +1247,7 @@ def adjust_saturation_torchvision(img, factor, gamma=0): return clip(result, img.dtype, MAX_VALUES_BY_DTYPE[img.dtype]) -def _adjust_hue_torchvision_uint8(img, factor): +def _adjust_hue_torchvision_uint8(img: np.ndarray, factor: float) -> np.ndarray: img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) lut = np.arange(0, 256, dtype=np.int16) @@ -1240,7 +1257,7 @@ def _adjust_hue_torchvision_uint8(img, factor): return cv2.cvtColor(img, cv2.COLOR_HSV2RGB) -def adjust_hue_torchvision(img, factor): +def adjust_hue_torchvision(img: np.ndarray, factor: float) -> np.ndarray: if is_grayscale_image(img): return img @@ -1308,26 +1325,28 @@ def superpixels( resize_fn = _maybe_process_in_chunks( cv2.resize, dsize=(orig_shape[1], orig_shape[0]), interpolation=interpolation ) - image = resize_fn(image) + return resize_fn(image) return image @clipped -def add_weighted(img1, alpha, img2, beta): +def add_weighted(img1: np.ndarray, alpha: float, img2: np.ndarray, beta: float) -> np.ndarray: return img1.astype(float) * alpha + img2.astype(float) * beta @clipped @preserve_shape -def unsharp_mask(image: np.ndarray, ksize: int, sigma: float = 0.0, alpha: float = 0.2, threshold: int = 10): +def unsharp_mask( + image: np.ndarray, ksize: int, sigma: float = 0.0, alpha: float = 0.2, threshold: int = 10 +) -> np.ndarray: blur_fn = _maybe_process_in_chunks(cv2.GaussianBlur, ksize=(ksize, ksize), sigmaX=sigma) input_dtype = image.dtype if input_dtype == np.uint8: image = to_float(image) elif input_dtype not in (np.uint8, np.float32): - raise ValueError("Unexpected dtype {} for UnsharpMask augmentation".format(input_dtype)) + raise ValueError(f"Unexpected dtype {input_dtype} for UnsharpMask augmentation") blur = blur_fn(image) residual = image - blur @@ -1350,7 +1369,7 @@ def pixel_dropout(image: np.ndarray, drop_mask: np.ndarray, drop_value: Union[fl if isinstance(drop_value, (int, float)) and drop_value == 0: drop_values = np.zeros_like(image) else: - drop_values = np.full_like(image, drop_value) # type: ignore + drop_values = np.full_like(image, drop_value) return np.where(drop_mask, drop_values, image) diff --git a/albumentations/augmentations/geometric/functional.py b/albumentations/augmentations/geometric/functional.py index 4bec18734..dd8098126 100644 --- a/albumentations/augmentations/geometric/functional.py +++ b/albumentations/augmentations/geometric/functional.py @@ -1,5 +1,5 @@ import math -from typing import List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast import cv2 import numpy as np @@ -16,12 +16,8 @@ from ... import random_utils from ...core.bbox_utils import denormalize_bbox, normalize_bbox -from ...core.transforms_interface import ( - BoxInternalType, - FillValueType, - ImageColorType, - KeypointInternalType, -) +from ...core.types import BoxInternalType, ImageColorType, KeypointInternalType +from ..core.transforms_interface import FillValueType __all__ = [ "optical_distortion", @@ -41,7 +37,6 @@ "resize", "scale", "keypoint_scale", - "py3round", "_func_max_size", "longest_max_size", "smallest_max_size", @@ -72,10 +67,12 @@ "keypoint_hflip", "keypoint_transpose", "keypoint_vflip", + "normalize_bbox", + "denormalize_bbox", ] -def bbox_rot90(bbox: BoxInternalType, factor: int, rows: int, cols: int) -> BoxInternalType: # skipcq: PYL-W0613 +def bbox_rot90(bbox: BoxInternalType, factor: int, rows: int, cols: int) -> BoxInternalType: """Rotates a bounding box by 90 degrees CCW (see np.rot90) Args: @@ -101,7 +98,9 @@ def bbox_rot90(bbox: BoxInternalType, factor: int, rows: int, cols: int) -> BoxI @angle_2pi_range -def keypoint_rot90(keypoint: KeypointInternalType, factor: int, rows: int, cols: int, **params) -> KeypointInternalType: +def keypoint_rot90( + keypoint: KeypointInternalType, factor: int, rows: int, cols: int, **params: Any +) -> KeypointInternalType: """Rotates a keypoint by 90 degrees CCW (see np.rot90) Args: @@ -139,7 +138,7 @@ def rotate( interpolation: int = cv2.INTER_LINEAR, border_mode: int = cv2.BORDER_REFLECT_101, value: Optional[ImageColorType] = None, -): +) -> np.ndarray: height, width = img.shape[:2] # for images we use additional shifts of (0.5, 0.5) as otherwise # we get an ugly black border for 90deg rotations @@ -194,7 +193,9 @@ def bbox_rotate(bbox: BoxInternalType, angle: float, method: str, rows: int, col @angle_2pi_range -def keypoint_rotate(keypoint, angle, rows, cols, **params): +def keypoint_rotate( + keypoint: KeypointInternalType, angle: float, rows: int, cols: int, **params: Any +) -> KeypointInternalType: """Rotate a keypoint by angle. Args: @@ -216,8 +217,15 @@ def keypoint_rotate(keypoint, angle, rows, cols, **params): @preserve_channel_dim def shift_scale_rotate( - img, angle, scale, dx, dy, interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_REFLECT_101, value=None -): + img: np.ndarray, + angle: float, + scale: float, + dx: int, + dy: int, + interpolation: int = cv2.INTER_LINEAR, + border_mode: int = cv2.BORDER_REFLECT_101, + value: Optional[Tuple[int, ...]] = None, +) -> np.ndarray: height, width = img.shape[:2] # for images we use additional shifts of (0.5, 0.5) as otherwise # we get an ugly black border for 90deg rotations @@ -233,7 +241,9 @@ def shift_scale_rotate( @angle_2pi_range -def keypoint_shift_scale_rotate(keypoint, angle, scale, dx, dy, rows, cols, **params): +def keypoint_shift_scale_rotate( + keypoint: KeypointInternalType, angle: float, scale: float, dx: int, dy: int, rows: int, cols: int, **params: Any +) -> KeypointInternalType: ( x, y, @@ -248,12 +258,22 @@ def keypoint_shift_scale_rotate(keypoint, angle, scale, dx, dy, rows, cols, **pa x, y = cv2.transform(np.array([[[x, y]]]), matrix).squeeze() angle = a + math.radians(angle) - scale = s * scale + scale *= s return x, y, angle, scale -def bbox_shift_scale_rotate(bbox, angle, scale, dx, dy, rotate_method, rows, cols, **kwargs): # skipcq: PYL-W0613 +def bbox_shift_scale_rotate( + bbox: BoxInternalType, + angle: float, + scale: float, + dx: int, + dy: int, + rotate_method: str, + rows: int, + cols: int, + **kwargs: Any, +) -> BoxInternalType: """Rotates, shifts and scales a bounding box. Rotation is made by angle degrees, scaling is made by scale factor and shifting is made by dx and dy. @@ -311,7 +331,7 @@ def elastic_transform( random_state: Optional[np.random.RandomState] = None, approximate: bool = False, same_dxdy: bool = False, -): +) -> np.ndarray: """Elastic deformation of images as described in [Simard2003]_ (with modifications). Based on https://gist.github.com/ernestum/601cdf56d2b424757de5 @@ -384,7 +404,7 @@ def elastic_transform( @preserve_channel_dim -def resize(img, height, width, interpolation=cv2.INTER_LINEAR): +def resize(img: np.ndarray, height: int, width: int, interpolation: int = cv2.INTER_LINEAR) -> np.ndarray: img_height, img_width = img.shape[:2] if height == img_height and width == img_width: return img @@ -415,22 +435,14 @@ def keypoint_scale(keypoint: KeypointInternalType, scale_x: float, scale_y: floa return x * scale_x, y * scale_y, angle, scale * max(scale_x, scale_y) -def py3round(number): - """Unified rounding in all python versions.""" - if abs(round(number) - number) == 0.5: - return int(2.0 * round(number / 2.0)) - - return int(round(number)) - - -def _func_max_size(img, max_size, interpolation, func): +def _func_max_size(img: np.ndarray, max_size: int, interpolation: int, func: Callable[..., Any]) -> np.ndarray: height, width = img.shape[:2] scale = max_size / float(func(width, height)) if scale != 1.0: - new_height, new_width = tuple(py3round(dim * scale) for dim in (height, width)) - img = resize(img, height=new_height, width=new_width, interpolation=interpolation) + new_height, new_width = tuple(round(dim * scale) for dim in (height, width)) + return resize(img, height=new_height, width=new_width, interpolation=interpolation) return img @@ -454,8 +466,8 @@ def perspective( border_mode: int, keep_size: bool, interpolation: int, -): - h, w = img.shape[:2] +) -> np.ndarray: + height, width = img.shape[:2] perspective_func = _maybe_process_in_chunks( cv2.warpPerspective, M=matrix, @@ -467,7 +479,7 @@ def perspective( warped = perspective_func(img) if keep_size: - return resize(warped, h, w, interpolation=interpolation) + return resize(warped, height, width, interpolation=interpolation) return warped @@ -494,7 +506,10 @@ def perspective_bbox( y1 = min(y1, y) y2 = max(y2, y) - return normalize_bbox((x1, y1, x2, y2), height if keep_size else max_height, width if keep_size else max_width) + return cast( + BoxInternalType, + normalize_bbox((x1, y1, x2, y2), height if keep_size else max_height, width if keep_size else max_width), + ) def rotation2DMatrixToEulerAngles(matrix: np.ndarray, y_up: bool = False) -> float: @@ -565,7 +580,7 @@ def warp_affine( def keypoint_affine( keypoint: KeypointInternalType, matrix: skimage.transform.ProjectiveTransform, - scale: dict, + scale: Dict[str, Any], ) -> KeypointInternalType: if _is_identity_matrix(matrix): return keypoint @@ -612,7 +627,7 @@ def bbox_affine( y_min = np.min(points[:, 1]) y_max = np.max(points[:, 1]) - return normalize_bbox((x_min, y_min, x_max, y_max), output_shape[0], output_shape[1]) + return cast(BoxInternalType, normalize_bbox((x_min, y_min, x_max, y_max), output_shape[0], output_shape[1])) @preserve_channel_dim @@ -662,7 +677,7 @@ def fix_point(pt1: float, pt2: float, max_val: float) -> Tuple[float, float]: x1, x2 = fix_point(x1, x2, cols) y1, y2 = fix_point(y1, y2, rows) - return normalize_bbox((x1, y1, x2, y2), rows, cols) + return cast(KeypointInternalType, normalize_bbox((x1, y1, x2, y2), rows, cols)) def keypoint_safe_rotate( @@ -749,7 +764,7 @@ def to_distance_maps( def from_distance_maps( distance_maps: np.ndarray, inverted: bool, - if_not_found_coords: Optional[Union[Sequence[int], dict]], + if_not_found_coords: Optional[Union[Sequence[int], Dict[str, Any]]], threshold: Optional[float] = None, ) -> List[Tuple[float, float]]: """Convert outputs of ``to_distance_maps()`` to ``KeypointsOnImage``. @@ -864,7 +879,7 @@ def bbox_piecewise_affine( y1 = keypoints_arr[:, 1].min() x2 = keypoints_arr[:, 0].max() y2 = keypoints_arr[:, 1].max() - return normalize_bbox((x1, y1, x2, y2), h, w) + return cast(BoxInternalType, normalize_bbox((x1, y1, x2, y2), h, w)) def vflip(img: np.ndarray) -> np.ndarray: @@ -893,7 +908,7 @@ def rot90(img: np.ndarray, factor: int) -> np.ndarray: return np.ascontiguousarray(img) -def bbox_vflip(bbox: BoxInternalType, rows: int, cols: int) -> BoxInternalType: # skipcq: PYL-W0613 +def bbox_vflip(bbox: BoxInternalType, rows: int, cols: int) -> BoxInternalType: """Flip a bounding box vertically around the x-axis. Args: @@ -909,7 +924,7 @@ def bbox_vflip(bbox: BoxInternalType, rows: int, cols: int) -> BoxInternalType: return x_min, 1 - y_max, x_max, 1 - y_min -def bbox_hflip(bbox: BoxInternalType, rows: int, cols: int) -> BoxInternalType: # skipcq: PYL-W0613 +def bbox_hflip(bbox: BoxInternalType, rows: int, cols: int) -> BoxInternalType: """Flip a bounding box horizontally around the y-axis. Args: @@ -949,7 +964,7 @@ def bbox_flip(bbox: BoxInternalType, d: int, rows: int, cols: int) -> BoxInterna bbox = bbox_hflip(bbox, rows, cols) bbox = bbox_vflip(bbox, rows, cols) else: - raise ValueError("Invalid d value {}. Valid values are -1, 0 and 1".format(d)) + raise ValueError(f"Invalid d value {d}. Valid values are -1, 0 and 1") return bbox @@ -1155,9 +1170,7 @@ def optical_distortion( camera_matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float32) distortion = np.array([k, k, 0, 0, 0], dtype=np.float32) - map1, map2 = cv2.initUndistortRectifyMap( - camera_matrix, distortion, None, None, (width, height), cv2.CV_32FC1 # type: ignore[attr-defined] - ) + map1, map2 = cv2.initUndistortRectifyMap(camera_matrix, distortion, None, None, (width, height), cv2.CV_32FC1) return cv2.remap(img, map1, map2, interpolation=interpolation, borderMode=border_mode, borderValue=value) @@ -1165,8 +1178,8 @@ def optical_distortion( def grid_distortion( img: np.ndarray, num_steps: int = 10, - xsteps: Tuple = (), - ysteps: Tuple = (), + xsteps: Tuple[()] = (), + ysteps: Tuple[()] = (), interpolation: int = cv2.INTER_LINEAR, border_mode: int = cv2.BORDER_REFLECT_101, value: Optional[ImageColorType] = None, diff --git a/albumentations/augmentations/geometric/resize.py b/albumentations/augmentations/geometric/resize.py index 913c08429..e9f278909 100644 --- a/albumentations/augmentations/geometric/resize.py +++ b/albumentations/augmentations/geometric/resize.py @@ -1,14 +1,15 @@ import random -from typing import Dict, Sequence, Tuple, Union +from typing import Any, Dict, Sequence, Tuple, Union import cv2 import numpy as np -from ...core.transforms_interface import ( +from ...core.transforms_interface import DualTransform, to_tuple +from ...core.types import ( BoxInternalType, - DualTransform, KeypointInternalType, - to_tuple, + ScaleFloatType, + ScaleIntType, ) from . import functional as F @@ -35,25 +36,35 @@ class RandomScale(DualTransform): uint8, float32 """ - def __init__(self, scale_limit=0.1, interpolation=cv2.INTER_LINEAR, always_apply=False, p=0.5): - super(RandomScale, self).__init__(always_apply, p) + def __init__( + self, + scale_limit: ScaleFloatType = 0.1, + interpolation: int = cv2.INTER_LINEAR, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.scale_limit = to_tuple(scale_limit, bias=1.0) self.interpolation = interpolation - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"scale": random.uniform(self.scale_limit[0], self.scale_limit[1])} - def apply(self, img, scale=0, interpolation=cv2.INTER_LINEAR, **params): + def apply( + self, img: np.ndarray, scale: float = 0, interpolation: int = cv2.INTER_LINEAR, **params: Any + ) -> np.ndarray: return F.scale(img, scale, interpolation) - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: # Bounding box coordinates are scale invariant return bbox - def apply_to_keypoint(self, keypoint, scale=0, **params): + def apply_to_keypoint( + self, keypoint: KeypointInternalType, scale: float = 0, **params: Any + ) -> KeypointInternalType: return F.keypoint_scale(keypoint, scale, scale) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return {"interpolation": self.interpolation, "scale_limit": to_tuple(self.scale_limit, bias=-1.0)} @@ -80,20 +91,22 @@ def __init__( always_apply: bool = False, p: float = 1, ): - super(LongestMaxSize, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.interpolation = interpolation self.max_size = max_size def apply( - self, img: np.ndarray, max_size: int = 1024, interpolation: int = cv2.INTER_LINEAR, **params + self, img: np.ndarray, max_size: int = 1024, interpolation: int = cv2.INTER_LINEAR, **params: Any ) -> np.ndarray: return F.longest_max_size(img, max_size=max_size, interpolation=interpolation) - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: # Bounding box coordinates are scale invariant return bbox - def apply_to_keypoint(self, keypoint: KeypointInternalType, max_size: int = 1024, **params) -> KeypointInternalType: + def apply_to_keypoint( + self, keypoint: KeypointInternalType, max_size: int = 1024, **params: Any + ) -> KeypointInternalType: height = params["rows"] width = params["cols"] @@ -130,19 +143,21 @@ def __init__( always_apply: bool = False, p: float = 1, ): - super(SmallestMaxSize, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.interpolation = interpolation self.max_size = max_size def apply( - self, img: np.ndarray, max_size: int = 1024, interpolation: int = cv2.INTER_LINEAR, **params + self, img: np.ndarray, max_size: int = 1024, interpolation: int = cv2.INTER_LINEAR, **params: Any ) -> np.ndarray: return F.smallest_max_size(img, max_size=max_size, interpolation=interpolation) - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return bbox - def apply_to_keypoint(self, keypoint: KeypointInternalType, max_size: int = 1024, **params) -> KeypointInternalType: + def apply_to_keypoint( + self, keypoint: KeypointInternalType, max_size: int = 1024, **params: Any + ) -> KeypointInternalType: height = params["rows"] width = params["cols"] @@ -174,25 +189,27 @@ class Resize(DualTransform): uint8, float32 """ - def __init__(self, height, width, interpolation=cv2.INTER_LINEAR, always_apply=False, p=1): - super(Resize, self).__init__(always_apply, p) + def __init__( + self, height: int, width: int, interpolation: int = cv2.INTER_LINEAR, always_apply: bool = False, p: float = 1 + ): + super().__init__(always_apply, p) self.height = height self.width = width self.interpolation = interpolation - def apply(self, img, interpolation=cv2.INTER_LINEAR, **params): + def apply(self, img: np.ndarray, interpolation: int = cv2.INTER_LINEAR, **params: Any) -> np.ndarray: return F.resize(img, height=self.height, width=self.width, interpolation=interpolation) - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: # Bounding box coordinates are scale invariant return bbox - def apply_to_keypoint(self, keypoint, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: height = params["rows"] width = params["cols"] scale_x = self.width / width scale_y = self.height / height return F.keypoint_scale(keypoint, scale_x, scale_y) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("height", "width", "interpolation") diff --git a/albumentations/augmentations/geometric/rotate.py b/albumentations/augmentations/geometric/rotate.py index 7578e60ea..32c6bb615 100644 --- a/albumentations/augmentations/geometric/rotate.py +++ b/albumentations/augmentations/geometric/rotate.py @@ -1,16 +1,19 @@ import math import random -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast import cv2 import numpy as np -from ...core.transforms_interface import ( +from ...core.transforms_interface import DualTransform, FillValueType, to_tuple +from ...core.types import ( BoxInternalType, - DualTransform, - FillValueType, + BoxType, KeypointInternalType, - to_tuple, + KeypointType, + ScaleFloatType, + ScaleIntType, + ScaleType, ) from ..crops import functional as FCrops from . import functional as F @@ -22,7 +25,7 @@ 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. + p: probability of applying the transform. Default: 0.5. Targets: image, mask, bboxes, keypoints @@ -31,24 +34,24 @@ class RandomRotate90(DualTransform): uint8, float32 """ - def apply(self, img, factor=0, **params): + def apply(self, img: np.ndarray, factor: float = 0, **params: Any) -> np.ndarray: """ 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): + def get_params(self) -> Dict[str, int]: # Random int in the range [0, 3] return {"factor": random.randint(0, 3)} - def apply_to_bbox(self, bbox, factor=0, **params): + def apply_to_bbox(self, bbox: BoxInternalType, factor: int = 0, **params: Any) -> BoxInternalType: return F.bbox_rot90(bbox, factor, **params) - def apply_to_keypoint(self, keypoint, factor=0, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, factor: int = 0, **params: Any) -> BoxInternalType: return F.keypoint_rot90(keypoint, factor, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -82,17 +85,17 @@ class Rotate(DualTransform): 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, + limit: ScaleIntType = 90, + interpolation: int = cv2.INTER_LINEAR, + border_mode: int = cv2.BORDER_REFLECT_101, + value: Optional[Union[int, float, Tuple[int, int], Tuple[float, float]]] = None, + mask_value: Optional[Union[int, float, Tuple[int, int], Tuple[float, float]]] = None, + rotate_method: str = "largest_box", + crop_border: bool = False, + always_apply: bool = False, + p: float = 0.5, ): - super(Rotate, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.limit = to_tuple(limit) self.interpolation = interpolation self.border_mode = border_mode @@ -105,35 +108,72 @@ def __init__( 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 - ): + self, + img: np.ndarray, + angle: float = 0, + interpolation: int = cv2.INTER_LINEAR, + x_min: int = -1, + x_max: int = -1, + y_min: int = -1, + y_max: int = -1, + **params: Any, + ) -> np.ndarray: 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 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) + def apply_to_mask( + self, + mask: np.ndarray, + angle: float, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + **params: Any, + ) -> np.ndarray: + img_out = F.rotate(mask, 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 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): + def apply_to_bbox( + self, + bbox: BoxInternalType, + angle: float, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + cols: int = 0, + rows: int = 0, + **params: Any, + ) -> np.ndarray: 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 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 - ): + self, + keypoint: KeypointInternalType, + angle: float, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + cols: int = 0, + rows: int = 0, + **params: Any, + ) -> KeypointInternalType: 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 FCrops.crop_keypoint_by_coords(keypoint_out, (x_min, y_min, x_max, y_max)) return keypoint_out @staticmethod - def _rotated_rect_with_max_area(h, w, angle): + def _rotated_rect_with_max_area(h: int, w: int, angle: float) -> Dict[str, int]: """ Given a rectangle of size wxh that has been rotated by 'angle' (in degrees), computes the width and height of the largest possible @@ -177,7 +217,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A out_params.update(self._rotated_rect_with_max_area(h, w, out_params["angle"])) return out_params - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("limit", "interpolation", "border_mode", "value", "mask_value", "rotate_method", "crop_border") @@ -220,20 +260,20 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(SafeRotate, self).__init__(always_apply, p) + super().__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(self, img: np.ndarray, matrix: np.ndarray = np.array(None), **params: Any) -> np.ndarray: + return F.safe_rotate(img, matrix, cast(int, 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_mask(self, mask: np.ndarray, matrix: np.ndarray = np.array(None), **params: Any) -> np.ndarray: + return F.safe_rotate(mask, 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: + def apply_to_bbox(self, bbox: BoxInternalType, cols: int = 0, rows: int = 0, **params: Any) -> BoxInternalType: return F.bbox_safe_rotate(bbox, params["matrix"], cols, rows) def apply_to_keypoint( @@ -244,7 +284,7 @@ def apply_to_keypoint( scale_y: float = 0, cols: int = 0, rows: int = 0, - **params + **params: Any, ) -> KeypointInternalType: return F.keypoint_safe_rotate(keypoint, params["matrix"], angle, scale_x, scale_y, cols, rows) diff --git a/albumentations/augmentations/geometric/transforms.py b/albumentations/augmentations/geometric/transforms.py index 945e4a33c..56cd92939 100644 --- a/albumentations/augmentations/geometric/transforms.py +++ b/albumentations/augmentations/geometric/transforms.py @@ -1,7 +1,7 @@ import math import random from enum import Enum -from typing import Dict, Optional, Sequence, Tuple, Union +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast import cv2 import numpy as np @@ -10,13 +10,20 @@ from albumentations.core.bbox_utils import denormalize_bbox, normalize_bbox from ... import random_utils -from ...core.transforms_interface import ( +from ...core.transforms_interface import DualTransform, to_tuple +from ...core.types import ( BoxInternalType, - DualTransform, + BoxType, + ColorType, ImageColorType, KeypointInternalType, + KeypointType, + NumType, + ScalarType, ScaleFloatType, - to_tuple, + ScaleIntType, + ScaleType, + SizeType, ) from ..functional import bbox_from_mask from . import functional as F @@ -81,20 +88,20 @@ class ShiftScaleRotate(DualTransform): def __init__( self, - shift_limit=0.0625, - scale_limit=0.1, - rotate_limit=45, - interpolation=cv2.INTER_LINEAR, - border_mode=cv2.BORDER_REFLECT_101, - value=None, - mask_value=None, - shift_limit_x=None, - shift_limit_y=None, - rotate_method="largest_box", - always_apply=False, - p=0.5, + shift_limit: ScaleFloatType = 0.0625, + scale_limit: ScaleFloatType = 0.1, + rotate_limit: int = 45, + interpolation: int = cv2.INTER_LINEAR, + border_mode: int = cv2.BORDER_REFLECT_101, + value: Optional[Tuple[int, ...]] = None, + mask_value: Optional[Tuple[int, ...]] = None, + shift_limit_x: Optional[ScaleFloatType] = None, + shift_limit_y: Optional[ScaleFloatType] = None, + rotate_method: str = "largest_box", + always_apply: bool = False, + p: float = 0.5, ): - super(ShiftScaleRotate, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.shift_limit_x = to_tuple(shift_limit_x if shift_limit_x is not None else shift_limit) self.shift_limit_y = to_tuple(shift_limit_y if shift_limit_y is not None else shift_limit) self.scale_limit = to_tuple(scale_limit, bias=1.0) @@ -108,16 +115,37 @@ def __init__( if self.rotate_method not in ["largest_box", "ellipse"]: raise ValueError(f"Rotation method {self.rotate_method} is not valid.") - def apply(self, img, angle=0, scale=0, dx=0, dy=0, interpolation=cv2.INTER_LINEAR, **params): + def apply( + self, + img: np.ndarray, + angle: float = 0, + scale: float = 0, + dx: int = 0, + dy: int = 0, + interpolation: int = cv2.INTER_LINEAR, + **params: Any, + ) -> np.ndarray: return F.shift_scale_rotate(img, angle, scale, dx, dy, interpolation, self.border_mode, self.value) - def apply_to_mask(self, img, angle=0, scale=0, dx=0, dy=0, **params): - return F.shift_scale_rotate(img, angle, scale, dx, dy, cv2.INTER_NEAREST, self.border_mode, self.mask_value) + def apply_to_mask( + self, mask: np.ndarray, angle: float = 0, scale: float = 0, dx: int = 0, dy: int = 0, **params: Any + ) -> np.ndarray: + return F.shift_scale_rotate(mask, angle, scale, dx, dy, cv2.INTER_NEAREST, self.border_mode, self.mask_value) - def apply_to_keypoint(self, keypoint, angle=0, scale=0, dx=0, dy=0, rows=0, cols=0, **params): + def apply_to_keypoint( + self, + keypoint: KeypointInternalType, + angle: float = 0, + scale: float = 0, + dx: int = 0, + dy: int = 0, + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> KeypointInternalType: return F.keypoint_shift_scale_rotate(keypoint, angle, scale, dx, dy, rows, cols) - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "angle": random.uniform(self.rotate_limit[0], self.rotate_limit[1]), "scale": random.uniform(self.scale_limit[0], self.scale_limit[1]), @@ -125,10 +153,12 @@ def get_params(self): "dy": random.uniform(self.shift_limit_y[0], self.shift_limit_y[1]), } - def apply_to_bbox(self, bbox, angle, scale, dx, dy, **params): + def apply_to_bbox( + self, bbox: BoxInternalType, angle: float, scale: float, dx: int, dy: int, **params: Any + ) -> BoxInternalType: return F.bbox_shift_scale_rotate(bbox, angle, scale, dx, dy, self.rotate_method, **params) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return { "shift_limit_x": self.shift_limit_x, "shift_limit_y": self.shift_limit_y, @@ -179,19 +209,19 @@ class ElasticTransform(DualTransform): def __init__( self, - alpha=1, - sigma=50, - alpha_affine=50, - interpolation=cv2.INTER_LINEAR, - border_mode=cv2.BORDER_REFLECT_101, - value=None, - mask_value=None, - always_apply=False, - approximate=False, - same_dxdy=False, - p=0.5, + alpha: float = 1, + sigma: float = 50, + alpha_affine: float = 50, + interpolation: int = cv2.INTER_LINEAR, + border_mode: int = cv2.BORDER_REFLECT_101, + value: Optional[Union[int, float, List[int], List[float]]] = None, + mask_value: Optional[Union[int, float, List[int], List[float]]] = None, + always_apply: bool = False, + approximate: bool = False, + same_dxdy: bool = False, + p: float = 0.5, ): - super(ElasticTransform, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.alpha = alpha self.alpha_affine = alpha_affine self.sigma = sigma @@ -202,7 +232,9 @@ def __init__( self.approximate = approximate self.same_dxdy = same_dxdy - def apply(self, img, random_state=None, interpolation=cv2.INTER_LINEAR, **params): + def apply( + self, img: np.ndarray, random_state: Optional[int] = None, interpolation: int = cv2.INTER_LINEAR, **params: Any + ) -> np.ndarray: return F.elastic_transform( img, self.alpha, @@ -216,9 +248,9 @@ def apply(self, img, random_state=None, interpolation=cv2.INTER_LINEAR, **params self.same_dxdy, ) - def apply_to_mask(self, img, random_state=None, **params): + def apply_to_mask(self, mask: np.ndarray, random_state: Optional[int] = None, **params: Any) -> np.ndarray: return F.elastic_transform( - img, + mask, self.alpha, self.sigma, self.alpha_affine, @@ -230,7 +262,9 @@ def apply_to_mask(self, img, random_state=None, **params): self.same_dxdy, ) - def apply_to_bbox(self, bbox, random_state=None, **params): + def apply_to_bbox( + self, bbox: BoxInternalType, random_state: Optional[int] = None, **params: Any + ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] mask = np.zeros((rows, cols), dtype=np.uint8) bbox_denorm = F.denormalize_bbox(bbox, rows, cols) @@ -249,13 +283,12 @@ def apply_to_bbox(self, bbox, random_state=None, **params): self.approximate, ) bbox_returned = bbox_from_mask(mask) - bbox_returned = F.normalize_bbox(bbox_returned, rows, cols) - return bbox_returned + return cast(BoxInternalType, F.normalize_bbox(bbox_returned, rows, cols)) - def get_params(self): + def get_params(self) -> Dict[str, int]: return {"random_state": random.randint(0, 10000)} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "alpha", "sigma", @@ -273,10 +306,10 @@ class Perspective(DualTransform): """Perform a random four point perspective transform of the input. Args: - scale (float or (float, float)): standard deviation of the normal distributions. These are used to sample + scale: standard deviation of the normal distributions. These are used to sample the random distances of the subimage's corners from the full image's corners. If scale is a single float value, the range will be (0, scale). Default: (0.05, 0.1). - keep_size (bool): Whether to resize image’s back to their original size after applying the perspective + keep_size: Whether to resize image’s back to their original size after applying the perspective transform. If set to False, the resulting images may end up having different shapes and will always be a list, never an array. Default: True pad_mode (OpenCV flag): OpenCV border mode. @@ -300,15 +333,15 @@ class Perspective(DualTransform): def __init__( self, - scale=(0.05, 0.1), - keep_size=True, - pad_mode=cv2.BORDER_CONSTANT, - pad_val=0, - mask_pad_val=0, - fit_output=False, - interpolation=cv2.INTER_LINEAR, - always_apply=False, - p=0.5, + scale: ScaleFloatType = (0.05, 0.1), + keep_size: bool = True, + pad_mode: int = cv2.BORDER_CONSTANT, + pad_val: Union[int, float, List[float], List[int]] = 0, + mask_pad_val: Union[int, float, List[float], List[int]] = 0, + fit_output: bool = False, + interpolation: int = cv2.INTER_LINEAR, + always_apply: bool = False, + p: float = 0.5, ): super().__init__(always_apply, p) self.scale = to_tuple(scale, 0) @@ -319,25 +352,46 @@ def __init__( self.fit_output = fit_output self.interpolation = interpolation - def apply(self, img, matrix=None, max_height=None, max_width=None, **params): + def apply( + self, + img: np.ndarray, + matrix: np.ndarray, + max_height: int, + max_width: int, + **params: Any, + ) -> np.ndarray: return F.perspective( img, matrix, max_width, max_height, self.pad_val, self.pad_mode, self.keep_size, params["interpolation"] ) - def apply_to_bbox(self, bbox, matrix=None, max_height=None, max_width=None, **params): + def apply_to_bbox( + self, + bbox: BoxInternalType, + matrix: np.ndarray, + max_height: int, + max_width: int, + **params: Any, + ) -> BoxInternalType: return F.perspective_bbox(bbox, params["rows"], params["cols"], matrix, max_width, max_height, self.keep_size) - def apply_to_keypoint(self, keypoint, matrix=None, max_height=None, max_width=None, **params): + def apply_to_keypoint( + self, + keypoint: KeypointInternalType, + matrix: np.ndarray, + max_height: int, + max_width: int, + **params: Any, + ) -> np.ndarray: return F.perspective_keypoint( keypoint, params["rows"], params["cols"], matrix, max_width, max_height, self.keep_size ) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): - h, w = params["image"].shape[:2] + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: + height, width = params["image"].shape[:2] scale = random_utils.uniform(*self.scale) points = random_utils.normal(0, scale, [4, 2]) @@ -351,8 +405,8 @@ def get_params_dependent_on_targets(self, params): # bottom left points[3, 1] = 1.0 - points[3, 1] # h = 1.0 - jitter - points[:, 0] *= w - points[:, 1] *= h + points[:, 0] *= width + points[:, 1] *= height # Obtain a consistent order of the points and unpack them individually. # Warning: don't just do (tl, tr, br, bl) = _order_points(...) @@ -405,13 +459,13 @@ def get_params_dependent_on_targets(self, params): m = cv2.getPerspectiveTransform(points, dst) if self.fit_output: - m, max_width, max_height = self._expand_transform(m, (h, w)) + m, max_width, max_height = self._expand_transform(m, (height, width)) return {"matrix": m, "max_height": max_height, "max_width": max_width, "interpolation": self.interpolation} @classmethod - def _expand_transform(cls, matrix, shape): - height, width = shape + def _expand_transform(cls, matrix: np.ndarray, shape: SizeType) -> Tuple[np.ndarray, int, int]: + height, width = shape[:2] # do not use width-1 or height-1 here, as for e.g. width=3, height=2, max_height # the bottom right coordinate is at (3.0, 2.0) and not (2.0, 1.0) rect = np.array([[0, 0], [width, 0], [width, height], [0, height]], dtype=np.float32) @@ -444,7 +498,7 @@ def _order_points(pts: np.ndarray) -> np.ndarray: return np.array([tl, tr, br, bl], dtype=np.float32) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return "scale", "keep_size", "pad_mode", "pad_val", "mask_pad_val", "fit_output", "interpolation" @@ -548,15 +602,15 @@ class Affine(DualTransform): def __init__( self, - scale: Optional[Union[float, Sequence[float], dict]] = None, - translate_percent: Optional[Union[float, Sequence[float], dict]] = None, - translate_px: Optional[Union[int, Sequence[int], dict]] = None, - rotate: Optional[Union[float, Sequence[float]]] = None, - shear: Optional[Union[float, Sequence[float], dict]] = None, + scale: Optional[Union[ScaleFloatType, Dict[str, Any]]] = None, + translate_percent: Optional[Union[float, Tuple[float, float], Dict[str, Any]]] = None, + translate_px: Optional[Union[int, Tuple[int, int], Dict[str, Any]]] = None, + rotate: Optional[ScaleFloatType] = None, + shear: Optional[Union[ScaleFloatType, Dict[str, Any]]] = None, interpolation: int = cv2.INTER_LINEAR, mask_interpolation: int = cv2.INTER_NEAREST, - cval: Union[int, float, Sequence[int], Sequence[float]] = 0, - cval_mask: Union[int, float, Sequence[int], Sequence[float]] = 0, + cval: Union[int, float, Tuple[int, int], Tuple[float, float]] = 0, + cval_mask: Union[int, float, Tuple[int, int], Tuple[float, float]] = 0, mode: int = cv2.BORDER_CONSTANT, fit_output: bool = False, keep_ratio: bool = False, @@ -591,11 +645,9 @@ def __init__( self.rotate_method = rotate_method if self.keep_ratio and self.scale["x"] != self.scale["y"]: - raise ValueError( - "When keep_ratio is True, the x and y scale range should be identical. got {}".format(self.scale) - ) + raise ValueError(f"When keep_ratio is True, the x and y scale range should be identical. got {self.scale}") - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "interpolation", "mask_interpolation", @@ -613,7 +665,9 @@ def get_transform_init_args_names(self): ) @staticmethod - def _handle_dict_arg(val: Union[float, Sequence[float], dict], name: str, default: float = 1.0): + def _handle_dict_arg( + val: Union[float, Tuple[float, float], Dict[str, Any]], name: str, default: float = 1.0 + ) -> Dict[str, Any]: if isinstance(val, dict): if "x" not in val and "y" not in val: raise ValueError( @@ -627,9 +681,9 @@ def _handle_dict_arg(val: Union[float, Sequence[float], dict], name: str, defaul @classmethod def _handle_translate_arg( cls, - translate_px: Optional[Union[float, Sequence[float], dict]], - translate_percent: Optional[Union[float, Sequence[float], dict]], - ): + translate_px: Optional[Union[float, Tuple[float, float], Dict[str, Any]]], + translate_percent: Optional[Union[float, Tuple[float, float], Dict[str, Any]]], + ) -> Any: if translate_percent is None and translate_px is None: translate_px = 0 @@ -652,12 +706,12 @@ def apply( img: np.ndarray, matrix: skimage.transform.ProjectiveTransform = None, output_shape: Sequence[int] = (), - **params + **params: Any, ) -> np.ndarray: return F.warp_affine( img, matrix, - interpolation=self.interpolation, + interpolation=cast(int, self.interpolation), cval=self.cval, mode=self.mode, output_shape=output_shape, @@ -665,13 +719,13 @@ def apply( def apply_to_mask( self, - img: np.ndarray, + mask: np.ndarray, matrix: skimage.transform.ProjectiveTransform = None, output_shape: Sequence[int] = (), - **params + **params: Any, ) -> np.ndarray: return F.warp_affine( - img, + mask, matrix, interpolation=self.mask_interpolation, cval=self.cval_mask, @@ -686,7 +740,7 @@ def apply_to_bbox( rows: int = 0, cols: int = 0, output_shape: Sequence[int] = (), - **params + **params: Any, ) -> BoxInternalType: return F.bbox_affine(bbox, matrix, self.rotate_method, rows, cols, output_shape) @@ -694,26 +748,26 @@ def apply_to_keypoint( self, keypoint: KeypointInternalType, matrix: Optional[skimage.transform.ProjectiveTransform] = None, - scale: Optional[dict] = None, - **params + scale: Optional[Dict[str, Any]] = None, + **params: Any, ) -> KeypointInternalType: assert scale is not None and matrix is not None return F.keypoint_affine(keypoint, matrix=matrix, scale=scale) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params: dict) -> dict: - h, w = params["image"].shape[:2] + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: + height, width = params["image"].shape[:2] translate: Dict[str, Union[int, float]] if self.translate_px is not None: translate = {key: random.randint(*value) for key, value in self.translate_px.items()} elif self.translate_percent is not None: translate = {key: random.uniform(*value) for key, value in self.translate_percent.items()} - translate["x"] = translate["x"] * w - translate["y"] = translate["y"] * h + translate["x"] = translate["x"] * width + translate["y"] = translate["y"] * height else: translate = {"x": 0, "y": 0} @@ -728,8 +782,8 @@ def get_params_dependent_on_targets(self, params: dict) -> dict: # for images we use additional shifts of (0.5, 0.5) as otherwise # we get an ugly black border for 90deg rotations - shift_x = w / 2 - 0.5 - shift_y = h / 2 - 0.5 + shift_x = width / 2 - 0.5 + shift_y = height / 2 - 0.5 matrix_to_topleft = skimage.transform.SimilarityTransform(translation=[-shift_x, -shift_y]) matrix_shear_y_rot = skimage.transform.AffineTransform(rotation=-np.pi / 2) @@ -855,8 +909,8 @@ class PiecewiseAffine(DualTransform): def __init__( self, scale: ScaleFloatType = (0.03, 0.05), - nb_rows: Union[int, Sequence[int]] = 4, - nb_cols: Union[int, Sequence[int]] = 4, + nb_rows: Union[ScaleIntType] = 4, + nb_cols: Union[ScaleIntType] = 4, interpolation: int = 1, mask_interpolation: int = 0, cval: int = 0, @@ -867,7 +921,7 @@ def __init__( keypoints_threshold: float = 0.01, p: float = 0.5, ): - super(PiecewiseAffine, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.scale = to_tuple(scale, scale) self.nb_rows = to_tuple(nb_rows, nb_rows) @@ -880,7 +934,7 @@ def __init__( self.absolute_scale = absolute_scale self.keypoints_threshold = keypoints_threshold - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "scale", "nb_rows", @@ -895,11 +949,11 @@ def get_transform_init_args_names(self): ) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params) -> dict: - h, w = params["image"].shape[:2] + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: + height, width = params["image"].shape[:2] nb_rows = np.clip(random.randint(*self.nb_rows), 2, None) nb_cols = np.clip(random.randint(*self.nb_cols), 2, None) @@ -915,8 +969,8 @@ def get_params_dependent_on_targets(self, params) -> dict: if not np.any(jitter > 0): return {"matrix": None} - y = np.linspace(0, h, nb_rows) - x = np.linspace(0, w, nb_cols) + y = np.linspace(0, height, nb_rows) + x = np.linspace(0, width, nb_cols) # (H, W) and (H, W) for H=rows, W=cols xx_src, yy_src = np.meshgrid(x, y) @@ -925,11 +979,11 @@ def get_params_dependent_on_targets(self, params) -> dict: points_src = np.dstack([yy_src.flat, xx_src.flat])[0] if self.absolute_scale: - jitter[:, 0] = jitter[:, 0] / h if h > 0 else 0.0 - jitter[:, 1] = jitter[:, 1] / w if w > 0 else 0.0 + jitter[:, 0] = jitter[:, 0] / height if height > 0 else 0.0 + jitter[:, 1] = jitter[:, 1] / width if width > 0 else 0.0 - jitter[:, 0] = jitter[:, 0] * h - jitter[:, 1] = jitter[:, 1] * w + jitter[:, 0] = jitter[:, 0] * height + jitter[:, 1] = jitter[:, 1] * width points_dest = np.copy(points_src) points_dest[:, 0] = points_dest[:, 0] + jitter[:, 0] @@ -939,8 +993,8 @@ def get_params_dependent_on_targets(self, params) -> dict: # This is necessary, as otherwise keypoints could be augmented # outside of the image plane and these would be replaced by # (-1, -1), which would not conform with the behaviour of the other augmenters. - points_dest[:, 0] = np.clip(points_dest[:, 0], 0, h - 1) - points_dest[:, 1] = np.clip(points_dest[:, 1], 0, w - 1) + points_dest[:, 0] = np.clip(points_dest[:, 0], 0, height - 1) + points_dest[:, 1] = np.clip(points_dest[:, 1], 0, width - 1) matrix = skimage.transform.PiecewiseAffineTransform() matrix.estimate(points_src[:, ::-1], points_dest[:, ::-1]) @@ -950,14 +1004,14 @@ def get_params_dependent_on_targets(self, params) -> dict: } def apply( - self, img: np.ndarray, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, **params + self, img: np.ndarray, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, **params: Any ) -> np.ndarray: - return F.piecewise_affine(img, matrix, self.interpolation, self.mode, self.cval) + return F.piecewise_affine(img, matrix, cast(int, self.interpolation), self.mode, self.cval) def apply_to_mask( - self, img: np.ndarray, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, **params + self, mask: np.ndarray, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, **params: Any ) -> np.ndarray: - return F.piecewise_affine(img, matrix, self.mask_interpolation, self.mode, self.cval_mask) + return F.piecewise_affine(mask, matrix, self.mask_interpolation, self.mode, self.cval_mask) def apply_to_bbox( self, @@ -965,7 +1019,7 @@ def apply_to_bbox( rows: int = 0, cols: int = 0, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, - **params + **params: Any, ) -> BoxInternalType: return F.bbox_piecewise_affine(bbox, matrix, rows, cols, self.keypoints_threshold) @@ -975,8 +1029,8 @@ def apply_to_keypoint( rows: int = 0, cols: int = 0, matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, - **params - ): + **params: Any, + ) -> KeypointInternalType: return F.keypoint_piecewise_affine(keypoint, matrix, rows, cols, self.keypoints_threshold) @@ -1032,7 +1086,7 @@ def __init__( if (min_width is None) == (pad_width_divisor is None): raise ValueError("Only one of 'min_width' and 'pad_width_divisor' parameters must be set") - super(PadIfNeeded, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.min_height = min_height self.min_width = min_width self.pad_width_divisor = pad_width_divisor @@ -1042,8 +1096,8 @@ def __init__( self.value = value self.mask_value = mask_value - def update_params(self, params, **kwargs): - params = super(PadIfNeeded, self).update_params(params, **kwargs) + def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + params = super().update_params(params, **kwargs) rows = params["rows"] cols = params["cols"] @@ -1090,7 +1144,13 @@ def update_params(self, params, **kwargs): return params def apply( - self, img: np.ndarray, pad_top: int = 0, pad_bottom: int = 0, pad_left: int = 0, pad_right: int = 0, **params + self, + img: np.ndarray, + pad_top: int = 0, + pad_bottom: int = 0, + pad_left: int = 0, + pad_right: int = 0, + **params: Any, ) -> np.ndarray: return F.pad_with_params( img, @@ -1103,10 +1163,16 @@ def apply( ) def apply_to_mask( - self, img: np.ndarray, pad_top: int = 0, pad_bottom: int = 0, pad_left: int = 0, pad_right: int = 0, **params + self, + mask: np.ndarray, + pad_top: int = 0, + pad_bottom: int = 0, + pad_left: int = 0, + pad_right: int = 0, + **params: Any, ) -> np.ndarray: return F.pad_with_params( - img, + mask, pad_top, pad_bottom, pad_left, @@ -1124,11 +1190,11 @@ def apply_to_bbox( pad_right: int = 0, rows: int = 0, cols: int = 0, - **params + **params: Any, ) -> BoxInternalType: x_min, y_min, x_max, y_max = denormalize_bbox(bbox, rows, cols)[:4] bbox = x_min + pad_left, y_min + pad_top, x_max + pad_left, y_max + pad_top - return normalize_bbox(bbox, rows + pad_top + pad_bottom, cols + pad_left + pad_right) + return cast(BoxInternalType, normalize_bbox(bbox, rows + pad_top + pad_bottom, cols + pad_left + pad_right)) def apply_to_keypoint( self, @@ -1137,12 +1203,12 @@ def apply_to_keypoint( pad_bottom: int = 0, pad_left: int = 0, pad_right: int = 0, - **params + **params: Any, ) -> KeypointInternalType: x, y, angle, scale = keypoint[:4] return x + pad_left, y + pad_top, angle, scale - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "min_height", "min_width", @@ -1204,16 +1270,16 @@ class VerticalFlip(DualTransform): uint8, float32 """ - def apply(self, img: np.ndarray, **params) -> np.ndarray: + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.vflip(img) - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_vflip(bbox, **params) - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_vflip(keypoint, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1230,7 +1296,7 @@ class HorizontalFlip(DualTransform): uint8, float32 """ - def apply(self, img: np.ndarray, **params) -> np.ndarray: + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: if img.ndim == 3 and img.shape[2] > 1 and img.dtype == np.uint8: # Opencv is faster than numpy only in case of # non-gray scale 8bits images @@ -1238,13 +1304,13 @@ def apply(self, img: np.ndarray, **params) -> np.ndarray: return F.hflip(img) - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_hflip(bbox, **params) - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_hflip(keypoint, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1261,7 +1327,7 @@ class Flip(DualTransform): uint8, float32 """ - def apply(self, img: np.ndarray, d: int = 0, **params) -> np.ndarray: + def apply(self, img: np.ndarray, d: int = 0, **params: Any) -> np.ndarray: """Args: d (int): code that specifies how to flip the input. 0 for vertical flipping, 1 for horizontal flipping, -1 for both vertical and horizontal flipping (which is also could be seen as rotating the input by @@ -1269,17 +1335,17 @@ def apply(self, img: np.ndarray, d: int = 0, **params) -> np.ndarray: """ return F.random_flip(img, d) - def get_params(self): + def get_params(self) -> Dict[str, int]: # Random int in the range [-1, 1] return {"d": random.randint(-1, 1)} - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_flip(bbox, **params) - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_flip(keypoint, **params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1296,16 +1362,16 @@ class Transpose(DualTransform): uint8, float32 """ - def apply(self, img: np.ndarray, **params) -> np.ndarray: + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.transpose(img) - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return F.bbox_transpose(bbox, 0, **params) - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return F.keypoint_transpose(keypoint) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1345,7 +1411,7 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(OpticalDistortion, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.shift_limit = to_tuple(shift_limit) self.distort_limit = to_tuple(distort_limit) self.interpolation = interpolation @@ -1354,14 +1420,22 @@ def __init__( self.mask_value = mask_value def apply( - self, img: np.ndarray, k: int = 0, dx: int = 0, dy: int = 0, interpolation: int = cv2.INTER_LINEAR, **params + self, + img: np.ndarray, + k: int = 0, + dx: int = 0, + dy: int = 0, + interpolation: int = cv2.INTER_LINEAR, + **params: Any, ) -> np.ndarray: return F.optical_distortion(img, k, dx, dy, interpolation, self.border_mode, self.value) - def apply_to_mask(self, img: np.ndarray, k: int = 0, dx: int = 0, dy: int = 0, **params) -> np.ndarray: - return F.optical_distortion(img, k, dx, dy, cv2.INTER_NEAREST, self.border_mode, self.mask_value) + def apply_to_mask(self, mask: np.ndarray, k: int = 0, dx: int = 0, dy: int = 0, **params: Any) -> np.ndarray: + return F.optical_distortion(mask, k, dx, dy, cv2.INTER_NEAREST, self.border_mode, self.mask_value) - def apply_to_bbox(self, bbox: BoxInternalType, k: int = 0, dx: int = 0, dy: int = 0, **params) -> BoxInternalType: + def apply_to_bbox( + self, bbox: BoxInternalType, k: int = 0, dx: int = 0, dy: int = 0, **params: Any + ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] mask = np.zeros((rows, cols), dtype=np.uint8) bbox_denorm = F.denormalize_bbox(bbox, rows, cols) @@ -1370,17 +1444,16 @@ def apply_to_bbox(self, bbox: BoxInternalType, k: int = 0, dx: int = 0, dy: int mask[y_min:y_max, x_min:x_max] = 1 mask = F.optical_distortion(mask, k, dx, dy, cv2.INTER_NEAREST, self.border_mode, self.mask_value) bbox_returned = bbox_from_mask(mask) - bbox_returned = F.normalize_bbox(bbox_returned, rows, cols) - return bbox_returned + return cast(BoxInternalType, F.normalize_bbox(bbox_returned, rows, cols)) - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "k": random.uniform(self.distort_limit[0], self.distort_limit[1]), "dx": round(random.uniform(self.shift_limit[0], self.shift_limit[1])), "dy": round(random.uniform(self.shift_limit[0], self.shift_limit[1])), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "distort_limit", "shift_limit", @@ -1429,7 +1502,7 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(GridDistortion, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.num_steps = num_steps self.distort_limit = to_tuple(distort_limit) self.interpolation = interpolation @@ -1439,16 +1512,25 @@ def __init__( self.normalized = normalized def apply( - self, img: np.ndarray, stepsx: Tuple = (), stepsy: Tuple = (), interpolation: int = cv2.INTER_LINEAR, **params + self, + img: np.ndarray, + stepsx: Tuple[()] = (), + stepsy: Tuple[()] = (), + interpolation: int = cv2.INTER_LINEAR, + **params: Any, ) -> np.ndarray: return F.grid_distortion(img, self.num_steps, stepsx, stepsy, interpolation, self.border_mode, self.value) - def apply_to_mask(self, img: np.ndarray, stepsx: Tuple = (), stepsy: Tuple = (), **params) -> np.ndarray: + def apply_to_mask( + self, mask: np.ndarray, stepsx: Tuple[()] = (), stepsy: Tuple[()] = (), **params: Any + ) -> np.ndarray: return F.grid_distortion( - img, self.num_steps, stepsx, stepsy, cv2.INTER_NEAREST, self.border_mode, self.mask_value + mask, self.num_steps, stepsx, stepsy, cv2.INTER_NEAREST, self.border_mode, self.mask_value ) - def apply_to_bbox(self, bbox: BoxInternalType, stepsx: Tuple = (), stepsy: Tuple = (), **params) -> BoxInternalType: + def apply_to_bbox( + self, bbox: BoxInternalType, stepsx: Tuple[()] = (), stepsy: Tuple[()] = (), **params: Any + ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] mask = np.zeros((rows, cols), dtype=np.uint8) bbox_denorm = F.denormalize_bbox(bbox, rows, cols) @@ -1459,10 +1541,9 @@ def apply_to_bbox(self, bbox: BoxInternalType, stepsx: Tuple = (), stepsy: Tuple mask, self.num_steps, stepsx, stepsy, cv2.INTER_NEAREST, self.border_mode, self.mask_value ) bbox_returned = bbox_from_mask(mask) - bbox_returned = F.normalize_bbox(bbox_returned, rows, cols) - return bbox_returned + return cast(BoxInternalType, F.normalize_bbox(bbox_returned, rows, cols)) - def _normalize(self, h, w, xsteps, ysteps): + def _normalize(self, h: int, w: int, xsteps: List[float], ysteps: List[float]) -> Dict[str, Any]: # compensate for smaller last steps in source image. x_step = w // self.num_steps last_x_step = min(w, ((self.num_steps + 1) * x_step)) - (self.num_steps * x_step) @@ -1481,19 +1562,19 @@ def _normalize(self, h, w, xsteps, ysteps): return {"stepsx": xsteps, "stepsy": ysteps} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): - h, w = params["image"].shape[:2] + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: + height, width = params["image"].shape[:2] stepsx = [1 + random.uniform(self.distort_limit[0], self.distort_limit[1]) for _ in range(self.num_steps + 1)] stepsy = [1 + random.uniform(self.distort_limit[0], self.distort_limit[1]) for _ in range(self.num_steps + 1)] if self.normalized: - return self._normalize(h, w, stepsx, stepsy) + return self._normalize(height, width, stepsx, stepsy) return {"stepsx": stepsx, "stepsy": stepsy} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return "num_steps", "distort_limit", "interpolation", "border_mode", "value", "mask_value", "normalized" diff --git a/albumentations/augmentations/transforms.py b/albumentations/augmentations/transforms.py index 4491db990..438d05929 100644 --- a/albumentations/augmentations/transforms.py +++ b/albumentations/augmentations/transforms.py @@ -1,12 +1,10 @@ -from __future__ import absolute_import, division - import math import numbers import random import warnings from enum import IntEnum from types import LambdaType -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast import cv2 import numpy as np @@ -24,10 +22,17 @@ from ..core.transforms_interface import ( DualTransform, ImageOnlyTransform, + Interpolation, NoOp, - ScaleFloatType, to_tuple, ) +from ..core.types import ( + BoxInternalType, + KeypointInternalType, + ScaleFloatType, + ScaleIntType, + ScaleType, +) from ..core.utils import format_args from . import functional as F @@ -93,18 +98,23 @@ class RandomGridShuffle(DualTransform): """ def __init__(self, grid: Tuple[int, int] = (3, 3), always_apply: bool = False, p: float = 0.5): - super(RandomGridShuffle, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.grid = grid - def apply(self, img: np.ndarray, tiles: np.ndarray = np.array(None), **params): + def apply(self, img: np.ndarray, tiles: np.ndarray = np.array(None), **params: Any) -> np.ndarray: return F.swap_tiles_on_image(img, tiles) - def apply_to_mask(self, img: np.ndarray, tiles: np.ndarray = np.array(None), **params): - return F.swap_tiles_on_image(img, tiles) + def apply_to_mask(self, mask: np.ndarray, tiles: np.ndarray = np.array(None), **params: Any) -> np.ndarray: + return F.swap_tiles_on_image(mask, tiles) def apply_to_keypoint( - self, keypoint: Tuple[float, ...], tiles: np.ndarray = np.array(None), rows: int = 0, cols: int = 0, **params - ): + self, + keypoint: KeypointInternalType, + tiles: np.ndarray = np.array(None), + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> KeypointInternalType: for ( current_left_up_corner_row, current_left_up_corner_col, @@ -120,17 +130,17 @@ def apply_to_keypoint( ): x = x - old_left_up_corner_col + current_left_up_corner_col y = y - old_left_up_corner_row + current_left_up_corner_row - keypoint = (x, y) + tuple(keypoint[2:]) + keypoint_result = (x, y) + tuple(keypoint[2:]) break - return keypoint + return cast(KeypointInternalType, keypoint_result) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: height, width = params["image"].shape[:2] n, m = self.grid if n <= 0 or m <= 0: - raise ValueError("Grid's values must be positive. Current grid [%s, %s]" % (n, m)) + raise ValueError(f"Grid's values must be positive. Current grid [{n}, {m}]") if n > height // 2 or m > width // 2: raise ValueError("Incorrect size cell of grid. Just shuffle pixels of image") @@ -174,10 +184,10 @@ def get_params_dependent_on_targets(self, params): return {"tiles": tiles} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("grid",) @@ -185,9 +195,9 @@ class Normalize(ImageOnlyTransform): """Normalization is applied by the formula: `img = (img - mean * max_pixel_value) / (std * max_pixel_value)` Args: - mean (float, list of float): mean values - std (float, list of float): std values - max_pixel_value (float): maximum possible pixel value + mean: mean values + std: std values + max_pixel_value: maximum possible pixel value Targets: image @@ -198,21 +208,21 @@ class Normalize(ImageOnlyTransform): def __init__( self, - mean=(0.485, 0.456, 0.406), - std=(0.229, 0.224, 0.225), - max_pixel_value=255.0, - always_apply=False, - p=1.0, + mean: Union[float, Sequence[float]] = (0.485, 0.456, 0.406), + std: Union[float, Sequence[float]] = (0.229, 0.224, 0.225), + max_pixel_value: float = 255.0, + always_apply: bool = False, + p: float = 1.0, ): - super(Normalize, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.mean = mean self.std = std self.max_pixel_value = max_pixel_value - def apply(self, image, **params): - return F.normalize(image, self.mean, self.std, self.max_pixel_value) + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: + return F.normalize(img, self.mean, self.std, self.max_pixel_value) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("mean", "std", "max_pixel_value") @@ -220,10 +230,8 @@ class ImageCompression(ImageOnlyTransform): """Decreases image quality by Jpeg, WebP compression of an image. Args: - quality_lower (float): lower bound on the image quality. - Should be in [0, 100] range for jpeg and [1, 100] for webp. - quality_upper (float): upper bound on the image quality. - Should be in [0, 100] range for jpeg and [1, 100] for webp. + quality_lower: lower bound on the image quality. Should be in [0, 100] range for jpeg and [1, 100] for webp. + quality_upper: upper bound on the image quality. Should be in [0, 100] range for jpeg and [1, 100] for webp. compression_type (ImageCompressionType): should be ImageCompressionType.JPEG or ImageCompressionType.WEBP. Default: ImageCompressionType.JPEG @@ -240,13 +248,13 @@ class ImageCompressionType(IntEnum): def __init__( self, - quality_lower=99, - quality_upper=100, - compression_type=ImageCompressionType.JPEG, - always_apply=False, - p=0.5, + quality_lower: int = 99, + quality_upper: int = 100, + compression_type: ImageCompressionType = ImageCompressionType.JPEG, + always_apply: bool = False, + p: float = 0.5, ): - super(ImageCompression, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.compression_type = ImageCompression.ImageCompressionType(compression_type) low_thresh_quality_assert = 0 @@ -255,19 +263,19 @@ def __init__( low_thresh_quality_assert = 1 if not low_thresh_quality_assert <= quality_lower <= 100: - raise ValueError("Invalid quality_lower. Got: {}".format(quality_lower)) + raise ValueError(f"Invalid quality_lower. Got: {quality_lower}") if not low_thresh_quality_assert <= quality_upper <= 100: - raise ValueError("Invalid quality_upper. Got: {}".format(quality_upper)) + raise ValueError(f"Invalid quality_upper. Got: {quality_upper}") self.quality_lower = quality_lower self.quality_upper = quality_upper - def apply(self, image, quality=100, image_type=".jpg", **params): - if not image.ndim == 2 and image.shape[-1] not in (1, 3, 4): + def apply(self, img: np.ndarray, quality: int = 100, image_type: str = ".jpg", **params: Any) -> np.ndarray: + if not img.ndim == 2 and img.shape[-1] not in (1, 3, 4): raise TypeError("ImageCompression transformation expects 1, 3 or 4 channel images.") - return F.image_compression(image, quality, image_type) + return F.image_compression(img, quality, image_type) - def get_params(self): + def get_params(self) -> Dict[str, Any]: image_type = ".jpg" if self.compression_type == ImageCompression.ImageCompressionType.WEBP: @@ -278,7 +286,7 @@ def get_params(self): "image_type": image_type, } - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return { "quality_lower": self.quality_lower, "quality_upper": self.quality_upper, @@ -290,8 +298,8 @@ class JpegCompression(ImageCompression): """Decreases image quality by Jpeg compression of an image. Args: - quality_lower (float): lower bound on the jpeg quality. Should be in [0, 100] range - quality_upper (float): upper bound on the jpeg quality. Should be in [0, 100] range + quality_lower: lower bound on the jpeg quality. Should be in [0, 100] range + quality_upper: upper bound on the jpeg quality. Should be in [0, 100] range Targets: image @@ -300,8 +308,8 @@ class JpegCompression(ImageCompression): uint8, float32 """ - def __init__(self, quality_lower=99, quality_upper=100, always_apply=False, p=0.5): - super(JpegCompression, self).__init__( + def __init__(self, quality_lower: int = 99, quality_upper: int = 100, always_apply: bool = False, p: float = 0.5): + super().__init__( quality_lower=quality_lower, quality_upper=quality_upper, compression_type=ImageCompression.ImageCompressionType.JPEG, @@ -313,7 +321,7 @@ def __init__(self, quality_lower=99, quality_upper=100, always_apply=False, p=0. FutureWarning, ) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, float]: return { "quality_lower": self.quality_lower, "quality_upper": self.quality_upper, @@ -326,9 +334,9 @@ class RandomSnow(ImageOnlyTransform): From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - snow_point_lower (float): lower_bond of the amount of snow. Should be in [0, 1] range - snow_point_upper (float): upper_bond of the amount of snow. Should be in [0, 1] range - brightness_coeff (float): larger number will lead to a more snow on the image. Should be >= 0 + snow_point_lower: lower_bond of the amount of snow. Should be in [0, 1] range + snow_point_upper: upper_bond of the amount of snow. Should be in [0, 1] range + brightness_coeff: larger number will lead to a more snow on the image. Should be >= 0 Targets: image @@ -339,13 +347,13 @@ class RandomSnow(ImageOnlyTransform): def __init__( self, - snow_point_lower=0.1, - snow_point_upper=0.3, - brightness_coeff=2.5, - always_apply=False, - p=0.5, + snow_point_lower: float = 0.1, + snow_point_upper: float = 0.3, + brightness_coeff: float = 2.5, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomSnow, self).__init__(always_apply, p) + super().__init__(always_apply, p) if not 0 <= snow_point_lower <= snow_point_upper <= 1: raise ValueError( @@ -354,19 +362,19 @@ def __init__( ) ) if brightness_coeff < 0: - raise ValueError("brightness_coeff must be greater than 0. Got: {}".format(brightness_coeff)) + raise ValueError(f"brightness_coeff must be greater than 0. Got: {brightness_coeff}") self.snow_point_lower = snow_point_lower self.snow_point_upper = snow_point_upper self.brightness_coeff = brightness_coeff - def apply(self, image, snow_point=0.1, **params): - return F.add_snow(image, snow_point, self.brightness_coeff) + def apply(self, img: np.ndarray, snow_point: float = 0.1, **params: Any) -> np.ndarray: + return F.add_snow(img, snow_point, self.brightness_coeff) - def get_params(self): + def get_params(self) -> Dict[str, np.ndarray]: return {"snow_point": random.uniform(self.snow_point_lower, self.snow_point_upper)} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("snow_point_lower", "snow_point_upper", "brightness_coeff") @@ -376,9 +384,9 @@ class RandomGravel(ImageOnlyTransform): From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - gravel_roi (float, float, float, float): (top-left x, top-left y, + gravel_roi: (top-left x, top-left y, bottom-right x, bottom right y). Should be in [0, 1] range - number_of_patches (int): no. of gravel patches required + number_of_patches: no. of gravel patches required Targets: image @@ -389,26 +397,25 @@ class RandomGravel(ImageOnlyTransform): def __init__( self, - gravel_roi: tuple = (0.1, 0.4, 0.9, 0.9), + gravel_roi: Tuple[float, float, float, float] = (0.1, 0.4, 0.9, 0.9), number_of_patches: int = 2, always_apply: bool = False, p: float = 0.5, ): - super(RandomGravel, self).__init__(always_apply, p) + super().__init__(always_apply, p) (gravel_lower_x, gravel_lower_y, gravel_upper_x, gravel_upper_y) = gravel_roi if not 0 <= gravel_lower_x < gravel_upper_x <= 1 or not 0 <= gravel_lower_y < gravel_upper_y <= 1: - raise ValueError("Invalid gravel_roi. Got: %s." % gravel_roi) + raise ValueError(f"Invalid gravel_roi. Got: {gravel_roi}.") if number_of_patches < 1: - raise ValueError("Invalid gravel number_of_patches. Got: %s." % number_of_patches) + raise ValueError(f"Invalid gravel number_of_patches. Got: {number_of_patches}.") self.gravel_roi = gravel_roi self.number_of_patches = number_of_patches - def generate_gravel_patch(self, rectangular_roi): + def generate_gravel_patch(self, rectangular_roi: Tuple[int, int, int, int]) -> np.ndarray: x1, y1, x2, y2 = rectangular_roi - gravels = [] area = abs((x2 - x1) * (y2 - y1)) count = area // 10 gravels = np.empty([count, 2], dtype=np.int64) @@ -416,14 +423,14 @@ def generate_gravel_patch(self, rectangular_roi): gravels[:, 1] = random_utils.randint(y1, y2, count) return gravels - def apply(self, image, gravels_infos=(), **params): - return F.add_gravel(image, gravels_infos) + def apply(self, img: np.ndarray, gravels_infos: List[Any] = [], **params: Any) -> np.ndarray: + return F.add_gravel(img, gravels_infos) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, np.ndarray]: img = params["image"] height, width = img.shape[:2] @@ -477,8 +484,8 @@ def get_params_dependent_on_targets(self, params): ) } - def get_transform_init_args_names(self): - return {"gravel_roi": self.gravel_roi, "number_of_patches": self.number_of_patches} + # def get_transform_init_args_names(self) -> Dict[str, Any]: + # return {"gravel_roi": self.gravel_roi, "number_of_patches": self.number_of_patches} class RandomRain(ImageOnlyTransform): @@ -505,33 +512,31 @@ class RandomRain(ImageOnlyTransform): def __init__( self, - slant_lower=-10, - slant_upper=10, - drop_length=20, - drop_width=1, - drop_color=(200, 200, 200), - blur_value=7, - brightness_coefficient=0.7, - rain_type=None, - always_apply=False, - p=0.5, + slant_lower: int = -10, + slant_upper: int = 10, + drop_length: int = 20, + drop_width: int = 1, + drop_color: Tuple[int, int, int] = (200, 200, 200), + blur_value: int = 7, + brightness_coefficient: float = 0.7, + rain_type: Optional[str] = None, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomRain, self).__init__(always_apply, p) + super().__init__(always_apply, p) if rain_type not in ["drizzle", "heavy", "torrential", None]: raise ValueError( "raint_type must be one of ({}). Got: {}".format(["drizzle", "heavy", "torrential", None], rain_type) ) if not -20 <= slant_lower <= slant_upper <= 20: - raise ValueError( - "Invalid combination of slant_lower and slant_upper. Got: {}".format((slant_lower, slant_upper)) - ) + raise ValueError(f"Invalid combination of slant_lower and slant_upper. Got: {(slant_lower, slant_upper)}") if not 1 <= drop_width <= 5: - raise ValueError("drop_width must be in range [1, 5]. Got: {}".format(drop_width)) + raise ValueError(f"drop_width must be in range [1, 5]. Got: {drop_width}") if not 0 <= drop_length <= 100: - raise ValueError("drop_length must be in range [0, 100]. Got: {}".format(drop_length)) + raise ValueError(f"drop_length must be in range [0, 100]. Got: {drop_length}") if not 0 <= brightness_coefficient <= 1: - raise ValueError("brightness_coefficient must be in range [0, 1]. Got: {}".format(brightness_coefficient)) + raise ValueError(f"brightness_coefficient must be in range [0, 1]. Got: {brightness_coefficient}") self.slant_lower = slant_lower self.slant_upper = slant_upper @@ -543,9 +548,16 @@ def __init__( self.brightness_coefficient = brightness_coefficient self.rain_type = rain_type - def apply(self, image, slant=10, drop_length=20, rain_drops=(), **params): + def apply( + self, + img: np.ndarray, + slant: int = 10, + drop_length: int = 20, + rain_drops: List[Tuple[int, int]] = [], + **params: Any, + ) -> np.ndarray: return F.add_rain( - image, + img, slant, drop_length, self.drop_width, @@ -556,10 +568,10 @@ def apply(self, image, slant=10, drop_length=20, rain_drops=(), **params): ) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] slant = int(random.uniform(self.slant_lower, self.slant_upper)) @@ -581,7 +593,7 @@ def get_params_dependent_on_targets(self, params): rain_drops = [] - for _i in range(num_drops): # If You want heavy rain, try increasing this + for _ in range(num_drops): # If You want heavy rain, try increasing this if slant < 0: x = random.randint(slant, width) else: @@ -593,7 +605,7 @@ def get_params_dependent_on_targets(self, params): return {"drop_length": drop_length, "slant": slant, "rain_drops": rain_drops} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( "slant_lower", "slant_upper", @@ -612,9 +624,9 @@ class RandomFog(ImageOnlyTransform): From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - fog_coef_lower (float): lower limit for fog intensity coefficient. Should be in [0, 1] range. - fog_coef_upper (float): upper limit for fog intensity coefficient. Should be in [0, 1] range. - alpha_coef (float): transparency of the fog circles. Should be in [0, 1] range. + fog_coef_lower: lower limit for fog intensity coefficient. Should be in [0, 1] range. + fog_coef_upper: upper limit for fog intensity coefficient. Should be in [0, 1] range. + alpha_coef: transparency of the fog circles. Should be in [0, 1] range. Targets: image @@ -625,13 +637,13 @@ class RandomFog(ImageOnlyTransform): def __init__( self, - fog_coef_lower=0.3, - fog_coef_upper=1, - alpha_coef=0.08, - always_apply=False, - p=0.5, + fog_coef_lower: float = 0.3, + fog_coef_upper: float = 1, + alpha_coef: float = 0.08, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomFog, self).__init__(always_apply, p) + super().__init__(always_apply, p) if not 0 <= fog_coef_lower <= fog_coef_upper <= 1: raise ValueError( @@ -640,20 +652,22 @@ def __init__( ) ) if not 0 <= alpha_coef <= 1: - raise ValueError("alpha_coef must be in range [0, 1]. Got: {}".format(alpha_coef)) + raise ValueError(f"alpha_coef must be in range [0, 1]. Got: {alpha_coef}") self.fog_coef_lower = fog_coef_lower self.fog_coef_upper = fog_coef_upper self.alpha_coef = alpha_coef - def apply(self, image, fog_coef=0.1, haze_list=(), **params): - return F.add_fog(image, fog_coef, self.alpha_coef, haze_list) + def apply( + self, img: np.ndarray, fog_coef: np.ndarray = 0.1, haze_list: List[Tuple[int, int]] = [], **params: Any + ) -> np.ndarray: + return F.add_fog(img, fog_coef, self.alpha_coef, haze_list) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] fog_coef = random.uniform(self.fog_coef_lower, self.fog_coef_upper) @@ -667,7 +681,7 @@ def get_params_dependent_on_targets(self, params): index = 1 while midx > -hw or midy > -hw: - for _i in range(hw // 10 * index): + for _ in range(hw // 10 * index): x = random.randint(midx, width - midx - hw) y = random.randint(midy, height - midy - hw) haze_list.append((x, y)) @@ -678,7 +692,7 @@ def get_params_dependent_on_targets(self, params): return {"haze_list": haze_list, "fog_coef": fog_coef} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("fog_coef_lower", "fog_coef_upper", "alpha_coef") @@ -688,16 +702,16 @@ class RandomSunFlare(ImageOnlyTransform): From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - flare_roi (float, float, float, float): region of the image where flare will - appear (x_min, y_min, x_max, y_max). All values should be in range [0, 1]. - angle_lower (float): should be in range [0, `angle_upper`]. - angle_upper (float): should be in range [`angle_lower`, 1]. - num_flare_circles_lower (int): lower limit for the number of flare circles. + flare_roi: region of the image where flare will appear (x_min, y_min, x_max, y_max). + All values should be in range [0, 1]. + angle_lower: should be in range [0, `angle_upper`]. + angle_upper: should be in range [`angle_lower`, 1]. + num_flare_circles_lower: lower limit for the number of flare circles. Should be in range [0, `num_flare_circles_upper`]. - num_flare_circles_upper (int): upper limit for the number of flare circles. + num_flare_circles_upper: upper limit for the number of flare circles. Should be in range [`num_flare_circles_lower`, inf]. - src_radius (int): - src_color ((int, int, int)): color of the flare + src_radius: + src_color: color of the flare Targets: image @@ -708,17 +722,17 @@ class RandomSunFlare(ImageOnlyTransform): def __init__( self, - flare_roi=(0, 0, 1, 0.5), - angle_lower=0, - angle_upper=1, - num_flare_circles_lower=6, - num_flare_circles_upper=10, - src_radius=400, - src_color=(255, 255, 255), - always_apply=False, - p=0.5, + flare_roi: Tuple[float, float, float, float] = (0, 0, 1, 0.5), + angle_lower: float = 0, + angle_upper: float = 1, + num_flare_circles_lower: int = 6, + num_flare_circles_upper: int = 10, + src_radius: int = 400, + src_color: Tuple[int, int, int] = (255, 255, 255), + always_apply: bool = False, + p: float = 0.5, ): - super(RandomSunFlare, self).__init__(always_apply, p) + super().__init__(always_apply, p) ( flare_center_lower_x, @@ -731,11 +745,9 @@ def __init__( not 0 <= flare_center_lower_x < flare_center_upper_x <= 1 or not 0 <= flare_center_lower_y < flare_center_upper_y <= 1 ): - raise ValueError("Invalid flare_roi. Got: {}".format(flare_roi)) + raise ValueError(f"Invalid flare_roi. Got: {flare_roi}") if not 0 <= angle_lower < angle_upper <= 1: - raise ValueError( - "Invalid combination of angle_lower nad angle_upper. Got: {}".format((angle_lower, angle_upper)) - ) + raise ValueError(f"Invalid combination of angle_lower nad angle_upper. Got: {(angle_lower, angle_upper)}") if not 0 <= num_flare_circles_lower < num_flare_circles_upper: raise ValueError( "Invalid combination of num_flare_circles_lower nad num_flare_circles_upper. Got: {}".format( @@ -757,9 +769,16 @@ def __init__( self.src_radius = src_radius self.src_color = src_color - def apply(self, image, flare_center_x=0.5, flare_center_y=0.5, circles=(), **params): + def apply( + self, + img: np.ndarray, + flare_center_x: float = 0.5, + flare_center_y: float = 0.5, + circles: List[Any] = [], + **params: Any, + ) -> np.ndarray: return F.add_sun_flare( - image, + img, flare_center_x, flare_center_y, self.src_radius, @@ -768,10 +787,10 @@ def apply(self, image, flare_center_x=0.5, flare_center_y=0.5, circles=(), **par ) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] height, width = img.shape[:2] @@ -790,7 +809,7 @@ def get_params_dependent_on_targets(self, params): x = [] y = [] - def line(t): + def line(t: float) -> Tuple[float, float]: return (flare_center_x + t * math.cos(angle), flare_center_y + t * math.sin(angle)) for t_val in range(-flare_center_x, width - flare_center_x, 10): @@ -798,7 +817,7 @@ def line(t): x.append(rand_x) y.append(rand_y) - for _i in range(num_circles): + for _ in range(num_circles): alpha = random.uniform(0.05, 0.2) r = random.randint(0, len(x) - 1) rad = random.randint(1, max(height // 100 - 2, 2)) @@ -822,7 +841,7 @@ def line(t): "flare_center_y": flare_center_y, } - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return { "flare_roi": ( self.flare_center_lower_x, @@ -845,13 +864,13 @@ class RandomShadow(ImageOnlyTransform): From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library Args: - shadow_roi (float, float, float, float): region of the image where shadows - will appear (x_min, y_min, x_max, y_max). All values should be in range [0, 1]. - num_shadows_lower (int): Lower limit for the possible number of shadows. + shadow_roi: region of the image where shadows + will appear. All values should be in range [0, 1]. + num_shadows_lower: Lower limit for the possible number of shadows. Should be in range [0, `num_shadows_upper`]. - num_shadows_upper (int): Lower limit for the possible number of shadows. + num_shadows_upper: Lower limit for the possible number of shadows. Should be in range [`num_shadows_lower`, inf]. - shadow_dimension (int): number of edges in the shadow polygons + shadow_dimension: number of edges in the shadow polygons Targets: image @@ -862,19 +881,19 @@ class RandomShadow(ImageOnlyTransform): def __init__( self, - shadow_roi=(0, 0.5, 1, 1), - num_shadows_lower=1, - num_shadows_upper=2, - shadow_dimension=5, - always_apply=False, - p=0.5, + shadow_roi: Tuple[float, float, float, float] = (0, 0.5, 1, 1), + num_shadows_lower: int = 1, + num_shadows_upper: int = 2, + shadow_dimension: int = 5, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomShadow, self).__init__(always_apply, p) + super().__init__(always_apply, p) (shadow_lower_x, shadow_lower_y, shadow_upper_x, shadow_upper_y) = shadow_roi if not 0 <= shadow_lower_x <= shadow_upper_x <= 1 or not 0 <= shadow_lower_y <= shadow_upper_y <= 1: - raise ValueError("Invalid shadow_roi. Got: {}".format(shadow_roi)) + raise ValueError(f"Invalid shadow_roi. Got: {shadow_roi}") if not 0 <= num_shadows_lower <= num_shadows_upper: raise ValueError( "Invalid combination of num_shadows_lower nad num_shadows_upper. Got: {}".format( @@ -889,14 +908,14 @@ def __init__( self.shadow_dimension = shadow_dimension - def apply(self, image, vertices_list=(), **params): - return F.add_shadow(image, vertices_list) + def apply(self, img: np.ndarray, vertices_list: List[List[Tuple[int, int]]] = [], **params: Any) -> np.ndarray: + return F.add_shadow(img, vertices_list) @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, List[np.ndarray]]: img = params["image"] height, width = img.shape[:2] @@ -921,7 +940,7 @@ def get_params_dependent_on_targets(self, params): return {"vertices_list": vertices_list} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: return ( "shadow_roi", "num_shadows_lower", @@ -934,7 +953,7 @@ class RandomToneCurve(ImageOnlyTransform): """Randomly change the relationship between bright and dark areas of the image by manipulating its tone curve. Args: - scale (float): standard deviation of the normal distribution. + scale: standard deviation of the normal distribution. Used to sample random distances to move two control points that modify the image's curve. Values should be in range [0, 1]. Default: 0.1 @@ -948,23 +967,23 @@ class RandomToneCurve(ImageOnlyTransform): def __init__( self, - scale=0.1, - always_apply=False, - p=0.5, + scale: float = 0.1, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomToneCurve, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.scale = scale - def apply(self, image, low_y, high_y, **params): - return F.move_tone_curve(image, low_y, high_y) + def apply(self, img: np.ndarray, low_y: float, high_y: float, **params: Any) -> np.ndarray: + return F.move_tone_curve(img, low_y, high_y) - def get_params(self): + def get_params(self) -> Dict[str, float]: return { "low_y": np.clip(random_utils.normal(loc=0.25, scale=self.scale), 0, 1), "high_y": np.clip(random_utils.normal(loc=0.75, scale=self.scale), 0, 1), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str]: return ("scale",) @@ -972,12 +991,12 @@ class HueSaturationValue(ImageOnlyTransform): """Randomly change hue, saturation and value of the input image. Args: - hue_shift_limit ((int, int) or int): range for changing hue. If hue_shift_limit is a single int, the range + hue_shift_limit: range for changing hue. If hue_shift_limit is a single int, the range will be (-hue_shift_limit, hue_shift_limit). Default: (-20, 20). - sat_shift_limit ((int, int) or int): range for changing saturation. If sat_shift_limit is a single int, + sat_shift_limit: range for changing saturation. If sat_shift_limit is a single int, the range will be (-sat_shift_limit, sat_shift_limit). Default: (-30, 30). - val_shift_limit ((int, int) or int): range for changing value. If val_shift_limit is a single int, the range - will be (-val_shift_limit, val_shift_limit). Default: (-20, 20). + val_shift_limit: range for changing value. If val_shift_limit is a single int, the range + will be. Default: (-20, 20). p (float): probability of applying the transform. Default: 0.5. Targets: @@ -989,30 +1008,32 @@ class HueSaturationValue(ImageOnlyTransform): def __init__( self, - hue_shift_limit=20, - sat_shift_limit=30, - val_shift_limit=20, - always_apply=False, - p=0.5, + hue_shift_limit: ScaleIntType = 20, + sat_shift_limit: ScaleIntType = 30, + val_shift_limit: ScaleIntType = 20, + always_apply: bool = False, + p: float = 0.5, ): - super(HueSaturationValue, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.hue_shift_limit = to_tuple(hue_shift_limit) self.sat_shift_limit = to_tuple(sat_shift_limit) self.val_shift_limit = to_tuple(val_shift_limit) - def apply(self, image, hue_shift=0, sat_shift=0, val_shift=0, **params): - if not is_rgb_image(image) and not is_grayscale_image(image): + def apply( + self, img: np.ndarray, hue_shift: int = 0, sat_shift: int = 0, val_shift: int = 0, **params: Any + ) -> np.ndarray: + if not is_rgb_image(img) and not is_grayscale_image(img): raise TypeError("HueSaturationValue transformation expects 1-channel or 3-channel images.") - return F.shift_hsv(image, hue_shift, sat_shift, val_shift) + return F.shift_hsv(img, hue_shift, sat_shift, val_shift) - def get_params(self): + def get_params(self) -> Dict[str, float]: return { "hue_shift": random.uniform(self.hue_shift_limit[0], self.hue_shift_limit[1]), "sat_shift": random.uniform(self.sat_shift_limit[0], self.sat_shift_limit[1]), "val_shift": random.uniform(self.val_shift_limit[0], self.val_shift_limit[1]), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("hue_shift_limit", "sat_shift_limit", "val_shift_limit") @@ -1020,9 +1041,9 @@ class Solarize(ImageOnlyTransform): """Invert all pixel values above a threshold. Args: - threshold ((int, int) or int, or (float, float) or float): range for solarizing threshold. + threshold: range for solarizing threshold. If threshold is a single value, the range will be [threshold, threshold]. Default: 128. - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1031,21 +1052,21 @@ class Solarize(ImageOnlyTransform): any """ - def __init__(self, threshold=128, always_apply=False, p=0.5): - super(Solarize, self).__init__(always_apply, p) + def __init__(self, threshold: ScaleType = 128, always_apply: bool = False, p: float = 0.5): + super().__init__(always_apply, p) if isinstance(threshold, (int, float)): self.threshold = to_tuple(threshold, low=threshold) else: self.threshold = to_tuple(threshold, low=0) - def apply(self, image, threshold=0, **params): - return F.solarize(image, threshold) + def apply(self, img: np.ndarray, threshold: int = 0, **params: Any) -> np.ndarray: + return F.solarize(img, threshold) - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"threshold": random.uniform(self.threshold[0], self.threshold[1])} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str]: return ("threshold",) @@ -1058,7 +1079,7 @@ class Posterize(ImageOnlyTransform): or list of ints [[r1, r1], [g1, g2], [b1, b2]]): number of high bits. If num_bits is a single value, the range will be [num_bits, num_bits]. Must be in range [0, 8]. Default: 4. - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1067,26 +1088,30 @@ class Posterize(ImageOnlyTransform): uint8 """ - def __init__(self, num_bits=4, always_apply=False, p=0.5): - super(Posterize, self).__init__(always_apply, p) + def __init__( + self, + num_bits: Union[int, Tuple[int, int], Tuple[int, int, int]] = 4, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) - if isinstance(num_bits, (list, tuple)): - if len(num_bits) == 3: - self.num_bits = [to_tuple(i, 0) for i in num_bits] - else: - self.num_bits = to_tuple(num_bits, 0) - else: + if isinstance(num_bits, int): self.num_bits = to_tuple(num_bits, num_bits) + elif isinstance(num_bits, Sequence) and len(num_bits) == 3: + self.num_bits = [to_tuple(i, 0) for i in num_bits] # type: ignore[assignment] + else: + self.num_bits = to_tuple(num_bits, 0) - def apply(self, image, num_bits=1, **params): - return F.posterize(image, num_bits) + def apply(self, img: np.ndarray, num_bits: int = 1, **params: Any) -> np.ndarray: + return F.posterize(img, num_bits) - def get_params(self): + def get_params(self) -> Dict[str, Any]: if len(self.num_bits) == 3: - return {"num_bits": [random.randint(i[0], i[1]) for i in self.num_bits]} - return {"num_bits": random.randint(self.num_bits[0], self.num_bits[1])} + return {"num_bits": [random.randint(int(i[0]), int(i[1])) for i in self.num_bits]} + return {"num_bits": random.randint(int(self.num_bits[0]), int(self.num_bits[1]))} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str]: return ("num_bits",) @@ -1111,37 +1136,37 @@ class Equalize(ImageOnlyTransform): def __init__( self, - mode="cv", - by_channels=True, - mask=None, - mask_params=(), - always_apply=False, - p=0.5, + mode: str = "cv", + by_channels: bool = True, + mask: Optional[np.ndarray] = None, + mask_params: Tuple[()] = (), + always_apply: bool = False, + p: float = 0.5, ): modes = ["cv", "pil"] if mode not in modes: raise ValueError("Unsupported equalization mode. Supports: {}. " "Got: {}".format(modes, mode)) - super(Equalize, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.mode = mode self.by_channels = by_channels self.mask = mask self.mask_params = mask_params - def apply(self, image, mask=None, **params): - return F.equalize(image, mode=self.mode, by_channels=self.by_channels, mask=mask) + def apply(self, img: np.ndarray, mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + return F.equalize(img, mode=self.mode, by_channels=self.by_channels, mask=mask) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: if not callable(self.mask): return {"mask": self.mask} return {"mask": self.mask(**params)} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] + list(self.mask_params) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("mode", "by_channels") @@ -1149,13 +1174,13 @@ class RGBShift(ImageOnlyTransform): """Randomly shift values for each channel of the input RGB image. Args: - r_shift_limit ((int, int) or int): range for changing values for the red channel. If r_shift_limit is a single + r_shift_limit: range for changing values for the red channel. If r_shift_limit is a single int, the range will be (-r_shift_limit, r_shift_limit). Default: (-20, 20). - g_shift_limit ((int, int) or int): range for changing values for the green channel. If g_shift_limit is a + g_shift_limit: range for changing values for the green channel. If g_shift_limit is a single int, the range will be (-g_shift_limit, g_shift_limit). Default: (-20, 20). - b_shift_limit ((int, int) or int): range for changing values for the blue channel. If b_shift_limit is a single + b_shift_limit: range for changing values for the blue channel. If b_shift_limit is a single int, the range will be (-b_shift_limit, b_shift_limit). Default: (-20, 20). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1166,30 +1191,30 @@ class RGBShift(ImageOnlyTransform): def __init__( self, - r_shift_limit=20, - g_shift_limit=20, - b_shift_limit=20, - always_apply=False, - p=0.5, + r_shift_limit: ScaleIntType = 20, + g_shift_limit: ScaleIntType = 20, + b_shift_limit: ScaleIntType = 20, + always_apply: bool = False, + p: float = 0.5, ): - super(RGBShift, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.r_shift_limit = to_tuple(r_shift_limit) self.g_shift_limit = to_tuple(g_shift_limit) self.b_shift_limit = to_tuple(b_shift_limit) - def apply(self, image, r_shift=0, g_shift=0, b_shift=0, **params): - if not is_rgb_image(image): + def apply(self, img: np.ndarray, r_shift: int = 0, g_shift: int = 0, b_shift: int = 0, **params: Any) -> np.ndarray: + if not is_rgb_image(img): raise TypeError("RGBShift transformation expects 3-channel images.") - return F.shift_rgb(image, r_shift, g_shift, b_shift) + return F.shift_rgb(img, r_shift, g_shift, b_shift) - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "r_shift": random.uniform(self.r_shift_limit[0], self.r_shift_limit[1]), "g_shift": random.uniform(self.g_shift_limit[0], self.g_shift_limit[1]), "b_shift": random.uniform(self.b_shift_limit[0], self.b_shift_limit[1]), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("r_shift_limit", "g_shift_limit", "b_shift_limit") @@ -1197,13 +1222,13 @@ class RandomBrightnessContrast(ImageOnlyTransform): """Randomly change brightness and contrast of the input image. Args: - brightness_limit ((float, float) or float): factor range for changing brightness. + brightness_limit: factor range for changing brightness. If limit is a single float, the range will be (-limit, limit). Default: (-0.2, 0.2). - contrast_limit ((float, float) or float): factor range for changing contrast. + contrast_limit: factor range for changing contrast. If limit is a single float, the range will be (-limit, limit). Default: (-0.2, 0.2). - brightness_by_max (Boolean): If True adjust contrast by image dtype maximum, + brightness_by_max: If True adjust contrast by image dtype maximum, else adjust contrast by image mean. - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1214,27 +1239,27 @@ class RandomBrightnessContrast(ImageOnlyTransform): def __init__( self, - brightness_limit=0.2, - contrast_limit=0.2, - brightness_by_max=True, - always_apply=False, - p=0.5, + brightness_limit: ScaleFloatType = 0.2, + contrast_limit: ScaleFloatType = 0.2, + brightness_by_max: bool = True, + always_apply: bool = False, + p: float = 0.5, ): - super(RandomBrightnessContrast, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.brightness_limit = to_tuple(brightness_limit) self.contrast_limit = to_tuple(contrast_limit) self.brightness_by_max = brightness_by_max - def apply(self, img, alpha=1.0, beta=0.0, **params): + def apply(self, img: np.ndarray, alpha: float = 1.0, beta: float = 0.0, **params: Any) -> np.ndarray: return F.brightness_contrast_adjust(img, alpha, beta, self.brightness_by_max) - def get_params(self): + def get_params(self) -> Dict[str, float]: return { "alpha": 1.0 + random.uniform(self.contrast_limit[0], self.contrast_limit[1]), "beta": 0.0 + random.uniform(self.brightness_limit[0], self.brightness_limit[1]), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("brightness_limit", "contrast_limit", "brightness_by_max") @@ -1242,9 +1267,9 @@ class RandomBrightness(RandomBrightnessContrast): """Randomly change brightness of the input image. Args: - limit ((float, float) or float): factor range for changing brightness. + limit: factor range for changing brightness. If limit is a single float, the range will be (-limit, limit). Default: (-0.2, 0.2). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1253,14 +1278,14 @@ class RandomBrightness(RandomBrightnessContrast): uint8, float32 """ - def __init__(self, limit=0.2, always_apply=False, p=0.5): - super(RandomBrightness, self).__init__(brightness_limit=limit, contrast_limit=0, always_apply=always_apply, p=p) + def __init__(self, limit: ScaleFloatType = 0.2, always_apply: bool = False, p: float = 0.5): + super().__init__(brightness_limit=limit, contrast_limit=0, always_apply=always_apply, p=p) warnings.warn( "This class has been deprecated. Please use RandomBrightnessContrast", FutureWarning, ) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return {"limit": self.brightness_limit} @@ -1268,9 +1293,9 @@ class RandomContrast(RandomBrightnessContrast): """Randomly change contrast of the input image. Args: - limit ((float, float) or float): factor range for changing contrast. + limit: factor range for changing contrast. If limit is a single float, the range will be (-limit, limit). Default: (-0.2, 0.2). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1279,14 +1304,14 @@ class RandomContrast(RandomBrightnessContrast): uint8, float32 """ - def __init__(self, limit=0.2, always_apply=False, p=0.5): - super(RandomContrast, self).__init__(brightness_limit=0, contrast_limit=limit, always_apply=always_apply, p=p) + def __init__(self, limit: ScaleFloatType = 0.2, always_apply: bool = False, p: float = 0.5): + super().__init__(brightness_limit=0, contrast_limit=limit, always_apply=always_apply, p=p) warnings.warn( f"{self.__class__.__name__} has been deprecated. Please use RandomBrightnessContrast", FutureWarning, ) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, ScaleFloatType]: return {"limit": self.contrast_limit} @@ -1294,12 +1319,12 @@ class GaussNoise(ImageOnlyTransform): """Apply gaussian noise to the input image. Args: - var_limit ((float, float) or float): variance range for noise. If var_limit is a single float, the range + var_limit: variance range for noise. If var_limit is a single float, the range will be (0, var_limit). Default: (10.0, 50.0). - mean (float): mean of the noise. Default: 0 - per_channel (bool): if set to True, noise will be sampled for each channel independently. + mean: mean of the noise. Default: 0 + per_channel: if set to True, noise will be sampled for each channel independently. Otherwise, the noise will be sampled once for all channels. Default: True - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1308,8 +1333,15 @@ class GaussNoise(ImageOnlyTransform): uint8, float32 """ - def __init__(self, var_limit=(10.0, 50.0), mean=0, per_channel=True, always_apply=False, p=0.5): - super(GaussNoise, self).__init__(always_apply, p) + def __init__( + self, + var_limit: ScaleFloatType = (10.0, 50.0), + mean: float = 0, + per_channel: bool = True, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) if isinstance(var_limit, (tuple, list)): if var_limit[0] < 0: raise ValueError("Lower var_limit should be non negative.") @@ -1322,17 +1354,15 @@ def __init__(self, var_limit=(10.0, 50.0), mean=0, per_channel=True, always_appl self.var_limit = (0, var_limit) else: - raise TypeError( - "Expected var_limit type to be one of (int, float, tuple, list), got {}".format(type(var_limit)) - ) + raise TypeError(f"Expected var_limit type to be one of (int, float, tuple, list), got {type(var_limit)}") self.mean = mean self.per_channel = per_channel - def apply(self, img, gauss=None, **params): + def apply(self, img: np.ndarray, gauss: Optional[float] = None, **params: Any) -> np.ndarray: return F.gauss_noise(img, gauss=gauss) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, float]: image = params["image"] var = random.uniform(self.var_limit[0], self.var_limit[1]) sigma = var**0.5 @@ -1347,10 +1377,10 @@ def get_params_dependent_on_targets(self, params): return {"gauss": gauss} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return ("var_limit", "per_channel", "mean") @@ -1372,22 +1402,35 @@ class ISONoise(ImageOnlyTransform): uint8 """ - def __init__(self, color_shift=(0.01, 0.05), intensity=(0.1, 0.5), always_apply=False, p=0.5): - super(ISONoise, self).__init__(always_apply, p) + def __init__( + self, + color_shift: Tuple[float, float] = (0.01, 0.05), + intensity: Tuple[float, float] = (0.1, 0.5), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.intensity = intensity self.color_shift = color_shift - def apply(self, img, color_shift=0.05, intensity=1.0, random_state=None, **params): + def apply( + self, + img: np.ndarray, + color_shift: float = 0.05, + intensity: float = 1.0, + random_state: Optional[int] = None, + **params: Any, + ) -> np.ndarray: return F.iso_noise(img, color_shift, intensity, np.random.RandomState(random_state)) - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "color_shift": random.uniform(self.color_shift[0], self.color_shift[1]), "intensity": random.uniform(self.intensity[0], self.intensity[1]), "random_state": random.randint(0, 65536), } - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("intensity", "color_shift") @@ -1395,10 +1438,10 @@ class CLAHE(ImageOnlyTransform): """Apply Contrast Limited Adaptive Histogram Equalization to the input image. Args: - clip_limit (float or (float, float)): upper threshold value for contrast limiting. + clip_limit: upper threshold value for contrast limiting. If clip_limit is a single float value, the range will be (1, clip_limit). Default: (1, 4). - tile_grid_size ((int, int)): size of grid for histogram equalization. Default: (8, 8). - p (float): probability of applying the transform. Default: 0.5. + tile_grid_size: size of grid for histogram equalization. Default: (8, 8). + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1407,21 +1450,27 @@ class CLAHE(ImageOnlyTransform): uint8 """ - def __init__(self, clip_limit=4.0, tile_grid_size=(8, 8), always_apply=False, p=0.5): - super(CLAHE, self).__init__(always_apply, p) + def __init__( + self, + clip_limit: ScaleFloatType = 4.0, + tile_grid_size: Tuple[int, int] = (8, 8), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.clip_limit = to_tuple(clip_limit, 1) - self.tile_grid_size = tuple(tile_grid_size) + self.tile_grid_size = cast(Tuple[int, int], tuple(tile_grid_size)) - def apply(self, img, clip_limit=2, **params): + def apply(self, img: np.ndarray, clip_limit: float = 2, **params: Any) -> np.ndarray: if not is_rgb_image(img) and not is_grayscale_image(img): raise TypeError("CLAHE transformation expects 1-channel or 3-channel images.") return F.clahe(img, clip_limit, self.tile_grid_size) - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"clip_limit": random.uniform(self.clip_limit[0], self.clip_limit[1])} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("clip_limit", "tile_grid_size") @@ -1429,7 +1478,7 @@ class ChannelShuffle(ImageOnlyTransform): """Randomly rearrange channels of the input RGB image. Args: - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1439,19 +1488,19 @@ class ChannelShuffle(ImageOnlyTransform): """ @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def apply(self, img, channels_shuffled=(0, 1, 2), **params): + def apply(self, img: np.ndarray, channels_shuffled: Tuple[int, int, int] = (0, 1, 2), **params: Any) -> np.ndarray: return F.channel_shuffle(img, channels_shuffled) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] ch_arr = list(range(img.shape[2])) random.shuffle(ch_arr) return {"channels_shuffled": ch_arr} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1460,7 +1509,7 @@ class InvertImg(ImageOnlyTransform): i.e., 255 for uint8 and 1.0 for float32. Args: - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1469,18 +1518,18 @@ class InvertImg(ImageOnlyTransform): uint8, float32 """ - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.invert(img) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () class RandomGamma(ImageOnlyTransform): """ Args: - gamma_limit (float or (float, float)): If gamma_limit is a single float value, - the range will be (-gamma_limit, gamma_limit). Default: (80, 120). + gamma_limit: If gamma_limit is a single float value, the range will be (-gamma_limit, gamma_limit). + Default: (80, 120). eps: Deprecated. Targets: @@ -1490,18 +1539,24 @@ class RandomGamma(ImageOnlyTransform): uint8, float32 """ - def __init__(self, gamma_limit=(80, 120), eps=None, always_apply=False, p=0.5): - super(RandomGamma, self).__init__(always_apply, p) + def __init__( + self, + gamma_limit: ScaleIntType = (80, 120), + eps: Optional[Any] = None, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.gamma_limit = to_tuple(gamma_limit) self.eps = eps - def apply(self, img, gamma=1, **params): + def apply(self, img: np.ndarray, gamma: float = 1, **params: Any) -> np.ndarray: return F.gamma_transform(img, gamma=gamma) - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"gamma": random.uniform(self.gamma_limit[0], self.gamma_limit[1]) / 100.0} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("gamma_limit", "eps") @@ -1510,7 +1565,7 @@ class ToGray(ImageOnlyTransform): than 127, invert the resulting grayscale image. Args: - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1519,7 +1574,7 @@ class ToGray(ImageOnlyTransform): uint8, float32 """ - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: if is_grayscale_image(img): warnings.warn("The image is already gray.") return img @@ -1528,7 +1583,7 @@ def apply(self, img, **params): return F.to_gray(img) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1536,7 +1591,7 @@ class ToRGB(ImageOnlyTransform): """Convert the input grayscale image to RGB. Args: - p (float): probability of applying the transform. Default: 1. + p: probability of applying the transform. Default: 1. Targets: image @@ -1545,10 +1600,10 @@ class ToRGB(ImageOnlyTransform): uint8, float32 """ - def __init__(self, always_apply=True, p=1.0): - super(ToRGB, self).__init__(always_apply=always_apply, p=p) + def __init__(self, always_apply: bool = True, p: float = 1.0): + super().__init__(always_apply=always_apply, p=p) - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: if is_rgb_image(img): warnings.warn("The image is already an RGB.") return img @@ -1557,7 +1612,7 @@ def apply(self, img, **params): return F.gray_to_rgb(img) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1565,7 +1620,7 @@ class ToSepia(ImageOnlyTransform): """Applies sepia filter to the input RGB image Args: - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image @@ -1574,18 +1629,18 @@ class ToSepia(ImageOnlyTransform): uint8, float32 """ - def __init__(self, always_apply=False, p=0.5): - super(ToSepia, self).__init__(always_apply, p) + def __init__(self, always_apply: bool = False, p: float = 0.5): + super().__init__(always_apply, p) self.sepia_transformation_matrix = np.array( [[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]] ) - def apply(self, image, **params): - if not is_rgb_image(image): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: + if not is_rgb_image(img): raise TypeError("ToSepia transformation expects 3-channel images.") - return F.linear_transformation_rgb(image, self.sepia_transformation_matrix) + return F.linear_transformation_rgb(img, self.sepia_transformation_matrix) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[()]: return () @@ -1598,8 +1653,8 @@ class ToFloat(ImageOnlyTransform): :class:`~albumentations.augmentations.transforms.FromFloat` Args: - max_value (float): maximum possible input value. Default: None. - p (float): probability of applying the transform. Default: 1.0. + max_value: maximum possible input value. Default: None. + p: probability of applying the transform. Default: 1.0. Targets: image @@ -1609,14 +1664,14 @@ class ToFloat(ImageOnlyTransform): """ - def __init__(self, max_value=None, always_apply=False, p=1.0): - super(ToFloat, self).__init__(always_apply, p) + def __init__(self, max_value: Optional[float] = None, always_apply: bool = False, p: float = 1.0): + super().__init__(always_apply, p) self.max_value = max_value - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.to_float(img, self.max_value) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str]: return ("max_value",) @@ -1628,10 +1683,10 @@ class FromFloat(ImageOnlyTransform): This is the inverse transform for :class:`~albumentations.augmentations.transforms.ToFloat`. Args: - max_value (float): maximum possible input value. Default: None. - dtype (string or numpy data type): data type of the output. See the `'Data types' page from the NumPy docs`_. + max_value: maximum possible input value. Default: None. + dtype: data type of the output. See the `'Data types' page from the NumPy docs`_. Default: 'uint16'. - p (float): probability of applying the transform. Default: 1.0. + p: probability of applying the transform. Default: 1.0. Targets: image @@ -1643,15 +1698,17 @@ class FromFloat(ImageOnlyTransform): https://docs.scipy.org/doc/numpy/user/basics.types.html """ - def __init__(self, dtype="uint16", max_value=None, always_apply=False, p=1.0): - super(FromFloat, self).__init__(always_apply, p) + def __init__( + self, dtype: str = "uint16", max_value: Optional[float] = None, always_apply: bool = False, p: float = 1.0 + ): + super().__init__(always_apply, p) self.dtype = np.dtype(dtype) self.max_value = max_value - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return F.from_float(img, self.dtype, self.max_value) - def get_transform_init_args(self): + def get_transform_init_args(self) -> Dict[str, Any]: return {"dtype": self.dtype.name, "max_value": self.max_value} @@ -1659,8 +1716,8 @@ class Downscale(ImageOnlyTransform): """Decreases image quality by downscaling and upscaling back. Args: - scale_min (float): lower bound on the image scale. Should be < 1. - scale_max (float): lower bound on the image scale. Should be . + scale_min: lower bound on the image scale. Should be < 1. + scale_max: lower bound on the image scale. Should be . interpolation: cv2 interpolation method. Could be: - single cv2 interpolation flag - selected method will be used for downscale and upscale. - dict(downscale=flag, upscale=flag) @@ -1674,11 +1731,6 @@ class Downscale(ImageOnlyTransform): uint8, float32 """ - class Interpolation: - def __init__(self, *, downscale: int = cv2.INTER_NEAREST, upscale: int = cv2.INTER_NEAREST): - self.downscale = downscale - self.upscale = upscale - def __init__( self, scale_min: float = 0.25, @@ -1687,20 +1739,20 @@ def __init__( always_apply: bool = False, p: float = 0.5, ): - super(Downscale, self).__init__(always_apply, p) + super().__init__(always_apply, p) if interpolation is None: - self.interpolation = self.Interpolation(downscale=cv2.INTER_NEAREST, upscale=cv2.INTER_NEAREST) + self.interpolation = Interpolation(downscale=cv2.INTER_NEAREST, upscale=cv2.INTER_NEAREST) warnings.warn( "Using default interpolation INTER_NEAREST, which is sub-optimal." "Please specify interpolation mode for downscale and upscale explicitly." "For additional information see this PR https://github.com/albumentations-team/albumentations/pull/584" ) elif isinstance(interpolation, int): - self.interpolation = self.Interpolation(downscale=interpolation, upscale=interpolation) - elif isinstance(interpolation, self.Interpolation): + self.interpolation = Interpolation(downscale=interpolation, upscale=interpolation) + elif isinstance(interpolation, Interpolation): self.interpolation = interpolation elif isinstance(interpolation, dict): - self.interpolation = self.Interpolation(**interpolation) + self.interpolation = Interpolation(**interpolation) else: raise ValueError( "Wrong interpolation data type. Supported types: `Optional[Union[int, Interpolation, Dict[str, int]]]`." @@ -1708,13 +1760,15 @@ def __init__( ) if scale_min > scale_max: - raise ValueError("Expected scale_min be less or equal scale_max, got {} {}".format(scale_min, scale_max)) + raise ValueError(f"Expected scale_min be less or equal scale_max, got {scale_min} {scale_max}") if scale_max >= 1: - raise ValueError("Expected scale_max to be less than 1, got {}".format(scale_max)) + raise ValueError(f"Expected scale_max to be less than 1, got {scale_max}") self.scale_min = scale_min self.scale_max = scale_max - def apply(self, img: np.ndarray, scale: Optional[float] = None, **params) -> np.ndarray: + def apply(self, img: np.ndarray, scale: float, **params: Any) -> np.ndarray: + if isinstance(self.interpolation, int): + raise ValueError("Should not be here, added for typing purposes. Please report this issue.") return F.downscale( img, scale=scale, @@ -1729,6 +1783,8 @@ def get_transform_init_args_names(self) -> Tuple[str, str]: return "scale_min", "scale_max" def _to_dict(self) -> Dict[str, Any]: + if isinstance(self.interpolation, int): + raise ValueError("Should not be here, added for typing purposes. Please report this issue.") result = super()._to_dict() result["interpolation"] = {"upscale": self.interpolation.upscale, "downscale": self.interpolation.downscale} return result @@ -1736,15 +1792,15 @@ def _to_dict(self) -> Dict[str, Any]: class Lambda(NoOp): """A flexible transformation class for using user-defined transformation functions per targets. - Function signature must include **kwargs to accept optinal arguments like interpolation method, image size, etc: + Function signature must include **kwargs to accept optional arguments like interpolation method, image size, etc: Args: - image (callable): Image transformation function. - mask (callable): Mask transformation function. - keypoint (callable): Keypoint transformation function. - bbox (callable): BBox transformation function. - always_apply (bool): Indicates whether this transformation should be always applied. - p (float): probability of applying the transform. Default: 1.0. + image: Image transformation function. + mask: Mask transformation function. + keypoint: Keypoint transformation function. + bbox: BBox transformation function. + always_apply: Indicates whether this transformation should be always applied. + p: probability of applying the transform. Default: 1.0. Targets: image, mask, bboxes, keypoints @@ -1755,15 +1811,15 @@ class Lambda(NoOp): def __init__( self, - image=None, - mask=None, - keypoint=None, - bbox=None, - name=None, - always_apply=False, - p=1.0, + image: Optional[Callable[..., Any]] = None, + mask: Optional[Callable[..., Any]] = None, + keypoint: Optional[Callable[..., Any]] = None, + bbox: Optional[Callable[..., Any]] = None, + name: Optional[str] = None, + always_apply: bool = False, + p: float = 1.0, ): - super(Lambda, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.name = name self.custom_apply_fns = {target_name: F.noop for target_name in ("image", "mask", "keypoint", "bbox")} @@ -1782,27 +1838,27 @@ def __init__( self.custom_apply_fns[target_name] = custom_apply_fn - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: fn = self.custom_apply_fns["image"] return fn(img, **params) - def apply_to_mask(self, mask, **params): + def apply_to_mask(self, mask: np.ndarray, **params: Any) -> np.ndarray: fn = self.custom_apply_fns["mask"] return fn(mask, **params) - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: fn = self.custom_apply_fns["bbox"] return fn(bbox, **params) - def apply_to_keypoint(self, keypoint, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: fn = self.custom_apply_fns["keypoint"] return fn(keypoint, **params) @classmethod - def is_serializable(cls): + def is_serializable(cls) -> bool: return False - def _to_dict(self): + def _to_dict(self) -> Dict[str, Any]: if self.name is None: raise ValueError( "To make a Lambda transform serializable you should provide the `name` argument, " @@ -1810,23 +1866,23 @@ def _to_dict(self): ) return {"__class_fullname__": self.get_class_fullname(), "__name__": self.name} - def __repr__(self): + def __repr__(self) -> str: state = {"name": self.name} - state.update(self.custom_apply_fns.items()) + state.update(self.custom_apply_fns.items()) # type: ignore[arg-type] state.update(self.get_base_init_args()) - return "{name}({args})".format(name=self.__class__.__name__, args=format_args(state)) + return f"{self.__class__.__name__}({format_args(state)})" class MultiplicativeNoise(ImageOnlyTransform): """Multiply image to random number or array of numbers. Args: - multiplier (float or tuple of floats): If single float image will be multiplied to this number. + multiplier: If single float image will be multiplied to this number. If tuple of float multiplier will be in range `[multiplier[0], multiplier[1])`. Default: (0.9, 1.1). - per_channel (bool): If `False`, same values for all channels will be used. + per_channel: If `False`, same values for all channels will be used. If `True` use sample values for each channels. Default False. - elementwise (bool): If `False` multiply multiply all pixels in an image with a random value sampled once. - If `True` Multiply image pixels with values that are pixelwise randomly sampled. Defaule: False. + elementwise: If `False` multiply multiply all pixels in an image with a random value sampled once. + If `True` Multiply image pixels with values that are pixelwise randomly sampled. Default: False. Targets: image @@ -1837,49 +1893,49 @@ class MultiplicativeNoise(ImageOnlyTransform): def __init__( self, - multiplier=(0.9, 1.1), - per_channel=False, - elementwise=False, - always_apply=False, - p=0.5, + multiplier: ScaleFloatType = (0.9, 1.1), + per_channel: bool = False, + elementwise: bool = False, + always_apply: bool = False, + p: float = 0.5, ): - super(MultiplicativeNoise, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.multiplier = to_tuple(multiplier, multiplier) self.per_channel = per_channel self.elementwise = elementwise - def apply(self, img, multiplier=np.array([1]), **kwargs): + def apply(self, img: np.ndarray, multiplier: float = np.array([1]), **kwargs: Any) -> np.ndarray: return F.multiply(img, multiplier) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: if self.multiplier[0] == self.multiplier[1]: return {"multiplier": np.array([self.multiplier[0]])} img = params["image"] - h, w = img.shape[:2] + height, width = img.shape[:2] if self.per_channel: - c = 1 if is_grayscale_image(img) else img.shape[-1] + num_channels = 1 if is_grayscale_image(img) else img.shape[-1] else: - c = 1 + num_channels = 1 if self.elementwise: - shape = [h, w, c] + shape = [height, width, num_channels] else: - shape = [c] + shape = [num_channels] - multiplier = random_utils.uniform(self.multiplier[0], self.multiplier[1], shape) + multiplier = random_utils.uniform(self.multiplier[0], self.multiplier[1], tuple(shape)) if is_grayscale_image(img) and img.ndim == 2: multiplier = np.squeeze(multiplier) return {"multiplier": multiplier} @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str]: return "multiplier", "per_channel", "elementwise" @@ -1888,7 +1944,7 @@ class FancyPCA(ImageOnlyTransform): "ImageNet Classification with Deep Convolutional Neural Networks" Args: - alpha (float): how much to perturb/scale the eigen vecs and vals. + alpha: how much to perturb/scale the eigen vecs and vals. scale is samples from gaussian distribution (mu=0, sigma=alpha) Targets: @@ -1903,18 +1959,17 @@ class FancyPCA(ImageOnlyTransform): https://pixelatedbrian.github.io/2018-04-29-fancy_pca/ """ - def __init__(self, alpha=0.1, always_apply=False, p=0.5): - super(FancyPCA, self).__init__(always_apply=always_apply, p=p) + def __init__(self, alpha: float = 0.1, always_apply: bool = False, p: float = 0.5): + super().__init__(always_apply=always_apply, p=p) self.alpha = alpha - def apply(self, img, alpha=0.1, **params): - img = F.fancy_pca(img, alpha) - return img + def apply(self, img: np.ndarray, alpha: float = 0.1, **params: Any) -> np.ndarray: + return F.fancy_pca(img, alpha) - def get_params(self): + def get_params(self) -> Dict[str, float]: return {"alpha": random.gauss(0, self.alpha)} - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str]: return ("alpha",) @@ -1941,19 +1996,19 @@ class ColorJitter(ImageOnlyTransform): def __init__( self, - brightness=0.2, - contrast=0.2, - saturation=0.2, - hue=0.2, - always_apply=False, - p=0.5, + brightness: ScaleFloatType = 0.2, + contrast: ScaleFloatType = 0.2, + saturation: ScaleFloatType = 0.2, + hue: ScaleFloatType = 0.2, + always_apply: bool = False, + p: float = 0.5, ): - super(ColorJitter, self).__init__(always_apply=always_apply, p=p) + super().__init__(always_apply=always_apply, p=p) self.brightness = self.__check_values(brightness, "brightness") self.contrast = self.__check_values(contrast, "contrast") self.saturation = self.__check_values(saturation, "saturation") - self.hue = self.__check_values(hue, "hue", offset=0, bounds=[-0.5, 0.5], clip=False) + self.hue = self.__check_values(hue, "hue", offset=0, bounds=(-0.5, 0.5), clip=False) self.transforms = [ F.adjust_brightness_torchvision, @@ -1963,22 +2018,28 @@ def __init__( ] @staticmethod - def __check_values(value, name, offset=1, bounds=(0, float("inf")), clip=True): + def __check_values( + value: ScaleFloatType, + name: str, + offset: float = 1, + bounds: Tuple[float, float] = (0, float("inf")), + clip: bool = True, + ) -> Tuple[float, float]: if isinstance(value, numbers.Number): if value < 0: - raise ValueError("If {} is a single number, it must be non negative.".format(name)) + raise ValueError(f"If {name} is a single number, it must be non negative.") value = [offset - value, offset + value] if clip: value[0] = max(value[0], 0) elif isinstance(value, (tuple, list)) and len(value) == 2: if not bounds[0] <= value[0] <= value[1] <= bounds[1]: - raise ValueError("{} values should be between {}".format(name, bounds)) + raise ValueError(f"{name} values should be between {bounds}") else: - raise TypeError("{} should be a single number or a list/tuple with length 2.".format(name)) + raise TypeError(f"{name} should be a single number or a list/tuple with length 2.") return value - def get_params(self): + def get_params(self) -> Dict[str, Any]: brightness = random.uniform(self.brightness[0], self.brightness[1]) contrast = random.uniform(self.contrast[0], self.contrast[1]) saturation = random.uniform(self.saturation[0], self.saturation[1]) @@ -1995,15 +2056,24 @@ def get_params(self): "order": order, } - def apply(self, img, brightness=1.0, contrast=1.0, saturation=1.0, hue=0, order=[0, 1, 2, 3], **params): + def apply( + self, + img: np.ndarray, + brightness: float = 1.0, + contrast: float = 1.0, + saturation: float = 1.0, + hue: float = 0, + order: List[int] = [0, 1, 2, 3], + **params: Any, + ) -> np.ndarray: if not is_rgb_image(img) and not is_grayscale_image(img): raise TypeError("ColorJitter transformation expects 1-channel or 3-channel images.") - params = [brightness, contrast, saturation, hue] + color_transforms = [brightness, contrast, saturation, hue] for i in order: - img = self.transforms[i](img, params[i]) + img = self.transforms[i](img, color_transforms[i]) # type: ignore[operator] return img - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: return ("brightness", "contrast", "saturation", "hue") @@ -2011,47 +2081,54 @@ class Sharpen(ImageOnlyTransform): """Sharpen the input image and overlays the result with the original image. Args: - alpha ((float, float)): range to choose the visibility of the sharpened image. At 0, only the original image is + alpha: range to choose the visibility of the sharpened image. At 0, only the original image is visible, at 1.0 only its sharpened version is visible. Default: (0.2, 0.5). - lightness ((float, float)): range to choose the lightness of the sharpened image. Default: (0.5, 1.0). - p (float): probability of applying the transform. Default: 0.5. + lightness: range to choose the lightness of the sharpened image. Default: (0.5, 1.0). + p: probability of applying the transform. Default: 0.5. Targets: image """ - def __init__(self, alpha=(0.2, 0.5), lightness=(0.5, 1.0), always_apply=False, p=0.5): - super(Sharpen, self).__init__(always_apply, p) + def __init__( + self, + alpha: Tuple[float, float] = (0.2, 0.5), + lightness: Tuple[float, float] = (0.5, 1.0), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.alpha = self.__check_values(to_tuple(alpha, 0.0), name="alpha", bounds=(0.0, 1.0)) self.lightness = self.__check_values(to_tuple(lightness, 0.0), name="lightness") @staticmethod - def __check_values(value, name, bounds=(0, float("inf"))): + def __check_values( + value: Tuple[float, float], name: str, bounds: Tuple[float, float] = (0, float("inf")) + ) -> Tuple[float, float]: if not bounds[0] <= value[0] <= value[1] <= bounds[1]: - raise ValueError("{} values should be between {}".format(name, bounds)) + raise ValueError(f"{name} values should be between {bounds}") return value @staticmethod - def __generate_sharpening_matrix(alpha_sample, lightness_sample): + def __generate_sharpening_matrix(alpha_sample: np.ndarray, lightness_sample: np.ndarray) -> np.ndarray: matrix_nochange = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32) matrix_effect = np.array( [[-1, -1, -1], [-1, 8 + lightness_sample, -1], [-1, -1, -1]], dtype=np.float32, ) - matrix = (1 - alpha_sample) * matrix_nochange + alpha_sample * matrix_effect - return matrix + return (1 - alpha_sample) * matrix_nochange + alpha_sample * matrix_effect - def get_params(self): + def get_params(self) -> Dict[str, np.ndarray]: alpha = random.uniform(*self.alpha) lightness = random.uniform(*self.lightness) sharpening_matrix = self.__generate_sharpening_matrix(alpha_sample=alpha, lightness_sample=lightness) return {"sharpening_matrix": sharpening_matrix} - def apply(self, img, sharpening_matrix=None, **params): + def apply(self, img: np.ndarray, sharpening_matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: return F.convolve(img, sharpening_matrix) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("alpha", "lightness") @@ -2059,28 +2136,36 @@ class Emboss(ImageOnlyTransform): """Emboss the input image and overlays the result with the original image. Args: - alpha ((float, float)): range to choose the visibility of the embossed image. At 0, only the original image is + alpha: range to choose the visibility of the embossed image. At 0, only the original image is visible,at 1.0 only its embossed version is visible. Default: (0.2, 0.5). - strength ((float, float)): strength range of the embossing. Default: (0.2, 0.7). - p (float): probability of applying the transform. Default: 0.5. + strength: strength range of the embossing. Default: (0.2, 0.7). + p: probability of applying the transform. Default: 0.5. Targets: image """ - def __init__(self, alpha=(0.2, 0.5), strength=(0.2, 0.7), always_apply=False, p=0.5): - super(Emboss, self).__init__(always_apply, p) + def __init__( + self, + alpha: Tuple[float, float] = (0.2, 0.5), + strength: Tuple[float, float] = (0.2, 0.7), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.alpha = self.__check_values(to_tuple(alpha, 0.0), name="alpha", bounds=(0.0, 1.0)) self.strength = self.__check_values(to_tuple(strength, 0.0), name="strength") @staticmethod - def __check_values(value, name, bounds=(0, float("inf"))): + def __check_values( + value: Tuple[float, float], name: str, bounds: Tuple[float, float] = (0, float("inf")) + ) -> Tuple[float, float]: if not bounds[0] <= value[0] <= value[1] <= bounds[1]: - raise ValueError("{} values should be between {}".format(name, bounds)) + raise ValueError(f"{name} values should be between {bounds}") return value @staticmethod - def __generate_emboss_matrix(alpha_sample, strength_sample): + def __generate_emboss_matrix(alpha_sample: np.ndarray, strength_sample: np.ndarray) -> np.ndarray: matrix_nochange = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32) matrix_effect = np.array( [ @@ -2090,19 +2175,18 @@ def __generate_emboss_matrix(alpha_sample, strength_sample): ], dtype=np.float32, ) - matrix = (1 - alpha_sample) * matrix_nochange + alpha_sample * matrix_effect - return matrix + return (1 - alpha_sample) * matrix_nochange + alpha_sample * matrix_effect - def get_params(self): + def get_params(self) -> Dict[str, np.ndarray]: alpha = random.uniform(*self.alpha) strength = random.uniform(*self.strength) emboss_matrix = self.__generate_emboss_matrix(alpha_sample=alpha, strength_sample=strength) return {"emboss_matrix": emboss_matrix} - def apply(self, img, emboss_matrix=None, **params): + def apply(self, img: np.ndarray, emboss_matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: return F.convolve(img, emboss_matrix) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("alpha", "strength") @@ -2151,8 +2235,8 @@ class Superpixels(ImageOnlyTransform): def __init__( self, - p_replace: Union[float, Sequence[float]] = 0.1, - n_segments: Union[int, Sequence[int]] = 100, + p_replace: ScaleFloatType = 0.1, + n_segments: ScaleIntType = 100, max_size: Optional[int] = 128, interpolation: int = cv2.INTER_LINEAR, always_apply: bool = False, @@ -2170,13 +2254,15 @@ def __init__( def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: return ("p_replace", "n_segments", "max_size", "interpolation") - def get_params(self) -> dict: + def get_params(self) -> Dict[str, Any]: n_segments = random.randint(*self.n_segments) p = random.uniform(*self.p_replace) return {"replace_samples": random_utils.random(n_segments) < p, "n_segments": n_segments} - def apply(self, img: np.ndarray, replace_samples: Sequence[bool] = (False,), n_segments: int = 1, **kwargs): - return F.superpixels(img, n_segments, replace_samples, self.max_size, self.interpolation) + def apply( + self, img: np.ndarray, replace_samples: Sequence[bool] = (False,), n_segments: int = 1, **kwargs: Any + ) -> np.ndarray: + return F.superpixels(img, n_segments, replace_samples, self.max_size, cast(int, self.interpolation)) class TemplateTransform(ImageOnlyTransform): @@ -2184,15 +2270,15 @@ class TemplateTransform(ImageOnlyTransform): Apply blending of input image with specified templates Args: templates (numpy array or list of numpy arrays): Images as template for transform. - img_weight ((float, float) or float): If single float will be used as weight for input image. + img_weight: If single float will be used as weight for input image. If tuple of float img_weight will be in range `[img_weight[0], img_weight[1])`. Default: 0.5. - template_weight ((float, float) or float): If single float will be used as weight for template. + template_weight: If single float will be used as weight for template. If tuple of float template_weight will be in range `[template_weight[0], template_weight[1])`. Default: 0.5. template_transform: transformation object which could be applied to template, must produce template the same size as input image. - name (string): (Optional) Name of transform, used only for deserialization. - p (float): probability of applying the transform. Default: 0.5. + name: (Optional) Name of transform, used only for deserialization. + p: probability of applying the transform. Default: 0.5. Targets: image Image types: @@ -2201,13 +2287,13 @@ class TemplateTransform(ImageOnlyTransform): def __init__( self, - templates, - img_weight=0.5, - template_weight=0.5, - template_transform=None, - name=None, - always_apply=False, - p=0.5, + templates: Union[np.ndarray, List[np.ndarray]], + img_weight: ScaleFloatType = 0.5, + template_weight: ScaleFloatType = 0.5, + template_transform: Optional[Callable[..., Any]] = None, + name: Optional[str] = None, + always_apply: bool = False, + p: float = 0.5, ): super().__init__(always_apply, p) @@ -2217,16 +2303,23 @@ def __init__( self.template_transform = template_transform self.name = name - def apply(self, img, template=None, img_weight=0.5, template_weight=0.5, **params): + def apply( + self, + img: np.ndarray, + template: Optional[np.ndarray] = None, + img_weight: float = 0.5, + template_weight: float = 0.5, + **params: Any, + ) -> np.ndarray: return F.add_weighted(img, img_weight, template, template_weight) - def get_params(self): + def get_params(self) -> Dict[str, float]: return { "img_weight": random.uniform(self.img_weight[0], self.img_weight[1]), "template_weight": random.uniform(self.template_weight[0], self.template_weight[1]), } - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: img = params["image"] template = random.choice(self.templates) @@ -2245,9 +2338,7 @@ def get_params_dependent_on_targets(self, params): raise ValueError("Image and template must be the same image type") if img.shape[:2] != template.shape[:2]: - raise ValueError( - "Image and template must be the same size, got {} and {}".format(img.shape[:2], template.shape[:2]) - ) + raise ValueError(f"Image and template must be the same size, got {img.shape[:2]} and {template.shape[:2]}") if get_num_channels(template) == 1 and get_num_channels(img) > 1: template = np.stack((template,) * get_num_channels(img), axis=-1) @@ -2258,14 +2349,14 @@ def get_params_dependent_on_targets(self, params): return {"template": template} @classmethod - def is_serializable(cls): + def is_serializable(cls) -> bool: return False @property - def targets_as_params(self): + def targets_as_params(self) -> List[str]: return ["image"] - def _to_dict(self): + def _to_dict(self) -> Dict[str, Any]: if self.name is None: raise ValueError( "To make a TemplateTransform serializable you should provide the `name` argument, " @@ -2278,12 +2369,12 @@ class RingingOvershoot(ImageOnlyTransform): """Create ringing or overshoot artefacts by conlvolving image with 2D sinc filter. Args: - blur_limit (int, (int, int)): maximum kernel size for sinc filter. + blur_limit: maximum kernel size for sinc filter. Should be in range [3, inf). Default: (7, 15). - cutoff (float, (float, float)): range to choose the cutoff frequency in radians. + cutoff: range to choose the cutoff frequency in radians. Should be in range (0, np.pi) Default: (np.pi / 4, np.pi / 2). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Reference: dsp.stackexchange.com/questions/58301/2-d-circularly-symmetric-low-pass-filter @@ -2295,22 +2386,24 @@ class RingingOvershoot(ImageOnlyTransform): def __init__( self, - blur_limit: Union[int, Sequence[int]] = (7, 15), - cutoff: Union[float, Sequence[float]] = (np.pi / 4, np.pi / 2), - always_apply=False, - p=0.5, + blur_limit: ScaleIntType = (7, 15), + cutoff: ScaleFloatType = (np.pi / 4, np.pi / 2), + always_apply: bool = False, + p: float = 0.5, ): - super(RingingOvershoot, self).__init__(always_apply, p) - self.blur_limit = to_tuple(blur_limit, 3) + super().__init__(always_apply, p) + self.blur_limit = cast(Tuple[int, int], to_tuple(blur_limit, 3)) self.cutoff = self.__check_values(to_tuple(cutoff, np.pi / 2), name="cutoff", bounds=(0, np.pi)) @staticmethod - def __check_values(value, name, bounds=(0, float("inf"))): + def __check_values( + value: Tuple[float, float], name: str, bounds: Tuple[float, float] = (0, float("inf")) + ) -> Tuple[float, float]: if not bounds[0] <= value[0] <= value[1] <= bounds[1]: raise ValueError(f"{name} values should be between {bounds}") return value - def get_params(self): + def get_params(self) -> Dict[str, np.ndarray]: ksize = random.randrange(self.blur_limit[0], self.blur_limit[1] + 1, 2) if ksize % 2 == 0: raise ValueError(f"Kernel size must be odd. Got: {ksize}") @@ -2332,10 +2425,10 @@ def get_params(self): return {"kernel": kernel} - def apply(self, img, kernel=None, **params): + def apply(self, img: np.ndarray, kernel: Optional[int] = None, **params: Any) -> np.ndarray: return F.convolve(img, kernel) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str]: return ("blur_limit", "cutoff") @@ -2344,21 +2437,21 @@ class UnsharpMask(ImageOnlyTransform): Sharpen the input image using Unsharp Masking processing and overlays the result with the original image. Args: - blur_limit (int, (int, int)): maximum Gaussian kernel size for blurring the input image. + blur_limit: maximum Gaussian kernel size for blurring the input image. Must be zero or odd and in range [0, inf). If set to 0 it will be computed from sigma as `round(sigma * (3 if img.dtype == np.uint8 else 4) * 2 + 1) + 1`. If set single value `blur_limit` will be in range (0, blur_limit). Default: (3, 7). - sigma_limit (float, (float, float)): Gaussian kernel standard deviation. Must be in range [0, inf). + sigma_limit: Gaussian kernel standard deviation. Must be in range [0, inf). If set single value `sigma_limit` will be in range (0, sigma_limit). If set to 0 sigma will be computed as `sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8`. Default: 0. - alpha (float, (float, float)): range to choose the visibility of the sharpened image. + alpha: range to choose the visibility of the sharpened image. At 0, only the original image is visible, at 1.0 only its sharpened version is visible. Default: (0.2, 0.5). - threshold (int): Value to limit sharpening only for areas with high pixel difference between original image + threshold: Value to limit sharpening only for areas with high pixel difference between original image and it's smoothed version. Higher threshold means less sharpening on flat areas. Must be in range [0, 255]. Default: 10. - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Reference: arxiv.org/pdf/2107.10833.pdf @@ -2369,15 +2462,15 @@ class UnsharpMask(ImageOnlyTransform): def __init__( self, - blur_limit: Union[int, Sequence[int]] = (3, 7), - sigma_limit: Union[float, Sequence[float]] = 0.0, - alpha: Union[float, Sequence[float]] = (0.2, 0.5), + blur_limit: ScaleIntType = (3, 7), + sigma_limit: ScaleFloatType = 0.0, + alpha: ScaleFloatType = (0.2, 0.5), threshold: int = 10, - always_apply=False, - p=0.5, + always_apply: bool = False, + p: float = 0.5, ): - super(UnsharpMask, self).__init__(always_apply, p) - self.blur_limit = to_tuple(blur_limit, 3) + super().__init__(always_apply, p) + self.blur_limit = cast(Tuple[int, int], to_tuple(blur_limit, 3)) self.sigma_limit = self.__check_values(to_tuple(sigma_limit, 0.0), name="sigma_limit") self.alpha = self.__check_values(to_tuple(alpha, 0.0), name="alpha", bounds=(0.0, 1.0)) self.threshold = threshold @@ -2392,23 +2485,25 @@ def __init__( raise ValueError("UnsharpMask supports only odd blur limits.") @staticmethod - def __check_values(value, name, bounds=(0, float("inf"))): + def __check_values( + value: Union[Tuple[int, int], Tuple[float, float]], name: str, bounds: Tuple[float, float] = (0, float("inf")) + ) -> Tuple[float, float]: if not bounds[0] <= value[0] <= value[1] <= bounds[1]: raise ValueError(f"{name} values should be between {bounds}") return value - def get_params(self): + def get_params(self) -> Dict[str, Any]: return { "ksize": random.randrange(self.blur_limit[0], self.blur_limit[1] + 1, 2), "sigma": random.uniform(*self.sigma_limit), "alpha": random.uniform(*self.alpha), } - def apply(self, img, ksize=3, sigma=0, alpha=0.2, **params): + def apply(self, img: np.ndarray, ksize: int = 3, sigma: int = 0, alpha: float = 0.2, **params: Any) -> np.ndarray: return F.unsharp_mask(img, ksize, sigma=sigma, alpha=alpha, threshold=self.threshold) - def get_transform_init_args_names(self): - return ("blur_limit", "sigma_limit", "alpha", "threshold") + def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: + return "blur_limit", "sigma_limit", "alpha", "threshold" class PixelDropout(DualTransform): @@ -2439,8 +2534,8 @@ def __init__( self, dropout_prob: float = 0.01, per_channel: bool = False, - drop_value: Optional[Union[float, Sequence[float]]] = 0, - mask_drop_value: Optional[Union[float, Sequence[float]]] = None, + drop_value: Optional[ScaleFloatType] = 0, + mask_drop_value: Optional[ScaleFloatType] = None, always_apply: bool = False, p: float = 0.5, ): @@ -2458,23 +2553,23 @@ def apply( img: np.ndarray, drop_mask: np.ndarray = np.array(None), drop_value: Union[float, Sequence[float]] = (), - **params + **params: Any, ) -> np.ndarray: return F.pixel_dropout(img, drop_mask, drop_value) - def apply_to_mask(self, img: np.ndarray, drop_mask: np.ndarray = np.array(None), **params) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, drop_mask: np.ndarray = np.array(None), **params: Any) -> np.ndarray: if self.mask_drop_value is None: - return img + return mask - if img.ndim == 2: + if mask.ndim == 2: drop_mask = np.squeeze(drop_mask) - return F.pixel_dropout(img, drop_mask, self.mask_drop_value) + return F.pixel_dropout(mask, drop_mask, self.mask_drop_value) - def apply_to_bbox(self, bbox, **params): + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return bbox - def apply_to_keypoint(self, keypoint, **params): + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return keypoint def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -2605,7 +2700,7 @@ def apply( mud: Optional[np.ndarray] = None, drops: Optional[np.ndarray] = None, mode: str = "", - **params + **params: Dict[str, Any], ) -> np.ndarray: return F.spatter(img, non_mud, mud, drops, mode) @@ -2614,7 +2709,7 @@ def targets_as_params(self) -> List[str]: return ["image"] def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: - h, w = params["image"].shape[:2] + height, width = params["image"].shape[:2] mean = random.uniform(self.mean[0], self.mean[1]) std = random.uniform(self.std[0], self.std[1]) @@ -2624,7 +2719,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A intensity = random.uniform(self.intensity[0], self.intensity[1]) color = np.array(self.color[mode]) / 255.0 - liquid_layer = random_utils.normal(size=(h, w), loc=mean, scale=std) + liquid_layer = random_utils.normal(size=(height, width), loc=mean, scale=std) liquid_layer = gaussian_filter(liquid_layer, sigma=sigma, mode="nearest") liquid_layer[liquid_layer < cutout_threshold] = 0 diff --git a/albumentations/augmentations/utils.py b/albumentations/augmentations/utils.py index 80ee2b9b2..f5400a6ab 100644 --- a/albumentations/augmentations/utils.py +++ b/albumentations/augmentations/utils.py @@ -1,12 +1,12 @@ from functools import wraps -from typing import Callable, Union +from typing import Any, Callable, Union import cv2 import numpy as np from typing_extensions import Concatenate, ParamSpec from albumentations.core.keypoints_utils import angle_to_2pi_range -from albumentations.core.transforms_interface import KeypointInternalType +from albumentations.core.types import KeypointInternalType __all__ = [ "read_bgr_image", @@ -38,24 +38,24 @@ } NPDTYPE_TO_OPENCV_DTYPE = { - np.uint8: cv2.CV_8U, # type: ignore[attr-defined] - np.uint16: cv2.CV_16U, # type: ignore[attr-defined] - np.int32: cv2.CV_32S, # type: ignore[attr-defined] - np.float32: cv2.CV_32F, # type: ignore[attr-defined] - np.float64: cv2.CV_64F, # type: ignore[attr-defined] - np.dtype("uint8"): cv2.CV_8U, # type: ignore[attr-defined] - np.dtype("uint16"): cv2.CV_16U, # type: ignore[attr-defined] - np.dtype("int32"): cv2.CV_32S, # type: ignore[attr-defined] - np.dtype("float32"): cv2.CV_32F, # type: ignore[attr-defined] - np.dtype("float64"): cv2.CV_64F, # type: ignore[attr-defined] + np.uint8: cv2.CV_8U, + np.uint16: cv2.CV_16U, + np.int32: cv2.CV_32S, + np.float32: cv2.CV_32F, + np.float64: cv2.CV_64F, + np.dtype("uint8"): cv2.CV_8U, + np.dtype("uint16"): cv2.CV_16U, + np.dtype("int32"): cv2.CV_32S, + np.dtype("float32"): cv2.CV_32F, + np.dtype("float64"): cv2.CV_64F, } -def read_bgr_image(path): +def read_bgr_image(path: str) -> np.ndarray: return cv2.imread(path, cv2.IMREAD_COLOR) -def read_rgb_image(path): +def read_rgb_image(path: str) -> np.ndarray: image = cv2.imread(path, cv2.IMREAD_COLOR) return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) @@ -169,7 +169,7 @@ def non_rgb_warning(image: np.ndarray) -> None: def _maybe_process_in_chunks( - process_fn: Callable[Concatenate[np.ndarray, P], np.ndarray], **kwargs + process_fn: Callable[Concatenate[np.ndarray, P], np.ndarray], **kwargs: Any ) -> Callable[[np.ndarray], np.ndarray]: """ Wrap OpenCV function to enable processing images with more than 4 channels. diff --git a/albumentations/core/bbox_utils.py b/albumentations/core/bbox_utils.py index 8ae0913ac..2c639c7d9 100644 --- a/albumentations/core/bbox_utils.py +++ b/albumentations/core/bbox_utils.py @@ -1,10 +1,8 @@ -from __future__ import division - -from typing import Any, Dict, List, Optional, Sequence, Tuple, TypeVar, cast +from typing import Any, Dict, List, Optional, Sequence, Tuple, cast import numpy as np -from .transforms_interface import BoxInternalType, BoxType +from .types import BoxInternalType, BoxType from .utils import DataProcessor, Params __all__ = [ @@ -26,8 +24,6 @@ "BboxParams", ] -TBox = TypeVar("TBox", BoxType, BoxInternalType) - class BboxParams(Params): """ @@ -70,7 +66,7 @@ def __init__( min_height: float = 0.0, check_each_transform: bool = True, ): - super(BboxParams, self).__init__(format, label_fields) + super().__init__(format, label_fields) self.min_area = min_area self.min_visibility = min_visibility self.min_width = min_width @@ -78,7 +74,7 @@ def __init__( self.check_each_transform = check_each_transform def _to_dict(self) -> Dict[str, Any]: - data = super(BboxParams, self)._to_dict() + data = super()._to_dict() data.update( { "min_area": self.min_area, @@ -120,7 +116,7 @@ def ensure_data_valid(self, data: Dict[str, Any]) -> None: if not all(i in data.keys() for i in self.params.label_fields): raise ValueError("Your 'label_fields' are not valid - them must have same names as params in dict") - def filter(self, data: Sequence, rows: int, cols: int) -> List: + def filter(self, data: Sequence[BoxType], rows: int, cols: int) -> List[BoxType]: self.params: BboxParams return filter_bboxes( data, @@ -132,17 +128,17 @@ def filter(self, data: Sequence, rows: int, cols: int) -> List: min_height=self.params.min_height, ) - def check(self, data: Sequence, rows: int, cols: int) -> None: + def check(self, data: Sequence[BoxType], rows: int, cols: int) -> None: check_bboxes(data) - def convert_from_albumentations(self, data: Sequence, rows: int, cols: int) -> List[BoxType]: + def convert_from_albumentations(self, data: Sequence[BoxType], rows: int, cols: int) -> List[BoxType]: return convert_bboxes_from_albumentations(data, self.params.format, rows, cols, check_validity=True) def convert_to_albumentations(self, data: Sequence[BoxType], rows: int, cols: int) -> List[BoxType]: return convert_bboxes_to_albumentations(data, self.params.format, rows, cols, check_validity=True) -def normalize_bbox(bbox: TBox, rows: int, cols: int) -> TBox: +def normalize_bbox(bbox: BoxType, rows: int, cols: int) -> BoxType: """Normalize coordinates of a bounding box. Divide x-coordinates by image width and y-coordinates by image height. @@ -166,14 +162,15 @@ def normalize_bbox(bbox: TBox, rows: int, cols: int) -> TBox: tail: Tuple[Any, ...] (x_min, y_min, x_max, y_max), tail = bbox[:4], tuple(bbox[4:]) + x_min /= cols + x_max /= cols + y_min /= rows + y_max /= rows - x_min, x_max = x_min / cols, x_max / cols - y_min, y_max = y_min / rows, y_max / rows - - return cast(BoxType, (x_min, y_min, x_max, y_max) + tail) # type: ignore + return cast(BoxType, (x_min, y_min, x_max, y_max) + tail) -def denormalize_bbox(bbox: TBox, rows: int, cols: int) -> TBox: +def denormalize_bbox(bbox: BoxType, rows: int, cols: int) -> BoxType: """Denormalize coordinates of a bounding box. Multiply x-coordinates by image width and y-coordinates by image height. This is an inverse operation for :func:`~albumentations.augmentations.bbox.normalize_bbox`. @@ -200,7 +197,7 @@ def denormalize_bbox(bbox: TBox, rows: int, cols: int) -> TBox: x_min, x_max = x_min * cols, x_max * cols y_min, y_max = y_min * rows, y_max * rows - return cast(BoxType, (x_min, y_min, x_max, y_max) + tail) # type: ignore + return cast(BoxType, (x_min, y_min, x_max, y_max) + tail) def normalize_bboxes(bboxes: Sequence[BoxType], rows: int, cols: int) -> List[BoxType]: @@ -344,7 +341,7 @@ def convert_bbox_to_albumentations( else: (x_min, y_min, x_max, y_max), tail = bbox[:4], bbox[4:] - bbox = (x_min, y_min, x_max, y_max) + tuple(tail) # type: ignore + bbox = (x_min, y_min, x_max, y_max) + tuple(tail) if source_format != "yolo": bbox = normalize_bbox(bbox, rows, cols) @@ -402,7 +399,7 @@ def convert_bbox_from_albumentations( def convert_bboxes_to_albumentations( - bboxes: Sequence[BoxType], source_format, rows, cols, check_validity=False + bboxes: Sequence[BoxType], source_format: str, rows: int, cols: int, check_validity: bool = False ) -> List[BoxType]: """Convert a list bounding boxes from a format specified in `source_format` to the format used by albumentations""" return [convert_bbox_to_albumentations(bbox, source_format, rows, cols, check_validity) for bbox in bboxes] @@ -496,7 +493,7 @@ def filter_bboxes( return resulting_boxes -def union_of_bboxes(height: int, width: int, bboxes: Sequence[BoxType], erosion_rate: float = 0.0) -> BoxType: +def union_of_bboxes(height: int, width: int, bboxes: Sequence[BoxType], erosion_rate: float = 0.0) -> BoxInternalType: """Calculate union of bounding boxes. Args: diff --git a/albumentations/core/composition.py b/albumentations/core/composition.py index d4f50c3cb..79fb18e0e 100644 --- a/albumentations/core/composition.py +++ b/albumentations/core/composition.py @@ -1,9 +1,7 @@ -from __future__ import division - import random -import typing import warnings from collections import defaultdict +from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast import numpy as np @@ -29,17 +27,19 @@ "KeypointParams", "ReplayCompose", "Sequential", + "TransformType", + "TransformsSeqType", ] REPR_INDENT_STEP = 2 -TransformType = typing.Union[BasicTransform, "BaseCompose"] -TransformsSeqType = typing.Sequence[TransformType] +TransformType = Union[BasicTransform, "BaseCompose"] +TransformsSeqType = List[TransformType] -def get_always_apply(transforms: typing.Union["BaseCompose", TransformsSeqType]) -> TransformsSeqType: - new_transforms: typing.List[TransformType] = [] - for transform in transforms: # type: ignore +def get_always_apply(transforms: Union["BaseCompose", TransformsSeqType]) -> TransformsSeqType: + new_transforms: TransformsSeqType = [] + for transform in transforms: if isinstance(transform, BaseCompose): new_transforms.extend(get_always_apply(transform)) elif transform.always_apply: @@ -61,13 +61,16 @@ def __init__(self, transforms: TransformsSeqType, p: float): self.replay_mode = False self.applied_in_replay = False + def __iter__(self) -> Iterator[TransformType]: + return iter(self.transforms) + def __len__(self) -> int: return len(self.transforms) - def __call__(self, *args, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, **data: Any) -> Dict[str, Any]: raise NotImplementedError - def __getitem__(self, item: int) -> TransformType: # type: ignore + def __getitem__(self, item: int) -> TransformType: return self.transforms[item] def __repr__(self) -> str: @@ -79,11 +82,11 @@ def indented_repr(self, indent: int = REPR_INDENT_STEP) -> str: for t in self.transforms: repr_string += "\n" if hasattr(t, "indented_repr"): - t_repr = t.indented_repr(indent + REPR_INDENT_STEP) # type: ignore + t_repr = t.indented_repr(indent + REPR_INDENT_STEP) else: t_repr = repr(t) repr_string += " " * indent + t_repr + "," - repr_string += "\n" + " " * (indent - REPR_INDENT_STEP) + "], {args})".format(args=format_args(args)) + repr_string += "\n" + " " * (indent - REPR_INDENT_STEP) + f"], {format_args(args)})" return repr_string @classmethod @@ -94,14 +97,14 @@ def get_class_fullname(cls) -> str: def is_serializable(cls) -> bool: return True - def _to_dict(self) -> typing.Dict[str, typing.Any]: + def _to_dict(self) -> Dict[str, Any]: return { "__class_fullname__": self.get_class_fullname(), "p": self.p, - "transforms": [t._to_dict() for t in self.transforms], # skipcq: PYL-W0212 + "transforms": [t._to_dict() for t in self.transforms], } - def get_dict_with_id(self) -> typing.Dict[str, typing.Any]: + def get_dict_with_id(self) -> Dict[str, Any]: return { "__class_fullname__": self.get_class_fullname(), "id": id(self), @@ -109,7 +112,7 @@ def get_dict_with_id(self) -> typing.Dict[str, typing.Any]: "transforms": [t.get_dict_with_id() for t in self.transforms], } - def add_targets(self, additional_targets: typing.Optional[typing.Dict[str, str]]) -> None: + def add_targets(self, additional_targets: Optional[Dict[str, str]]) -> None: if additional_targets: for t in self.transforms: t.add_targets(additional_targets) @@ -135,15 +138,15 @@ class Compose(BaseCompose): def __init__( self, transforms: TransformsSeqType, - bbox_params: typing.Optional[typing.Union[dict, "BboxParams"]] = None, - keypoint_params: typing.Optional[typing.Union[dict, "KeypointParams"]] = None, - additional_targets: typing.Optional[typing.Dict[str, str]] = None, + bbox_params: Optional[Union[Dict[str, Any], "BboxParams"]] = None, + keypoint_params: Optional[Union[Dict[str, Any], "KeypointParams"]] = None, + additional_targets: Optional[Dict[str, str]] = None, p: float = 1.0, is_check_shapes: bool = True, ): - super(Compose, self).__init__(transforms, p) + super().__init__(transforms, p) - self.processors: typing.Dict[str, typing.Union[BboxProcessor, KeypointsProcessor]] = {} + self.processors: Dict[str, Union[BboxProcessor, KeypointsProcessor]] = {} if bbox_params: if isinstance(bbox_params, dict): b_params = BboxParams(**bbox_params) @@ -188,7 +191,7 @@ def _disable_check_args_for_transforms(transforms: TransformsSeqType) -> None: def _disable_check_args(self) -> None: self.is_check_args = False - def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool = False, **data: Any) -> Dict[str, Any]: if args: raise KeyError("You have to pass data to augmentations as named arguments, for example: aug(image=image)") if self.is_check_args: @@ -218,7 +221,7 @@ def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, return data - def _check_data_post_transform(self, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def _check_data_post_transform(self, data: Any) -> Dict[str, Any]: rows, cols = get_shape(data["image"]) for p in self.processors.values(): @@ -229,32 +232,28 @@ def _check_data_post_transform(self, data: typing.Dict[str, typing.Any]) -> typi data[data_name] = p.filter(data[data_name], rows, cols) return data - def _to_dict(self) -> typing.Dict[str, typing.Any]: - dictionary = super(Compose, self)._to_dict() + def _to_dict(self) -> Dict[str, Any]: + dictionary = super()._to_dict() bbox_processor = self.processors.get("bboxes") keypoints_processor = self.processors.get("keypoints") dictionary.update( { - "bbox_params": bbox_processor.params._to_dict() if bbox_processor else None, # skipcq: PYL-W0212 - "keypoint_params": keypoints_processor.params._to_dict() # skipcq: PYL-W0212 - if keypoints_processor - else None, + "bbox_params": bbox_processor.params._to_dict() if bbox_processor else None, + "keypoint_params": (keypoints_processor.params._to_dict() if keypoints_processor else None), "additional_targets": self.additional_targets, "is_check_shapes": self.is_check_shapes, } ) return dictionary - def get_dict_with_id(self) -> typing.Dict[str, typing.Any]: + def get_dict_with_id(self) -> Dict[str, Any]: dictionary = super().get_dict_with_id() bbox_processor = self.processors.get("bboxes") keypoints_processor = self.processors.get("keypoints") dictionary.update( { - "bbox_params": bbox_processor.params._to_dict() if bbox_processor else None, # skipcq: PYL-W0212 - "keypoint_params": keypoints_processor.params._to_dict() # skipcq: PYL-W0212 - if keypoints_processor - else None, + "bbox_params": bbox_processor.params._to_dict() if bbox_processor else None, + "keypoint_params": (keypoints_processor.params._to_dict() if keypoints_processor else None), "additional_targets": self.additional_targets, "params": None, "is_check_shapes": self.is_check_shapes, @@ -262,22 +261,21 @@ def get_dict_with_id(self) -> typing.Dict[str, typing.Any]: ) return dictionary - def _check_args(self, **kwargs) -> None: + def _check_args(self, **kwargs: Any) -> None: checked_single = ["image", "mask"] checked_multi = ["masks"] check_bbox_param = ["bboxes"] - # ["bboxes", "keypoints"] could be almost any type, no need to check them shapes = [] for data_name, data in kwargs.items(): internal_data_name = self.additional_targets.get(data_name, data_name) if internal_data_name in checked_single: if not isinstance(data, np.ndarray): - raise TypeError("{} must be numpy array type".format(data_name)) + raise TypeError(f"{data_name} must be numpy array type") shapes.append(data.shape[:2]) if internal_data_name in checked_multi: if data is not None and len(data): if not isinstance(data[0], np.ndarray): - raise TypeError("{} must be list of numpy arrays".format(data_name)) + raise TypeError(f"{data_name} must be list of numpy arrays") shapes.append(data[0].shape[:2]) if internal_data_name in check_bbox_param and self.processors.get("bboxes") is None: raise ValueError("bbox_params must be specified for bbox transformations") @@ -290,7 +288,7 @@ def _check_args(self, **kwargs) -> None: ) @staticmethod - def _make_targets_contiguous(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + def _make_targets_contiguous(data: Any) -> Dict[str, Any]: result = {} for key, value in data.items(): if isinstance(value, np.ndarray): @@ -309,12 +307,12 @@ class OneOf(BaseCompose): """ def __init__(self, transforms: TransformsSeqType, p: float = 0.5): - super(OneOf, self).__init__(transforms, p) + super().__init__(transforms, p) transforms_ps = [t.p for t in self.transforms] s = sum(transforms_ps) self.transforms_ps = [t / s for t in transforms_ps] - def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool = False, **data: Any) -> Dict[str, Any]: if self.replay_mode: for t in self.transforms: data = t(**data) @@ -339,14 +337,14 @@ class SomeOf(BaseCompose): """ def __init__(self, transforms: TransformsSeqType, n: int, replace: bool = True, p: float = 1): - super(SomeOf, self).__init__(transforms, p) + super().__init__(transforms, p) self.n = n self.replace = replace transforms_ps = [t.p for t in self.transforms] s = sum(transforms_ps) self.transforms_ps = [t / s for t in transforms_ps] - def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *arg: Any, force_apply: bool = False, **data: Any) -> Dict[str, Any]: if self.replay_mode: for t in self.transforms: data = t(**data) @@ -354,13 +352,13 @@ def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, if self.transforms_ps and (force_apply or random.random() < self.p): idx = random_utils.choice(len(self.transforms), size=self.n, replace=self.replace, p=self.transforms_ps) - for i in idx: # type: ignore + for i in idx: t = self.transforms[i] data = t(force_apply=True, **data) return data - def _to_dict(self) -> typing.Dict[str, typing.Any]: - dictionary = super(SomeOf, self)._to_dict() + def _to_dict(self) -> Dict[str, Any]: + dictionary = super()._to_dict() dictionary.update({"n": self.n, "replace": self.replace}) return dictionary @@ -370,20 +368,20 @@ class OneOrOther(BaseCompose): def __init__( self, - first: typing.Optional[TransformType] = None, - second: typing.Optional[TransformType] = None, - transforms: typing.Optional[TransformsSeqType] = None, + first: Optional[TransformType] = None, + second: Optional[TransformType] = None, + transforms: Optional[TransformsSeqType] = None, p: float = 0.5, ): if transforms is None: if first is None or second is None: raise ValueError("You must set both first and second or set transforms argument.") transforms = [first, second] - super(OneOrOther, self).__init__(transforms, p) + super().__init__(transforms, p) if len(self.transforms) != 2: warnings.warn("Length of transforms is not equal to 2.") - def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool = False, **data: Any) -> Dict[str, Any]: if self.replay_mode: for t in self.transforms: data = t(**data) @@ -401,17 +399,15 @@ class PerChannel(BaseCompose): Args: transforms (list): list of transformations to compose. channels (sequence): channels to apply the transform to. Pass None to apply to all. - Default: None (apply to all) + Default: None (apply to all) p (float): probability of applying the transform. Default: 0.5. """ - def __init__( - self, transforms: TransformsSeqType, channels: typing.Optional[typing.Sequence[int]] = None, p: float = 0.5 - ): - super(PerChannel, self).__init__(transforms, p) + def __init__(self, transforms: TransformsSeqType, channels: Optional[Sequence[int]] = None, p: float = 0.5): + super().__init__(transforms, p) self.channels = channels - def __call__(self, *args, force_apply: bool = False, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool = False, **data: Any) -> Dict[str, Any]: if force_apply or random.random() < self.p: image = data["image"] @@ -435,22 +431,20 @@ class ReplayCompose(Compose): def __init__( self, transforms: TransformsSeqType, - bbox_params: typing.Optional[typing.Union[dict, "BboxParams"]] = None, - keypoint_params: typing.Optional[typing.Union[dict, "KeypointParams"]] = None, - additional_targets: typing.Optional[typing.Dict[str, str]] = None, + bbox_params: Optional[Union[Dict[str, Any], "BboxParams"]] = None, + keypoint_params: Optional[Union[Dict[str, Any], "KeypointParams"]] = None, + additional_targets: Optional[Dict[str, str]] = None, p: float = 1.0, is_check_shapes: bool = True, save_key: str = "replay", ): - super(ReplayCompose, self).__init__( - transforms, bbox_params, keypoint_params, additional_targets, p, is_check_shapes - ) + super().__init__(transforms, bbox_params, keypoint_params, additional_targets, p, is_check_shapes) self.set_deterministic(True, save_key=save_key) self.save_key = save_key - def __call__(self, *args, force_apply: bool = False, **kwargs) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool = False, **kwargs: Any) -> Dict[str, Any]: kwargs[self.save_key] = defaultdict(dict) - result = super(ReplayCompose, self).__call__(force_apply=force_apply, **kwargs) + result = super().__call__(force_apply=force_apply, **kwargs) serialized = self.get_dict_with_id() self.fill_with_params(serialized, result[self.save_key]) self.fill_applied(serialized) @@ -458,13 +452,13 @@ def __call__(self, *args, force_apply: bool = False, **kwargs) -> typing.Dict[st return result @staticmethod - def replay(saved_augmentations: typing.Dict[str, typing.Any], **kwargs) -> typing.Dict[str, typing.Any]: + def replay(saved_augmentations: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: augs = ReplayCompose._restore_for_replay(saved_augmentations) return augs(force_apply=True, **kwargs) @staticmethod def _restore_for_replay( - transform_dict: typing.Dict[str, typing.Any], lambda_transforms: typing.Optional[dict] = None + transform_dict: Dict[str, Any], lambda_transforms: Optional[Dict[str, Any]] = None ) -> TransformType: """ Args: @@ -490,21 +484,21 @@ def _restore_for_replay( ] transform = cls(**args) - transform = typing.cast(BasicTransform, transform) + transform = cast(BasicTransform, transform) if isinstance(transform, BasicTransform): transform.params = params transform.replay_mode = True transform.applied_in_replay = applied return transform - def fill_with_params(self, serialized: dict, all_params: dict) -> None: + def fill_with_params(self, serialized: Dict[str, Any], all_params: Any) -> None: params = all_params.get(serialized.get("id")) serialized["params"] = params del serialized["id"] for transform in serialized.get("transforms", []): self.fill_with_params(transform, all_params) - def fill_applied(self, serialized: typing.Dict[str, typing.Any]) -> bool: + def fill_applied(self, serialized: Dict[str, Any]) -> bool: if "transforms" in serialized: applied = [self.fill_applied(t) for t in serialized["transforms"]] serialized["applied"] = any(applied) @@ -512,8 +506,8 @@ def fill_applied(self, serialized: typing.Dict[str, typing.Any]) -> bool: serialized["applied"] = serialized.get("params") is not None return serialized["applied"] - def _to_dict(self) -> typing.Dict[str, typing.Any]: - dictionary = super(ReplayCompose, self)._to_dict() + def _to_dict(self) -> Dict[str, Any]: + dictionary = super()._to_dict() dictionary.update({"save_key": self.save_key}) return dictionary @@ -546,7 +540,7 @@ class Sequential(BaseCompose): def __init__(self, transforms: TransformsSeqType, p: float = 0.5): super().__init__(transforms, p) - def __call__(self, *args, **data) -> typing.Dict[str, typing.Any]: + def __call__(self, *args: Any, force_apply: bool, **data: Any) -> Dict[str, Any]: for t in self.transforms: data = t(**data) return data diff --git a/albumentations/core/keypoints_utils.py b/albumentations/core/keypoints_utils.py index 9272a7376..ae9ca337e 100644 --- a/albumentations/core/keypoints_utils.py +++ b/albumentations/core/keypoints_utils.py @@ -1,10 +1,8 @@ -from __future__ import division - import math -import typing import warnings -from typing import Any, Dict, List, Optional, Sequence, Tuple +from typing import Any, Dict, List, Optional, Sequence +from .types import KeypointType from .utils import DataProcessor, Params __all__ = [ @@ -49,19 +47,19 @@ class KeypointParams(Params): def __init__( self, - format: str, # skipcq: PYL-W0622 + format: str, label_fields: Optional[Sequence[str]] = None, remove_invisible: bool = True, angle_in_degrees: bool = True, check_each_transform: bool = True, ): - super(KeypointParams, self).__init__(format, label_fields) + super().__init__(format, label_fields) self.remove_invisible = remove_invisible self.angle_in_degrees = angle_in_degrees self.check_each_transform = check_each_transform def _to_dict(self) -> Dict[str, Any]: - data = super(KeypointParams, self)._to_dict() + data = super()._to_dict() data.update( { "remove_invisible": self.remove_invisible, @@ -117,14 +115,29 @@ def ensure_transforms_valid(self, transforms: Sequence[object]) -> None: ) break - def filter(self, data: Sequence[Sequence], rows: int, cols: int) -> Sequence[Sequence]: + def filter(self, data: Sequence[KeypointType], rows: int, cols: int) -> Sequence[KeypointType]: + """ + The function filters a sequence of data based on the number of rows and columns, and returns a + sequence of keypoints. + + :param data: The `data` parameter is a sequence of sequences. Each inner sequence represents a + set of keypoints + :type data: Sequence[Sequence] + :param rows: The `rows` parameter represents the number of rows in the data matrix. It specifies + the number of rows that will be used for filtering the keypoints + :type rows: int + :param cols: The parameter "cols" represents the number of columns in the grid that the + keypoints will be filtered on + :type cols: int + :return: a sequence of KeypointType objects. + """ self.params: KeypointParams return filter_keypoints(data, rows, cols, remove_invisible=self.params.remove_invisible) - def check(self, data: Sequence[Sequence], rows: int, cols: int) -> None: + def check(self, data: Sequence[KeypointType], rows: int, cols: int) -> None: check_keypoints(data, rows, cols) - def convert_from_albumentations(self, data: Sequence[Sequence], rows: int, cols: int) -> List[Tuple]: + def convert_from_albumentations(self, data: Sequence[KeypointType], rows: int, cols: int) -> List[KeypointType]: params = self.params return convert_keypoints_from_albumentations( data, @@ -135,7 +148,7 @@ def convert_from_albumentations(self, data: Sequence[Sequence], rows: int, cols: angle_in_degrees=params.angle_in_degrees, ) - def convert_to_albumentations(self, data: Sequence[Sequence], rows: int, cols: int) -> List[Tuple]: + def convert_to_albumentations(self, data: Sequence[KeypointType], rows: int, cols: int) -> List[KeypointType]: params = self.params return convert_keypoints_to_albumentations( data, @@ -147,7 +160,7 @@ def convert_to_albumentations(self, data: Sequence[Sequence], rows: int, cols: i ) -def check_keypoint(kp: Sequence, rows: int, cols: int) -> None: +def check_keypoint(kp: KeypointType, rows: int, cols: int) -> None: """Check if keypoint coordinates are less than image shapes""" for name, value, size in zip(["x", "y"], kp[:2], [cols, rows]): if not 0 <= value < size: @@ -158,16 +171,18 @@ def check_keypoint(kp: Sequence, rows: int, cols: int) -> None: angle = kp[2] if not (0 <= angle < 2 * math.pi): - raise ValueError("Keypoint angle must be in range [0, 2 * PI). Got: {angle}".format(angle=angle)) + raise ValueError(f"Keypoint angle must be in range [0, 2 * PI). Got: {angle}") -def check_keypoints(keypoints: Sequence[Sequence], rows: int, cols: int) -> None: +def check_keypoints(keypoints: Sequence[KeypointType], rows: int, cols: int) -> None: """Check if keypoints boundaries are less than image shapes""" for kp in keypoints: check_keypoint(kp, rows, cols) -def filter_keypoints(keypoints: Sequence[Sequence], rows: int, cols: int, remove_invisible: bool) -> Sequence[Sequence]: +def filter_keypoints( + keypoints: Sequence[KeypointType], rows: int, cols: int, remove_invisible: bool +) -> Sequence[KeypointType]: if not remove_invisible: return keypoints @@ -183,15 +198,15 @@ def filter_keypoints(keypoints: Sequence[Sequence], rows: int, cols: int, remove def convert_keypoint_to_albumentations( - keypoint: Sequence, + keypoint: KeypointType, source_format: str, rows: int, cols: int, check_validity: bool = False, angle_in_degrees: bool = True, -) -> Tuple: +) -> KeypointType: if source_format not in keypoint_formats: - raise ValueError("Unknown target_format {}. Supported formats are: {}".format(source_format, keypoint_formats)) + raise ValueError(f"Unknown target_format {source_format}. Supported formats are: {keypoint_formats}") if source_format == "xy": (x, y), tail = keypoint[:2], tuple(keypoint[2:]) @@ -222,15 +237,15 @@ def convert_keypoint_to_albumentations( def convert_keypoint_from_albumentations( - keypoint: Sequence, + keypoint: KeypointType, target_format: str, rows: int, cols: int, check_validity: bool = False, angle_in_degrees: bool = True, -) -> Tuple: +) -> KeypointType: if target_format not in keypoint_formats: - raise ValueError("Unknown target_format {}. Supported formats are: {}".format(target_format, keypoint_formats)) + raise ValueError(f"Unknown target_format {target_format}. Supported formats are: {keypoint_formats}") (x, y, angle, scale), tail = keypoint[:4], tuple(keypoint[4:]) angle = angle_to_2pi_range(angle) @@ -239,33 +254,30 @@ def convert_keypoint_from_albumentations( if angle_in_degrees: angle = math.degrees(angle) - kp: Tuple if target_format == "xy": - kp = (x, y) - elif target_format == "yx": - kp = (y, x) - elif target_format == "xya": - kp = (x, y, angle) - elif target_format == "xys": - kp = (x, y, scale) - elif target_format == "xyas": - kp = (x, y, angle, scale) - elif target_format == "xysa": - kp = (x, y, scale, angle) - else: - raise ValueError(f"Invalid target format. Got: {target_format}") + return (x, y) + tail + if target_format == "yx": + return (y, x) + tail + if target_format == "xya": + return (x, y, angle) + tail + if target_format == "xys": + return (x, y, scale) + tail + if target_format == "xyas": + return (x, y, angle, scale) + tail + if target_format == "xysa": + return (x, y, scale, angle) + tail - return kp + tail + raise ValueError(f"Invalid target format. Got: {target_format}") def convert_keypoints_to_albumentations( - keypoints: Sequence[Sequence], + keypoints: Sequence[KeypointType], source_format: str, rows: int, cols: int, check_validity: bool = False, angle_in_degrees: bool = True, -) -> List[Tuple]: +) -> List[KeypointType]: return [ convert_keypoint_to_albumentations(kp, source_format, rows, cols, check_validity, angle_in_degrees) for kp in keypoints @@ -273,13 +285,13 @@ def convert_keypoints_to_albumentations( def convert_keypoints_from_albumentations( - keypoints: Sequence[Sequence], + keypoints: Sequence[KeypointType], target_format: str, rows: int, cols: int, check_validity: bool = False, angle_in_degrees: bool = True, -) -> List[Tuple]: +) -> List[KeypointType]: return [ convert_keypoint_from_albumentations(kp, target_format, rows, cols, check_validity, angle_in_degrees) for kp in keypoints diff --git a/albumentations/core/serialization.py b/albumentations/core/serialization.py index 394d31d5a..8cdcb2da9 100644 --- a/albumentations/core/serialization.py +++ b/albumentations/core/serialization.py @@ -1,10 +1,8 @@ -from __future__ import absolute_import - import json import typing import warnings from abc import ABC, ABCMeta, abstractmethod -from typing import IO, Any, Callable, Dict, Optional, Tuple, Type, Union +from typing import Any, Dict, Optional, Tuple, Type, Union try: import yaml @@ -33,18 +31,13 @@ def shorten_class_name(class_fullname: str) -> str: return class_fullname -def get_shortest_class_fullname(cls: Type) -> str: - class_fullname = "{cls.__module__}.{cls.__name__}".format(cls=cls) - return shorten_class_name(class_fullname) - - class SerializableMeta(ABCMeta): """ A metaclass that is used to register classes in `SERIALIZABLE_REGISTRY` or `NON_SERIALIZABLE_REGISTRY` so they can be found later while deserializing transformation pipeline using classes full names. """ - def __new__(mcs, name: str, bases: Tuple[type, ...], *args, **kwargs) -> "SerializableMeta": + def __new__(mcs, name: str, bases: Tuple[type, ...], *args: Any, **kwargs: Any) -> "SerializableMeta": cls_obj = super().__new__(mcs, name, bases, *args, **kwargs) if name != "Serializable" and ABC not in bases: if cls_obj.is_serializable(): @@ -141,9 +134,7 @@ def instantiate_nonserializable( ) result_transform = nonserializable.get(name) if transform is None: - raise ValueError( - "Non-serializable transform with {name} was not found in `nonserializable`".format(name=name) - ) + raise ValueError(f"Non-serializable transform with {name} was not found in `nonserializable`") return result_transform return None @@ -181,7 +172,7 @@ def from_dict( def check_data_format(data_format: str) -> None: if data_format not in {"json", "yaml"}: - raise ValueError("Unknown data_format {}. Supported formats are: 'json' and 'yaml'".format(data_format)) + raise ValueError(f"Unknown data_format {data_format}. Supported formats are: 'json' and 'yaml'") def save( @@ -201,9 +192,13 @@ def save( """ check_data_format(data_format) transform_dict = transform.to_dict(on_not_implemented_error=on_not_implemented_error) - dump_fn = json.dump if data_format == "json" else yaml.safe_dump with open(filepath, "w") as f: - dump_fn(transform_dict, f) # type: ignore + if data_format == "yaml": + if not yaml_available: + raise ValueError("You need to install PyYAML to save a pipeline in yaml format") + yaml.safe_dump(transform_dict, f, default_flow_style=False) + elif data_format == "json": + json.dump(transform_dict, f) def load( @@ -245,3 +240,17 @@ def register_additional_transforms() -> None: import albumentations.pytorch except ImportError: pass + + +def get_shortest_class_fullname(cls: Type[Any]) -> str: + """ + The function `get_shortest_class_fullname` takes a class object as input and returns its shortened + full name. + + :param cls: The parameter `cls` is of type `Type[BasicCompose]`, which means it expects a class that + is a subclass of `BasicCompose` + :type cls: Type[BasicCompose] + :return: a string, which is the shortened version of the full class name. + """ + class_fullname = "{cls.__module__}.{cls.__name__}".format(cls=cls) + return shorten_class_name(class_fullname) diff --git a/albumentations/core/transforms_interface.py b/albumentations/core/transforms_interface.py index eec2cd304..df64f92c6 100644 --- a/albumentations/core/transforms_interface.py +++ b/albumentations/core/transforms_interface.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import random from copy import deepcopy from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast @@ -9,73 +7,75 @@ import numpy as np from .serialization import Serializable, get_shortest_class_fullname +from .types import ( + BoxInternalType, + BoxType, + KeypointInternalType, + KeypointType, + ScalarType, + ScaleType, +) from .utils import format_args -__all__ = [ - "to_tuple", - "BasicTransform", - "DualTransform", - "ImageOnlyTransform", - "NoOp", - "BoxType", - "KeypointType", - "ImageColorType", - "ScaleFloatType", - "ScaleIntType", - "ImageColorType", -] - -NumType = Union[int, float, np.ndarray] -BoxInternalType = Tuple[float, float, float, float] -BoxType = Union[BoxInternalType, Tuple[float, float, float, float, Any]] -KeypointInternalType = Tuple[float, float, float, float] -KeypointType = Union[KeypointInternalType, Tuple[float, float, float, float, Any]] -ImageColorType = Union[float, Sequence[float]] - -ScaleFloatType = Union[float, Tuple[float, float]] -ScaleIntType = Union[int, Tuple[int, int]] +__all__ = ["to_tuple", "BasicTransform", "DualTransform", "ImageOnlyTransform", "NoOp"] + FillValueType = Optional[Union[int, float, Sequence[int], Sequence[float]]] -def to_tuple(param, low=None, bias=None): - """Convert input argument to min-max tuple +def to_tuple( + param: ScaleType, + low: Optional[ScaleType] = None, + bias: Optional[ScalarType] = None, +) -> Union[Tuple[int, int], Tuple[float, float]]: + """ + Convert input argument to a min-max tuple. + Args: - param (scalar, tuple or list of 2+ elements): Input value. - If value is scalar, return value would be (offset - value, offset + value). - If value is tuple, return value would be value + offset (broadcasted). - low: Second element of tuple can be passed as optional argument - bias: An offset factor added to each element + param: Input value which could be a scalar or a sequence of exactly 2 scalars. + low: Second element of the tuple, provided as an optional argument for when `param` is a scalar. + bias: An offset added to both elements of the tuple. + + Returns: + A tuple of two scalars, optionally adjusted by `bias`. + Raises ValueError for invalid combinations or types of arguments. """ + # Validate mutually exclusive arguments if low is not None and bias is not None: - raise ValueError("Arguments low and bias are mutually exclusive") + raise ValueError("Arguments 'low' and 'bias' cannot be used together.") - if param is None: - return param + if isinstance(param, Sequence) and len(param) == 2: + min_val, max_val = min(param), max(param) - if isinstance(param, (int, float)): - if low is None: - param = -param, +param + # Handle scalar input + elif isinstance(param, (int, float)): + if isinstance(low, (int, float)): + # Use low and param to create a tuple + min_val, max_val = (low, param) if low < param else (param, low) else: - param = (low, param) if low < param else (param, low) - elif isinstance(param, Sequence): - if len(param) != 2: - raise ValueError("to_tuple expects 1 or 2 values") - param = tuple(param) + # Create a symmetric tuple around 0 + min_val, max_val = -param, param else: - raise ValueError("Argument param must be either scalar (int, float) or tuple") + raise ValueError("Argument 'param' must be either a scalar or a sequence of 2 elements.") + # Apply bias if provided if bias is not None: - return tuple(bias + x for x in param) + return (bias + min_val, bias + max_val) - return tuple(param) + return min_val, max_val + + +class Interpolation: + def __init__(self, downscale: int = cv2.INTER_NEAREST, upscale: int = cv2.INTER_NEAREST): + self.downscale = downscale + self.upscale = upscale class BasicTransform(Serializable): call_backup = None - interpolation: Any - fill_value: Any - mask_fill_value: Any + interpolation: Union[int, Interpolation] + fill_value: ScalarType + mask_fill_value: Optional[ScalarType] def __init__(self, always_apply: bool = False, p: float = 0.5): self.p = p @@ -89,7 +89,7 @@ def __init__(self, always_apply: bool = False, p: float = 0.5): self.replay_mode = False self.applied_in_replay = False - def __call__(self, *args, force_apply: bool = False, **kwargs) -> Dict[str, Any]: + def __call__(self, *args: Any, force_apply: bool = False, **kwargs: Any) -> Any: if args: raise KeyError("You have to pass data to augmentations as named arguments, for example: aug(image=image)") if self.replay_mode: @@ -119,7 +119,7 @@ def __call__(self, *args, force_apply: bool = False, **kwargs) -> Dict[str, Any] return kwargs - def apply_with_params(self, params: Dict[str, Any], **kwargs) -> Dict[str, Any]: # skipcq: PYL-W0613 + def apply_with_params(self, params: Dict[str, Any], **kwargs: Any) -> Any: if params is None: return kwargs params = self.update_params(params, **kwargs) @@ -142,9 +142,9 @@ def set_deterministic(self, flag: bool, save_key: str = "replay") -> "BasicTrans def __repr__(self) -> str: state = self.get_base_init_args() state.update(self.get_transform_init_args()) - return "{name}({args})".format(name=self.__class__.__name__, args=format_args(state)) + return f"{self.__class__.__name__}({format_args(state)})" - def _get_target_function(self, key: str) -> Callable: + def _get_target_function(self, key: str) -> Callable[..., Any]: transform_key = key if key in self._additional_targets: transform_key = self._additional_targets.get(key, key) @@ -152,20 +152,20 @@ def _get_target_function(self, key: str) -> Callable: target_function = self.targets.get(transform_key, lambda x, **p: x) return target_function - def apply(self, img: np.ndarray, **params) -> np.ndarray: + def apply(self, img: np.ndarray, *args: Any, **params: Any) -> np.ndarray: raise NotImplementedError - def get_params(self) -> Dict: + def get_params(self) -> Dict[str, Any]: return {} @property - def targets(self) -> Dict[str, Callable]: + def targets(self) -> Dict[str, Callable[..., Any]]: # you must specify targets in subclass # for example: ('image', 'mask') # ('image', 'boxes') raise NotImplementedError - def update_params(self, params: Dict[str, Any], **kwargs) -> Dict[str, Any]: + def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: if hasattr(self, "interpolation"): params["interpolation"] = self.interpolation if hasattr(self, "fill_value"): @@ -176,10 +176,10 @@ def update_params(self, params: Dict[str, Any], **kwargs) -> Dict[str, Any]: return params @property - def target_dependence(self) -> Dict: + def target_dependence(self) -> Dict[str, Any]: return {} - def add_targets(self, additional_targets: Dict[str, str]): + def add_targets(self, additional_targets: Dict[str, str]) -> None: """Add targets to transform them the same way as one of existing targets ex: {'target_image': 'image'} ex: {'obj1_mask': 'mask', 'obj2_mask': 'mask'} @@ -204,7 +204,7 @@ def get_class_fullname(cls) -> str: return get_shortest_class_fullname(cls) @classmethod - def is_serializable(cls): + def is_serializable(cls) -> bool: return True def get_transform_init_args_names(self) -> Tuple[str, ...]: @@ -235,7 +235,7 @@ class DualTransform(BasicTransform): """Transform for segmentation task.""" @property - def targets(self) -> Dict[str, Callable]: + def targets(self) -> Dict[str, Callable[..., Any]]: return { "image": self.apply, "mask": self.apply_to_mask, @@ -244,25 +244,31 @@ def targets(self) -> Dict[str, Callable]: "keypoints": self.apply_to_keypoints, } - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, *args: Any, **params: Any) -> BoxInternalType: raise NotImplementedError("Method apply_to_bbox is not implemented in class " + self.__class__.__name__) - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, *args: Any, **params: Any) -> KeypointInternalType: raise NotImplementedError("Method apply_to_keypoint is not implemented in class " + self.__class__.__name__) - def apply_to_bboxes(self, bboxes: Sequence[BoxType], **params) -> List[BoxType]: - return [self.apply_to_bbox(tuple(bbox[:4]), **params) + tuple(bbox[4:]) for bbox in bboxes] # type: ignore + def apply_to_bboxes(self, bboxes: Sequence[BoxType], *args: Any, **params: Any) -> Sequence[BoxType]: + return [ + self.apply_to_bbox(cast(BoxInternalType, tuple(cast(BoxInternalType, bbox[:4]))), **params) + + tuple(bbox[4:]) + for bbox in bboxes + ] - def apply_to_keypoints(self, keypoints: Sequence[KeypointType], **params) -> List[KeypointType]: - return [ # type: ignore - self.apply_to_keypoint(tuple(keypoint[:4]), **params) + tuple(keypoint[4:]) # type: ignore + def apply_to_keypoints( + self, keypoints: Sequence[KeypointType], *args: Any, **params: Any + ) -> Sequence[KeypointType]: + return [ + self.apply_to_keypoint(cast(KeypointInternalType, tuple(keypoint[:4])), **params) + tuple(keypoint[4:]) for keypoint in keypoints ] - def apply_to_mask(self, img: np.ndarray, **params) -> np.ndarray: - return self.apply(img, **{k: cv2.INTER_NEAREST if k == "interpolation" else v for k, v in params.items()}) + def apply_to_mask(self, mask: np.ndarray, *args: Any, **params: Any) -> np.ndarray: + return self.apply(mask, **{k: cv2.INTER_NEAREST if k == "interpolation" else v for k, v in params.items()}) - def apply_to_masks(self, masks: Sequence[np.ndarray], **params) -> List[np.ndarray]: + def apply_to_masks(self, masks: Sequence[np.ndarray], **params: Any) -> List[np.ndarray]: return [self.apply_to_mask(mask, **params) for mask in masks] @@ -270,24 +276,24 @@ class ImageOnlyTransform(BasicTransform): """Transform applied to image only.""" @property - def targets(self) -> Dict[str, Callable]: + def targets(self) -> Dict[str, Callable[..., Any]]: return {"image": self.apply} class NoOp(DualTransform): """Does nothing""" - def apply_to_keypoint(self, keypoint: KeypointInternalType, **params) -> KeypointInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, **params: Any) -> KeypointInternalType: return keypoint - def apply_to_bbox(self, bbox: BoxInternalType, **params) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: return bbox - def apply(self, img: np.ndarray, **params) -> np.ndarray: + def apply(self, img: np.ndarray, **params: Any) -> np.ndarray: return img - def apply_to_mask(self, img: np.ndarray, **params) -> np.ndarray: - return img + def apply_to_mask(self, mask: np.ndarray, **params: Any) -> np.ndarray: + return mask - def get_transform_init_args_names(self) -> Tuple: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return () diff --git a/albumentations/core/types.py b/albumentations/core/types.py new file mode 100644 index 000000000..815dd9464 --- /dev/null +++ b/albumentations/core/types.py @@ -0,0 +1,26 @@ +from typing import Any, Sequence, Tuple, Union + +import numpy as np + +ScalarType = Union[int, float] +ColorType = Union[int, float, Tuple[int, int, int], Tuple[float, float, float]] +SizeType = Sequence[int] + +BoxInternalType = Tuple[float, float, float, float] +BoxType = Union[BoxInternalType, Tuple[float, float, float, float, Any], Tuple[float, float, float, float]] +KeypointInternalType = Tuple[float, float, float, float] +KeypointType = Union[KeypointInternalType, Tuple[float, float, float, float, Any]] + +BoxOrKeypointType = Union[BoxType, KeypointType] + +ScaleFloatType = Union[float, Tuple[float, float]] +ScaleIntType = Union[int, Tuple[int, int]] + +ScaleType = Union[ScaleFloatType, ScaleIntType] + +NumType = Union[int, float, np.ndarray] + +ImageColorType = Union[float, Sequence[float]] + +IntNumType = Union[np.integer, np.ndarray] +FloatNumType = Union[np.floating, np.ndarray] diff --git a/albumentations/core/utils.py b/albumentations/core/utils.py index 971700f7f..6d0d85736 100644 --- a/albumentations/core/utils.py +++ b/albumentations/core/utils.py @@ -1,24 +1,21 @@ -from __future__ import absolute_import - from abc import ABC, abstractmethod -from typing import Any, Dict, Optional, Sequence, Tuple +from typing import Any, Dict, List, Optional, Sequence, Union import numpy as np +import torch from .serialization import Serializable +from .types import BoxOrKeypointType, SizeType -def get_shape(img: Any) -> Tuple[int, int]: +def get_shape(img: Union[np.ndarray, torch.Tensor]) -> SizeType: if isinstance(img, np.ndarray): rows, cols = img.shape[:2] return rows, cols try: - import torch - if torch.is_tensor(img): - rows, cols = img.shape[-2:] - return rows, cols + return img.shape[-2:] except ImportError: pass @@ -27,7 +24,7 @@ def get_shape(img: Any) -> Tuple[int, int]: ) -def format_args(args_dict: Dict): +def format_args(args_dict: Dict[str, Any]) -> str: formatted_args = [] for k, v in args_dict.items(): if isinstance(v, str): @@ -82,7 +79,9 @@ def preprocess(self, data: Dict[str, Any]) -> None: for data_name in self.data_fields: data[data_name] = self.check_and_convert(data[data_name], rows, cols, direction="to") - def check_and_convert(self, data: Sequence, rows: int, cols: int, direction: str = "to") -> Sequence: + def check_and_convert( + self, data: List[BoxOrKeypointType], rows: int, cols: int, direction: str = "to" + ) -> List[BoxOrKeypointType]: if self.params.format == "albumentations": self.check(data, rows, cols) return data @@ -95,19 +94,21 @@ def check_and_convert(self, data: Sequence, rows: int, cols: int, direction: str raise ValueError(f"Invalid direction. Must be `to` or `from`. Got `{direction}`") @abstractmethod - def filter(self, data: Sequence, rows: int, cols: int) -> Sequence: + def filter(self, data: Sequence[BoxOrKeypointType], rows: int, cols: int) -> Sequence[BoxOrKeypointType]: pass @abstractmethod - def check(self, data: Sequence, rows: int, cols: int) -> None: + def check(self, data: List[BoxOrKeypointType], rows: int, cols: int) -> None: pass @abstractmethod - def convert_to_albumentations(self, data: Sequence, rows: int, cols: int) -> Sequence: + def convert_to_albumentations(self, data: List[BoxOrKeypointType], rows: int, cols: int) -> List[BoxOrKeypointType]: pass @abstractmethod - def convert_from_albumentations(self, data: Sequence, rows: int, cols: int) -> Sequence: + def convert_from_albumentations( + self, data: List[BoxOrKeypointType], rows: int, cols: int + ) -> List[BoxOrKeypointType]: pass def add_label_fields_to_data(self, data: Dict[str, Any]) -> Dict[str, Any]: diff --git a/albumentations/imgaug/stubs.py b/albumentations/imgaug/stubs.py index 1eac33300..946b1164c 100644 --- a/albumentations/imgaug/stubs.py +++ b/albumentations/imgaug/stubs.py @@ -1,3 +1,5 @@ +from typing import Any + __all__ = [ "IAAEmboss", "IAASuperpixels", @@ -13,15 +15,19 @@ class IAAStub: - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): cls_name = self.__class__.__name__ - doc_link = "https://albumentations.ai/docs/api_reference/augmentations" + self.doc_link + # Use getattr to safely access subclass attributes with a default value + doc_link = "https://albumentations.ai/docs/api_reference/augmentations" + getattr( + self, "doc_link", "/default_doc_link" + ) + alternative = getattr(self, "alternative", "DefaultAlternative") raise RuntimeError( f"You are trying to use a deprecated augmentation '{cls_name}' which depends on the imgaug library, " f"but imgaug is not installed.\n\n" "There are two options to fix this error:\n" "1. [Recommended]. Switch to the Albumentations' implementation of the augmentation with the same API: " - f"{self.alternative} - {doc_link}\n" + f"{alternative} - {doc_link}\n" "2. Install a version of Albumentations that contains imgaug by running " "'pip install -U albumentations[imgaug]'." ) diff --git a/albumentations/imgaug/transforms.py b/albumentations/imgaug/transforms.py index 9d1c8cae6..4f581a003 100644 --- a/albumentations/imgaug/transforms.py +++ b/albumentations/imgaug/transforms.py @@ -12,6 +12,9 @@ import imgaug.imgaug.augmenters as iaa import warnings +from typing import Any, Dict, Optional, Sequence, Tuple, Type, cast + +import numpy as np from albumentations.core.bbox_utils import ( convert_bboxes_from_albumentations, @@ -29,6 +32,7 @@ ImageOnlyTransform, to_tuple, ) +from ..core.types import BoxType, KeypointType, ScaleFloatType __all__ = [ "BasicIAATransform", @@ -48,24 +52,26 @@ class BasicIAATransform(BasicTransform): - def __init__(self, always_apply=False, p=0.5): - super(BasicIAATransform, self).__init__(always_apply, p) + def __init__(self, always_apply: bool = False, p: float = 0.5): + super().__init__(always_apply, p) @property - def processor(self): + def processor(self) -> Type[iaa.Augmenter]: return iaa.Noop() - def update_params(self, params, **kwargs): - params = super(BasicIAATransform, self).update_params(params, **kwargs) + def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]: + params = super().update_params(params, **kwargs) params["deterministic_processor"] = self.processor.to_deterministic() return params - def apply(self, img, deterministic_processor=None, **params): + def apply(self, img: np.ndarray, deterministic_processor: Any, **params: Any) -> np.ndarray: return deterministic_processor.augment_image(img) class DualIAATransform(DualTransform, BasicIAATransform): - def apply_to_bboxes(self, bboxes, deterministic_processor=None, rows=0, cols=0, **params): + def apply_to_bboxes( + self, bboxes: Sequence[BoxType], deterministic_processor: Any, rows: int = 0, cols: int = 0, **params: Any + ) -> Sequence[BoxType]: if len(bboxes) > 0: bboxes = convert_bboxes_from_albumentations(bboxes, "pascal_voc", rows=rows, cols=cols) @@ -87,7 +93,14 @@ def apply_to_bboxes(self, bboxes, deterministic_processor=None, rows=0, cols=0, inside Compose with keypoints format other than 'xy'. """ - def apply_to_keypoints(self, keypoints, deterministic_processor=None, rows=0, cols=0, **params): + def apply_to_keypoints( + self, + keypoints: Sequence[KeypointType], + deterministic_processor: Any, + rows: int = 0, + cols: int = 0, + **params: Any, + ) -> Sequence[KeypointType]: if len(keypoints) > 0: keypoints = convert_keypoints_from_albumentations(keypoints, "xy", rows=rows, cols=cols) keypoints_t = ia.KeypointsOnImage([ia.Keypoint(*kp[:2]) for kp in keypoints], (rows, cols)) @@ -95,7 +108,9 @@ def apply_to_keypoints(self, keypoints, deterministic_processor=None, rows=0, co bboxes_t = [[kp.x, kp.y] + list(kp_orig[2:]) for (kp, kp_orig) in zip(keypoints_t, keypoints)] - keypoints = convert_keypoints_to_albumentations(bboxes_t, "xy", rows=rows, cols=cols) + keypoints = convert_keypoints_to_albumentations( + cast(Sequence[KeypointType], bboxes_t), "xy", rows=rows, cols=cols + ) return keypoints @@ -106,8 +121,17 @@ class ImageOnlyIAATransform(ImageOnlyTransform, BasicIAATransform): class IAACropAndPad(DualIAATransform): """This augmentation is deprecated. Please use CropAndPad instead.""" - def __init__(self, px=None, percent=None, pad_mode="constant", pad_cval=0, keep_size=True, always_apply=False, p=1): - super(IAACropAndPad, self).__init__(always_apply, p) + def __init__( + self, + px: Optional[float] = None, + percent: Optional[float] = None, + pad_mode: str = "constant", + pad_cval: float = 0, + keep_size: bool = True, + always_apply: bool = False, + p: float = 1, + ): + super().__init__(always_apply, p) self.px = px self.percent = percent self.pad_mode = pad_mode @@ -116,40 +140,40 @@ def __init__(self, px=None, percent=None, pad_mode="constant", pad_cval=0, keep_ warnings.warn("IAACropAndPad is deprecated. Please use CropAndPad instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.CropAndPad(self.px, self.percent, self.pad_mode, self.pad_cval, self.keep_size) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("px", "percent", "pad_mode", "pad_cval", "keep_size") class IAAFliplr(DualIAATransform): """This augmentation is deprecated. Please use HorizontalFlip instead.""" - def __init__(self, always_apply=False, p=0.5): + def __init__(self, always_apply: bool = False, p: float = 0.5): super().__init__(always_apply, p) warnings.warn("IAAFliplr is deprecated. Please use HorizontalFlip instead.", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Fliplr(1) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return () class IAAFlipud(DualIAATransform): """This augmentation is deprecated. Please use VerticalFlip instead.""" - def __init__(self, always_apply=False, p=0.5): + def __init__(self, always_apply: bool = False, p: float = 0.5): super().__init__(always_apply, p) warnings.warn("IAAFlipud is deprecated. Please use VerticalFlip instead.", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Flipud(1) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return () @@ -167,18 +191,24 @@ class IAAEmboss(ImageOnlyIAATransform): image """ - def __init__(self, alpha=(0.2, 0.5), strength=(0.2, 0.7), always_apply=False, p=0.5): - super(IAAEmboss, self).__init__(always_apply, p) + def __init__( + self, + alpha: Tuple[float, float] = (0.2, 0.5), + strength: Tuple[float, float] = (0.2, 0.7), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.alpha = to_tuple(alpha, 0.0) self.strength = to_tuple(strength, 0.0) warnings.warn("This augmentation is deprecated. Please use Emboss instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Emboss(self.alpha, self.strength) - def get_transform_init_args_names(self): - return ("alpha", "strength") + def get_transform_init_args_names(self) -> Tuple[str, ...]: + return "alpha", "strength" class IAASuperpixels(ImageOnlyIAATransform): @@ -197,18 +227,18 @@ class IAASuperpixels(ImageOnlyIAATransform): image """ - def __init__(self, p_replace=0.1, n_segments=100, always_apply=False, p=0.5): - super(IAASuperpixels, self).__init__(always_apply, p) + def __init__(self, p_replace: float = 0.1, n_segments: int = 100, always_apply: bool = False, p: float = 0.5): + super().__init__(always_apply, p) self.p_replace = p_replace self.n_segments = n_segments warnings.warn("IAASuperpixels is deprecated. Please use Superpixels instead.", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Superpixels(p_replace=self.p_replace, n_segments=self.n_segments) - def get_transform_init_args_names(self): - return ("p_replace", "n_segments") + def get_transform_init_args_names(self) -> Tuple[str, ...]: + return "p_replace", "n_segments" class IAASharpen(ImageOnlyIAATransform): @@ -224,18 +254,24 @@ class IAASharpen(ImageOnlyIAATransform): image """ - def __init__(self, alpha=(0.2, 0.5), lightness=(0.5, 1.0), always_apply=False, p=0.5): - super(IAASharpen, self).__init__(always_apply, p) + def __init__( + self, + alpha: Tuple[float, float] = (0.2, 0.5), + lightness: Tuple[float, float] = (0.5, 1.0), + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.alpha = to_tuple(alpha, 0) self.lightness = to_tuple(lightness, 0) warnings.warn("IAASharpen is deprecated. Please use Sharpen instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Sharpen(self.alpha, self.lightness) - def get_transform_init_args_names(self): - return ("alpha", "lightness") + def get_transform_init_args_names(self) -> Tuple[str, str]: + return "alpha", "lightness" class IAAAdditiveGaussianNoise(ImageOnlyIAATransform): @@ -244,8 +280,8 @@ class IAAAdditiveGaussianNoise(ImageOnlyIAATransform): This augmentation is deprecated. Please use GaussNoise instead. Args: - loc (int): mean of the normal distribution that generates the noise. Default: 0. - scale ((float, float)): standard deviation of the normal distribution that generates the noise. + loc: mean of the normal distribution that generates the noise. Default: 0. + scale: standard deviation of the normal distribution that generates the noise. Default: (0.01 * 255, 0.05 * 255). p (float): probability of applying the transform. Default: 0.5. @@ -253,23 +289,30 @@ class IAAAdditiveGaussianNoise(ImageOnlyIAATransform): image """ - def __init__(self, loc=0, scale=(0.01 * 255, 0.05 * 255), per_channel=False, always_apply=False, p=0.5): - super(IAAAdditiveGaussianNoise, self).__init__(always_apply, p) + def __init__( + self, + loc: int = 0, + scale: Tuple[float, float] = (0.01 * 255, 0.05 * 255), + per_channel: bool = False, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) self.loc = loc self.scale = to_tuple(scale, 0.0) self.per_channel = per_channel warnings.warn("IAAAdditiveGaussianNoise is deprecated. Please use GaussNoise instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.AdditiveGaussianNoise(self.loc, self.scale, self.per_channel) - def get_transform_init_args_names(self): - return ("loc", "scale", "per_channel") + def get_transform_init_args_names(self) -> Tuple[str, str, str]: + return "loc", "scale", "per_channel" class IAAPiecewiseAffine(DualIAATransform): - """Place a regular grid of points on the input and randomly move the neighbourhood of these point around + """Place a regular grid of points on the input and randomly move the neighborhood of these point around via affine transformations. This augmentation is deprecated. Please use PiecewiseAffine instead. @@ -277,19 +320,27 @@ class IAAPiecewiseAffine(DualIAATransform): Note: This class introduce interpolation artifacts to mask if it has values other than {0;1} Args: - scale ((float, float): factor range that determines how far each point is moved. Default: (0.03, 0.05). - nb_rows (int): number of rows of points that the regular grid should have. Default: 4. - nb_cols (int): number of columns of points that the regular grid should have. Default: 4. - p (float): probability of applying the transform. Default: 0.5. + scale: factor range that determines how far each point is moved. Default: (0.03, 0.05). + nb_rows: number of rows of points that the regular grid should have. Default: 4. + nb_cols: number of columns of points that the regular grid should have. Default: 4. + p: probability of applying the transform. Default: 0.5. Targets: image, mask """ def __init__( - self, scale=(0.03, 0.05), nb_rows=4, nb_cols=4, order=1, cval=0, mode="constant", always_apply=False, p=0.5 + self, + scale: Tuple[float, float] = (0.03, 0.05), + nb_rows: int = 4, + nb_cols: int = 4, + order: int = 1, + cval: int = 0, + mode: str = "constant", + always_apply: bool = False, + p: float = 0.5, ): - super(IAAPiecewiseAffine, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.scale = to_tuple(scale, 0.0) self.nb_rows = nb_rows self.nb_cols = nb_cols @@ -299,15 +350,15 @@ def __init__( warnings.warn("This IAAPiecewiseAffine is deprecated. Please use PiecewiseAffine instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.PiecewiseAffine(self.scale, self.nb_rows, self.nb_cols, self.order, self.cval, self.mode) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, str, str, str, str, str]: return ("scale", "nb_rows", "nb_cols", "order", "cval", "mode") class IAAAffine(DualIAATransform): - """Place a regular grid of points on the input and randomly move the neighbourhood of these point around + """Place a regular grid of points on the input and randomly move the neighborhood of these point around via affine transformations. This augmentation is deprecated. Please use Affine instead. @@ -315,7 +366,7 @@ class IAAAffine(DualIAATransform): Note: This class introduce interpolation artifacts to mask if it has values other than {0;1} Args: - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image, mask @@ -323,18 +374,18 @@ class IAAAffine(DualIAATransform): def __init__( self, - scale=1.0, - translate_percent=None, - translate_px=None, - rotate=0.0, - shear=0.0, - order=1, - cval=0, - mode="reflect", - always_apply=False, - p=0.5, + scale: float, + translate_percent: float, + translate_px: float, + rotate: float = 0.0, + shear: float = 0.0, + order: int = 1, + cval: float = 0, + mode: str = "reflect", + always_apply: bool = False, + p: float = 0.5, ): - super(IAAAffine, self).__init__(always_apply, p) + super().__init__(always_apply, p) self.scale = to_tuple(scale, 1.0) self.translate_percent = to_tuple(translate_percent, 0) self.translate_px = to_tuple(translate_px, 0) @@ -346,7 +397,7 @@ def __init__( warnings.warn("This IAAAffine is deprecated. Please use Affine instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.Affine( self.scale, self.translate_percent, @@ -358,7 +409,7 @@ def processor(self): self.mode, ) - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("scale", "translate_percent", "translate_px", "rotate", "shear", "order", "cval", "mode") @@ -369,23 +420,29 @@ class IAAPerspective(Perspective): Note: This class introduce interpolation artifacts to mask if it has values other than {0;1} Args: - scale ((float, float): standard deviation of the normal distributions. These are used to sample + scale: standard deviation of the normal distributions. These are used to sample the random distances of the subimage's corners from the full image's corners. Default: (0.05, 0.1). - p (float): probability of applying the transform. Default: 0.5. + p: probability of applying the transform. Default: 0.5. Targets: image, mask """ - def __init__(self, scale=(0.05, 0.1), keep_size=True, always_apply=False, p=0.5): - super(IAAPerspective, self).__init__(always_apply, p) + def __init__( + self, + scale: ScaleFloatType = (0.05, 0.1), + keep_size: bool = True, + always_apply: bool = False, + p: float = 0.5, + ): + super().__init__(always_apply, p) # type: ignore[arg-type] self.scale = to_tuple(scale, 1.0) self.keep_size = keep_size warnings.warn("This IAAPerspective is deprecated. Please use Perspective instead", FutureWarning) @property - def processor(self): + def processor(self) -> np.ndarray: return iaa.PerspectiveTransform(self.scale, keep_size=self.keep_size) - def get_transform_init_args_names(self): - return ("scale", "keep_size") + def get_transform_init_args_names(self) -> Tuple[str, str]: + return "scale", "keep_size" diff --git a/albumentations/pytorch/__init__.py b/albumentations/pytorch/__init__.py index 25cf792a3..7986cdd64 100644 --- a/albumentations/pytorch/__init__.py +++ b/albumentations/pytorch/__init__.py @@ -1,3 +1 @@ -from __future__ import absolute_import - from .transforms import * diff --git a/albumentations/pytorch/functional.py b/albumentations/pytorch/functional.py index edf3ed946..8d67fe323 100644 --- a/albumentations/pytorch/functional.py +++ b/albumentations/pytorch/functional.py @@ -1,18 +1,18 @@ -from __future__ import division +from typing import Optional import numpy as np import torch import torchvision.transforms.functional as F -def img_to_tensor(im, normalize=None): - tensor = torch.from_numpy(np.moveaxis(im / (255.0 if im.dtype == np.uint8 else 1), -1, 0).astype(np.float32)) +def img_to_tensor(image: np.ndarray, normalize: Optional[bool] = None) -> torch.Tensor: + tensor = torch.from_numpy(np.moveaxis(image / (255.0 if image.dtype == np.uint8 else 1), -1, 0).astype(np.float32)) if normalize is not None: return F.normalize(tensor, **normalize) return tensor -def mask_to_tensor(mask, num_classes, sigmoid): +def mask_to_tensor(mask: np.ndarray, num_classes: int, sigmoid: bool) -> torch.Tensor: if num_classes > 1: if not sigmoid: # softmax diff --git a/albumentations/pytorch/transforms.py b/albumentations/pytorch/transforms.py index 8bc204b35..bfcb9a2fd 100644 --- a/albumentations/pytorch/transforms.py +++ b/albumentations/pytorch/transforms.py @@ -1,6 +1,4 @@ -from __future__ import absolute_import - -import warnings +from typing import Any, Dict, List, Optional, Tuple import numpy as np import torch @@ -11,14 +9,14 @@ __all__ = ["ToTensorV2"] -def img_to_tensor(im, normalize=None): +def img_to_tensor(im: np.ndarray, normalize: Optional[bool] = None) -> torch.Tensor: tensor = torch.from_numpy(np.moveaxis(im / (255.0 if im.dtype == np.uint8 else 1), -1, 0).astype(np.float32)) if normalize is not None: return F.normalize(tensor, **normalize) return tensor -def mask_to_tensor(mask, num_classes, sigmoid): +def mask_to_tensor(mask: np.ndarray, num_classes: int, sigmoid: bool) -> torch.Tensor: if num_classes > 1: if not sigmoid: # softmax @@ -48,7 +46,7 @@ class ToTensor(BasicTransform): """ - def __init__(self, num_classes=1, sigmoid=True, normalize=None): + def __init__(self, num_classes: int = 1, sigmoid: bool = True, normalize: Optional[bool] = None): raise RuntimeError( "`ToTensor` is obsolete and it was removed from Albumentations. Please use `ToTensorV2` instead - " "https://albumentations.ai/docs/api_reference/pytorch/transforms/" @@ -72,15 +70,15 @@ class ToTensorV2(BasicTransform): p (float): Probability of applying the transform. Default: 1.0. """ - def __init__(self, transpose_mask=False, always_apply=True, p=1.0): - super(ToTensorV2, self).__init__(always_apply=always_apply, p=p) + def __init__(self, transpose_mask: bool = False, always_apply: bool = True, p: float = 1.0): + super().__init__(always_apply=always_apply, p=p) self.transpose_mask = transpose_mask @property - def targets(self): + def targets(self) -> Dict[str, Any]: return {"image": self.apply, "mask": self.apply_to_mask, "masks": self.apply_to_masks} - def apply(self, img, **params): # skipcq: PYL-W0613 + def apply(self, img: np.ndarray, **params: Any) -> torch.Tensor: if len(img.shape) not in [2, 3]: raise ValueError("Albumentations only supports images in HW or HWC format") @@ -89,16 +87,16 @@ def apply(self, img, **params): # skipcq: PYL-W0613 return torch.from_numpy(img.transpose(2, 0, 1)) - def apply_to_mask(self, mask, **params): # skipcq: PYL-W0613 + def apply_to_mask(self, mask: np.ndarray, **params: Any) -> torch.Tensor: if self.transpose_mask and mask.ndim == 3: mask = mask.transpose(2, 0, 1) return torch.from_numpy(mask) - def apply_to_masks(self, masks, **params): + def apply_to_masks(self, masks: List[np.ndarray], **params: Any) -> List[torch.Tensor]: return [self.apply_to_mask(mask, **params) for mask in masks] - def get_transform_init_args_names(self): + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("transpose_mask",) - def get_params_dependent_on_targets(self, params): + def get_params_dependent_on_targets(self, params: Any) -> Dict[str, Any]: return {} diff --git a/albumentations/random_utils.py b/albumentations/random_utils.py index 6c7c8798a..d49f97983 100644 --- a/albumentations/random_utils.py +++ b/albumentations/random_utils.py @@ -1,15 +1,10 @@ -# Use `Any` as the return type to avoid mypy problems with Union data types, -# because numpy can return single number and ndarray - import random as py_random -from typing import Any, Optional, Sequence, Type, Union +from typing import Any, Optional, Sequence, Union import numpy as np +from numpy.typing import DTypeLike -from .core.transforms_interface import NumType - -IntNumType = Union[int, np.ndarray] -Size = Union[int, Sequence[int]] +from .core.types import FloatNumType, IntNumType, NumType, SizeType def get_random_state() -> np.random.RandomState: @@ -19,40 +14,44 @@ def get_random_state() -> np.random.RandomState: def uniform( low: NumType = 0.0, high: NumType = 1.0, - size: Optional[Size] = None, + size: Optional[Union[SizeType, int]] = None, random_state: Optional[np.random.RandomState] = None, -) -> Any: +) -> FloatNumType: if random_state is None: random_state = get_random_state() return random_state.uniform(low, high, size) -def rand(d0: NumType, d1: NumType, *more, random_state: Optional[np.random.RandomState] = None, **kwargs) -> Any: +def rand( + d0: NumType, d1: NumType, *more: Any, random_state: Optional[np.random.RandomState] = None, **kwargs: Any +) -> np.ndarray: if random_state is None: random_state = get_random_state() - return random_state.rand(d0, d1, *more, **kwargs) # type: ignore + return random_state.rand(d0, d1, *more, **kwargs) -def randn(d0: NumType, d1: NumType, *more, random_state: Optional[np.random.RandomState] = None, **kwargs) -> Any: +def randn( + d0: NumType, d1: NumType, *more: Any, random_state: Optional[np.random.RandomState] = None, **kwargs: Any +) -> np.ndarray: if random_state is None: random_state = get_random_state() - return random_state.randn(d0, d1, *more, **kwargs) # type: ignore + return random_state.randn(d0, d1, *more, **kwargs) def normal( loc: NumType = 0.0, scale: NumType = 1.0, - size: Optional[Size] = None, + size: Optional[SizeType] = None, random_state: Optional[np.random.RandomState] = None, -) -> Any: +) -> FloatNumType: if random_state is None: random_state = get_random_state() return random_state.normal(loc, scale, size) def poisson( - lam: NumType = 1.0, size: Optional[Size] = None, random_state: Optional[np.random.RandomState] = None -) -> Any: + lam: NumType = 1.0, size: Optional[SizeType] = None, random_state: Optional[np.random.RandomState] = None +) -> IntNumType: if random_state is None: random_state = get_random_state() return random_state.poisson(lam, size) @@ -60,7 +59,7 @@ def poisson( def permutation( x: Union[int, Sequence[float], np.ndarray], random_state: Optional[np.random.RandomState] = None -) -> Any: +) -> np.ndarray: if random_state is None: random_state = get_random_state() return random_state.permutation(x) @@ -69,28 +68,28 @@ def permutation( def randint( low: IntNumType, high: Optional[IntNumType] = None, - size: Optional[Size] = None, - dtype: Type = np.int32, + size: Optional[Union[SizeType, int]] = None, + dtype: DTypeLike = np.int32, random_state: Optional[np.random.RandomState] = None, -) -> Any: +) -> IntNumType: if random_state is None: random_state = get_random_state() return random_state.randint(low, high, size, dtype) -def random(size: Optional[NumType] = None, random_state: Optional[np.random.RandomState] = None) -> Any: +def random(size: Optional[NumType] = None, random_state: Optional[np.random.RandomState] = None) -> FloatNumType: if random_state is None: random_state = get_random_state() - return random_state.random(size) # type: ignore + return random_state.random(size) def choice( a: NumType, - size: Optional[Size] = None, + size: Optional[Union[SizeType, int]] = None, replace: bool = True, p: Optional[Union[Sequence[float], np.ndarray]] = None, random_state: Optional[np.random.RandomState] = None, -) -> Any: +) -> np.ndarray: if random_state is None: random_state = get_random_state() - return random_state.choice(a, size, replace, p) # type: ignore + return random_state.choice(a, size, replace, p) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..ff36785e1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +# NOTE: you have to use single-quoted strings in TOML for regular expressions. +# It's the equivalent of r-strings in Python. Multiline strings are treated as +# verbose regular expressions by Black. Use [ ] to denote a significant space +# character. + +[tool.mypy] +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true + +# for strict mypy: (this is the tricky one :-)) +disallow_untyped_defs = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + + +[tool.black] +line-length = 120 +target-version = ['py310'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | node_modules + setup.py +)/ +''' diff --git a/requirements-dev.txt b/requirements-dev.txt index f76f52eb4..a49c26806 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,8 @@ -flake8==5.0.3 -flake8-docstrings==1.6.0 -isort==5.11.5 -mypy==0.991 +black==24.2.0 +flake8==7.0.0 +isort==5.13.2 +mypy==1.8.0 +pre_commit>=3.5.0 +types-pkg-resources types-PyYAML types-setuptools -types-pkg-resources -black==22.6.0 -pre_commit>=3.5.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index c3bab691c..6ca40b23b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,5 @@ [flake8] max-line-length = 120 exclude =.git,__pycache__,docs/source/conf.py,build,dist -ignore = I101,I201,F401,F403,S001,D100,D101,D102,D103,D104,D105,D106,D107,D200,D205,D400,W504,D202,E203,W503,B006 +ignore = W503, E203, F401, F403 inline-quotes = " - -[mypy] -ignore_missing_imports = True -warn_redundant_casts = True -warn_no_return = True