Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transforms #2148

Merged
merged 7 commits into from
Nov 17, 2024
Merged

Add transforms #2148

merged 7 commits into from
Nov 17, 2024

Conversation

ternaus
Copy link
Collaborator

@ternaus ternaus commented Nov 17, 2024

Addresses:

#2106
#2109
#2094

Summary by Sourcery

Add the RandomPosterize transform as an alias for Posterize to support Kornia API compatibility, and introduce input validation for tuple bounds.

New Features:

  • Introduce the RandomPosterize transform, which reduces the number of bits for each color channel, as an alias for the existing Posterize transform to maintain compatibility with the Kornia API.

Enhancements:

  • Add a validator function check_range_bounds to ensure tuple values are within specified bounds, enhancing input validation.

Tests:

  • Add a test case for the new RandomPosterize transform to ensure its functionality.

Summary by Sourcery

Introduce new transforms RandomPosterize, RandomSaturation, and GaussianNoise to enhance image augmentation capabilities and ensure compatibility with Kornia and torchvision APIs. Improve input validation with a new range bounds checker and update documentation to reflect these changes.

New Features:

  • Introduce the RandomPosterize transform as an alias for Posterize to support Kornia API compatibility.
  • Add the RandomSaturation transform to adjust the saturation of RGB images.
  • Implement the GaussianNoise transform as a specialized version of GaussNoise following torchvision's API.

Enhancements:

  • Add a validator function check_range_bounds to ensure tuple values are within specified bounds, enhancing input validation.

Documentation:

  • Update README to include new transforms: RandomPosterize, RandomSaturation, and GaussianNoise.

Tests:

  • Add test cases for the new RandomPosterize, RandomSaturation, and GaussianNoise transforms to ensure their functionality.

Copy link
Contributor

sourcery-ai bot commented Nov 17, 2024

Reviewer's Guide by Sourcery

This PR introduces three new image transformation classes (RandomPosterize, RandomSaturation, GaussianNoise) and enhances input validation for tuple bounds. The changes focus on improving API compatibility with other libraries like Kornia and torchvision while maintaining backward compatibility through aliases and parameter validation.

Class diagram for new and updated transforms

classDiagram
    class Posterize {
        <<interface>>
    }
    class RandomPosterize {
        num_bits: tuple[int, int] = (3, 3)
        p: float = 0.5
        get_transform_init_args_names() tuple[str, ...]
    }
    Posterize <|-- RandomPosterize
    note for RandomPosterize "Alias for Posterize for Kornia API compatibility"

    class ColorJitter {
        <<interface>>
    }
    class RandomSaturation {
        saturation: tuple[float, float] = (1.0, 1.0)
        p: float = 0.5
        get_transform_init_args_names() tuple[str]
    }
    ColorJitter <|-- RandomSaturation
    note for RandomSaturation "Specialized version of ColorJitter for saturation"

    class GaussNoise {
        <<interface>>
    }
    class GaussianNoise {
        mean: float = 0.0
        sigma: float = 0.1
        p: float = 0.5
        get_transform_init_args_names() tuple[str, ...]
    }
    GaussNoise <|-- GaussianNoise
    note for GaussianNoise "Specialized version of GaussNoise following torchvision's API"
Loading

File-Level Changes

Change Details Files
Added new image transformation classes for better API compatibility
  • Added RandomPosterize as an alias for Posterize to match Kornia's API
  • Added RandomSaturation as a specialized version of ColorJitter
  • Added GaussianNoise as a specialized version of GaussNoise matching torchvision's API
albumentations/augmentations/tk/transform.py
Enhanced input validation for transformation parameters
  • Added check_range_bounds validator function for tuple value validation
  • Implemented validation for saturation, noise, and posterization parameters
  • Added model validator to handle legacy parameter conversion in GaussNoise
albumentations/core/pydantic.py
albumentations/augmentations/transforms.py
Updated GaussNoise implementation with improved parameter handling
  • Refactored to use normalized std_range and mean_range parameters
  • Added backward compatibility for legacy var_limit and mean parameters
  • Improved noise generation behavior to align with torchvision/kornia
albumentations/augmentations/transforms.py
Added test coverage for new transforms and functionality
  • Added tests for RandomPosterize, RandomSaturation, and GaussianNoise
  • Updated existing test cases to accommodate new parameter validation
  • Added new transforms to augmentation definition lists
tests/test_transforms.py
tests/aug_definitions.py
tests/test_augmentations.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time. You can also use
    this command to specify where the summary should be inserted.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ternaus ternaus marked this pull request as draft November 17, 2024 22:37
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ternaus - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

albumentations/core/pydantic.py Outdated Show resolved Hide resolved
albumentations/augmentations/tk/transform.py Show resolved Hide resolved
albumentations/augmentations/tk/transform.py Show resolved Hide resolved
Copy link

codecov bot commented Nov 17, 2024

Codecov Report

Attention: Patch coverage is 90.00000% with 7 lines in your changes missing coverage. Please review.

Project coverage is 90.39%. Comparing base (b1a79c2) to head (b3b0aea).
Report is 280 commits behind head on main.

Files with missing lines Patch % Lines
albumentations/augmentations/transforms.py 87.50% 4 Missing ⚠️
albumentations/core/pydantic.py 70.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           main    #2148       +/-   ##
=========================================
+ Coverage      0   90.39%   +90.39%     
=========================================
  Files         0       48       +48     
  Lines         0     8143     +8143     
