From cd50269a34a8f3c29b1814c509319873a4e61f58 Mon Sep 17 00:00:00 2001 From: Vladimir Iglovikov Date: Fri, 3 May 2024 20:35:52 -0700 Subject: [PATCH] Fix blur (#1700) * Added heavy refactoring * Added heavy refactoring * Updated interface for Downscale * Updated Downscale transform * Fix in CI --- .github/workflows/ci.yml | 6 +- .pre-commit-config.yaml | 9 +- CONTRIBUTING.md | 26 ++ .../augmentations/blur/transforms.py | 22 +- .../augmentations/crops/transforms.py | 242 +++++++------ .../augmentations/domain_adaptation.py | 10 +- .../augmentations/dropout/channel_dropout.py | 2 +- .../augmentations/dropout/coarse_dropout.py | 10 +- .../augmentations/dropout/grid_dropout.py | 4 +- .../augmentations/dropout/mask_dropout.py | 6 +- albumentations/augmentations/functional.py | 4 +- .../augmentations/geometric/functional.py | 2 +- .../augmentations/geometric/resize.py | 20 +- .../augmentations/geometric/rotate.py | 89 ++--- .../augmentations/geometric/transforms.py | 124 +++---- albumentations/augmentations/transforms.py | 328 +++++++++--------- albumentations/augmentations/utils.py | 4 +- albumentations/core/transforms_interface.py | 2 +- albumentations/core/types.py | 2 +- albumentations/random_utils.py | 6 +- pyproject.toml | 1 + requirements-dev.txt | 3 +- tests/test_blur.py | 97 ++++++ tests/test_transforms.py | 161 +++------ tools/check_defaults.py | 34 ++ 25 files changed, 664 insertions(+), 550 deletions(-) create mode 100644 tests/test_blur.py create mode 100644 tools/check_defaults.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a655281f..f0be24323 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,10 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Update pip - run: python -m pip install --upgrade pip + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install . - name: Install dev requirements run: pip install -r requirements-dev.txt - name: Run checks diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f27fa73eb..87efdd895 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,13 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - id: requirements-txt-fixer + - repo: local + hooks: + - id: check-defaults-in-apply + name: Check defaults in apply method of BasicTransform subclasses + entry: python -m tools.check_defaults + language: system + files: '\.py$' - repo: local hooks: - id: check-docstrings @@ -66,7 +73,7 @@ repos: - id: codespell additional_dependencies: ["tomli"] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.39.0 + rev: v0.40.0 hooks: - id: markdownlint - repo: https://github.com/tox-dev/pyproject-fmt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93826dd87..dad895399 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -180,6 +180,32 @@ Even if a parameter defined as `Tuple`, the transform should work correctly with To maintain determinism and reproducibility, handle all probability calculations within the `get_params` or `get_params_dependent_on_targets` methods. These calculations should not occur in the `apply_xxx` or `__init__` methods, as it is crucial to separate configuration from execution in our codebase. +### Specific Guidelines for Method Definitions + +#### Handling `apply_xxx` Methods + +When contributing code related to transformation methods, specifically methods that start with `apply_` (e.g., `apply_to_mask`, `apply_to_bbox`), please adhere to the following guidelines: + +**No Default Arguments**: Do not use default arguments in `apply_xxx` methods. Every parameter should be explicitly required, promoting clarity and reducing hidden behaviors that can arise from default values. + +### Examples + +Here are a few examples to illustrate these guidelines: + +**Incorrect** method definition: + +```python +def apply_to_mask(self, mask, fill_value=0): # Default value not allowed + # implementation +``` + +**Correct** method definition: + +```python +def apply_to_mask(self, mask, fill_value): # No default values + # implementation +``` + ## Guidelines for Modifying Existing Code Maintaining the stability and usability of Albumentations for all users is a priority. When contributing, it's important to follow these guidelines for modifying existing code: diff --git a/albumentations/augmentations/blur/transforms.py b/albumentations/augmentations/blur/transforms.py index 3ce508c06..a728fac1a 100644 --- a/albumentations/augmentations/blur/transforms.py +++ b/albumentations/augmentations/blur/transforms.py @@ -72,11 +72,11 @@ def __init__(self, blur_limit: ScaleIntType = 7, always_apply: bool = False, p: super().__init__(always_apply, p) self.blur_limit = cast(Tuple[int, int], blur_limit) - def apply(self, img: np.ndarray, kernel: int = 3, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: int, **params: Any) -> np.ndarray: return F.blur(img, kernel) def get_params(self) -> Dict[str, Any]: - return {"ksize": int(random.choice(list(range(self.blur_limit[0], self.blur_limit[1] + 1, 2))))} + return {"kernel": random_utils.choice(list(range(self.blur_limit[0], self.blur_limit[1] + 1, 2)))} def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("blur_limit",) @@ -133,7 +133,7 @@ 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: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: np.ndarray, **params: Any) -> np.ndarray: return FMain.convolve(img, kernel=kernel) def get_params(self) -> Dict[str, Any]: @@ -194,7 +194,7 @@ class MedianBlur(Blur): def __init__(self, blur_limit: ScaleIntType = 7, always_apply: bool = False, p: float = 0.5): super().__init__(blur_limit, always_apply, p) - def apply(self, img: np.ndarray, kernel: int = 3, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: int, **params: Any) -> np.ndarray: return F.median_blur(img, kernel) @@ -260,7 +260,7 @@ def __init__( self.blur_limit = cast(Tuple[int, int], blur_limit) self.sigma_limit = cast(Tuple[float, float], sigma_limit) - def apply(self, img: np.ndarray, ksize: int = 3, sigma: float = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, ksize: int, sigma: float, **params: Any) -> np.ndarray: return F.gaussian_blur(img, ksize, sigma=sigma) def get_params(self) -> Dict[str, float]: @@ -318,7 +318,7 @@ def __init__( self.iterations = iterations self.mode = mode - def apply(self, img: np.ndarray, *args: Any, dxy: np.ndarray = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, *args: Any, dxy: np.ndarray, **params: Any) -> np.ndarray: if dxy is None: msg = "dxy is None" raise ValueError(msg) @@ -450,7 +450,7 @@ def __init__( self.beta_limit = cast(Tuple[float, float], beta_limit) self.noise_limit = cast(Tuple[float, float], noise_limit) - def apply(self, img: np.ndarray, kernel: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: np.ndarray, **params: Any) -> np.ndarray: return FMain.convolve(img, kernel=kernel) def get_params(self) -> Dict[str, np.ndarray]: @@ -532,7 +532,7 @@ def __init__( self.radius = cast(Tuple[int, int], radius) self.alias_blur = cast(Tuple[float, float], alias_blur) - def apply(self, img: np.ndarray, radius: int = 3, alias_blur: float = 0.5, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, radius: int, alias_blur: float, **params: Any) -> np.ndarray: return F.defocus(img, radius, alias_blur) def get_params(self) -> Dict[str, Any]: @@ -582,11 +582,7 @@ def __init__( self.max_factor = cast(Tuple[float, float], max_factor) self.step_factor = cast(Tuple[float, float], step_factor) - def apply(self, img: np.ndarray, zoom_factors: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: - if zoom_factors is None: - msg = "zoom_factors is None" - raise ValueError(msg) - + def apply(self, img: np.ndarray, zoom_factors: np.ndarray, **params: Any) -> np.ndarray: return F.zoom_blur(img, zoom_factors) def get_params(self) -> Dict[str, Any]: diff --git a/albumentations/augmentations/crops/transforms.py b/albumentations/augmentations/crops/transforms.py index f113c170d..359d3c5b7 100644 --- a/albumentations/augmentations/crops/transforms.py +++ b/albumentations/augmentations/crops/transforms.py @@ -21,6 +21,7 @@ ) from albumentations.core.transforms_interface import BaseTransformInitSchema, DualTransform from albumentations.core.types import ( + NUM_MULTI_CHANNEL_DIMENSIONS, BoxInternalType, ColorType, KeypointInternalType, @@ -46,7 +47,6 @@ ] TWO = 2 -THREE = 3 class CropInitSchema(BaseTransformInitSchema): @@ -81,7 +81,7 @@ def __init__(self, height: int, width: int, always_apply: bool = False, p: float self.height = height self.width = width - def apply(self, img: np.ndarray, h_start: int = 0, w_start: int = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, h_start: int, w_start: int, **params: Any) -> np.ndarray: return F.random_crop(img, self.height, self.width, h_start, w_start) def get_params(self) -> Dict[str, float]: @@ -248,10 +248,10 @@ def __init__( def apply( self, img: np.ndarray, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> np.ndarray: return F.crop(img, x_min, y_min, x_max, y_max) @@ -259,10 +259,10 @@ def apply( def apply_to_bbox( self, bbox: BoxInternalType, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> BoxInternalType: return F.bbox_crop( @@ -278,10 +278,10 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) @@ -293,7 +293,7 @@ def _preprocess_mask(self, mask: np.ndarray) -> np.ndarray: ignore_values_np = np.array(self.ignore_values) mask = np.where(np.isin(mask, ignore_values_np), 0, mask) - if mask.ndim == THREE and self.ignore_channels is not None: + if mask.ndim == NUM_MULTI_CHANNEL_DIMENSIONS and self.ignore_channels is not None: target_channels = np.array([ch for ch in range(mask.shape[-1]) if ch not in self.ignore_channels]) mask = np.take(mask, target_channels, axis=-1) @@ -320,7 +320,7 @@ def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any] mask_height, mask_width = mask.shape[:2] if mask.any(): - mask = mask.sum(axis=-1) if mask.ndim == THREE else mask + mask = mask.sum(axis=-1) if mask.ndim == NUM_MULTI_CHANNEL_DIMENSIONS else mask non_zero_yx = np.argwhere(mask) y, x = random.choice(non_zero_yx) x_min = x - random.randint(0, self.width - 1) @@ -373,11 +373,11 @@ def __init__( 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, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, + interpolation: int, **params: Any, ) -> np.ndarray: crop = F.random_crop(img, crop_height, crop_width, h_start, w_start) @@ -386,12 +386,12 @@ def apply( 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, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, + rows: int, + cols: int, **params: Any, ) -> BoxInternalType: return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) @@ -399,12 +399,12 @@ def apply_to_bbox( 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, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, + rows: int, + cols: int, **params: Any, ) -> KeypointInternalType: keypoint = F.keypoint_random_crop(keypoint, crop_height, crop_width, h_start, w_start, rows, cols) @@ -440,8 +440,20 @@ class InitSchema(BaseTransformInitSchema): p: ProbabilityType = 1 min_max_height: OnePlusIntRangeType w2h_ratio: Annotated[float, Field(gt=0, description="Aspect ratio of crop.")] - width: Optional[int] = None - height: Optional[int] = None + width: Optional[int] = Field( + None, + deprecated=( + "Initializing with 'size' as an integer and a separate 'width' is deprecated. " + "Please use a tuple (height, width) for the 'size' argument." + ), + ) + height: Optional[int] = Field( + None, + deprecated=( + "Initializing with 'height' and 'width' is deprecated. " + "Please use a tuple (height, width) for the 'size' argument." + ), + ) size: Optional[ScaleIntType] = None @model_validator(mode="after") @@ -449,12 +461,6 @@ def process(self) -> Self: if isinstance(self.size, int): if isinstance(self.width, int): self.size = (self.size, self.width) - warn( - "Initializing with 'size' as an integer and a separate 'width' is deprecated. " - "Please use a tuple (height, width) for the 'size' argument.", - DeprecationWarning, - stacklevel=2, - ) else: msg = "If size is an integer, width as integer must be specified." raise TypeError(msg) @@ -464,13 +470,6 @@ def process(self) -> Self: message = "If 'size' is not provided, both 'height' and 'width' must be specified." raise ValueError(message) self.size = (self.height, self.width) - warn( - "Initializing with 'height' and 'width' is deprecated. " - "Please use a tuple (height, width) for the 'size' argument.", - DeprecationWarning, - stacklevel=2, - ) - return self def __init__( @@ -528,8 +527,12 @@ class RandomResizedCrop(_BaseRandomSizedCrop): class InitSchema(BaseTransformInitSchema): scale: ZeroOneRangeType = (0.08, 1.0) ratio: NonNegativeFloatRangeType = (0.75, 1.3333333333333333) - width: Optional[int] = None - height: Optional[int] = None + width: Optional[int] = Field( + None, deprecated="Initializing with 'height' and 'width' is deprecated. Use size instead." + ) + height: Optional[int] = Field( + None, deprecated="Initializing with 'height' and 'width' is deprecated. Use size instead." + ) size: Optional[ScaleIntType] = None p: ProbabilityType = 1 interpolation: InterpolationType = cv2.INTER_LINEAR @@ -539,12 +542,6 @@ def process(self) -> Self: if isinstance(self.size, int): if isinstance(self.width, int): self.size = (self.size, self.width) - warn( - "Initializing with 'size' as an integer and a separate 'width' is deprecated. " - "Please use a tuple (height, width) for the 'size' argument.", - DeprecationWarning, - stacklevel=2, - ) else: msg = "If size is an integer, width as integer must be specified." raise TypeError(msg) @@ -554,12 +551,6 @@ def process(self) -> Self: message = "If 'size' is not provided, both 'height' and 'width' must be specified." raise ValueError(message) self.size = (self.height, self.width) - warn( - "Initializing with 'height' and 'width' is deprecated. " - "Please use a tuple (height, width) for the 'size' argument.", - DeprecationWarning, - stacklevel=2, - ) return self @@ -692,10 +683,10 @@ def __init__( def apply( self, img: np.ndarray, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> np.ndarray: return F.clamping_crop(img, x_min, y_min, x_max, y_max) @@ -722,10 +713,10 @@ def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType def apply_to_keypoint( self, keypoint: KeypointInternalType, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) @@ -769,10 +760,10 @@ def __init__(self, erosion_rate: float = 0.0, always_apply: bool = False, p: flo def apply( self, img: np.ndarray, - crop_height: int = 0, - crop_width: int = 0, - h_start: int = 0, - w_start: int = 0, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, **params: Any, ) -> np.ndarray: return F.random_crop(img, crop_height, crop_width, h_start, w_start) @@ -808,12 +799,12 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, U 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, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, + rows: int, + cols: int, **params: Any, ) -> BoxInternalType: return F.bbox_random_crop(bbox, crop_height, crop_width, h_start, w_start, rows, cols) @@ -872,15 +863,14 @@ def __init__( 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, + crop_height: int, + crop_width: int, + h_start: int, + w_start: int, **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) + return FGeometric.resize(crop, self.height, self.width, self.interpolation) def get_transform_init_args_names(self) -> Tuple[str, ...]: return (*super().get_transform_init_args_names(), "height", "width", "interpolation") @@ -1042,12 +1032,12 @@ def __init__( def apply( self, img: np.ndarray, - crop_params: Sequence[int] = (), - pad_params: Sequence[int] = (), - pad_value: float = 0, - rows: int = 0, - cols: int = 0, - interpolation: int = cv2.INTER_LINEAR, + crop_params: Sequence[int], + pad_params: Sequence[int], + pad_value: float, + rows: int, + cols: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.crop_and_pad( @@ -1065,12 +1055,12 @@ def apply( def apply_to_mask( self, 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, + crop_params: Sequence[int], + pad_params: Sequence[int], + pad_value_mask: float, + rows: int, + cols: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.crop_and_pad( @@ -1088,12 +1078,12 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - crop_params: Optional[Sequence[int]] = None, - pad_params: Optional[Sequence[int]] = None, - rows: int = 0, - cols: int = 0, - result_rows: int = 0, - result_cols: int = 0, + crop_params: Sequence[int], + pad_params: Sequence[int], + rows: int, + cols: int, + result_rows: int, + result_cols: int, **params: Any, ) -> BoxInternalType: return F.crop_and_pad_bbox(bbox, crop_params, pad_params, rows, cols, result_rows, result_cols) @@ -1101,12 +1091,12 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - crop_params: Optional[Sequence[int]] = None, - pad_params: Optional[Sequence[int]] = None, - rows: int = 0, - cols: int = 0, - result_rows: int = 0, - result_cols: int = 0, + crop_params: Sequence[int], + pad_params: Sequence[int], + rows: int, + cols: int, + result_rows: int, + result_cols: int, **params: Any, ) -> KeypointInternalType: return F.crop_and_pad_keypoint( @@ -1359,10 +1349,10 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, i def apply( self, img: np.ndarray, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> np.ndarray: return F.clamping_crop(img, x_min, y_min, x_max, y_max) @@ -1370,10 +1360,10 @@ def apply( def apply_to_mask( self, mask: np.ndarray, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> np.ndarray: return F.clamping_crop(mask, x_min, y_min, x_max, y_max) @@ -1381,10 +1371,10 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] @@ -1393,10 +1383,10 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - x_min: int = 0, - x_max: int = 0, - y_min: int = 0, - y_max: int = 0, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> KeypointInternalType: return F.crop_keypoint_by_coords(keypoint, crop_coords=(x_min, y_min, x_max, y_max)) diff --git a/albumentations/augmentations/domain_adaptation.py b/albumentations/augmentations/domain_adaptation.py index c70c02357..28d3f81f0 100644 --- a/albumentations/augmentations/domain_adaptation.py +++ b/albumentations/augmentations/domain_adaptation.py @@ -1,5 +1,5 @@ import random -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Dict, List, Literal, Sequence, Tuple, cast import cv2 import numpy as np @@ -94,8 +94,8 @@ def __init__( def apply( self: np.ndarray, img: np.ndarray, - reference_image: Optional[np.ndarray] = None, - blend_ratio: float = 0.5, + reference_image: np.ndarray, + blend_ratio: float, **params: Any, ) -> np.ndarray: return apply_histogram(img, reference_image, blend_ratio) @@ -193,8 +193,8 @@ def __init__( def apply( self, img: np.ndarray, - target_image: Optional[np.ndarray] = None, - beta: float = 0.1, + target_image: np.ndarray, + beta: float, **params: Any, ) -> np.ndarray: return fourier_domain_adaptation(img, target_image, beta) diff --git a/albumentations/augmentations/dropout/channel_dropout.py b/albumentations/augmentations/dropout/channel_dropout.py index 0a406810d..571be5e62 100644 --- a/albumentations/augmentations/dropout/channel_dropout.py +++ b/albumentations/augmentations/dropout/channel_dropout.py @@ -51,7 +51,7 @@ def __init__( self.channel_drop_range = channel_drop_range self.fill_value = fill_value - def apply(self, img: np.ndarray, channels_to_drop: Tuple[int, ...] = (0,), **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, channels_to_drop: Tuple[int, ...], **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]) -> Dict[str, Any]: diff --git a/albumentations/augmentations/dropout/coarse_dropout.py b/albumentations/augmentations/dropout/coarse_dropout.py index 2b58e312b..b36127ba6 100644 --- a/albumentations/augmentations/dropout/coarse_dropout.py +++ b/albumentations/augmentations/dropout/coarse_dropout.py @@ -163,8 +163,8 @@ def __init__( def apply( self, img: np.ndarray, - fill_value: Union[ColorType, Literal["random"]] = 0, - holes: Iterable[Tuple[int, int, int, int]] = (), + fill_value: Union[ColorType, Literal["random"]], + holes: Iterable[Tuple[int, int, int, int]], **params: Any, ) -> np.ndarray: return cutout(img, holes, fill_value) @@ -172,8 +172,8 @@ def apply( def apply_to_mask( self, mask: np.ndarray, - mask_fill_value: ScalarType = 0, - holes: Iterable[Tuple[int, int, int, int]] = (), + mask_fill_value: ScalarType, + holes: Iterable[Tuple[int, int, int, int]], **params: Any, ) -> np.ndarray: if mask_fill_value is None: @@ -234,7 +234,7 @@ def targets_as_params(self) -> List[str]: def apply_to_keypoints( self, keypoints: Sequence[KeypointType], - holes: Iterable[Tuple[int, int, int, int]] = (), + holes: Iterable[Tuple[int, int, int, int]], **params: Any, ) -> List[KeypointType]: return [keypoint for keypoint in keypoints if not any(keypoint_in_hole(keypoint, hole) for hole in holes)] diff --git a/albumentations/augmentations/dropout/grid_dropout.py b/albumentations/augmentations/dropout/grid_dropout.py index 5a0280939..db5a8885e 100644 --- a/albumentations/augmentations/dropout/grid_dropout.py +++ b/albumentations/augmentations/dropout/grid_dropout.py @@ -90,13 +90,13 @@ def __init__( self.fill_value = fill_value self.mask_fill_value = mask_fill_value - def apply(self, img: np.ndarray, holes: Iterable[Tuple[int, int, int, int]] = (), **params: Any) -> 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, mask: np.ndarray, - holes: Iterable[Tuple[int, int, int, int]] = (), + holes: Iterable[Tuple[int, int, int, int]], **params: Any, ) -> np.ndarray: if self.mask_fill_value is None: diff --git a/albumentations/augmentations/dropout/mask_dropout.py b/albumentations/augmentations/dropout/mask_dropout.py index 3068ebc8e..75509ff6a 100644 --- a/albumentations/augmentations/dropout/mask_dropout.py +++ b/albumentations/augmentations/dropout/mask_dropout.py @@ -1,5 +1,5 @@ import random -from typing import Any, Dict, List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Tuple, Union, cast import cv2 import numpy as np @@ -92,7 +92,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A del params["mask"] return params - def apply(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, dropout_mask: np.ndarray, **params: Any) -> np.ndarray: if dropout_mask is None: return img @@ -107,7 +107,7 @@ def apply(self, img: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **pa return img - def apply_to_mask(self, mask: np.ndarray, dropout_mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, dropout_mask: np.ndarray, **params: Any) -> np.ndarray: if dropout_mask is None: return mask diff --git a/albumentations/augmentations/functional.py b/albumentations/augmentations/functional.py index 0a2770624..7eb577f4f 100644 --- a/albumentations/augmentations/functional.py +++ b/albumentations/augmentations/functional.py @@ -551,8 +551,8 @@ def convolve(img: np.ndarray, kernel: np.ndarray) -> np.ndarray: @preserve_channel_dim -def image_compression(img: np.ndarray, quality: int, image_type: np.dtype) -> np.ndarray: - if image_type in [".jpeg", ".jpg"]: +def image_compression(img: np.ndarray, quality: int, image_type: Literal[".jpg", ".webp", ".jpeg"]) -> np.ndarray: + if image_type in {".jpeg", ".jpg"}: quality_flag = cv2.IMWRITE_JPEG_QUALITY elif image_type == ".webp": quality_flag = cv2.IMWRITE_WEBP_QUALITY diff --git a/albumentations/augmentations/geometric/functional.py b/albumentations/augmentations/geometric/functional.py index f767b6410..c1ef2b241 100644 --- a/albumentations/augmentations/geometric/functional.py +++ b/albumentations/augmentations/geometric/functional.py @@ -425,7 +425,7 @@ def elastic_transform( @preserve_channel_dim 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: + if (height, width) == img.shape[:2]: return img resize_fn = _maybe_process_in_chunks(cv2.resize, dsize=(width, height), interpolation=interpolation) return resize_fn(img) diff --git a/albumentations/augmentations/geometric/resize.py b/albumentations/augmentations/geometric/resize.py index ade7b3f9f..9f576dfeb 100644 --- a/albumentations/augmentations/geometric/resize.py +++ b/albumentations/augmentations/geometric/resize.py @@ -72,8 +72,8 @@ def get_params(self) -> Dict[str, float]: def apply( self, img: np.ndarray, - scale: float = 0, - interpolation: int = cv2.INTER_LINEAR, + scale: float, + interpolation: int, **params: Any, ) -> np.ndarray: return F.scale(img, scale, interpolation) @@ -85,7 +85,7 @@ def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType def apply_to_keypoint( self, keypoint: KeypointInternalType, - scale: float = 0, + scale: float, **params: Any, ) -> KeypointInternalType: return F.keypoint_scale(keypoint, scale, scale) @@ -149,8 +149,8 @@ def __init__( def apply( self, img: np.ndarray, - max_size: int = 1024, - interpolation: int = cv2.INTER_LINEAR, + max_size: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.longest_max_size(img, max_size=max_size, interpolation=interpolation) @@ -162,7 +162,7 @@ def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType def apply_to_keypoint( self, keypoint: KeypointInternalType, - max_size: int = 1024, + max_size: int, **params: Any, ) -> KeypointInternalType: height = params["rows"] @@ -214,8 +214,8 @@ def __init__( def apply( self, img: np.ndarray, - max_size: int = 1024, - interpolation: int = cv2.INTER_LINEAR, + max_size: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.smallest_max_size(img, max_size=max_size, interpolation=interpolation) @@ -226,7 +226,7 @@ def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType def apply_to_keypoint( self, keypoint: KeypointInternalType, - max_size: int = 1024, + max_size: int, **params: Any, ) -> KeypointInternalType: height = params["rows"] @@ -282,7 +282,7 @@ def __init__( self.width = width self.interpolation = interpolation - def apply(self, img: np.ndarray, interpolation: int = cv2.INTER_LINEAR, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, interpolation: int, **params: Any) -> np.ndarray: return F.resize(img, height=self.height, width=self.width, interpolation=interpolation) def apply_to_bbox(self, bbox: BoxInternalType, **params: Any) -> BoxInternalType: diff --git a/albumentations/augmentations/geometric/rotate.py b/albumentations/augmentations/geometric/rotate.py index 7149558f1..411fd3bd4 100644 --- a/albumentations/augmentations/geometric/rotate.py +++ b/albumentations/augmentations/geometric/rotate.py @@ -41,7 +41,7 @@ class RandomRotate90(DualTransform): _targets = (Targets.IMAGE, Targets.MASK, Targets.BBOXES, Targets.KEYPOINTS) - def apply(self, img: np.ndarray, factor: float = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, factor: float, **params: Any) -> np.ndarray: """Args: factor (int): number of times the input will be rotated by 90 degrees. @@ -52,10 +52,10 @@ 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: BoxInternalType, factor: int = 0, **params: Any) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, factor: int, **params: Any) -> BoxInternalType: return F.bbox_rot90(bbox, factor, **params) - def apply_to_keypoint(self, keypoint: KeypointInternalType, factor: int = 0, **params: Any) -> BoxInternalType: + def apply_to_keypoint(self, keypoint: KeypointInternalType, factor: int, **params: Any) -> BoxInternalType: return F.keypoint_rot90(keypoint, factor, **params) def get_transform_init_args_names(self) -> Tuple[()]: @@ -115,7 +115,7 @@ class InitSchema(RotateInitSchema): def __init__( self, - limit: ScaleFloatType = 90, + limit: ScaleFloatType = (-90, 90), interpolation: int = cv2.INTER_LINEAR, border_mode: int = cv2.BORDER_REFLECT_101, value: Optional[ColorType] = None, @@ -137,16 +137,16 @@ def __init__( def apply( self, img: np.ndarray, - angle: float = 0, - interpolation: int = cv2.INTER_LINEAR, - x_min: Optional[int] = None, - x_max: Optional[int] = None, - y_min: Optional[int] = None, - y_max: Optional[int] = None, + angle: float, + interpolation: int, + x_min: int, + x_max: int, + y_min: int, + y_max: int, **params: Any, ) -> np.ndarray: img_out = F.rotate(img, angle, interpolation, self.border_mode, self.value) - if self.crop_border and x_min is not None and x_max is not None and y_min is not None and y_max is not None: + if self.crop_border: return FCrops.crop(img_out, x_min, y_min, x_max, y_max) return img_out @@ -154,48 +154,48 @@ def apply_to_mask( self, mask: np.ndarray, angle: float, - x_min: Optional[int] = None, - x_max: Optional[int] = None, - y_min: Optional[int] = None, - y_max: Optional[int] = None, + 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 and x_min is not None and x_max is not None and y_min is not None and y_max is not None: + if self.crop_border: return FCrops.crop(img_out, x_min, y_min, x_max, y_max) return img_out def apply_to_bbox( self, bbox: BoxInternalType, - angle: float = 0, - x_min: Optional[int] = None, - x_max: Optional[int] = None, - y_min: Optional[int] = None, - y_max: Optional[int] = None, - cols: int = 0, - rows: int = 0, + angle: float, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + cols: int, + rows: int, **params: Any, ) -> np.ndarray: bbox_out = F.bbox_rotate(bbox, angle, self.rotate_method, rows, cols) - if self.crop_border and x_min is not None and x_max is not None and y_min is not None and y_max is not None: + if self.crop_border: 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: KeypointInternalType, - angle: float = 0, - x_min: Optional[int] = None, - x_max: Optional[int] = None, - y_min: Optional[int] = None, - y_max: Optional[int] = None, - cols: int = 0, - rows: int = 0, + angle: float, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + cols: int, + rows: int, **params: Any, ) -> KeypointInternalType: keypoint_out = F.keypoint_rotate(keypoint, angle, rows, cols, **params) - if self.crop_border and x_min is not None and x_max is not None and y_min is not None and y_max is not None: + if self.crop_border: return FCrops.crop_keypoint_by_coords(keypoint_out, (x_min, y_min, x_max, y_max)) return keypoint_out @@ -241,6 +241,9 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A if self.crop_border: height, width = params["image"].shape[:2] out_params.update(self._rotated_rect_with_max_area(height, width, out_params["angle"])) + else: + out_params.update({"x_min": -1, "x_max": -1, "y_min": -1, "y_max": -1}) + return out_params def get_transform_init_args_names(self) -> Tuple[str, ...]: @@ -299,23 +302,23 @@ def __init__( self.value = value self.mask_value = mask_value - def apply(self, img: np.ndarray, matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: - return F.safe_rotate(img, matrix, cast(int, self.interpolation), self.value, self.border_mode) + def apply(self, img: np.ndarray, matrix: np.ndarray, **params: Any) -> np.ndarray: + return F.safe_rotate(img, matrix, self.interpolation, self.value, self.border_mode) - def apply_to_mask(self, mask: np.ndarray, matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, matrix: np.ndarray, **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: Any) -> BoxInternalType: + def apply_to_bbox(self, bbox: BoxInternalType, cols: int, rows: int, **params: Any) -> BoxInternalType: return F.bbox_safe_rotate(bbox, params["matrix"], cols, rows) def apply_to_keypoint( self, keypoint: KeypointInternalType, - angle: float = 0, - scale_x: float = 0, - scale_y: float = 0, - cols: int = 0, - rows: int = 0, + angle: float, + scale_x: float, + scale_y: float, + cols: int, + rows: int, **params: Any, ) -> KeypointInternalType: return F.keypoint_safe_rotate(keypoint, params["matrix"], angle, scale_x, scale_y, cols, rows) @@ -362,5 +365,5 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A return {"matrix": rotation_mat, "angle": angle, "scale_x": scale_x, "scale_y": scale_y} - def get_transform_init_args_names(self) -> Tuple[str, str, str, str, str]: - return ("limit", "interpolation", "border_mode", "value", "mask_value") + def get_transform_init_args_names(self) -> Tuple[str, ...]: + return "limit", "interpolation", "border_mode", "value", "mask_value" diff --git a/albumentations/augmentations/geometric/transforms.py b/albumentations/augmentations/geometric/transforms.py index 584798fe7..bb8a91bf8 100644 --- a/albumentations/augmentations/geometric/transforms.py +++ b/albumentations/augmentations/geometric/transforms.py @@ -142,8 +142,8 @@ def __init__( def apply( self, img: np.ndarray, - random_state: Optional[int] = None, - interpolation: int = cv2.INTER_LINEAR, + random_seed: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.elastic_transform( @@ -154,12 +154,12 @@ def apply( interpolation, self.border_mode, self.value, - np.random.RandomState(random_state), + np.random.RandomState(random_seed), self.approximate, self.same_dxdy, ) - def apply_to_mask(self, mask: np.ndarray, random_state: Optional[int] = None, **params: Any) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, random_seed: int, **params: Any) -> np.ndarray: return F.elastic_transform( mask, self.alpha, @@ -168,7 +168,7 @@ def apply_to_mask(self, mask: np.ndarray, random_state: Optional[int] = None, ** cv2.INTER_NEAREST, self.border_mode, self.mask_value, - np.random.RandomState(random_state), + np.random.RandomState(random_seed), self.approximate, self.same_dxdy, ) @@ -176,7 +176,7 @@ def apply_to_mask(self, mask: np.ndarray, random_state: Optional[int] = None, ** def apply_to_bbox( self, bbox: BoxInternalType, - random_state: Optional[int] = None, + random_seed: int, **params: Any, ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] @@ -193,14 +193,14 @@ def apply_to_bbox( cv2.INTER_NEAREST, self.border_mode, self.mask_value, - np.random.RandomState(random_state), + np.random.RandomState(random_seed), self.approximate, ) bbox_returned = bbox_from_mask(mask) return cast(BoxInternalType, F.normalize_bbox(bbox_returned, rows, cols)) def get_params(self) -> Dict[str, int]: - return {"random_state": random.randint(0, 10000)} + return {"random_seed": random_utils.get_random_seed()} def get_transform_init_args_names(self) -> Tuple[str, ...]: return ( @@ -682,14 +682,14 @@ def _handle_translate_arg( def apply( self, img: np.ndarray, - matrix: skimage.transform.ProjectiveTransform = None, - output_shape: Sequence[int] = (), + matrix: skimage.transform.ProjectiveTransform, + output_shape: Sequence[int], **params: Any, ) -> np.ndarray: return F.warp_affine( img, matrix, - interpolation=cast(int, self.interpolation), + interpolation=self.interpolation, cval=self.cval, mode=self.mode, output_shape=output_shape, @@ -698,8 +698,8 @@ def apply( def apply_to_mask( self, mask: np.ndarray, - matrix: skimage.transform.ProjectiveTransform = None, - output_shape: Sequence[int] = (), + matrix: skimage.transform.ProjectiveTransform, + output_shape: Sequence[int], **params: Any, ) -> np.ndarray: return F.warp_affine( @@ -714,10 +714,10 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - matrix: skimage.transform.ProjectiveTransform = None, - rows: int = 0, - cols: int = 0, - output_shape: Sequence[int] = (), + matrix: skimage.transform.ProjectiveTransform, + rows: int, + cols: int, + output_shape: Sequence[int], **params: Any, ) -> BoxInternalType: return F.bbox_affine(bbox, matrix, self.rotate_method, rows, cols, output_shape) @@ -725,8 +725,8 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - matrix: Optional[skimage.transform.ProjectiveTransform] = None, - scale: Optional[Dict[str, Any]] = None, + matrix: skimage.transform.ProjectiveTransform, + scale: Dict[str, Any], **params: Any, ) -> KeypointInternalType: if scale is None: @@ -1153,15 +1153,15 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A def apply( self, img: np.ndarray, - matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, + matrix: skimage.transform.PiecewiseAffineTransform, **params: Any, ) -> np.ndarray: - return F.piecewise_affine(img, matrix, cast(int, self.interpolation), self.mode, self.cval) + return F.piecewise_affine(img, matrix, self.interpolation, self.mode, self.cval) def apply_to_mask( self, mask: np.ndarray, - matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, + matrix: skimage.transform.PiecewiseAffineTransform, **params: Any, ) -> np.ndarray: return F.piecewise_affine(mask, matrix, self.mask_interpolation, self.mode, self.cval_mask) @@ -1169,9 +1169,9 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - rows: int = 0, - cols: int = 0, - matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, + rows: int, + cols: int, + matrix: skimage.transform.PiecewiseAffineTransform, **params: Any, ) -> BoxInternalType: return F.bbox_piecewise_affine(bbox, matrix, rows, cols, self.keypoints_threshold) @@ -1179,9 +1179,9 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - rows: int = 0, - cols: int = 0, - matrix: Optional[skimage.transform.PiecewiseAffineTransform] = None, + rows: int, + cols: int, + matrix: skimage.transform.PiecewiseAffineTransform, **params: Any, ) -> KeypointInternalType: return F.keypoint_piecewise_affine(keypoint, matrix, rows, cols, self.keypoints_threshold) @@ -1347,10 +1347,10 @@ def update_params(self, params: Dict[str, Any], **kwargs: Any) -> Dict[str, Any] def apply( self, img: np.ndarray, - pad_top: int = 0, - pad_bottom: int = 0, - pad_left: int = 0, - pad_right: int = 0, + pad_top: int, + pad_bottom: int, + pad_left: int, + pad_right: int, **params: Any, ) -> np.ndarray: return F.pad_with_params( @@ -1366,10 +1366,10 @@ def apply( def apply_to_mask( self, mask: np.ndarray, - pad_top: int = 0, - pad_bottom: int = 0, - pad_left: int = 0, - pad_right: int = 0, + pad_top: int, + pad_bottom: int, + pad_left: int, + pad_right: int, **params: Any, ) -> np.ndarray: return F.pad_with_params( @@ -1385,12 +1385,12 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - pad_top: int = 0, - pad_bottom: int = 0, - pad_left: int = 0, - pad_right: int = 0, - rows: int = 0, - cols: int = 0, + pad_top: int, + pad_bottom: int, + pad_left: int, + pad_right: int, + rows: int, + cols: int, **params: Any, ) -> BoxInternalType: x_min, y_min, x_max, y_max = denormalize_bbox(bbox, rows, cols)[:4] @@ -1400,10 +1400,10 @@ def apply_to_bbox( def apply_to_keypoint( self, keypoint: KeypointInternalType, - pad_top: int = 0, - pad_bottom: int = 0, - pad_left: int = 0, - pad_right: int = 0, + pad_top: int, + pad_bottom: int, + pad_left: int, + pad_right: int, **params: Any, ) -> KeypointInternalType: x, y, angle, scale = keypoint[:4] @@ -1542,7 +1542,7 @@ class Flip(DualTransform): _targets = (Targets.IMAGE, Targets.MASK, Targets.BBOXES, Targets.KEYPOINTS) - def apply(self, img: np.ndarray, d: int = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, d: int, **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 @@ -1656,23 +1656,23 @@ def __init__( def apply( self, img: np.ndarray, - k: int = 0, - dx: int = 0, - dy: int = 0, - interpolation: int = cv2.INTER_LINEAR, + k: int, + dx: int, + dy: int, + interpolation: int, **params: Any, ) -> np.ndarray: return F.optical_distortion(img, k, dx, dy, interpolation, self.border_mode, self.value) - def apply_to_mask(self, mask: np.ndarray, k: int = 0, dx: int = 0, dy: int = 0, **params: Any) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, k: int, dx: int, dy: int, **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, + k: int, + dx: int, + dy: int, **params: Any, ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] @@ -1790,9 +1790,9 @@ def __init__( def apply( self, img: np.ndarray, - stepsx: Tuple[()] = (), - stepsy: Tuple[()] = (), - interpolation: int = cv2.INTER_LINEAR, + stepsx: Tuple[()], + stepsy: Tuple[()], + interpolation: int, **params: Any, ) -> np.ndarray: return F.grid_distortion(img, self.num_steps, stepsx, stepsy, interpolation, self.border_mode, self.value) @@ -1800,8 +1800,8 @@ def apply( def apply_to_mask( self, mask: np.ndarray, - stepsx: Tuple[()] = (), - stepsy: Tuple[()] = (), + stepsx: Tuple[()], + stepsy: Tuple[()], **params: Any, ) -> np.ndarray: return F.grid_distortion( @@ -1817,8 +1817,8 @@ def apply_to_mask( def apply_to_bbox( self, bbox: BoxInternalType, - stepsx: Tuple[()] = (), - stepsy: Tuple[()] = (), + stepsx: Tuple[()], + stepsy: Tuple[()], **params: Any, ) -> BoxInternalType: rows, cols = params["rows"], params["cols"] diff --git a/albumentations/augmentations/transforms.py b/albumentations/augmentations/transforms.py index 33692d2de..725ddcddf 100644 --- a/albumentations/augmentations/transforms.py +++ b/albumentations/augmentations/transforms.py @@ -8,10 +8,10 @@ import cv2 import numpy as np -from pydantic import Field, ValidationInfo, field_validator, model_validator +from pydantic import AfterValidator, BaseModel, Field, ValidationInfo, field_validator, model_validator from scipy import special from scipy.ndimage import gaussian_filter -from typing_extensions import Annotated, Literal, Self +from typing_extensions import Annotated, Literal, Self, TypedDict from albumentations import random_utils from albumentations.augmentations.blur.functional import blur @@ -26,10 +26,13 @@ InterpolationType, NonNegativeFloatRangeType, OnePlusFloatRangeType, + OnePlusIntNonDecreasingRangeType, OnePlusIntRangeType, ProbabilityType, SymmetricRangeType, ZeroOneRangeType, + check_01_range, + check_nondecreasing_range, ) from albumentations.core.transforms_interface import ( BaseTransformInitSchema, @@ -39,6 +42,8 @@ NoOp, ) from albumentations.core.types import ( + MONO_CHANNEL_DIMENSIONS, + NUM_RGB_CHANNELS, BoxInternalType, ChromaticAberrationMode, ColorType, @@ -102,9 +107,6 @@ "Morphological", ] -NUM_BITS_ARRAY_LENGTH = 3 -GRAYSCALE_SHAPE_LEN = 2 -NUM_RGB_CHANNELS = 3 NUM_BITS_ARRAY_LENGTH = 3 MAX_JPEG_QUALITY = 100 TWENTY = 20 @@ -360,8 +362,10 @@ def __init__( self.quality_upper = quality_upper self.compression_type = compression_type - def apply(self, img: np.ndarray, quality: int = 100, image_type: str = ".jpg", **params: Any) -> np.ndarray: - if img.ndim != GRAYSCALE_SHAPE_LEN and img.shape[-1] not in (1, 3, 4): + def apply( + self, img: np.ndarray, quality: int, image_type: Literal[".jpg", ".webp", ".jpeg"], **params: Any + ) -> np.ndarray: + if img.ndim != MONO_CHANNEL_DIMENSIONS and img.shape[-1] not in (1, 3, 4): msg = "ImageCompression transformation expects 1, 3 or 4 channel images." raise TypeError(msg) return F.image_compression(img, quality, image_type) @@ -429,21 +433,19 @@ def __init__( self.snow_point_upper = snow_point_upper self.brightness_coeff = brightness_coeff - def apply(self, img: np.ndarray, snow_point: float = 0.1, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, snow_point: float, **params: Any) -> np.ndarray: return F.add_snow(img, snow_point, self.brightness_coeff) 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) -> Tuple[str, str, str]: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("snow_point_lower", "snow_point_upper", "brightness_coeff") class RandomGravel(ImageOnlyTransform): """Add gravels. - From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library - Args: gravel_roi: (top-left x, top-left y, bottom-right x, bottom right y). Should be in [0, 1] range @@ -455,6 +457,9 @@ class RandomGravel(ImageOnlyTransform): Image types: uint8, float32 + Reference: + https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library + """ class InitSchema(BaseTransformInitSchema): @@ -491,7 +496,7 @@ def generate_gravel_patch(self, rectangular_roi: Tuple[int, int, int, int]) -> n gravels[:, 1] = random_utils.randint(y1, y2, count) return gravels - def apply(self, img: np.ndarray, gravels_infos: Optional[List[Any]] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, gravels_infos: List[Any], **params: Any) -> np.ndarray: if gravels_infos is None: gravels_infos = [] return F.add_gravel(img, gravels_infos) @@ -561,8 +566,6 @@ def get_transform_init_args_names(self) -> Tuple[str, str]: class RandomRain(ImageOnlyTransform): """Adds rain effects. - From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library - Args: slant_lower: should be in range [-20, 20]. slant_upper: should be in range [-20, 20]. @@ -579,6 +582,9 @@ class RandomRain(ImageOnlyTransform): Image types: uint8, float32 + Reference: + https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library + """ class InitSchema(BaseTransformInitSchema): @@ -629,13 +635,11 @@ def __init__( def apply( self, img: np.ndarray, - slant: int = 10, - drop_length: int = 20, - rain_drops: Optional[List[Tuple[int, int]]] = None, + slant: int, + drop_length: int, + rain_drops: List[Tuple[int, int]], **params: Any, ) -> np.ndarray: - if rain_drops is None: - rain_drops = [] return F.add_rain( img, slant, @@ -712,7 +716,6 @@ class RandomFog(ImageOnlyTransform): Reference: https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library - """ class InitSchema(BaseTransformInitSchema): @@ -743,12 +746,10 @@ def __init__( def apply( self, img: np.ndarray, - fog_coef: np.ndarray = 0.1, - haze_list: Optional[List[Tuple[int, int]]] = None, + fog_coef: np.ndarray, + haze_list: List[Tuple[int, int]], **params: Any, ) -> np.ndarray: - if haze_list is None: - haze_list = [] return F.add_fog(img, fog_coef, self.alpha_coef, haze_list) @property @@ -787,8 +788,6 @@ def get_transform_init_args_names(self) -> Tuple[str, str, str]: class RandomSunFlare(ImageOnlyTransform): """Simulates Sun Flare for the image - From https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library - Args: 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]. @@ -807,6 +806,9 @@ class RandomSunFlare(ImageOnlyTransform): Image types: uint8, float32 + Reference: + https://github.com/UjjwalSaxena/Automold--Road-Augmentation-Library + """ class InitSchema(BaseTransformInitSchema): @@ -867,9 +869,9 @@ def __init__( def apply( self, img: np.ndarray, - flare_center_x: float = 0.5, - flare_center_y: float = 0.5, - circles: Optional[List[Any]] = None, + flare_center_x: float, + flare_center_y: float, + circles: List[Any], **params: Any, ) -> np.ndarray: if circles is None: @@ -976,39 +978,31 @@ class InitSchema(BaseTransformInitSchema): default=(0, 0.5, 1, 1), description="Region of the image where shadows will appear", ) - num_shadows_limit: Tuple[int, int] = Field(default=(1, 2)) + num_shadows_limit: OnePlusIntNonDecreasingRangeType = (1, 2) num_shadows_lower: Optional[int] = Field( default=None, description="Lower limit for the possible number of shadows", + deprecated="num_shadows_lower is deprecated. Use num_shadows_limit instead.", ) num_shadows_upper: Optional[int] = Field( default=None, description="Upper limit for the possible number of shadows", + deprecated="num_shadows_upper is deprecated. Use num_shadows_limit instead.", ) shadow_dimension: int = Field(default=5, description="Number of edges in the shadow polygons", gt=0) @model_validator(mode="after") def validate_shadows(self) -> Self: - if self.num_shadows_limit[0] > self.num_shadows_limit[1]: - msg = "num_shadows_limit[0] must be less than or equal to num_shadows_limit[1]." - raise ValueError(msg) + if self.num_shadows_lower is not None or self.num_shadows_upper is not None: + self.num_shadows_limit = cast(Tuple[int, int], (self.num_shadows_lower, self.num_shadows_upper)) + self.num_shadows_lower = None + self.num_shadows_upper = None shadow_lower_x, shadow_lower_y, shadow_upper_x, shadow_upper_y = self.shadow_roi if not 0 <= shadow_lower_x <= shadow_upper_x <= 1 or not 0 <= shadow_lower_y <= shadow_upper_y <= 1: raise ValueError(f"Invalid shadow_roi. Got: {self.shadow_roi}") - if self.num_shadows_lower is not None or self.num_shadows_upper is not None: - warn( - "`num_shadows_lower` and `num_shadows_upper` are deprecated. " - "Use `num_shadows_limit` as tuple (num_shadows_lower, num_shadows_upper) instead.", - DeprecationWarning, - stacklevel=2, - ) - self.num_shadows_limit = cast(Tuple[int, int], (self.num_shadows_lower, self.num_shadows_upper)) - self.num_shadows_lower = None - self.num_shadows_upper = None - return self def __init__( @@ -1027,9 +1021,7 @@ def __init__( self.shadow_dimension = shadow_dimension self.num_shadows_limit = num_shadows_limit - def apply(self, img: np.ndarray, vertices_list: Optional[List[np.ndarray]] = None, **params: Any) -> np.ndarray: - if vertices_list is None: - vertices_list = [] + def apply(self, img: np.ndarray, vertices_list: List[np.ndarray], **params: Any) -> np.ndarray: return F.add_shadow(img, vertices_list) @property @@ -1157,9 +1149,9 @@ def __init__( def apply( self, img: np.ndarray, - hue_shift: int = 0, - sat_shift: int = 0, - val_shift: int = 0, + hue_shift: int, + sat_shift: int, + val_shift: int, **params: Any, ) -> np.ndarray: if not is_rgb_image(img) and not is_grayscale_image(img): @@ -1174,7 +1166,7 @@ def get_params(self) -> Dict[str, float]: "val_shift": random.uniform(self.val_shift_limit[0], self.val_shift_limit[1]), } - def get_transform_init_args_names(self) -> Tuple[str, str, str]: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("hue_shift_limit", "sat_shift_limit", "val_shift_limit") @@ -1201,7 +1193,7 @@ def __init__(self, threshold: ScaleType = (128, 128), always_apply: bool = False super().__init__(always_apply=always_apply, p=p) self.threshold = cast(Tuple[float, float], threshold) - def apply(self, img: np.ndarray, threshold: int = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, threshold: int, **params: Any) -> np.ndarray: return F.solarize(img, threshold) def get_params(self) -> Dict[str, float]: @@ -1254,7 +1246,7 @@ def __init__( super().__init__(always_apply=always_apply, p=p) self.num_bits = cast(Union[Tuple[int, ...], List[Tuple[int, ...]]], num_bits) - def apply(self, img: np.ndarray, num_bits: int = 1, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, num_bits: int, **params: Any) -> np.ndarray: return F.posterize(img, num_bits) def get_params(self) -> Dict[str, Any]: @@ -1312,7 +1304,7 @@ def __init__( self.mask = mask self.mask_params = mask_params - def apply(self, img: np.ndarray, mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, mask: np.ndarray, **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: Dict[str, Any]) -> Dict[str, Any]: @@ -1367,7 +1359,7 @@ def __init__( self.g_shift_limit = cast(Tuple[float, float], g_shift_limit) self.b_shift_limit = cast(Tuple[float, float], b_shift_limit) - def apply(self, img: np.ndarray, r_shift: int = 0, g_shift: int = 0, b_shift: int = 0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, r_shift: int, g_shift: int, b_shift: int, **params: Any) -> np.ndarray: if not is_rgb_image(img): msg = "RGBShift transformation expects 3-channel images." raise TypeError(msg) @@ -1422,7 +1414,7 @@ def __init__( self.contrast_limit = cast(Tuple[float, float], contrast_limit) self.brightness_by_max = brightness_by_max - def apply(self, img: np.ndarray, alpha: float = 1.0, beta: float = 0.0, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, alpha: float, beta: float, **params: Any) -> np.ndarray: return F.brightness_contrast_adjust(img, alpha, beta, self.brightness_by_max) def get_params(self) -> Dict[str, float]: @@ -1472,7 +1464,7 @@ def __init__( self.mean = mean self.per_channel = per_channel - def apply(self, img: np.ndarray, gauss: Optional[float] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, gauss: float, **params: Any) -> np.ndarray: return F.gauss_noise(img, gauss=gauss) def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, float]: @@ -1484,7 +1476,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, f gauss = random_utils.normal(self.mean, sigma, image.shape) else: gauss = random_utils.normal(self.mean, sigma, image.shape[:2]) - if len(image.shape) > GRAYSCALE_SHAPE_LEN: + if image.ndim > MONO_CHANNEL_DIMENSIONS: gauss = np.expand_dims(gauss, -1) return {"gauss": gauss} @@ -1493,7 +1485,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, f def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self) -> Tuple[str, str, str]: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return ("var_limit", "per_channel", "mean") @@ -1541,18 +1533,18 @@ def __init__( def apply( self, img: np.ndarray, - color_shift: float = 0.05, - intensity: float = 1.0, - random_state: Optional[int] = None, + color_shift: float, + intensity: float, + random_seed: int, **params: Any, ) -> np.ndarray: - return F.iso_noise(img, color_shift, intensity, np.random.RandomState(random_state)) + return F.iso_noise(img, color_shift, intensity, np.random.RandomState(random_seed)) def get_params(self) -> Dict[str, Any]: return { "color_shift": random_utils.uniform(self.color_shift[0], self.color_shift[1]), "intensity": random_utils.uniform(self.intensity[0], self.intensity[1]), - "random_state": random_utils.randint(0, 65536), + "random_seed": random_utils.get_random_seed(), } def get_transform_init_args_names(self) -> Tuple[str, str]: @@ -1591,7 +1583,7 @@ def __init__( self.clip_limit = cast(Tuple[float, float], clip_limit) self.tile_grid_size = tile_grid_size - def apply(self, img: np.ndarray, clip_limit: float = 2, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, clip_limit: float, **params: Any) -> np.ndarray: if not is_rgb_image(img) and not is_grayscale_image(img): msg = "CLAHE transformation expects 1-channel or 3-channel images." raise TypeError(msg) @@ -1623,7 +1615,7 @@ class ChannelShuffle(ImageOnlyTransform): def targets_as_params(self) -> List[str]: return ["image"] - def apply(self, img: np.ndarray, channels_shuffled: Tuple[int, ...] = (0, 1, 2), **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, channels_shuffled: Tuple[int, ...], **params: Any) -> np.ndarray: return F.channel_shuffle(img, channels_shuffled) def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -1696,7 +1688,7 @@ def __init__( super().__init__(always_apply, p) self.gamma_limit = cast(Tuple[float, float], gamma_limit) - def apply(self, img: np.ndarray, gamma: float = 1, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, gamma: float, **params: Any) -> np.ndarray: return F.gamma_transform(img, gamma=gamma) def get_params(self) -> Dict[str, float]: @@ -1884,17 +1876,28 @@ def get_transform_init_args(self) -> Dict[str, Any]: return {"dtype": self.dtype.name, "max_value": self.max_value} +class InterpolationDict(TypedDict): + upscale: int + downscale: int + + +class InterpolationPydantic(BaseModel): + upscale: InterpolationType + downscale: InterpolationType + + class Downscale(ImageOnlyTransform): - """Decreases image quality by downscaling and upscaling back. + """Decreases image quality by downscaling and then upscaling it back to its original size. Args: - scale_min: lower bound on the image scale. Should be <= scale_max. - scale_max: upper bound on the image scale. Should be < 1. - interpolation: cv2 interpolation method. Could be: - - single cv2 interpolation flag - selected method will be used for downscale and upscale. - - dict(downscale=flag, upscale=flag) - - Downscale.Interpolation(downscale=flag, upscale=flag) - - Default: Interpolation(downscale=cv2.INTER_NEAREST, upscale=cv2.INTER_NEAREST) + scale_range (Tuple[float, float]): A tuple defining the minimum and maximum scale to which the image + will be downscaled. The range should be between 0 and 1, inclusive at minimum and exclusive at maximum. + The first value should be less than or equal to the second value. + interpolation_pair (InterpolationDict): A dictionary specifying the interpolation methods to use for + downscaling and upscaling. Should include keys 'downscale' and 'upscale' with cv2 interpolation + flags as values. + Example: {"downscale": cv2.INTER_NEAREST, "upscale": cv2.INTER_LINEAR}. + always_apply (bool): If set to True, the transform will always be applied. Defaults to False. Targets: image @@ -1902,78 +1905,93 @@ class Downscale(ImageOnlyTransform): Image types: uint8, float32 + Note: + Previous parameters `scale_min`, `scale_max`, and `interpolation` are deprecated. Use `scale_range` + and `interpolation_pair` for specifying scaling bounds and interpolation methods respectively. + + Example: + >>> transform = Downscale(scale_range=(0.5, 0.9), interpolation_pair={"downscale": cv2.INTER_AREA, + "upscale": cv2.INTER_CUBIC}) + >>> transformed = transform(image=img) """ class InitSchema(BaseTransformInitSchema): - scale_min: float = Field(default=0.25, ge=0, le=1, description="Lower bound on the image scale.") - scale_max: float = Field(default=0.25, ge=0, lt=1, description="Upper bound on the image scale.") - interpolation: Optional[Union[int, Interpolation, Dict[str, int]]] = Field( + scale_min: Optional[float] = Field( + default=None, + ge=0, + le=1, + description="Lower bound on the image scale.", + deprecated="Use scale_range instead.", + ) + scale_max: Optional[float] = Field( + default=None, + ge=0, + lt=1, + description="Upper bound on the image scale.", + deprecated="Use scale_range instead.", + ) + + interpolation: Optional[Union[int, Interpolation, InterpolationDict]] = Field( default_factory=lambda: Interpolation(downscale=cv2.INTER_NEAREST, upscale=cv2.INTER_NEAREST), - description="CV2 interpolation method or a dictionary specifying downscale and upscale methods.", + deprecated="Use interpolation_pair instead.", ) + interpolation_pair: InterpolationPydantic - @model_validator(mode="after") - def validate_scale(self) -> Self: - if self.scale_min > self.scale_max: - msg = "scale_min must be less than or equal to scale_max" - raise ValueError(msg) - return self + scale_range: Annotated[ + Tuple[float, float], AfterValidator(check_01_range), AfterValidator(check_nondecreasing_range) + ] = (0.25, 0.25) - @field_validator("interpolation") - @classmethod - def set_interpolation(cls, v: Any) -> Interpolation: - if isinstance(v, dict): - return Interpolation(**v) - if isinstance(v, int): - return Interpolation(downscale=v, upscale=v) - if isinstance(v, Interpolation): - return v - if v is None: - return Interpolation(downscale=cv2.INTER_NEAREST, upscale=cv2.INTER_NEAREST) + @model_validator(mode="after") + def validate_params(self) -> Self: + if self.scale_min is not None and self.scale_max is not None: + self.scale_range = (self.scale_min, self.scale_max) + self.scale_min = None + self.scale_max = None + + if self.interpolation is not None: + if isinstance(self.interpolation, dict): + self.interpolation_pair = InterpolationPydantic(**self.interpolation) + elif isinstance(self.interpolation, int): + self.interpolation_pair = InterpolationPydantic( + upscale=self.interpolation, downscale=self.interpolation + ) + elif isinstance(self.interpolation, Interpolation): + self.interpolation_pair = InterpolationPydantic( + upscale=self.interpolation.upscale, downscale=self.interpolation.downscale + ) + self.interpolation = None - msg = ( - "Interpolation must be an int, Interpolation instance, " - "or dict specifying downscale and upscale methods." - ) - raise ValueError(msg) + return self def __init__( self, - scale_min: float = 0.25, - scale_max: float = 0.25, - interpolation: Optional[Union[int, Interpolation, Dict[str, int]]] = None, + scale_min: Optional[float] = None, + scale_max: Optional[float] = None, + interpolation: Optional[Union[int, Interpolation, InterpolationDict]] = None, + scale_range: Tuple[float, float] = (0.25, 0.25), + interpolation_pair: InterpolationDict = InterpolationDict( + {"upscale": cv2.INTER_NEAREST, "downscale": cv2.INTER_NEAREST} + ), always_apply: bool = False, p: float = 0.5, ): super().__init__(always_apply=always_apply, p=p) - self.scale_min = scale_min - self.scale_max = scale_max - self.interpolation = cast(Interpolation, interpolation) + self.scale_range = scale_range + self.interpolation_pair = interpolation_pair def apply(self, img: np.ndarray, scale: float, **params: Any) -> np.ndarray: - if isinstance(self.interpolation, int): - msg = "Should not be here, added for typing purposes. Please report this issue." - raise TypeError(msg) return F.downscale( img, scale=scale, - down_interpolation=self.interpolation.downscale, - up_interpolation=self.interpolation.upscale, + down_interpolation=self.interpolation_pair["downscale"], + up_interpolation=self.interpolation_pair["upscale"], ) def get_params(self) -> Dict[str, Any]: - return {"scale": random.uniform(self.scale_min, self.scale_max)} + return {"scale": random.uniform(self.scale_range[0], self.scale_range[1])} def get_transform_init_args_names(self) -> Tuple[str, str]: - return "scale_min", "scale_max" - - def to_dict_private(self) -> Dict[str, Any]: - if isinstance(self.interpolation, int): - msg = "Should not be here, added for typing purposes. Please report this issue." - raise TypeError(msg) - result = super().to_dict_private() - result["interpolation"] = {"upscale": self.interpolation.upscale, "downscale": self.interpolation.downscale} - return result + return ("scale_range", "interpolation_pair") class Lambda(NoOp): @@ -2107,7 +2125,7 @@ def __init__( self.per_channel = per_channel self.elementwise = elementwise - def apply(self, img: np.ndarray, multiplier: float = np.array([1]), **kwargs: Any) -> np.ndarray: + def apply(self, img: np.ndarray, multiplier: float, **kwargs: Any) -> np.ndarray: return F.multiply(img, multiplier) def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -2123,7 +2141,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A shape = [height, width, num_channels] if self.elementwise else [num_channels] multiplier = random_utils.uniform(self.multiplier[0], self.multiplier[1], tuple(shape)) - if is_grayscale_image(img) and img.ndim == GRAYSCALE_SHAPE_LEN: + if img.ndim == MONO_CHANNEL_DIMENSIONS: multiplier = np.squeeze(multiplier) return {"multiplier": multiplier} @@ -2132,7 +2150,7 @@ def get_params_dependent_on_targets(self, params: Dict[str, Any]) -> Dict[str, A def targets_as_params(self) -> List[str]: return ["image"] - def get_transform_init_args_names(self) -> Tuple[str, str, str]: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return "multiplier", "per_channel", "elementwise" @@ -2164,7 +2182,7 @@ def __init__(self, alpha: float = 0.1, always_apply: bool = False, p: float = 0. super().__init__(always_apply=always_apply, p=p) self.alpha = alpha - def apply(self, img: np.ndarray, alpha: float = 0.1, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, alpha: float, **params: Any) -> np.ndarray: return F.fancy_pca(img, alpha) def get_params(self) -> Dict[str, float]: @@ -2272,11 +2290,11 @@ def get_params(self) -> Dict[str, Any]: def apply( self, img: np.ndarray, - brightness: float = 1.0, - contrast: float = 1.0, - saturation: float = 1.0, - hue: float = 0, - order: Optional[List[int]] = None, + brightness: float, + contrast: float, + saturation: float, + hue: float, + order: List[int], **params: Any, ) -> np.ndarray: if order is None: @@ -2338,7 +2356,7 @@ def get_params(self) -> Dict[str, np.ndarray]: sharpening_matrix = self.__generate_sharpening_matrix(alpha_sample=alpha, lightness_sample=lightness) return {"sharpening_matrix": sharpening_matrix} - def apply(self, img: np.ndarray, sharpening_matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, sharpening_matrix: np.ndarray, **params: Any) -> np.ndarray: return F.convolve(img, sharpening_matrix) def get_transform_init_args_names(self) -> Tuple[str, str]: @@ -2393,7 +2411,7 @@ def get_params(self) -> Dict[str, np.ndarray]: emboss_matrix = self.__generate_emboss_matrix(alpha_sample=alpha, strength_sample=strength) return {"emboss_matrix": emboss_matrix} - def apply(self, img: np.ndarray, emboss_matrix: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, emboss_matrix: np.ndarray, **params: Any) -> np.ndarray: return F.convolve(img, emboss_matrix) def get_transform_init_args_names(self) -> Tuple[str, str]: @@ -2476,11 +2494,11 @@ def get_params(self) -> Dict[str, Any]: def apply( self, img: np.ndarray, - replace_samples: Sequence[bool] = (False,), - n_segments: int = 1, + replace_samples: Sequence[bool], + n_segments: int, **kwargs: Any, ) -> np.ndarray: - return F.superpixels(img, n_segments, replace_samples, self.max_size, cast(int, self.interpolation)) + return F.superpixels(img, n_segments, replace_samples, self.max_size, self.interpolation) class TemplateTransform(ImageOnlyTransform): @@ -2548,9 +2566,9 @@ def __init__( def apply( self, img: np.ndarray, - template: Optional[np.ndarray] = None, - img_weight: float = 0.5, - template_weight: float = 0.5, + template: np.ndarray, + img_weight: float, + template_weight: float, **params: Any, ) -> np.ndarray: return F.add_weighted(img, img_weight, template, template_weight) @@ -2674,7 +2692,7 @@ def get_params(self) -> Dict[str, np.ndarray]: return {"kernel": kernel} - def apply(self, img: np.ndarray, kernel: Optional[int] = None, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, kernel: int, **params: Any) -> np.ndarray: return F.convolve(img, kernel) def get_transform_init_args_names(self) -> Tuple[str, str]: @@ -2746,10 +2764,10 @@ def get_params(self) -> Dict[str, Any]: "alpha": random.uniform(*self.alpha), } - def apply(self, img: np.ndarray, ksize: int = 3, sigma: int = 0, alpha: float = 0.2, **params: Any) -> np.ndarray: + def apply(self, img: np.ndarray, ksize: int, sigma: int, alpha: float, **params: Any) -> np.ndarray: return F.unsharp_mask(img, ksize, sigma=sigma, alpha=alpha, threshold=self.threshold) - def get_transform_init_args_names(self) -> Tuple[str, str, str, str]: + def get_transform_init_args_names(self) -> Tuple[str, ...]: return "blur_limit", "sigma_limit", "alpha", "threshold" @@ -2817,17 +2835,17 @@ def __init__( def apply( self, img: np.ndarray, - drop_mask: Optional[np.ndarray] = None, - drop_value: Union[float, Sequence[float]] = (), + drop_mask: np.ndarray, + drop_value: Union[float, Sequence[float]], **params: Any, ) -> np.ndarray: return F.pixel_dropout(img, drop_mask, drop_value) - def apply_to_mask(self, mask: np.ndarray, drop_mask: Optional[np.ndarray] = None, **params: Any) -> np.ndarray: + def apply_to_mask(self, mask: np.ndarray, drop_mask: np.ndarray, **params: Any) -> np.ndarray: if self.mask_drop_value is None: return mask - if mask.ndim == GRAYSCALE_SHAPE_LEN: + if mask.ndim == MONO_CHANNEL_DIMENSIONS: drop_mask = np.squeeze(drop_mask) return F.pixel_dropout(mask, drop_mask, self.mask_drop_value) @@ -2987,10 +3005,10 @@ def __init__( def apply( self, img: np.ndarray, - non_mud: Optional[np.ndarray] = None, - mud: Optional[np.ndarray] = None, - drops: Optional[np.ndarray] = None, - mode: SpatterMode = "mud", + non_mud: np.ndarray, + mud: np.ndarray, + drops: np.ndarray, + mode: SpatterMode, **params: Dict[str, Any], ) -> np.ndarray: return F.spatter(img, non_mud, mud, drops, mode) @@ -3111,10 +3129,10 @@ def __init__( def apply( self, img: np.ndarray, - primary_distortion_red: float = -0.02, - secondary_distortion_red: float = -0.05, - primary_distortion_blue: float = -0.02, - secondary_distortion_blue: float = -0.05, + primary_distortion_red: float, + secondary_distortion_red: float, + primary_distortion_blue: float, + secondary_distortion_blue: float, **params: Any, ) -> np.ndarray: return F.chromatic_aberration( @@ -3123,7 +3141,7 @@ def apply( secondary_distortion_red, primary_distortion_blue, secondary_distortion_blue, - cast(int, self.interpolation), + self.interpolation, ) def get_params(self) -> Dict[str, float]: diff --git a/albumentations/augmentations/utils.py b/albumentations/augmentations/utils.py index d9ec5c1d5..845966064 100644 --- a/albumentations/augmentations/utils.py +++ b/albumentations/augmentations/utils.py @@ -10,7 +10,7 @@ from albumentations.core.types import ( MONO_CHANNEL_DIMENSIONS, NUM_MULTI_CHANNEL_DIMENSIONS, - RGB_NUM_CHANNELS, + NUM_RGB_CHANNELS, KeypointInternalType, ) @@ -154,7 +154,7 @@ def get_num_channels(image: np.ndarray) -> int: def is_rgb_image(image: np.ndarray) -> bool: - return get_num_channels(image) == RGB_NUM_CHANNELS + return get_num_channels(image) == NUM_RGB_CHANNELS def is_grayscale_image(image: np.ndarray) -> bool: diff --git a/albumentations/core/transforms_interface.py b/albumentations/core/transforms_interface.py index ff253bcc0..8504f89fc 100644 --- a/albumentations/core/transforms_interface.py +++ b/albumentations/core/transforms_interface.py @@ -50,7 +50,7 @@ class BasicTransform(Serializable, metaclass=CombinedMeta): Callable[..., Any], ] # mapping for targets (plus additional targets) and methods for which they depend call_backup = None - interpolation: Union[int, Interpolation] + interpolation: int fill_value: ColorType mask_fill_value: Optional[ColorType] # replay mode params diff --git a/albumentations/core/types.py b/albumentations/core/types.py index 5c4bbaa03..0834374c6 100644 --- a/albumentations/core/types.py +++ b/albumentations/core/types.py @@ -69,4 +69,4 @@ class ImageCompressionType(IntEnum): NUM_MULTI_CHANNEL_DIMENSIONS = 3 MONO_CHANNEL_DIMENSIONS = 2 -RGB_NUM_CHANNELS = 3 +NUM_RGB_CHANNELS = 3 diff --git a/albumentations/random_utils.py b/albumentations/random_utils.py index 8f9f2cb67..707e47f8e 100644 --- a/albumentations/random_utils.py +++ b/albumentations/random_utils.py @@ -7,8 +7,12 @@ from .core.types import FloatNumType, IntNumType, NumType, SizeType +def get_random_seed() -> int: + return py_random.randint(0, (1 << 32) - 1) + + def get_random_state() -> np.random.RandomState: - return np.random.RandomState(py_random.randint(0, (1 << 32) - 1)) + return np.random.RandomState(get_random_seed()) def uniform( diff --git a/pyproject.toml b/pyproject.toml index c245af966..3009b926b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ ignore = [ "D106", "EM101", "COM812", + "B008", ] # Allow fix for all enabled rules (when `--fix`) is provided. diff --git a/requirements-dev.txt b/requirements-dev.txt index 29854ee11..cda9452b5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,9 @@ deepdiff>=6.7.1 mypy>=1.10.0 pre_commit>=3.5.0 -pytest>=8.0.2 +pytest>=8.2.0 pytest_cov>=4.1.0 +pytest_mock>=3.14.0 requests>=2.31.0 ruff>=0.4.2 tomli>=2.0.1 diff --git a/tests/test_blur.py b/tests/test_blur.py new file mode 100644 index 000000000..9763896c4 --- /dev/null +++ b/tests/test_blur.py @@ -0,0 +1,97 @@ +import albumentations as A +import pytest +import numpy as np +from albumentations.augmentations.blur.functional import gaussian_blur +from tests.conftest import UINT8_IMAGES +from tests.utils import set_seed + + +@pytest.mark.parametrize("aug", [A.Blur, A.MedianBlur, A.MotionBlur]) +@pytest.mark.parametrize("blur_limit_input, blur_limit_used", [ [(3, 3), (3, 3)], [(13, 13), (13, 13)]] ) +@pytest.mark.parametrize("image", UINT8_IMAGES) +def test_blur_kernel_generation(image, aug, blur_limit_input, blur_limit_used): + aug = aug(blur_limit=blur_limit_input, p=1) + + assert aug.blur_limit == blur_limit_used + aug(image=image)["image"] + + + +@pytest.mark.parametrize(["val_uint8"], [[0], [1], [128], [255]]) +def test_glass_blur_float_uint8_diff_less_than_two(val_uint8): + x_uint8 = np.zeros((5, 5)).astype(np.uint8) + x_uint8[2, 2] = val_uint8 + + x_float32 = np.zeros((5, 5)).astype(np.float32) + x_float32[2, 2] = val_uint8 / 255.0 + + glassblur = A.GlassBlur(always_apply=True, max_delta=1) + + set_seed(0) + blur_uint8 = glassblur(image=x_uint8)["image"] + + set_seed(0) + blur_float32 = glassblur(image=x_float32)["image"] + + # Before comparison, rescale the blur_float32 to [0, 255] + diff = np.abs(blur_uint8 - blur_float32 * 255) + + # The difference between the results of float32 and uint8 will be at most 2. + assert np.all(diff <= 2.0) + +@pytest.mark.parametrize(["val_uint8"], [[0], [1], [128], [255]]) +def test_advanced_blur_float_uint8_diff_less_than_two(val_uint8): + x_uint8 = np.zeros((5, 5)).astype(np.uint8) + x_uint8[2, 2] = val_uint8 + + x_float32 = np.zeros((5, 5)).astype(np.float32) + x_float32[2, 2] = val_uint8 / 255.0 + + adv_blur = A.AdvancedBlur(blur_limit=(3, 5), always_apply=True) + + set_seed(0) + adv_blur_uint8 = adv_blur(image=x_uint8)["image"] + + set_seed(0) + adv_blur_float32 = adv_blur(image=x_float32)["image"] + + # Before comparison, rescale the adv_blur_float32 to [0, 255] + diff = np.abs(adv_blur_uint8 - adv_blur_float32 * 255) + + # The difference between the results of float32 and uint8 will be at most 2. + assert np.all(diff <= 2.0) + + +@pytest.mark.parametrize( + ["params"], + [ + [{"blur_limit": (2, 5)}], + [{"blur_limit": (3, 6)}], + [{"sigma_x_limit": (0.0, 1.0), "sigma_y_limit": (0.0, 1.0)}], + [{"beta_limit": (0.1, 0.9)}], + [{"beta_limit": (1.1, 8.0)}], + ], +) +def test_advanced_blur_raises_on_incorrect_params(params): + with pytest.raises(ValueError): + A.AdvancedBlur(**params) + + +@pytest.mark.parametrize( + ["blur_limit", "sigma", "result_blur", "result_sigma"], + [ + [[0, 0], [1, 1], 0, 1], + [[1, 1], [0, 0], 1, 0], + [[1, 1], [1, 1], 1, 1], + [[0, 0], [0, 0], 3, 0], + [[0, 3], [0, 0], 3, 0], + [[0, 3], [0.1, 0.1], 3, 0.1], + ], +) +def test_gaus_blur_limits(blur_limit, sigma, result_blur, result_sigma): + img = np.zeros([100, 100, 3], dtype=np.uint8) + + aug = A.Compose([A.GaussianBlur(blur_limit=blur_limit, sigma_limit=sigma, p=1)]) + + res = aug(image=img)["image"] + assert np.allclose(res, gaussian_blur(img, result_blur, result_sigma)) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 73c0fcfbe..0b6b39be8 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -13,6 +13,7 @@ import albumentations.augmentations.functional as F import albumentations.augmentations.geometric.functional as FGeometric from albumentations.augmentations.blur.functional import gaussian_blur +from albumentations.random_utils import get_random_seed from tests.conftest import IMAGES, SQUARE_MULTI_UINT8_IMAGE, SQUARE_UINT8_IMAGE from .utils import get_dual_transforms, get_image_only_transforms, get_transforms, set_seed @@ -99,10 +100,15 @@ def test_grid_distortion_steps(size): def test_elastic_transform_interpolation(monkeypatch, interpolation): image = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8) mask = np.random.randint(low=0, high=2, size=(100, 100), dtype=np.uint8) + + random_seed = get_random_seed() + monkeypatch.setattr( - "albumentations.augmentations.geometric.ElasticTransform.get_params", lambda *_: {"random_state": 1111} + "albumentations.augmentations.geometric.ElasticTransform.get_params", lambda *_: {"random_seed": random_seed} ) + aug = A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, interpolation=interpolation, p=1) + data = aug(image=image, mask=mask) expected_image = FGeometric.elastic_transform( image, @@ -111,7 +117,7 @@ def test_elastic_transform_interpolation(monkeypatch, interpolation): alpha_affine=50, interpolation=interpolation, border_mode=cv2.BORDER_REFLECT_101, - random_state=np.random.RandomState(1111), + random_state=np.random.RandomState(random_seed), ) expected_mask = FGeometric.elastic_transform( mask, @@ -120,7 +126,7 @@ def test_elastic_transform_interpolation(monkeypatch, interpolation): alpha_affine=50, interpolation=cv2.INTER_NEAREST, border_mode=cv2.BORDER_REFLECT_101, - random_state=np.random.RandomState(1111), + random_state=np.random.RandomState(random_seed), ) assert np.array_equal(data["image"], expected_image) assert np.array_equal(data["mask"], expected_mask) @@ -316,7 +322,7 @@ def test_additional_targets_for_image_only(augmentation_cls, params): aug = A.Compose([augmentation_cls(always_apply=True, **params)]) aug.add_targets(additional_targets={"image2": "image"}) - for _i in range(10): + for _ in range(10): image1 = np.random.randint(low=0, high=256, size=(100, 100, 3), dtype=np.uint8) image2 = image1.copy() res = aug(image=image1, image2=image2) @@ -697,26 +703,6 @@ def test_grid_dropout_params(ratio, holes_number_x, holes_number_y, unit_size_mi assert (holes[0][3] - holes[0][1]) == max(1, int(ratio * 256 // holes_number_y)) -@pytest.mark.parametrize( - ["blur_limit", "sigma", "result_blur", "result_sigma"], - [ - [[0, 0], [1, 1], 0, 1], - [[1, 1], [0, 0], 1, 0], - [[1, 1], [1, 1], 1, 1], - [[0, 0], [0, 0], 3, 0], - [[0, 3], [0, 0], 3, 0], - [[0, 3], [0.1, 0.1], 3, 0.1], - ], -) -def test_gaus_blur_limits(blur_limit, sigma, result_blur, result_sigma): - img = np.zeros([100, 100, 3], dtype=np.uint8) - - aug = A.Compose([A.GaussianBlur(blur_limit=blur_limit, sigma_limit=sigma, p=1)]) - - res = aug(image=img)["image"] - assert np.allclose(res, gaussian_blur(img, result_blur, result_sigma)) - - @pytest.mark.parametrize( ["blur_limit", "sigma", "result_blur", "result_sigma"], [ @@ -846,29 +832,6 @@ def test_hue_saturation_value_float_uint8_equal(hue, sat, val): assert _max <= 10, f"Max value: {_max}" -@pytest.mark.parametrize(["val_uint8"], [[0], [1], [128], [255]]) -def test_glass_blur_float_uint8_diff_less_than_two(val_uint8): - x_uint8 = np.zeros((5, 5)).astype(np.uint8) - x_uint8[2, 2] = val_uint8 - - x_float32 = np.zeros((5, 5)).astype(np.float32) - x_float32[2, 2] = val_uint8 / 255.0 - - glassblur = A.GlassBlur(always_apply=True, max_delta=1) - - set_seed(0) - blur_uint8 = glassblur(image=x_uint8)["image"] - - set_seed(0) - blur_float32 = glassblur(image=x_float32)["image"] - - # Before comparison, rescale the blur_float32 to [0, 255] - diff = np.abs(blur_uint8 - blur_float32 * 255) - - # The difference between the results of float32 and uint8 will be at most 2. - assert np.all(diff <= 2.0) - - def test_perspective_keep_size(): h, w = 100, 100 img = np.zeros([h, w, 3], dtype=np.uint8) @@ -982,44 +945,6 @@ def test_template_transform_incorrect_channels(img_channels, template_channels): assert str(exc_info.value) == message -@pytest.mark.parametrize(["val_uint8"], [[0], [1], [128], [255]]) -def test_advanced_blur_float_uint8_diff_less_than_two(val_uint8): - x_uint8 = np.zeros((5, 5)).astype(np.uint8) - x_uint8[2, 2] = val_uint8 - - x_float32 = np.zeros((5, 5)).astype(np.float32) - x_float32[2, 2] = val_uint8 / 255.0 - - adv_blur = A.AdvancedBlur(blur_limit=(3, 5), always_apply=True) - - set_seed(0) - adv_blur_uint8 = adv_blur(image=x_uint8)["image"] - - set_seed(0) - adv_blur_float32 = adv_blur(image=x_float32)["image"] - - # Before comparison, rescale the adv_blur_float32 to [0, 255] - diff = np.abs(adv_blur_uint8 - adv_blur_float32 * 255) - - # The difference between the results of float32 and uint8 will be at most 2. - assert np.all(diff <= 2.0) - - -@pytest.mark.parametrize( - ["params"], - [ - [{"blur_limit": (2, 5)}], - [{"blur_limit": (3, 6)}], - [{"sigma_x_limit": (0.0, 1.0), "sigma_y_limit": (0.0, 1.0)}], - [{"beta_limit": (0.1, 0.9)}], - [{"beta_limit": (1.1, 8.0)}], - ], -) -def test_advanced_blur_raises_on_incorrect_params(params): - with pytest.raises(ValueError): - A.AdvancedBlur(**params) - - @pytest.mark.parametrize( ["params"], [ @@ -1323,28 +1248,8 @@ def test_random_crop_interfaces_vs_torchvision(height, width, scale, ratio): assert transformed_image_albu.shape == transformed_image_pt_np.shape assert transform_albu_height_is_size.shape == transformed_image_pt_np.shape -@pytest.mark.parametrize("size, width, height, expected_warning", [ - ((100, 200), None, None, None), - (None, 200, 100, DeprecationWarning), - (100, None, None, TypeError), -]) -def test_deprecation_warnings(size, width, height, expected_warning): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - if expected_warning == TypeError: - with pytest.raises(TypeError): - A.RandomResizedCrop(size=size, width=width, height=height) - else: - A.RandomResizedCrop(size=size, width=width, height=height) - if expected_warning is DeprecationWarning: - assert len(w) == 1 - assert issubclass(w[-1].category, expected_warning) - else: - assert not w - warnings.resetwarnings() - - @pytest.mark.parametrize("num_shadows_limit, num_shadows_lower, num_shadows_upper, expected_warning", [ + ((1, 1), None, None, None), ((1, 2), None, None, None), ((2, 3), None, None, None), ((1, 2), 1, None, DeprecationWarning), @@ -1362,20 +1267,23 @@ def test_deprecation_warnings_random_shadow( Test deprecation warnings for RandomShadow """ with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + warnings.simplefilter("always") # Change the filter to capture all warnings if expected_warning == ValueError: with pytest.raises(ValueError): A.RandomShadow(num_shadows_limit=num_shadows_limit, num_shadows_lower=num_shadows_lower, num_shadows_upper=num_shadows_upper, p=1) - else: + elif expected_warning is DeprecationWarning: A.RandomShadow(num_shadows_limit=num_shadows_limit, num_shadows_lower=num_shadows_lower, num_shadows_upper=num_shadows_upper, p=1) - if expected_warning is DeprecationWarning: - assert len(w) == 1 - assert issubclass(w[-1].category, expected_warning) + for warning in w: + print(f"Warning captured: {warning.category.__name__}, Message: '{warning.message}'") + + if warning.category is DeprecationWarning: + print(f"Deprecation Warning: {warning.message}") + assert any(issubclass(warning.category, DeprecationWarning) for warning in w), \ + "No DeprecationWarning found" else: - assert not w - warnings.resetwarnings() + assert not w, "Unexpected warnings raised" @pytest.mark.parametrize("image", IMAGES) @pytest.mark.parametrize("grid", [ @@ -1421,6 +1329,7 @@ def test_random_crop_from_borders(image, bboxes, keypoints, crop_left, crop_righ keypoint_params=A.KeypointParams("xy")) assert aug(image=image, mask=image, bboxes=bboxes, keypoints=keypoints) + @pytest.mark.parametrize("params, expected", [ # Default values ({}, {"num_holes_range": (1, 1), "hole_height_range": (8, 8), "hole_width_range": (8, 8)}), @@ -1576,3 +1485,29 @@ def test_selective_channel(augmentation_cls, params): assert not np.array_equal(image[..., channel], transformed_image[..., channel]) else: assert np.array_equal(image[..., channel], transformed_image[..., channel]) + + +@pytest.mark.parametrize("params, expected", [ + # Default values + ({}, {"scale_range": (0.25, 0.25), "interpolation_pair": {"downscale": cv2.INTER_NEAREST, "upscale": cv2.INTER_NEAREST}}), + # Boundary values + ({"scale_range": (0.1, 0.9)}, {"scale_range": (0.1, 0.9)}), + ({"interpolation_pair": {"downscale": cv2.INTER_LINEAR, "upscale": cv2.INTER_CUBIC}}, {"interpolation_pair": {"downscale": cv2.INTER_LINEAR, "upscale": cv2.INTER_CUBIC}}), + # Deprecated values handling + ({"scale_min": 0.1, "scale_max": 0.9}, {"scale_range": (0.1, 0.9)}), + ({"interpolation": cv2.INTER_AREA}, {"interpolation_pair": {"downscale": cv2.INTER_AREA, "upscale": cv2.INTER_AREA}}), +]) +def test_downscale_functionality(params, expected): + aug = A.Downscale(**params, p=1) + aug_dict = aug.get_transform_init_args() + for key, value in expected.items(): + assert aug_dict[key] == value, f"Failed on {key} with value {value}" + +@pytest.mark.parametrize("params", [ + ({"scale_range": (0.9, 0.1)}), # Invalid range, max < min + ({"scale_range": (1.1, 1.2)}), # Values outside valid scale range (0, 1) + ({"interpolation_pair": {"downscale": 9999, "upscale": 9999}}), # Invalid interpolation method +]) +def test_downscale_invalid_input(params): + with pytest.raises(Exception): + aug = A.Downscale(**params, p=1) diff --git a/tools/check_defaults.py b/tools/check_defaults.py new file mode 100644 index 000000000..d4732031e --- /dev/null +++ b/tools/check_defaults.py @@ -0,0 +1,34 @@ +import sys +import inspect +import albumentations +from albumentations.core.transforms_interface import BasicTransform + +def check_apply_methods(cls): + """Check for issues in 'apply' methods related to default arguments and Optional type annotations.""" + issues = [] + for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith('apply'): + signature = inspect.signature(method) + for param in signature.parameters.values(): + # Check for default values + if param.default is not inspect.Parameter.empty: + issues.append(f"Default argument found in {cls.__name__}.{name} for parameter {param.name} with default value {param.default}") + return issues + +def is_subclass_of_basic_transform(cls): + """Check if a given class is a subclass of BasicTransform, excluding BasicTransform itself.""" + return issubclass(cls, BasicTransform) and cls is not BasicTransform + +def main(): + issues = [] + # Check all classes in the albumentations module + for name, cls in inspect.getmembers(albumentations, predicate=inspect.isclass): + if is_subclass_of_basic_transform(cls): + issues.extend(check_apply_methods(cls)) + + if issues: + print("\n".join(issues)) + sys.exit(1) # Exit with error status 1 if there are any issues + +if __name__ == "__main__": + main()