Repository: kornia/kornia

Issue:
Implement `cv::integral`
## 🚀 Feature

Implement the pytorch version of `cv::integral`

https://docs.opencv.org/4.6.0/d7/d1b/group__imgproc__misc.html#ga97b87bec26908237e8ba0f6e96d23e28

Some reference:
- https://github.com/scikit-image/scikit-image/blob/main/skimage/transform/integral.py
- https://discuss.pytorch.org/t/is-there-an-elegant-way-to-calculate-integral-image-using-pytorch-api/22669/5

______________________________________________________________________

#### Consider also to contribute to Kornia universe projects :)

<sub>

- [**Tutorials**](https://github.com/kornia/tutorials): our repository containing the tutorials.

</sub>



Discussion:
Hello. Some questions regarding this.
1.  Is the expected output a tensor with all values as sum of left values in its row and top values in its column?
2. Which module does it go to?
well check the scikit implementation -- it can be easily implemented with cumsum /cc @ducha-aiki 

```python
def integral_image(image: Tensor) -> Tensor:
    S: Tensor = image
    S = S.cumsum(dim=-1)  # along columns
    S = S.cumsum(dim=-2)  # along rows
    return S
```



Resolving patch:
diff --git a/kornia/enhance/__init__.py b/kornia/enhance/__init__.py
--- a/kornia/enhance/__init__.py
+++ b/kornia/enhance/__init__.py
@@ -32,6 +32,7 @@
 from .core import AddWeighted, add_weighted
 from .equalization import equalize_clahe
 from .histogram import histogram, histogram2d, image_histogram2d
+from .integral import IntegralImage, IntegralTensor, integral_image, integral_tensor
 from .normalize import Denormalize, Normalize, denormalize, normalize, normalize_min_max
 from .shift_rgb import shift_rgb
 from .zca import ZCAWhitening, linear_transform, zca_mean, zca_whiten
@@ -82,4 +83,8 @@
     "zca_whiten",
     "linear_transform",
     "ZCAWhitening",
+    "integral_tensor",
+    "integral_image",
+    "IntegralImage",
+    "IntegralTensor",
 ]
diff --git a/kornia/enhance/integral.py b/kornia/enhance/integral.py
new file mode 100644
--- /dev/null
+++ b/kornia/enhance/integral.py
@@ -0,0 +1,128 @@
+from typing import Optional, Tuple
+
+from kornia.core import Module, Tensor
+from kornia.testing import KORNIA_CHECK, KORNIA_CHECK_SHAPE
+
+
+def integral_tensor(input: Tensor, dim: Optional[Tuple[int, ...]] = None) -> Tensor:
+    """Calculates integral of the input tensor.
+
+    The algorithm computes the integral image by summing over the specified dimensions.
+
+    In case dim is specified, the contained dimensions must be unique and sorted in ascending order
+    and not exceed the number of dimensions of the input tensor.
+
+    Args:
+        input: the input tensor with shape :math:`(*, D)`. Where D is the number of dimensions.
+        dim: the dimension to be summed.
+
+    Returns:
+        Integral tensor for the input tensor with shape :math:`(*, D)`.
+
+    Examples:
+        >>> input = torch.ones(3, 5)
+        >>> output = integral_tensor(input, (-2, -1))
+        >>> output
+        tensor([[ 1.,  2.,  3.,  4.,  5.],
+                [ 2.,  4.,  6.,  8., 10.],
+                [ 3.,  6.,  9., 12., 15.]])
+    """
+    KORNIA_CHECK_SHAPE(input, ["*", "D"])
+
+    if dim is None:
+        dim = (-1,)
+
+    KORNIA_CHECK(len(dim) > 0, "dim must be a non-empty tuple.")
+    KORNIA_CHECK(len(dim) <= len(input.shape), "dim must be a tuple of length <= input.shape.")
+
+    output = input
+    for i in dim:
+        output = output.cumsum(i)
+    return output
+
+
+def integral_image(image: Tensor) -> Tensor:
+    r"""Calculates integral of the input image tensor.
+
+    This particular version sums over the last two dimensions.
+
+    Args:
+        image: the input image tensor with shape :math:`(*, H, W)`.
+
+    Returns:
+        Integral tensor for the input image tensor with shape :math:`(*, H, W)`.
+
+    Examples:
+        >>> input = torch.ones(1, 5, 5)
+        >>> output = integral_image(input)
+        >>> output
+        tensor([[[ 1.,  2.,  3.,  4.,  5.],
+                 [ 2.,  4.,  6.,  8., 10.],
+                 [ 3.,  6.,  9., 12., 15.],
+                 [ 4.,  8., 12., 16., 20.],
+                 [ 5., 10., 15., 20., 25.]]])
+    """
+    KORNIA_CHECK_SHAPE(image, ["*", "H", "W"])
+
+    return integral_tensor(image, (-2, -1))
+
+
+class IntegralTensor(Module):
+    r"""Calculates integral of the input tensor.
+
+    Args:
+        image: the input tensor with shape :math:`(B,C,H,W)`.
+
+    Returns:
+        Integral tensor for the input tensor with shape :math:`(B,C,H,W)`.
+
+    Shape:
+        - Input: :math:`(B, C, H, W)`
+        - Output: :math:`(B, C, H, W)`
+
+    Examples:
+        >>> input = torch.ones(3, 5)
+        >>> dim = (-2, -1)
+        >>> output = IntegralTensor(dim)(input)
+        >>> output
+        tensor([[ 1.,  2.,  3.,  4.,  5.],
+                [ 2.,  4.,  6.,  8., 10.],
+                [ 3.,  6.,  9., 12., 15.]])
+    """
+
+    def __init__(self, dim: Optional[Tuple[int, ...]] = None) -> None:
+        super().__init__()
+        self.dim = dim
+
+    def forward(self, input: Tensor) -> Tensor:
+        return integral_tensor(input, self.dim)
+
+
+class IntegralImage(Module):
+    """Calculates integral of the input image tensor.
+
+    This particular version sums over the last two dimensions.
+
+    Args:
+        image: the input image tensor with shape :math:`(B,C,H,W)`.
+
+    Returns:
+        Integral tensor for the input image tensor with shape :math:`(B,C,H,W)`.
+
+    Shape:
+        - Input: :math:`(B, C, H, W)`
+        - Output: :math:`(B, C, H, W)`
+
+    Examples:
+        >>> input = torch.ones(1, 5, 5)
+        >>> output = IntegralImage()(input)
+        >>> output
+        tensor([[[ 1.,  2.,  3.,  4.,  5.],
+                 [ 2.,  4.,  6.,  8., 10.],
+                 [ 3.,  6.,  9., 12., 15.],
+                 [ 4.,  8., 12., 16., 20.],
+                 [ 5., 10., 15., 20., 25.]]])
+    """
+
+    def forward(self, input: Tensor) -> Tensor:
+        return integral_image(input)


Test patch:
diff --git a/test/enhance/test_integral.py b/test/enhance/test_integral.py
new file mode 100644
--- /dev/null
+++ b/test/enhance/test_integral.py
@@ -0,0 +1,86 @@
+import pytest
+import torch
+
+from kornia.enhance.integral import IntegralImage, IntegralTensor, integral_image, integral_tensor
+from kornia.testing import BaseTester
+
+
+class TestIntegralTensor(BaseTester):
+    def test_smoke(self, device, dtype):
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=dtype)
+        output = integral_tensor(tensor)
+        assert output.shape == (1, 1, 4, 4)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 1)])
+    def test_cardinality(self, device, dtype, shape):
+        tensor = torch.rand(*shape, device=device, dtype=dtype)
+        output = integral_tensor(tensor)
+        assert output.shape == shape
+
+    def test_exception(self, device, dtype):
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=dtype)
+        with pytest.raises(Exception):
+            dim = (0, 1, 2, 3, 4)
+            integral_tensor(tensor, dim)
+        with pytest.raises(Exception):
+            dim = (4, 5)
+            integral_tensor(tensor, dim)
+        with pytest.raises(Exception) as errinfo:
+            integral_tensor(tensor, ())
+        assert 'dim must be a non-empty tuple.' in str(errinfo)
+
+    def test_module(self, device, dtype):
+        mod = IntegralTensor()
+        op = integral_tensor
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=dtype)
+        self.assert_close(mod(tensor), op(tensor))
+
+    def test_gradcheck(self, device):
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=torch.float64, requires_grad=True)
+        self.gradcheck(integral_tensor, (tensor,))
+
+    def test_value(self, device, dtype):
+        tensor = torch.tensor([[[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]]], device=device, dtype=dtype)
+        output = integral_tensor(tensor)
+        expected = torch.tensor([[[[1.0, 3.0, 6.0], [4.0, 9.0, 15.0], [7.0, 15.0, 24.0]]]], device=device, dtype=dtype)
+        self.assert_close(output, expected)
+
+
+class TestIntegralImage(BaseTester):
+    def test_smoke(self, device, dtype):
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=dtype)
+        output = integral_image(tensor)
+        assert output.shape == (1, 1, 4, 4)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 1)])
+    def test_cardinality(self, device, dtype, shape):
+        tensor = torch.rand(*shape, device=device, dtype=dtype)
+        output = integral_image(tensor)
+        assert output.shape == shape
+
+    def test_exception(self, device, dtype):
+        tensor = torch.rand(4, device=device, dtype=dtype)
+        with pytest.raises(Exception):
+            integral_image(tensor)
+
+    def test_module(self, device, dtype):
+        mod = IntegralImage()
+        op = integral_image
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=dtype)
+        self.assert_close(mod(tensor), op(tensor))
+
+    def test_gradcheck(self, device):
+        tensor = torch.rand(1, 1, 4, 4, device=device, dtype=torch.float64, requires_grad=True)
+        self.gradcheck(integral_image, (tensor,))
+
+    def test_values(self, device, dtype):
+        tensor = torch.tensor(
+            [[[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]]], device=device, dtype=dtype
+        )
+        output = integral_image(tensor)
+        expected = torch.tensor(
+            [[[[1, 3, 6, 10], [6.0, 14.0, 24.0, 36.0], [15.0, 33.0, 54.0, 78.0], [28.0, 60.0, 96.0, 136.0]]]],
+            device=device,
+            dtype=dtype,
+        )
+        self.assert_close(output, expected)