Repository: kornia/kornia

Issue:
Add `adjust_sigmoid` to kornia.enhance
We would like to have a consistent functionality against scitkit-image `adjust_sigmoid`

See: https://scikit-image.org/docs/stable/api/skimage.exposure.html#adjust-sigmoid

## Motivation

Useful for image manipulation and  to compose complex augmentations 

______________________________________________________________________

#### Consider also to contribute to Kornia universe projects :)

<sub>

- [**Tutorials**](https://github.com/kornia/tutorials): our repository containing the tutorials.

</sub>



Discussion:
Whether already exists such implementation or it should be created from scratch? Can you point to module where to add such implementation?
@dobosevych the idea is to write from scratch following scimitar implementation and place it here https://github.com/kornia/kornia/blob/master/kornia/enhance/adjust.py
@edgarriba 
I would like to make a contribuion. All I need to do is add the function, add the docstring for the function, and add the test? I couldn't figure out how to add an image, do I need to add an image for an example?
@Beluuuuuuga you are right -- adding the images to docs is optional but recommended (if we want to keep a healthy documentation). We do that through the following python script: https://github.com/kornia/kornia/blob/master/docs/generate_example_images.py (it's a bit messy -- open to make it better)
I made some tests and plan to do PR on Saturday
https://colab.research.google.com/drive/1OZhHLoJri2wStYnXoFwWSupsT1--m4Ks?authuser=1#scrollTo=Ka7IamUGEdKJ

Resolving patch:
diff --git a/kornia/enhance/__init__.py b/kornia/enhance/__init__.py
--- a/kornia/enhance/__init__.py
+++ b/kornia/enhance/__init__.py
@@ -5,8 +5,10 @@
     AdjustContrastWithMeanSubtraction,
     AdjustGamma,
     AdjustHue,
+    AdjustLog,
     AdjustSaturation,
     AdjustSaturationWithGraySubtraction,
+    AdjustSigmoid,
     Invert,
     adjust_brightness,
     adjust_brightness_accumulative,
@@ -15,9 +17,11 @@
     adjust_gamma,
     adjust_hue,
     adjust_hue_raw,
+    adjust_log,
     adjust_saturation,
     adjust_saturation_raw,
     adjust_saturation_with_gray_subtraction,
+    adjust_sigmoid,
     equalize,
     equalize3d,
     invert,
@@ -42,6 +46,8 @@
     "adjust_saturation_with_gray_subtraction",
     "adjust_hue_raw",
     "adjust_saturation_raw",
+    "adjust_sigmoid",
+    "adjust_log",
     "solarize",
     "equalize",
     "equalize3d",
@@ -56,6 +62,8 @@
     "AdjustHue",
     "AdjustSaturation",
     "AdjustSaturationWithGraySubtraction",
+    "AdjustSigmoid",
+    "AdjustLog",
     "Invert",
     "add_weighted",
     "AddWeighted",
diff --git a/kornia/enhance/adjust.py b/kornia/enhance/adjust.py
--- a/kornia/enhance/adjust.py
+++ b/kornia/enhance/adjust.py
@@ -529,6 +529,76 @@ def adjust_brightness_accumulative(image: Tensor, factor: Union[float, Tensor],
     return img_adjust
 
 
+def adjust_sigmoid(image: Tensor, cutoff: float = 0.5, gain: float = 10, inv: bool = False) -> Tensor:
+    """Adjust sigmoid correction on the input image tensor.
+
+    The input image is expected to be in the range of [0, 1].
+
+    Reference:
+    [1]: Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast Enhancement Functions",
+        http://markfairchild.org/PDFs/PAP07.pdf
+
+    Args:
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        cutoff: The cutoff of sigmoid function.
+        gain: The multiplier of sigmoid function.
+        inv: If is set to True the function will return the inverse sigmoid correction.
+
+    Returns:
+         Adjusted tensor in the shape of :math:`(*, H, W)`.
+
+    Example:
+        >>> x = torch.ones(1, 1, 2, 2)
+        >>> adjust_sigmoid(x, gain=0)
+        tensor([[[[0.5000, 0.5000],
+                  [0.5000, 0.5000]]]])
+    """
+    KORNIA_CHECK_IS_TENSOR(image, "Expected shape (*, H, W)")
+
+    if inv:
+        img_adjust = (1 - 1 / (1 + (gain * (cutoff - image)).exp()))
+    else:
+        img_adjust = (1 / (1 + (gain * (cutoff - image)).exp()))
+    return img_adjust
+
+
+def adjust_log(image: Tensor, gain: float = 1, inv: bool = False, clip_output: bool = True) -> Tensor:
+    """Adjust log correction on the input image tensor.
+
+    The input image is expected to be in the range of [0, 1].
+
+    Reference:
+    [1]: http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf
+
+    Args:
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        gain: The multiplier of logarithmic function.
+        inv:  If is set to True the function will return the inverse logarithmic correction.
+        clip_output: Whether to clip the output image with range of [0, 1].
+
+    Returns:
+        Adjusted tensor in the shape of :math:`(*, H, W)`.
+
+    Example:
+        >>> x = torch.zeros(1, 1, 2, 2)
+        >>> adjust_log(x, inv=True)
+        tensor([[[[0., 0.],
+                  [0., 0.]]]])
+    """
+    KORNIA_CHECK_IS_TENSOR(image, "Expected shape (*, H, W)")
+
+    if inv:
+        img_adjust = (2 ** image - 1) * gain
+    else:
+        img_adjust = (1 + image).log2() * gain
+
+    # truncate between pixel values
+    if clip_output:
+        img_adjust = img_adjust.clamp(min=0.0, max=1.0)
+
+    return img_adjust
+
+
 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.
@@ -1230,6 +1300,79 @@ def forward(self, input: Tensor) -> Tensor:
         return adjust_brightness(input, self.brightness_factor)
 
 
+class AdjustSigmoid(Module):
+    r"""Adjust the contrast of an image tensor or performs sigmoid correction on the input image tensor.
+
+    The input image is expected to be in the range of [0, 1].
+
+    Reference:
+    [1]: Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast Enhancement Functions",
+        http://markfairchild.org/PDFs/PAP07.pdf
+
+    Args:
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        cutoff: The cutoff of sigmoid function.
+        gain: The multiplier of sigmoid function.
+        inv: If is set to True the function will return the negative sigmoid correction.
+
+    Example:
+        >>> x = torch.ones(1, 1, 2, 2)
+        >>> AdjustSigmoid(gain=0)(x)
+        tensor([[[[0.5000, 0.5000],
+                  [0.5000, 0.5000]]]])
+    """
+
+    def __init__(
+        self,
+        cutoff: float = 0.5,
+        gain: float = 10,
+        inv: bool = False,
+    ) -> None:
+        super().__init__()
+        self.cutoff: float = cutoff
+        self.gain: float = gain
+        self.inv: bool = inv
+
+    def forward(self, image: Tensor) -> Tensor:
+        return adjust_sigmoid(image, cutoff=self.cutoff, gain=self.gain, inv=self.inv)
+
+
+class AdjustLog(Module):
+    """Adjust log correction on the input image tensor.
+
+    The input image is expected to be in the range of [0, 1].
+
+    Reference:
+    [1]: http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf
+
+    Args:
+        image: Image to be adjusted in the shape of :math:`(*, H, W)`.
+        gain: The multiplier of logarithmic function.
+        inv:  If is set to True the function will return the inverse logarithmic correction.
+        clip_output: Whether to clip the output image with range of [0, 1].
+
+    Example:
+        >>> x = torch.zeros(1, 1, 2, 2)
+        >>> AdjustLog(inv=True)(x)
+        tensor([[[[0., 0.],
+                  [0., 0.]]]])
+    """
+
+    def __init__(
+        self,
+        gain: float = 1,
+        inv: bool = False,
+        clip_output: bool = True,
+    ) -> None:
+        super().__init__()
+        self.gain: float = gain
+        self.inv: bool = inv
+        self.clip_output: bool = clip_output
+
+    def forward(self, image: Tensor) -> Tensor:
+        return adjust_log(image, gain=self.gain, inv=self.inv, clip_output=self.clip_output)
+
+
 class AdjustBrightnessAccumulative(Module):
     r"""Adjust Brightness of an image accumulatively.
 


Test patch:
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
@@ -637,6 +637,102 @@ def test_gradcheck_accumulative(self, device, dtype):
                          raise_exception=True)
 
 
+class TestAdjustSigmoid:
+    @pytest.mark.parametrize("shape", [(3, 4, 4), (2, 3, 4, 4)])
+    def test_shape_sigmoid(self, shape, device):
+        inputs = torch.ones(*shape, device=device)
+        f = kornia.enhance.adjust_sigmoid
+        assert f(inputs).shape == torch.Size(shape)
+
+    def test_sigmoid(self, device, dtype):
+        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]]],
+            ],
+            device=device,
+            dtype=dtype,
+        )  # 2x3x2x2
+
+        expected = torch.tensor(
+            [
+                [[[0.99330715, 0.99330715], [0.99330715, 0.99330715]], [[0.5, 0.5], [0.5, 0.5]],
+                 [[0.07585818, 0.07585818], [0.07585818, 0.07585818]]],
+                [[[0.00669285, 0.00669285], [0.00669285, 0.00669285]],
+                 [[0.11920292, 0.11920292], [0.11920292, 0.11920292]],
+                 [[0.73105858, 0.73105858], [0.73105858, 0.73105858]]],
+            ],
+            device=device,
+            dtype=dtype,
+        )  # 2x3x2x2
+
+        f = kornia.enhance.AdjustSigmoid()
+        assert_close(f(data), expected)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        op = kornia.enhance.adjust_sigmoid
+        op_jit = torch.jit.script(op)
+        assert_close(op(img), op_jit(img))
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        bs, channels, height, width = 1, 2, 3, 3
+        inputs = torch.ones(bs, channels, height, width, device=device, dtype=dtype)
+        inputs = tensor_to_gradcheck_var(inputs)
+        assert gradcheck(kornia.enhance.adjust_sigmoid, inputs, raise_exception=True)
+
+
+class TestAdjustLog:
+    @pytest.mark.parametrize("shape", [(3, 4, 4), (2, 3, 4, 4)])
+    def test_shape_sigmoid(self, shape, device):
+        inputs = torch.ones(*shape, device=device)
+        f = kornia.enhance.adjust_log
+
+        assert f(inputs).shape == torch.Size(shape)
+
+    def test_log(self, device, dtype):
+        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]]],
+            ],
+            device=device,
+            dtype=dtype,
+        )  # 2x3x2x2
+
+        expected = torch.tensor(
+            [
+                [[[1, 1], [1, 1]], [[0.5849625, 0.5849625], [0.5849625, 0.5849625]],
+                 [[0.32192809, 0.32192809], [0.32192809, 0.32192809]]],
+                [[[0, 0], [0, 0]], [[0.37851162, 0.37851162], [0.37851162, 0.37851162]],
+                 [[0.67807191, 0.67807191], [0.67807191, 0.67807191]]],
+            ],
+            device=device,
+            dtype=dtype,
+        )  # 2x3x2x2
+
+        f = kornia.enhance.AdjustLog()
+        assert_close(f(data), expected)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        op = kornia.enhance.adjust_log
+        op_jit = torch.jit.script(op)
+        assert_close(op(img), op_jit(img))
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        bs, channels, height, width = 1, 2, 3, 3
+        inputs = torch.ones(bs, channels, height, width, device=device, dtype=dtype)
+        inputs = tensor_to_gradcheck_var(inputs)
+        assert gradcheck(kornia.enhance.adjust_log, (inputs, 0.1), raise_exception=True)
+
+
 class TestEqualize:
     @pytest.mark.parametrize("shape", [(3, 4, 4), (2, 3, 4, 4), (3, 2, 3, 3, 4, 4)])
     def test_shape_equalize(self, shape, device, dtype):