=========================================
+ Hits          0     7361     +7361     
- Misses        0      782      +782     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ternaus ternaus marked this pull request as ready for review November 17, 2024 23:54
@ternaus ternaus merged commit 51756d8 into main Nov 17, 2024
16 checks passed
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ternaus - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Security: all looks good
  • 🟡 Testing: 2 issues found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

hue=(0.0, 0.0), # No hue change
p=p,
)
self.saturation = saturation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Remove redundant saturation attribute storage as it's already stored in parent class

The saturation parameter is being stored both in the parent ColorJitter class and in RandomSaturation. This creates unnecessary duplication.

sigma = self.py_random.uniform(*self.std_range)

sigma *= max_value
mean = self.py_random.uniform(*self.mean_range) * max_value

if self.per_channel:
target_shape = image.shape
if self.noise_scale_factor == 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Extract duplicated noise generation logic into a helper method

The noise generation code is duplicated between per_channel and non-per_channel branches. A helper method would reduce duplication and improve maintainability.

def _generate_noise(self, target_shape, mean, sigma):
    if self.noise_scale_factor == 1:
        return self.random_generator.normal(mean, sigma, target_shape)
    return self.random_generator.normal(mean, sigma, target_shape) * self.noise_scale_factor

Comment on lines 1777 to +1780
data={"image": image},
)

assert np.abs(mean - apply_params["gauss"].mean()) < 0.5
assert np.abs(mean - apply_params["gauss"].mean() / MAX_VALUES_BY_DTYPE[image.dtype]) < 0.5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): GaussNoise test tolerance seems too high

The test allows for a large deviation (0.5) when checking the mean of the generated noise. Consider tightening this tolerance to ensure more precise noise generation, especially since the mean is now normalized to [0,1] range.

Comment on lines 1777 to 1783
data={"image": image},
)

assert np.abs(mean - apply_params["gauss"].mean()) < 0.5
assert np.abs(mean - apply_params["gauss"].mean() / MAX_VALUES_BY_DTYPE[image.dtype]) < 0.5
result = A.Compose([aug], seed=42)(image=image)

assert not (result["image"] >= image).all()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing test cases for GaussianNoise std_range parameter

The test only verifies the mean parameter. Add test cases to verify that the standard deviation (sigma) of the generated noise matches the specified std_range parameter.

def test_gauss_noise(mean, image, std=0.1):
    aug = A.GaussNoise(p=1, noise_scale_factor=1.0, mean_range=(mean, mean), std_range=(std, std))
    aug.set_random_seed(42)

    apply_params = aug.get_params_dependent_on_data(data={"image": image})

    assert np.abs(mean - apply_params["gauss"].mean() / MAX_VALUES_BY_DTYPE[image.dtype]) < 0.5
    assert np.abs(std - apply_params["gauss"].std() / MAX_VALUES_BY_DTYPE[image.dtype]) < 0.1
    result = A.Compose([aug], seed=42)(image=image)

    assert not (result["image"] >= image).all()

self.per_channel = per_channel
self.noise_scale_factor = noise_scale_factor

self.var_limit = var_limit

def apply(self, img: np.ndarray, gauss: np.ndarray, **params: Any) -> np.ndarray:
return fmain.add_noise(img, gauss)

def get_params_dependent_on_data(self, params: dict[str, Any], data: dict[str, Any]) -> dict[str, float]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting noise generation logic into a helper method to reduce code duplication.

The noise generation logic has unnecessary duplication between the per_channel branches. Consider extracting the common logic into a helper method:

def _generate_noise(self, target_shape: tuple, mean: float, sigma: float) -> np.ndarray:
    if self.noise_scale_factor == 1:
        return self.random_generator.normal(mean, sigma, target_shape)
    return fmain.generate_approx_gaussian_noise(
        target_shape,
        mean,
        sigma,
        self.noise_scale_factor,
        self.random_generator,
    )

def get_params_dependent_on_data(self, params: dict[str, Any], data: dict[str, Any]) -> dict[str, float]:
    image = data["image"] if "image" in data else data["images"][0]
    max_value = MAX_VALUES_BY_DTYPE[image.dtype]

    sigma = self.py_random.uniform(*self.std_range) * max_value
    mean = self.py_random.uniform(*self.mean_range) * max_value

    if self.per_channel:
        gauss = self._generate_noise(image.shape, mean, sigma)
    else:
        gauss = self._generate_noise(image.shape[:2], mean, sigma)
        if image.ndim > MONO_CHANNEL_DIMENSIONS:
            gauss = np.expand_dims(gauss, -1)

    return {"gauss": gauss}

This refactoring:

  1. Eliminates code duplication in noise generation
  2. Improves readability by separating noise generation from shape handling
  3. Makes the logic flow clearer and easier to maintain

if self.mean is not None:
self.mean_range = (0.0, 0.0)

if self.mean is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Merge repeated if statements into single if (merge-repeated-ifs)

Suggested change
if self.mean is not None:


def validator(value: tuple[Number, Number]) -> tuple[Number, Number]:
if max_val is None:
if not (value[0] >= min_val and value[1] >= min_val):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Simplify logical expression using De Morgan identities (de-morgan)

Suggested change
if not (value[0] >= min_val and value[1] >= min_val):
if value[0] < min_val or value[1] < min_val:

@ternaus ternaus deleted the add_transforms branch November 17, 2024 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant