Repository: kornia/kornia

Issue:
[Question] Significant difference in performance between Kornia and Torchvision image augmentations
I have trained two models that use the same sequence of image augmentations but in Torchvision and Kornia and I’m observing a significant difference in the performance of these models. I understand that despite fixing random seeds, these augmentations might still be different which might cause some difference in the test accuracies, but on average, I assume that both of these models should end with similar accuracies, especially when these values are averaged over multiple seeds. However, this is not the case.

```
# PyTorch transformation
train_orig_transform = transforms.Compose([
    transforms.RandomResizedCrop(32),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomApply([transforms.ColorJitter(0.4, 0.4, 0.4, 0.1)], p=0.8),
    transforms.RandomGrayscale(p=0.2),
    transforms.ToTensor(),
    transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])
    ])
```

This is the Kornia version of the above PyTorch transformation

```

class KorniaAugmentationModule(nn.Module):
    def __init__(self, batch_size=512):
        super().__init__()
        # These are standard values for CIFAR10
        self.mu = torch.Tensor([0.4914, 0.4822, 0.4465])
        self.sigma = torch.Tensor([0.2023, 0.1994, 0.2010])

        self.hor_flip_prob = 0.5
        self.jit_prob = 0.8
        self.gs_prob = 0.2

        self.crop = K.RandomResizedCrop(size=(32, 32), same_on_batch=False)
        self.hor_flip = K.RandomHorizontalFlip(p=self.hor_flip_prob, same_on_batch=False)
        self.jit = K.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.1, p=self.jit_prob, same_on_batch=False)
        self.rand_grayscale =  K.RandomGrayscale(p=self.gs_prob, same_on_batch=False)
        
        self.normalize = K.Normalize(self.mu, self.sigma)

    # Note that I should only normalize in test mode; no other type of augmentation should be performed
    
    def forward(self, x, params=None, mode='train'):
        B = x.shape[0]
        if mode == 'train':
                x = self.crop(x, params['crop_params'])
                x = self.hor_flip(x, params['hor_flip_params'])
                x[params['jit_batch_probs']] = self.jit(x[params['jit_batch_probs']], params['jit_params'])
                x = self.rand_grayscale(x, params['grayscale_params'])

        x = self.normalize(x)
        return x

```
Rest of the code for training and testing these models is shared.

These are the training loss and testing accuracy curves for kornia (orange) and torchvision (green)

<img width="714" alt="Screen Shot 2020-09-27 at 6 56 35 PM" src="https://user-images.githubusercontent.com/7864463/94378043-1f2d1300-00f4-11eb-8654-59c477b4f8d0.png">

The difference in test accuracies between the two models is nearly ~11% which is very significant.

I have posted this question on PyTorch discussion forum as well. Could you please give pointers on why this behavior is being observed, is this expected and if not, what could be ways to debug this?




Discussion:
One difference between kornia and torchvision is the implementation of ColorJitter. You may refer to #565.

Not pretty confident but may need a different suite of parameters.
@FGeri can you check this. You did some speed improvements on color jitter
@ashwinipokle Could you check if KorniaAugmentationModule is fed with tensors between [0.,1.]? Torchvision works with PIL images [0,255] but Kornia with tensors [0.,1.].
@FGeri you are right. torchvision recently add `ConvertImageDtype(dtype: torch.dtype)`
Hi, I am not the original poster but I am trying to implement the same augmentation pipeline with Kornia (for SimCLR) and seeing a similar issue - I can confirm that it's present with the input range set appropriately. Any other ideas for what could be causing the discrepancy? @ashwinipokle did you ever figure out the issue? Thanks!
@sachit-menon @ashwinipokle Could you share the dataloading part for both the torchvision and kornia part as well as the augmentation pipeline? And how ```params``` is set?
Good news! I found a similar issue this week. I haven't had time to file a proper issue yet, but in a nutshell kornia's RandomResizedCrop has an off-by-one bug in random_crop_size_generator.

This means that:
1. The last row and column of the input image is never sampled.
2. To make matters worse, it also means the entire unmodified image is also never sampled, even with a scale of 1.0.

This is particularly noticeable on CIFAR, as several percent of the pixels are never used for training. In addition, the network never gets to see an unscaled image at training time and as scaling/center-cropping is not standard at test-time for CIFAR this badly affects test-time accuracy too.

You can see this by running:
`random_crop_size_generator(100, (2, 2), scale=torch.tensor([0.999, 1.]), ratio=torch.tensor([1., 1.]))['size']`

Expected: some of the sizes are [2, 2]
Observed: all of the sizes are [1, 1] 

This causes me to see a ~10% test-time performance drop in SimCLR.
@mark-arm thanks for reporting ! Please, take a look at this PR https://github.com/kornia/kornia/pull/770 which probably will fix the mentioned issue. Consider to follow the discussion about unit testing different random generation precision. We'll resume the activity next week after cvpr deadline.
Thanks for the quick response @edgarriba. I've looked through the source changes in #770 and don't see anything that would obviously correct this issue. Is there something I'm missing? I'm on vacation this week but will check back next Monday!
@mark-arm ok, then maybe #770 needs better attention to such corner cases. /cc @shijianjian consider this comments
@mark-arm I have got it fixed in #770.
@ashwinipokle did you try to repro your issue after #770?
Maybe, if possible, provide the test run. I would try to remove color jitter in one case to rule out other causes. 
@ahirner I am no longer using the Kornia codebase as I had issues with reproducibility. I ended up modifying PyTorch codebase so that it meets my requirements.  
Same issue here. I used kornia augmentations for a few days after finding that GPU augmentation provides 2x training speedup with my code, but then I also found a drop of few % in accuracy, so now I have to give up kornia augmentations.
Is there any plan to add torchvision-equivalent color jittering to kornia?
@gmberton kornia has the enough low level functionality to write your own color jitter or any other augmentation related ops. We do not plan to match torchvision since it’s another project/team and seeks different uses cases funded mainly by a very well known company plus it’s child foundations. You can check our budgets here https://opencollective.com/kornia

If anything is required please, consider to fund our project with contributions or money.

All our contributors put their free time in the project and no one is getting any kind of salary.
Everyone is very welcomed to improve any part of the code base.
@edgarriba thank you for your quick reply, I just became a proud financial contributor of kornia :)
Regarding the issue, I understand that there's no plan to support torchvision-like color-jittering, so I think I'll just use [this adaptation](https://github.com/rdevon/kornia/commit/09d95360837e557a5d83f3bd891f0271d546ee62) (from [this issue](https://github.com/kornia/kornia/issues/565)) for my own code.
If you think something like this could be added to future versions, I'd be happy to contribute.
@gmberton thanks so much! Are you in our slack ? We can discuss over there how to make a proper integration for different backend libraries. @lferraz was interested in taking a look into that. /cc @shijianjian 
I checked the code in https://github.com/rdevon/kornia/commit/09d95360837e557a5d83f3bd891f0271d546ee62 and I think the easiest approach is to add a flag (OPENCV/PIL) to those 2 funcionalities. By default set OPENCV and then we do not need to change anything else. What do you think @edgarriba @shijianjian @gmberton ? 

we are talking about 10 lines of code... shouldn't be a drama.
@lferraz using a flag sounds great to me
@gmberton Are you okay to file a PR on this ?

Resolving patch:
diff --git a/kornia/enhance/__init__.py b/kornia/enhance/__init__.py
--- a/kornia/enhance/__init__.py
+++ b/kornia/enhance/__init__.py
@@ -7,6 +7,7 @@
     Invert,
     adjust_brightness,
     adjust_contrast,
+    adjust_contrast_with_mean_subtraction,
     adjust_gamma,
     adjust_hue,
     adjust_hue_raw,
@@ -28,6 +29,7 @@
 __all__ = [
     "adjust_brightness",
     "adjust_contrast",
+    "adjust_contrast_with_mean_subtraction",
     "adjust_gamma",
     "adjust_hue",
     "adjust_saturation",
@@ -51,7 +53,13 @@
     "histogram",
     "histogram2d",
     "image_histogram2d",
-    "normalize", "normalize_min_max", "denormalize", "Normalize",
+    "normalize",
+    "normalize_min_max",
+    "denormalize",
+    "Normalize",
     "Denormalize",
-    "zca_mean", "zca_whiten", "linear_transform", "ZCAWhitening",
+    "zca_mean",
+    "zca_whiten",
+    "linear_transform",
+    "ZCAWhitening",
 ]
diff --git a/kornia/enhance/adjust.py b/kornia/enhance/adjust.py
--- a/kornia/enhance/adjust.py
+++ b/kornia/enhance/adjust.py
@@ -2,23 +2,23 @@
 from typing import Optional, Union
 
 import torch
-import torch.nn as nn
+from torch import Tensor
+from torch.nn import Module, Parameter
 
-from kornia.color.hsv import hsv_to_rgb, rgb_to_hsv
+from kornia.color import hsv_to_rgb, rgb_to_grayscale, rgb_to_hsv
+from kornia.testing import KORNIA_CHECK, KORNIA_CHECK_IS_TENSOR
 from kornia.utils.helpers import _torch_histc_cast
 from kornia.utils.image import perform_keep_shape_image, perform_keep_shape_video
 
 
-def adjust_saturation_raw(input: torch.Tensor, saturation_factor: Union[float, torch.Tensor]) -> torch.Tensor:
+def adjust_saturation_raw(input: Tensor, saturation_factor: Union[float, Tensor]) -> Tensor:
     r"""Adjust color saturation of an image. Expecting input to be in hsv format already."""
 
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(saturation_factor, (float, torch.Tensor)):
-        raise TypeError(
-            f"The saturation_factor should be a float number or torch.Tensor." f"Got {type(saturation_factor)}"
-        )
+    if not isinstance(saturation_factor, (float, Tensor)):
+        raise TypeError(f"The saturation_factor should be a float number or Tensor." f"Got {type(saturation_factor)}")
 
     if isinstance(saturation_factor, float):
         saturation_factor = torch.as_tensor(saturation_factor)
@@ -36,15 +36,15 @@ def adjust_saturation_raw(input: torch.Tensor, saturation_factor: Union[float, t
     h, s, v = torch.chunk(input, chunks=3, dim=-3)
 
     # transform the hue value and appl module
-    s_out: torch.Tensor = torch.clamp(s * saturation_factor, min=0, max=1)
+    s_out: Tensor = torch.clamp(s * saturation_factor, min=0, max=1)
 
     # pack back back the corrected hue
-    out: torch.Tensor = torch.cat([h, s_out, v], dim=-3)
+    out: Tensor = torch.cat([h, s_out, v], dim=-3)
 
     return out
 
 
-def adjust_saturation(input: torch.Tensor, saturation_factor: Union[float, torch.Tensor]) -> torch.Tensor:
+def adjust_saturation(input: Tensor, saturation_factor: Union[float, Tensor]) -> Tensor:
     r"""Adjust color saturation of an image.
 
     .. image:: _static/img/adjust_saturation.png
@@ -75,26 +75,26 @@ def adjust_saturation(input: torch.Tensor, saturation_factor: Union[float, torch
     """
 
     # convert the rgb image to hsv
-    x_hsv: torch.Tensor = rgb_to_hsv(input)
+    x_hsv: Tensor = rgb_to_hsv(input)
 
     # perform the conversion
-    x_adjusted: torch.Tensor = adjust_saturation_raw(x_hsv, saturation_factor)
+    x_adjusted: Tensor = adjust_saturation_raw(x_hsv, saturation_factor)
 
     # convert back to rgb
-    out: torch.Tensor = hsv_to_rgb(x_adjusted)
+    out: Tensor = hsv_to_rgb(x_adjusted)
 
     return out
 
 
-def adjust_hue_raw(input: torch.Tensor, hue_factor: Union[float, torch.Tensor]) -> torch.Tensor:
+def adjust_hue_raw(input: Tensor, hue_factor: Union[float, Tensor]) -> Tensor:
     r"""Adjust hue of an image. Expecting input to be in hsv format already."""
 
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(hue_factor, (float, torch.Tensor)):
+    if not isinstance(hue_factor, (float, Tensor)):
         raise TypeError(
-            f"The hue_factor should be a float number or torch.Tensor in the range between"
+            f"The hue_factor should be a float number or Tensor in the range between"
             f" [-PI, PI]. Got {type(hue_factor)}"
         )
 
@@ -115,15 +115,15 @@ def adjust_hue_raw(input: torch.Tensor, hue_factor: Union[float, torch.Tensor])
 
     # transform the hue value and appl module
     divisor: float = 2 * pi
-    h_out: torch.Tensor = torch.fmod(h + hue_factor, divisor)
+    h_out: Tensor = torch.fmod(h + hue_factor, divisor)
 
     # pack back back the corrected hue
-    out: torch.Tensor = torch.cat([h_out, s, v], dim=-3)
+    out: Tensor = torch.cat([h_out, s, v], dim=-3)
 
     return out
 
 
-def adjust_hue(input: torch.Tensor, hue_factor: Union[float, torch.Tensor]) -> torch.Tensor:
+def adjust_hue(input: Tensor, hue_factor: Union[float, Tensor]) -> Tensor:
     r"""Adjust hue of an image.
 
     .. image:: _static/img/adjust_hue.png
@@ -156,20 +156,18 @@ def adjust_hue(input: torch.Tensor, hue_factor: Union[float, torch.Tensor]) -> t
     """
 
     # convert the rgb image to hsv
-    x_hsv: torch.Tensor = rgb_to_hsv(input)
+    x_hsv: Tensor = rgb_to_hsv(input)
 
     # perform the conversion
-    x_adjusted: torch.Tensor = adjust_hue_raw(x_hsv, hue_factor)
+    x_adjusted: Tensor = adjust_hue_raw(x_hsv, hue_factor)
 
     # convert back to rgb
-    out: torch.Tensor = hsv_to_rgb(x_adjusted)
+    out: Tensor = hsv_to_rgb(x_adjusted)
 
     return out
 
 
-def adjust_gamma(
-    input: torch.Tensor, gamma: Union[float, torch.Tensor], gain: Union[float, torch.Tensor] = 1.0
-) -> torch.Tensor:
+def adjust_gamma(input: Tensor, gamma: Union[float, Tensor], gain: Union[float, Tensor] = 1.0) -> Tensor:
     r"""Perform gamma correction on an image.
 
     .. image:: _static/img/adjust_contrast.png
@@ -203,20 +201,20 @@ def adjust_gamma(
         torch.Size([2, 5, 3, 3])
     """
 
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(gamma, (float, torch.Tensor)):
-        raise TypeError(f"The gamma should be a positive float or torch.Tensor. Got {type(gamma)}")
+    if not isinstance(gamma, (float, Tensor)):
+        raise TypeError(f"The gamma should be a positive float or Tensor. Got {type(gamma)}")
 
-    if not isinstance(gain, (float, torch.Tensor)):
-        raise TypeError(f"The gain should be a positive float or torch.Tensor. Got {type(gain)}")
+    if not isinstance(gain, (float, Tensor)):
+        raise TypeError(f"The gain should be a positive float or Tensor. Got {type(gain)}")
 
     if isinstance(gamma, float):
-        gamma = torch.tensor([gamma])
+        gamma = Tensor([gamma])
 
     if isinstance(gain, float):
-        gain = torch.tensor([gain])
+        gain = Tensor([gain])
 
     gamma = gamma.to(input.device).to(input.dtype)
     gain = gain.to(input.device).to(input.dtype)
@@ -234,28 +232,37 @@ def adjust_gamma(
         gain = torch.unsqueeze(gain, dim=-1)
 
     # Apply the gamma correction
-    x_adjust: torch.Tensor = gain * torch.pow(input, gamma)
+    x_adjust: Tensor = gain * torch.pow(input, gamma)
 
     # Truncate between pixel values
-    out: torch.Tensor = torch.clamp(x_adjust, 0.0, 1.0)
+    out: Tensor = torch.clamp(x_adjust, 0.0, 1.0)
 
     return out
 
 
-def adjust_contrast(input: torch.Tensor, contrast_factor: Union[float, torch.Tensor]) -> torch.Tensor:
-    r"""Adjust Contrast of an image.
+def adjust_contrast(image: Tensor, factor: Union[float, Tensor], clip_output: bool = True) -> Tensor:
+    r"""Adjust the contrast of an image tensor.
 
     .. image:: _static/img/adjust_contrast.png
 
-    This implementation aligns OpenCV, not PIL. Hence, the output differs from TorchVision.
-    The input image is expected to be in the range of [0, 1].
+    This implementation follows Szeliski's book convention, where contrast is defined as
+    a `multiplicative` operation directly to raw pixel values. Beware that other frameworks
+    might use different conventions which can be difficult to reproduce exact results.
+
+    The input image and factor is expected to be in the range of [0, 1].
+
+    .. tip::
+        This is not the preferred way to adjust the contrast of an image. Ideally one must
+        implement :func:`kornia.enhance.adjust_gamma`. More details in the following link:
+        https://scikit-image.org/docs/dev/auto_examples/color_exposure/plot_log_gamma.html#sphx-glr-auto-examples-color-exposure-plot-log-gamma-py
 
     Args:
-        input: Image to be adjusted in the shape of :math:`(*, H, W)`.
-        contrast_factor: Contrast adjust factor per element
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        factor: Contrast adjust factor per element
             in the batch. 0 generates a completely black image, 1 does not modify
             the input image while any other non-negative number modify the
             brightness by this factor.
+            clip_output: whether to clip the output image with range of [0, 1].
 
     Return:
         Adjusted image in the shape of :math:`(*, H, W)`.
@@ -265,6 +272,7 @@ def adjust_contrast(input: torch.Tensor, contrast_factor: Union[float, torch.Ten
        image_enhancement.html>`__.
 
     Example:
+        >>> import torch
         >>> x = torch.ones(1, 1, 2, 2)
         >>> adjust_contrast(x, 0.5)
         tensor([[[[0.5000, 0.5000],
@@ -275,49 +283,111 @@ def adjust_contrast(input: torch.Tensor, contrast_factor: Union[float, torch.Ten
         >>> adjust_contrast(x, y).shape
         torch.Size([2, 5, 3, 3])
     """
+    KORNIA_CHECK_IS_TENSOR(image, "Expected shape (*, H, W)")
+    KORNIA_CHECK(isinstance(factor, (float, Tensor)), "Factor should be float or Tensor.")
 
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if isinstance(factor, float):
+        # TODO: figure out how to create later a tensor without importing torch
+        factor = torch.as_tensor(factor, device=image.device, dtype=image.dtype)
+    elif isinstance(factor, Tensor):
+        factor = factor.to(image.device, image.dtype)
 
-    if not isinstance(contrast_factor, (float, torch.Tensor)):
-        raise TypeError(f"The factor should be either a float or torch.Tensor. " f"Got {type(contrast_factor)}")
+    # make factor broadcastable
+    while len(factor.shape) != len(image.shape):
+        factor = factor[..., None]
 
-    if isinstance(contrast_factor, float):
-        contrast_factor = torch.tensor([contrast_factor])
+    KORNIA_CHECK((factor >= 0).any(), f"Contrast factor must be positive. Got {factor}")
 
-    contrast_factor = contrast_factor.to(input.device).to(input.dtype)
+    # Apply contrast factor to each channel
+    img_adjust: Tensor = image * factor
 
-    if (contrast_factor < 0).any():
-        raise ValueError(f"Contrast factor must be non-negative. Got {contrast_factor}")
+    # Truncate between pixel values
+    if clip_output:
+        img_adjust = img_adjust.clamp(min=0.0, max=1.0)
 
-    for _ in range(len(input.shape) - len(contrast_factor.shape)):
-        contrast_factor = torch.unsqueeze(contrast_factor, dim=-1)
+    return img_adjust
 
-    # Apply contrast factor to each channel
-    x_adjust: torch.Tensor = input * contrast_factor
 
-    # Truncate between pixel values
-    out: torch.Tensor = torch.clamp(x_adjust, 0.0, 1.0)
+def adjust_contrast_with_mean_subtraction(image: Tensor, factor: Union[float, Tensor]) -> Tensor:
+    r"""Adjust the contrast of an image tensor by subtracting the mean over channels.
 
-    return out
+    .. note::
+        this is just a convenience function to have compatibility with Pil. For exact
+        definition of image contrast adjustmen consider using :func:`kornia.enhance.adjust_gamma`.
 
+    Args:
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        factor: Contrast adjust factor per element
+            in the batch. 0 generates a completely black image, 1 does not modify
+            the input image while any other non-negative number modify the
+            brightness by this factor.
+
+    Return:
+        Adjusted image in the shape of :math:`(*, H, W)`.
+
+    Example:
+        >>> import torch
+        >>> x = torch.ones(1, 1, 2, 2)
+        >>> adjust_contrast_with_mean_subtraction(x, 0.5)
+        tensor([[[[0.5000, 0.5000],
+                  [0.5000, 0.5000]]]])
+
+        >>> x = torch.ones(2, 5, 3, 3)
+        >>> y = torch.tensor([0.65, 0.50])
+        >>> adjust_contrast_with_mean_subtraction(x, y).shape
+        torch.Size([2, 5, 3, 3])
+    """
+    KORNIA_CHECK_IS_TENSOR(image, "Expected shape (*, H, W)")
+    KORNIA_CHECK(isinstance(factor, (float, Tensor)), "Factor should be float or Tensor.")
+
+    if isinstance(factor, float):
+        # TODO: figure out how to create later a tensor without importing torch
+        factor = torch.as_tensor(factor, device=image.device, dtype=image.dtype)
+    elif isinstance(factor, Tensor):
+        factor = factor.to(image.device, image.dtype)
+
+    # make factor broadcastable
+    while len(factor.shape) != len(image.shape):
+        factor = factor[..., None]
+
+    KORNIA_CHECK((factor >= 0).any(), f"Contrast factor must be positive. Got {factor}")
+
+    if image.shape[-3] == 3:
+        img_mean = rgb_to_grayscale(image).mean((-2, -1), True)
+    else:
+        img_mean = image.mean()
+
+    # Apply contrast factor subtracting the mean
+    return image * factor + img_mean * (1 - factor)
 
-def adjust_brightness(input: torch.Tensor, brightness_factor: Union[float, torch.Tensor]) -> torch.Tensor:
-    r"""Adjust Brightness of an image.
+
+def adjust_brightness(image: Tensor, factor: Union[float, Tensor], clip_output=True) -> Tensor:
+    r"""Adjust the brightness of an image tensor.
 
     .. image:: _static/img/adjust_brightness.png
 
-    This implementation aligns OpenCV, not PIL. Hence, the output differs from TorchVision.
-    The input image is expected to be in the range of [0, 1].
+    This implementation follows Szeliski's book convention, where brightness is defined as
+    an `additive` operation directly to raw pixel and shift its values according the applied
+    factor and range of the image values. Beware that other framework might use different
+    conventions which can be difficult to reproduce exact results.
+
+    The input image and factor is expected to be in the range of [0, 1].
+
+    .. tip::
+        By applying a large factor might prouce clipping or loss of image detail. We recommenda to
+        apply small factors to avoid the mentioned issues. Ideally one must implement the adjustment
+        of image intensity with other techniques suchs as :func:`kornia.enhance.adjust_gamma`. More
+        details in the following link:
+        https://scikit-image.org/docs/dev/auto_examples/color_exposure/plot_log_gamma.html#sphx-glr-auto-examples-color-exposure-plot-log-gamma-py
 
     Args:
-        input: image to be adjusted in the shape of :math:`(*, H, W)`.
-        brightness_factor: Brightness adjust factor per element
-            in the batch. 0 does not modify the input image while any other number modify the
-            brightness.
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        factor: Brightness adjust factor per element in the batch. It's recommended to
+            bound the factor by [0, 1]. 0 does not modify the input image while any other
+            number modify the brightness.
 
     Return:
-        Adjusted image in the shape of :math:`(*, H, W)`.
+        Adjusted tensor in the shape of :math:`(*, H, W)`.
 
     .. note::
        See a working example `here <https://kornia-tutorials.readthedocs.io/en/latest/
@@ -334,50 +404,50 @@ def adjust_brightness(input: torch.Tensor, brightness_factor: Union[float, torch
         >>> adjust_brightness(x, y).shape
         torch.Size([2, 5, 3, 3])
     """
+    KORNIA_CHECK_IS_TENSOR(image, "Expected shape (*, H, W)")
+    KORNIA_CHECK(isinstance(factor, (float, Tensor)), "Factor should be float or Tensor.")
 
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    # convert factor to a tensor
+    if isinstance(factor, float):
+        # TODO: figure out how to create later a tensor without importing torch
+        factor = torch.as_tensor(factor, device=image.device, dtype=image.dtype)
+    elif isinstance(factor, Tensor):
+        factor = factor.to(image.device, image.dtype)
 
-    if not isinstance(brightness_factor, (float, torch.Tensor)):
-        raise TypeError(f"The factor should be either a float or torch.Tensor. " f"Got {type(brightness_factor)}")
+    # make factor broadcastable
+    while len(factor.shape) != len(image.shape):
+        factor = factor[..., None]
 
-    if isinstance(brightness_factor, float):
-        brightness_factor = torch.tensor([brightness_factor])
+    # shift pixel values
+    img_adjust: Tensor = image + factor
 
-    brightness_factor = brightness_factor.to(input.device).to(input.dtype)
+    # truncate between pixel values
+    if clip_output:
+        img_adjust = img_adjust.clamp(min=0.0, max=1.0)
 
-    for _ in range(len(input.shape) - len(brightness_factor.shape)):
-        brightness_factor = torch.unsqueeze(brightness_factor, dim=-1)
+    return img_adjust
 
-    # Apply brightness factor to each channel
-    x_adjust: torch.Tensor = input + brightness_factor
 
-    # Truncate between pixel values
-    out: torch.Tensor = torch.clamp(x_adjust, 0.0, 1.0)
-
-    return out
-
-
-def _solarize(input: torch.Tensor, thresholds: Union[float, torch.Tensor] = 0.5) -> torch.Tensor:
+def _solarize(input: Tensor, thresholds: Union[float, Tensor] = 0.5) -> Tensor:
     r"""For each pixel in the image, select the pixel if the value is less than the threshold.
     Otherwise, subtract 1.0 from the pixel.
 
     Args:
-        input (torch.Tensor): image or batched images to solarize.
-        thresholds (float or torch.Tensor): solarize thresholds.
+        input: image or batched images to solarize.
+        thresholds: solarize thresholds.
             If int or one element tensor, input will be solarized across the whole batch.
             If 1-d tensor, input will be solarized element-wise, len(thresholds) == len(input).
 
     Returns:
-        torch.Tensor: Solarized images.
+        Solarized images.
     """
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(thresholds, (float, torch.Tensor)):
-        raise TypeError(f"The factor should be either a float or torch.Tensor. " f"Got {type(thresholds)}")
+    if not isinstance(thresholds, (float, Tensor)):
+        raise TypeError(f"The factor should be either a float or Tensor. " f"Got {type(thresholds)}")
 
-    if isinstance(thresholds, torch.Tensor) and len(thresholds.shape) != 0:
+    if isinstance(thresholds, Tensor) and len(thresholds.shape) != 0:
         if not (input.size(0) == len(thresholds) and len(thresholds.shape) == 1):
             raise AssertionError(f"thresholds must be a 1-d vector of shape ({input.size(0)},). Got {thresholds}")
         # TODO: I am not happy about this line, but no easy to do batch-wise operation
@@ -388,10 +458,8 @@ def _solarize(input: torch.Tensor, thresholds: Union[float, torch.Tensor] = 0.5)
 
 
 def solarize(
-    input: torch.Tensor,
-    thresholds: Union[float, torch.Tensor] = 0.5,
-    additions: Optional[Union[float, torch.Tensor]] = None,
-) -> torch.Tensor:
+    input: Tensor, thresholds: Union[float, Tensor] = 0.5, additions: Optional[Union[float, Tensor]] = None
+) -> Tensor:
     r"""For each pixel in the image less than threshold.
 
     .. image:: _static/img/solarize.png
@@ -424,26 +492,26 @@ def solarize(
         >>> solarize(x, thresholds, additions).shape
         torch.Size([2, 4, 3, 3])
     """
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(thresholds, (float, torch.Tensor)):
-        raise TypeError(f"The factor should be either a float or torch.Tensor. " f"Got {type(thresholds)}")
+    if not isinstance(thresholds, (float, Tensor)):
+        raise TypeError(f"The factor should be either a float or Tensor. " f"Got {type(thresholds)}")
 
     if isinstance(thresholds, float):
-        thresholds = torch.tensor(thresholds)
+        thresholds = torch.as_tensor(thresholds)
 
     if additions is not None:
-        if not isinstance(additions, (float, torch.Tensor)):
-            raise TypeError(f"The factor should be either a float or torch.Tensor. " f"Got {type(additions)}")
+        if not isinstance(additions, (float, Tensor)):
+            raise TypeError(f"The factor should be either a float or Tensor. " f"Got {type(additions)}")
 
         if isinstance(additions, float):
-            additions = torch.tensor(additions)
+            additions = torch.as_tensor(additions)
 
         if not torch.all((additions < 0.5) * (additions > -0.5)):
             raise AssertionError(f"The value of 'addition' is between -0.5 and 0.5. Got {additions}.")
 
-        if isinstance(additions, torch.Tensor) and len(additions.shape) != 0:
+        if isinstance(additions, Tensor) and len(additions.shape) != 0:
             if not (input.size(0) == len(additions) and len(additions.shape) == 1):
                 raise AssertionError(f"additions must be a 1-d vector of shape ({input.size(0)},). Got {additions}")
             # TODO: I am not happy about this line, but no easy to do batch-wise operation
@@ -456,7 +524,7 @@ def solarize(
 
 
 @perform_keep_shape_image
-def posterize(input: torch.Tensor, bits: Union[int, torch.Tensor]) -> torch.Tensor:
+def posterize(input: Tensor, bits: Union[int, Tensor]) -> Tensor:
     r"""Reduce the number of bits for each color channel.
 
     .. image:: _static/img/posterize.png
@@ -483,14 +551,14 @@ def posterize(input: torch.Tensor, bits: Union[int, torch.Tensor]) -> torch.Tens
         >>> posterize(x, bits).shape
         torch.Size([2, 6, 3, 3])
     """
-    if not isinstance(input, torch.Tensor):
-        raise TypeError(f"Input type is not a torch.Tensor. Got {type(input)}")
+    if not isinstance(input, Tensor):
+        raise TypeError(f"Input type is not a Tensor. Got {type(input)}")
 
-    if not isinstance(bits, (int, torch.Tensor)):
-        raise TypeError(f"bits type is not an int or torch.Tensor. Got {type(bits)}")
+    if not isinstance(bits, (int, Tensor)):
+        raise TypeError(f"bits type is not an int or Tensor. Got {type(bits)}")
 
     if isinstance(bits, int):
-        bits = torch.tensor(bits)
+        bits = torch.as_tensor(bits)
 
     # TODO: find a better way to check boundaries on tensors
     # if not torch.all((bits >= 0) * (bits <= 8)) and bits.dtype == torch.int:
@@ -502,13 +570,13 @@ def posterize(input: torch.Tensor, bits: Union[int, torch.Tensor]) -> torch.Tens
     # Ref: https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/autoaugment.py#L222
     # Potential approach: implementing kornia.LUT with floating points
     # https://github.com/albumentations-team/albumentations/blob/master/albumentations/augmentations/functional.py#L472
-    def _left_shift(input: torch.Tensor, shift: torch.Tensor):
-        return ((input * 255).to(torch.uint8) * (2 ** shift)).to(input.dtype) / 255.0
+    def _left_shift(input: Tensor, shift: Tensor):
+        return ((input * 255).to(torch.uint8) * (2**shift)).to(input.dtype) / 255.0
 
-    def _right_shift(input: torch.Tensor, shift: torch.Tensor):
-        return (input * 255).to(torch.uint8) / (2 ** shift).to(input.dtype) / 255.0
+    def _right_shift(input: Tensor, shift: Tensor):
+        return (input * 255).to(torch.uint8) / (2**shift).to(input.dtype) / 255.0
 
-    def _posterize_one(input: torch.Tensor, bits: torch.Tensor):
+    def _posterize_one(input: Tensor, bits: Tensor):
         # Single bits value condition
         if bits == 0:
             return torch.zeros_like(input)
@@ -536,7 +604,7 @@ def _posterize_one(input: torch.Tensor, bits: torch.Tensor):
             "Batch and channel must be equal between bits and input. "
             f"Got {bits.shape}, {input.shape[:len(bits.shape)]}."
         )
-    _input = input.view(-1, *input.shape[len(bits.shape):])
+    _input = input.view(-1, *input.shape[len(bits.shape) :])
     _bits = bits.flatten()
     for i in range(input.shape[0]):
         res.append(_posterize_one(_input[i], _bits[i]))
@@ -544,7 +612,7 @@ def _posterize_one(input: torch.Tensor, bits: torch.Tensor):
 
 
 @perform_keep_shape_image
-def sharpness(input: torch.Tensor, factor: Union[float, torch.Tensor]) -> torch.Tensor:
+def sharpness(input: Tensor, factor: Union[float, Tensor]) -> Tensor:
     r"""Apply sharpness to the input tensor.
 
     .. image:: _static/img/sharpness.png
@@ -566,8 +634,8 @@ def sharpness(input: torch.Tensor, factor: Union[float, torch.Tensor]) -> torch.
         >>> sharpness(x, 0.5).shape
         torch.Size([1, 1, 5, 5])
     """
-    if not isinstance(factor, torch.Tensor):
-        factor = torch.tensor(factor, device=input.device, dtype=input.dtype)
+    if not isinstance(factor, Tensor):
+        factor = torch.as_tensor(factor, device=input.device, dtype=input.dtype)
 
     if len(factor.size()) != 0 and factor.shape != torch.Size([input.size(0)]):
         raise AssertionError(
@@ -576,7 +644,7 @@ def sharpness(input: torch.Tensor, factor: Union[float, torch.Tensor]) -> torch.
         )
 
     kernel = (
-        torch.tensor([[1, 1, 1], [1, 5, 1], [1, 1, 1]], dtype=input.dtype, device=input.device)
+        torch.as_tensor([[1, 1, 1], [1, 5, 1], [1, 1, 1]], dtype=input.dtype, device=input.device)
         .view(1, 1, 3, 3)
         .repeat(input.size(1), 1, 1, 1)
         / 13
@@ -598,7 +666,7 @@ def sharpness(input: torch.Tensor, factor: Union[float, torch.Tensor]) -> torch.
     return torch.stack([_blend_one(result[i], input[i], factor[i]) for i in range(len(factor))])
 
 
-def _blend_one(input1: torch.Tensor, input2: torch.Tensor, factor: torch.Tensor) -> torch.Tensor:
+def _blend_one(input1: Tensor, input2: Tensor, factor: Tensor) -> Tensor:
     r"""Blend two images into one.
 
     Args:
@@ -609,12 +677,12 @@ def _blend_one(input1: torch.Tensor, input2: torch.Tensor, factor: torch.Tensor)
     Returns:
         : image tensor with the batch in the zero position.
     """
-    if not isinstance(input1, torch.Tensor):
+    if not isinstance(input1, Tensor):
         raise AssertionError(f"`input1` must be a tensor. Got {input1}.")
-    if not isinstance(input2, torch.Tensor):
+    if not isinstance(input2, Tensor):
         raise AssertionError(f"`input1` must be a tensor. Got {input2}.")
 
-    if isinstance(factor, torch.Tensor) and len(factor.size()) != 0:
+    if isinstance(factor, Tensor) and len(factor.size()) != 0:
         raise AssertionError(f"Factor shall be a float or single element tensor. Got {factor}.")
     if factor == 0.0:
         return input1
@@ -640,7 +708,7 @@ def _build_lut(histo, step):
 
 
 # Code taken from: https://github.com/pytorch/vision/pull/796
-def _scale_channel(im: torch.Tensor) -> torch.Tensor:
+def _scale_channel(im: Tensor) -> Tensor:
     r"""Scale the data in the channel to implement equalize.
 
     Args:
@@ -652,17 +720,17 @@ def _scale_channel(im: torch.Tensor) -> torch.Tensor:
     min_ = im.min()
     max_ = im.max()
 
-    if min_.item() < 0.0 and not torch.isclose(min_, torch.tensor(0.0, dtype=min_.dtype)):
+    if min_.item() < 0.0 and not torch.isclose(min_, torch.as_tensor(0.0, dtype=min_.dtype)):
         raise ValueError(f"Values in the input tensor must greater or equal to 0.0. Found {min_.item()}.")
 
-    if max_.item() > 1.0 and not torch.isclose(max_, torch.tensor(1.0, dtype=max_.dtype)):
+    if max_.item() > 1.0 and not torch.isclose(max_, torch.as_tensor(1.0, dtype=max_.dtype)):
         raise ValueError(f"Values in the input tensor must lower or equal to 1.0. Found {max_.item()}.")
 
     ndims = len(im.shape)
     if ndims not in (2, 3):
         raise TypeError(f"Input tensor must have 2 or 3 dimensions. Found {ndims}.")
 
-    im = im * 255.
+    im = im * 255.0
     # Compute the histogram of the image channel.
     histo = _torch_histc_cast(im, bins=256, min=0, max=255)
     # For the purposes of computing the step, filter out the nonzeros.
@@ -682,7 +750,7 @@ def _scale_channel(im: torch.Tensor) -> torch.Tensor:
 
 
 @perform_keep_shape_image
-def equalize(input: torch.Tensor) -> torch.Tensor:
+def equalize(input: Tensor) -> Tensor:
     r"""Apply equalize on the input tensor.
 
     .. image:: _static/img/equalize.png
@@ -711,7 +779,7 @@ def equalize(input: torch.Tensor) -> torch.Tensor:
 
 
 @perform_keep_shape_video
-def equalize3d(input: torch.Tensor) -> torch.Tensor:
+def equalize3d(input: Tensor) -> Tensor:
     r"""Equalize the values for a 3D volumetric tensor.
 
     Implements Equalize function for a sequence of images using PyTorch ops based on uint8 format:
@@ -733,7 +801,7 @@ def equalize3d(input: torch.Tensor) -> torch.Tensor:
     return torch.stack(res)
 
 
-def invert(input: torch.Tensor, max_val: torch.Tensor = torch.tensor(1.0)) -> torch.Tensor:
+def invert(input: Tensor, max_val: Tensor = torch.tensor(1.0)) -> Tensor:
     r"""Invert the values of an input tensor by its maximum value.
 
     .. image:: _static/img/invert.png
@@ -749,21 +817,21 @@ def invert(input: torch.Tensor, max_val: torch.Tensor = torch.tensor(1.0)) -> to
         torch.Size([1, 2, 4, 4])
 
         >>> img = 255. * torch.rand(1, 2, 3, 4, 4)
-        >>> invert(img, torch.tensor(255.)).shape
+        >>> invert(img, torch.as_tensor(255.).shape
         torch.Size([1, 2, 3, 4, 4])
 
         >>> img = torch.rand(1, 3, 4, 4)
-        >>> invert(img, torch.tensor([[[[1.]]]])).shape
+        >>> invert(img, torch.as_tensor([[[[1.]]]])).shape
         torch.Size([1, 3, 4, 4])
     """
-    if not isinstance(input, torch.Tensor):
-        raise AssertionError(f"Input is not a torch.Tensor. Got: {type(input)}")
-    if not isinstance(max_val, torch.Tensor):
-        raise AssertionError(f"max_val is not a torch.Tensor. Got: {type(max_val)}")
+    if not isinstance(input, Tensor):
+        raise AssertionError(f"Input is not a Tensor. Got: {type(input)}")
+    if not isinstance(max_val, Tensor):
+        raise AssertionError(f"max_val is not a Tensor. Got: {type(max_val)}")
     return max_val.to(input.dtype) - input
 
 
-class AdjustSaturation(nn.Module):
+class AdjustSaturation(Module):
     r"""Adjust color saturation of an image.
 
     The input image is expected to be an RGB image in the range of [0, 1].
@@ -798,15 +866,15 @@ class AdjustSaturation(nn.Module):
         tensor(0.)
     """
 
-    def __init__(self, saturation_factor: Union[float, torch.Tensor]) -> None:
+    def __init__(self, saturation_factor: Union[float, Tensor]) -> None:
         super().__init__()
-        self.saturation_factor: Union[float, torch.Tensor] = saturation_factor
+        self.saturation_factor: Union[float, Tensor] = saturation_factor
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return adjust_saturation(input, self.saturation_factor)
 
 
-class AdjustHue(nn.Module):
+class AdjustHue(Module):
     r"""Adjust hue of an image.
 
     The input image is expected to be an RGB image in the range of [0, 1].
@@ -842,15 +910,15 @@ class AdjustHue(nn.Module):
         torch.Size([2, 3, 3, 3])
     """
 
-    def __init__(self, hue_factor: Union[float, torch.Tensor]) -> None:
+    def __init__(self, hue_factor: Union[float, Tensor]) -> None:
         super().__init__()
-        self.hue_factor: Union[float, torch.Tensor] = hue_factor
+        self.hue_factor: Union[float, Tensor] = hue_factor
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return adjust_hue(input, self.hue_factor)
 
 
-class AdjustGamma(nn.Module):
+class AdjustGamma(Module):
     r"""Perform gamma correction on an image.
 
     The input image is expected to be in the range of [0, 1].
@@ -879,16 +947,16 @@ class AdjustGamma(nn.Module):
         torch.Size([2, 5, 3, 3])
     """
 
-    def __init__(self, gamma: Union[float, torch.Tensor], gain: Union[float, torch.Tensor] = 1.0) -> None:
+    def __init__(self, gamma: Union[float, Tensor], gain: Union[float, Tensor] = 1.0) -> None:
         super().__init__()
-        self.gamma: Union[float, torch.Tensor] = gamma
-        self.gain: Union[float, torch.Tensor] = gain
+        self.gamma: Union[float, Tensor] = gamma
+        self.gain: Union[float, Tensor] = gain
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return adjust_gamma(input, self.gamma, self.gain)
 
 
-class AdjustContrast(nn.Module):
+class AdjustContrast(Module):
     r"""Adjust Contrast of an image.
 
     This implementation aligns OpenCV, not PIL. Hence, the output differs from TorchVision.
@@ -917,15 +985,15 @@ class AdjustContrast(nn.Module):
         torch.Size([2, 5, 3, 3])
     """
 
-    def __init__(self, contrast_factor: Union[float, torch.Tensor]) -> None:
+    def __init__(self, contrast_factor: Union[float, Tensor]) -> None:
         super().__init__()
-        self.contrast_factor: Union[float, torch.Tensor] = contrast_factor
+        self.contrast_factor: Union[float, Tensor] = contrast_factor
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return adjust_contrast(input, self.contrast_factor)
 
 
-class AdjustBrightness(nn.Module):
+class AdjustBrightness(Module):
     r"""Adjust Brightness of an image.
 
     This implementation aligns OpenCV, not PIL. Hence, the output differs from TorchVision.
@@ -953,15 +1021,15 @@ class AdjustBrightness(nn.Module):
         torch.Size([2, 5, 3, 3])
     """
 
-    def __init__(self, brightness_factor: Union[float, torch.Tensor]) -> None:
+    def __init__(self, brightness_factor: Union[float, Tensor]) -> None:
         super().__init__()
-        self.brightness_factor: Union[float, torch.Tensor] = brightness_factor
+        self.brightness_factor: Union[float, Tensor] = brightness_factor
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return adjust_brightness(input, self.brightness_factor)
 
 
-class Invert(nn.Module):
+class Invert(Module):
     r"""Invert the values of an input tensor by its maximum value.
 
     Args:
@@ -975,20 +1043,20 @@ class Invert(nn.Module):
         torch.Size([1, 2, 4, 4])
 
         >>> img = 255. * torch.rand(1, 2, 3, 4, 4)
-        >>> Invert(torch.tensor(255.))(img).shape
+        >>> Invert(torch.as_tensor(255.))(img).shape
         torch.Size([1, 2, 3, 4, 4])
 
         >>> img = torch.rand(1, 3, 4, 4)
-        >>> Invert(torch.tensor([[[[1.]]]]))(img).shape
+        >>> Invert(torch.as_tensor([[[[1.]]]]))(img).shape
         torch.Size([1, 3, 4, 4])
     """
 
-    def __init__(self, max_val: torch.Tensor = torch.tensor(1.0)) -> None:
+    def __init__(self, max_val: Tensor = torch.tensor(1.0)) -> None:
         super().__init__()
-        if not isinstance(max_val, nn.Parameter):
+        if not isinstance(max_val, Parameter):
             self.register_buffer("max_val", max_val)
         else:
             self.max_val = max_val
 
-    def forward(self, input: torch.Tensor) -> torch.Tensor:
+    def forward(self, input: Tensor) -> Tensor:
         return invert(input, self.max_val)


Test patch:
diff --git a/kornia/testing/__init__.py b/kornia/testing/__init__.py
--- a/kornia/testing/__init__.py
+++ b/kornia/testing/__init__.py
@@ -4,7 +4,7 @@
 from abc import ABC, abstractmethod
 from copy import deepcopy
 from itertools import product
-from typing import Any, Optional
+from typing import Any, Iterable, Optional, Tuple, Type, TypeVar, Union, cast
 
 import torch
 from torch import Tensor
@@ -185,6 +185,11 @@ def assert_close(
 # Logger api
 
 
+def KORNIA_CHECK(condition, msg: Optional[str] = None):
+    if not condition:
+        raise Exception(f"{condition} not true.\n{msg}")
+
+
 def KORNIA_CHECK_IS_TENSOR(x, msg: Optional[str] = None):
     if not isinstance(x, Tensor):
         raise TypeError(f"Not a Tensor type. Got: {type(x)}.\n{msg}")
diff --git a/test/enhance/test_adjust.py b/test/enhance/test_adjust.py
--- a/test/enhance/test_adjust.py
+++ b/test/enhance/test_adjust.py
@@ -391,15 +391,7 @@ def test_gradcheck(self, device, dtype):
         img = tensor_to_gradcheck_var(img)  # to var
         assert gradcheck(kornia.enhance.adjust_contrast, (img, 2.0), raise_exception=True)
 
-
-class TestAdjustBrightness:
-    @pytest.mark.parametrize("shape", [(3, 4, 4), (2, 3, 3, 3), (4, 3, 3, 1, 1)])
-    def test_cardinality(self, device, dtype, shape):
-        img = torch.rand(shape, device=device, dtype=dtype)
-        out = kornia.enhance.adjust_brightness(img, 1.0)
-        assert out.shape == shape
-
-    def test_factor_zero(self, device, dtype):
+    def test_factor_one_rgb(self, device, dtype):
         # prepare input data
         data = torch.tensor(
             [[[1.0, 1.0], [1.0, 1.0]], [[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]]],
@@ -407,91 +399,98 @@ def test_factor_zero(self, device, dtype):
             dtype=dtype,
         )  # 3x2x2
 
-        expected = data.clone()
-
-        f = kornia.enhance.AdjustBrightness(0.0)
-        assert_close(f(data), expected)
-
-    def test_factor_one(self, device, dtype):
-        # prepare input data
-        data = torch.tensor(
+        expected = torch.tensor(
             [[[1.0, 1.0], [1.0, 1.0]], [[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]]],
             device=device,
             dtype=dtype,
         )  # 3x2x2
 
-        expected = torch.ones_like(data)
-
-        f = kornia.enhance.AdjustBrightness(1.0)
-        assert_close(f(data), expected)
+        f = kornia.enhance.adjust_contrast_with_mean_subtraction
+        assert_close(f(data, 1.0), expected)
 
-    def test_factor_minus(self, device, dtype):
+    def test_factor_zero_rgb(self, device, dtype):
         # prepare input data
         data = torch.tensor(
-            [[[1.0, 1.0], [1.0, 1.0]], [[0.75, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.25, 0.25]]],
+            [[[1.0, 1.0], [1.0, 1.0]], [[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]]],
             device=device,
             dtype=dtype,
         )  # 3x2x2
 
         expected = torch.tensor(
-            [[[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]], [[0.0, 0.0], [0.0, 0.0]]],
+            [
+                [[0.6210, 0.6210], [0.6210, 0.6210]],
+                [[0.6210, 0.6210], [0.6210, 0.6210]],
+                [[0.6210, 0.6210], [0.6210, 0.6210]],
+            ],
             device=device,
             dtype=dtype,
         )  # 3x2x2
 
-        f = kornia.enhance.AdjustBrightness(-0.5)
-        assert_close(f(data), expected)
+        f = kornia.enhance.adjust_contrast_with_mean_subtraction
+        assert_close(f(data, 0.0), expected)
 
-    def test_factor_tensor(self, device, dtype):
+    def test_factor_zero_nchannels(self, device, dtype):
         # prepare input data
         data = torch.tensor(
             [
                 [[1.0, 1.0], [1.0, 1.0]],
                 [[0.5, 0.5], [0.5, 0.5]],
                 [[0.25, 0.25], [0.25, 0.25]],
-                [[0.5, 0.5], [0.5, 0.5]],
+                [[0.25, 0.25], [0.25, 0.25]],
             ],
             device=device,
             dtype=dtype,
-        )  # 4x2x2
+        )  # 3x2x2
 
-        factor = torch.tensor([0, 0.5, 0.75, 2], device=device, dtype=dtype)
+        expected = torch.tensor(
+            [[[0.5, 0.5], [0.5, 0.5]], [[0.5, 0.5], [0.5, 0.5]], [[0.5, 0.5], [0.5, 0.5]], [[0.5, 0.5], [0.5, 0.5]]],
+            device=device,
+            dtype=dtype,
+        )  # 3x2x2
 
-        expected = torch.ones_like(data)
+        f = kornia.enhance.adjust_contrast_with_mean_subtraction
+        assert_close(f(data, 0.0), expected)
 
-        f = kornia.enhance.AdjustBrightness(factor)
-        assert_close(f(data), expected)
 
-    def test_factor_tensor_color(self, device, dtype):
+class TestAdjustBrightness:
+    @pytest.mark.parametrize("shape", [(3, 4, 4), (2, 3, 3, 3), (4, 3, 3, 1, 1)])
+    def test_cardinality(self, device, dtype, shape):
+        img = torch.rand(shape, device=device, dtype=dtype)
+        out = kornia.enhance.adjust_brightness(img, 1.0)
+        assert out.shape == shape
+
+    def test_factor_zero(self, device, dtype):
         # prepare input data
         data = torch.tensor(
-            [
-                [[[1.0, 1.0], [1.0, 1.0]], [[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]]],
-                [[[0.0, 0.0], [0.0, 0.0]], [[0.3, 0.3], [0.3, 0.3]], [[0.6, 0.6], [0.6, 0.6]]],
-            ],
+            [[[1.0, 1.0], [1.0, 1.0]], [[0.5, 0.5], [0.5, 0.5]], [[0.25, 0.25], [0.25, 0.25]]],
             device=device,
             dtype=dtype,
-        )  # 2x3x2x2
+        )  # 3x2x2
 
-        expected = torch.tensor(
-            [
-                [[[1.0, 1.0], [1.0, 1.0]], [[0.75, 0.75], [0.75, 0.75]], [[0.5, 0.5], [0.5, 0.5]]],
-                [[[0.1, 0.1], [0.1, 0.1]], [[0.4, 0.4], [0.4, 0.4]], [[0.7, 0.7], [0.7, 0.7]]],
-            ],
-            device=device,
-            dtype=dtype,
-        )  # 2x3x2x2
+        f = kornia.enhance.AdjustBrightness(0.0)
+        assert_close(f(data), data)
 
-        factor = torch.tensor([0.25, 0.1], device=device, dtype=dtype)
+    def test_factor_saturat(self, device, dtype):
+        # prepare input data
+        data = 0.5 * torch.ones(1, 4, 3, 2, device=device, dtype=dtype)
+        ones = torch.ones_like(data)
 
-        f = kornia.enhance.AdjustBrightness(factor)
-        assert_close(f(data), expected)
+        f = kornia.enhance.AdjustBrightness(0.6)
+        assert_close(f(data), ones)
+
+    @pytest.mark.parametrize("channels", [1, 4, 5])
+    def test_factor_tensor(self, device, dtype, channels):
+        # prepare input data
+        data = torch.ones(channels, 2, 3, device=device, dtype=dtype)  # Cx2x3
+        factor = torch.arange(0, 1, channels, device=device, dtype=dtype)
+        out = kornia.enhance.adjust_brightness(data, factor)
+        assert out.shape == (channels, 2, 3)
 
     def test_gradcheck(self, device, dtype):
         batch_size, channels, height, width = 2, 3, 4, 5
-        img = torch.ones(batch_size, channels, height, width, device=device, dtype=dtype)
+        img = torch.rand(batch_size, channels, height, width, device=device, dtype=dtype)
         img = tensor_to_gradcheck_var(img)  # to var
-        assert gradcheck(kornia.enhance.adjust_brightness, (img, 2.0), raise_exception=True)
+        assert gradcheck(kornia.enhance.adjust_brightness, (img, 1.0), raise_exception=True)
 
 
 class TestEqualize: