Repository: kornia/kornia

Issue:
Implement sepia filter
## 🚀 Feature

Add a sepia filter operator in `kornia.filters`

resource: https://stackoverflow.com/questions/23802725/using-numpy-to-apply-a-sepia-effect-to-a-3d-array

______________________________________________________________________

#### Consider also to contribute to Kornia universe projects :)

<sub>

- [**Tutorials**](https://github.com/kornia/tutorials): our repository containing the tutorials.

</sub>



Discussion:
Hi, I can work on this 😄 

Resolving patch:
diff --git a/kornia/color/__init__.py b/kornia/color/__init__.py
--- a/kornia/color/__init__.py
+++ b/kornia/color/__init__.py
@@ -22,6 +22,7 @@
     rgba_to_bgr,
     rgba_to_rgb,
 )
+from .sepia import Sepia, sepia_from_rgb
 from .xyz import RgbToXyz, XyzToRgb, rgb_to_xyz, xyz_to_rgb
 from .ycbcr import RgbToYcbcr, YcbcrToRgb, rgb_to_y, rgb_to_ycbcr, ycbcr_to_rgb
 from .yuv import (
@@ -104,4 +105,8 @@
     "RgbaToRgb",
     "RgbaToBgr",
     "RgbToLinearRgb",
+    "Sepia",
+    "sepia",
 ]
+
+sepia = sepia_from_rgb
diff --git a/kornia/color/sepia.py b/kornia/color/sepia.py
new file mode 100644
--- /dev/null
+++ b/kornia/color/sepia.py
@@ -0,0 +1,79 @@
+import torch
+
+from kornia.core import Module, Tensor
+
+
+def sepia_from_rgb(input: Tensor, rescale: bool = True, eps: float = 1e-6) -> Tensor:
+    r"""Apply to a tensor the sepia filter.
+
+    Args:
+        input: the input tensor with shape of :math:`(*, C, H, W)`.
+        rescale: If True, the output tensor will be rescaled (max values be 1. or 255).
+        eps: scalar to enforce numerical stability.
+
+    Returns:
+        Tensor: The sepia tensor of same size and numbers of channels
+        as the input with shape :math:`(*, C, H, W)`.
+
+    Example:
+        >>> input = torch.ones(3, 1, 1)
+        >>> sepia_from_rgb(input, rescale=False)
+        tensor([[[1.3510]],
+        <BLANKLINE>
+                [[1.2030]],
+        <BLANKLINE>
+                [[0.9370]]])
+    """
+    if len(input.shape) < 3 or input.shape[-3] != 3:
+        raise ValueError(f"Input size must have a shape of (*, 3, H, W). Got {input.shape}")
+
+    r = input[..., 0, :, :]
+    g = input[..., 1, :, :]
+    b = input[..., 2, :, :]
+
+    r_out = 0.393 * r + 0.769 * g + 0.189 * b
+    g_out = 0.349 * r + 0.686 * g + 0.168 * b
+    b_out = 0.272 * r + 0.534 * g + 0.131 * b
+
+    sepia_out = torch.stack([r_out, g_out, b_out], dim=-3)
+
+    if rescale:
+        max_values = sepia_out.amax(dim=-1).amax(dim=-1)
+        sepia_out = sepia_out / (max_values[..., None, None] + eps)
+
+    return sepia_out
+
+
+class Sepia(Module):
+    r"""Module that apply the sepia filter to tensors.
+
+    Args:
+        input: the input tensor with shape of :math:`(*, C, H, W)`.
+        rescale: If True, the output tensor will be rescaled (max values be 1. or 255).
+        eps: scalar to enforce numerical stability.
+
+    Returns:
+        Tensor: The sepia tensor of same size and numbers of channels
+        as the input with shape :math:`(*, C, H, W)`.
+
+    Example:
+        >>>
+        >>> input = torch.ones(3, 1, 1)
+        >>> Sepia(rescale=False)(input)
+        tensor([[[1.3510]],
+        <BLANKLINE>
+                [[1.2030]],
+        <BLANKLINE>
+                [[0.9370]]])
+    """
+
+    def __init__(self, rescale: bool = True, eps: float = 1e-6) -> None:
+        self.rescale = rescale
+        self.eps = eps
+        super().__init__()
+
+    def __repr__(self) -> str:
+        return self.__class__.__name__ + f'(rescale={self.rescale}, eps={self.eps})'
+
+    def forward(self, input: Tensor) -> Tensor:
+        return sepia_from_rgb(input, rescale=self.rescale, eps=self.eps)


Test patch:
diff --git a/test/color/test_sepia.py b/test/color/test_sepia.py
new file mode 100644
--- /dev/null
+++ b/test/color/test_sepia.py
@@ -0,0 +1,83 @@
+import pytest
+import torch
+from torch.autograd import gradcheck
+
+import kornia
+import kornia.testing as utils
+from kornia.testing import BaseTester
+
+
+class TestSepia(BaseTester):
+    def test_smoke(self, device, dtype):
+        input_tensor = torch.tensor(
+            [[[0.1, 1.0], [0.2, 0.1]], [[0.1, 0.8], [0.2, 0.5]], [[0.1, 0.3], [0.2, 0.8]]], device=device, dtype=dtype
+        )
+
+        # With rescale
+        expected_tensor = torch.tensor(
+            [[[0.1269, 1.0], [0.2537, 0.5400]], [[0.1269, 1.0], [0.2537, 0.5403]], [[0.1269, 1.0], [0.2538, 0.5403]]],
+            device=device,
+            dtype=dtype,
+        )
+        actual = kornia.color.sepia(input_tensor, rescale=True)
+
+        assert actual.shape[:] == (3, 2, 2)
+        self.assert_close(actual, expected_tensor, rtol=1e-2, atol=1e-2)
+
+        # Without rescale
+        expected_tensor = torch.tensor(
+            [
+                [[0.1351, 1.0649], [0.2702, 0.5750]],
+                [[0.1203, 0.9482], [0.2406, 0.5123]],
+                [[0.0937, 0.7385], [0.1874, 0.3990]],
+            ],
+            device=device,
+            dtype=dtype,
+        )
+
+        actual = kornia.color.sepia(input_tensor, rescale=False)
+        assert actual.shape[:] == (3, 2, 2)
+        self.assert_close(actual, expected_tensor, rtol=1e-2, atol=1e-2)
+
+    def test_exception(self, device, dtype):
+        with pytest.raises(ValueError):
+            kornia.color.sepia(torch.rand(size=(4, 1, 1), dtype=dtype, device=device))
+
+    @pytest.mark.parametrize("batch_shape", [(1, 3, 8, 15), (2, 3, 11, 7), (3, 8, 15)])
+    def test_cardinality(self, batch_shape, device, dtype):
+        input_tensor = torch.rand(batch_shape, device=device, dtype=dtype)
+        actual = kornia.color.sepia(input_tensor)
+        assert actual.shape == batch_shape
+
+    def test_noncontiguous(self, device, dtype):
+        batch_size = 3
+        inp = torch.rand(3, 5, 5, device=device, dtype=dtype).expand(batch_size, -1, -1, -1)
+
+        actual = kornia.color.sepia(inp)
+
+        assert inp.is_contiguous() is False
+        assert actual.is_contiguous()
+        self.assert_close(actual, actual)
+
+    def test_gradcheck(self, device, dtype):
+        # test parameters
+        batch_shape = (1, 3, 5, 5)
+
+        # evaluate function gradient
+        input_tensor = torch.rand(batch_shape, device=device, dtype=dtype)
+        input_tensor = utils.tensor_to_gradcheck_var(input_tensor)
+        assert gradcheck(kornia.color.sepia, (input_tensor,), raise_exception=True)
+
+    def test_jit(self, device, dtype):
+        op = kornia.color.sepia
+        op_script = torch.jit.script(op)
+
+        img = torch.ones(1, 3, 5, 5, device=device, dtype=dtype)
+        self.assert_close(op(img), op_script(img))
+
+    def test_module(self, device, dtype):
+        op = kornia.color.sepia
+        op_module = kornia.color.Sepia()
+
+        img = torch.ones(1, 3, 5, 5, device=device, dtype=dtype)
+        self.assert_close(op(img), op_module(img))