Repository: kornia/kornia

Issue:
[Feature] Add padding equal to None option for SSIM to match the original MATLAB implementation
## 🚀 Feature
Add padding equal to None option for SSIM  to match the original MATLAB implementation

## Motivation
I am comparing `kornia` and `skimage` for computing SSIM. I find that there is always a slight difference between the two results. I read the code and found that the differences come from `kornia` using the reflect padding in the gaussian weighting function to keep the size of the image the same, while `skimage` doesn't use padding here. I also check [the MATLAB implementation](http://www.cns.nyu.edu/~lcv/ssim/ssim_index.m) of SSIM from the original authors, they also don't use padding. 

When I remove the reflect padding in `kornia`, the SSIM would return a more similar result with `skimage`.  So I think it would be better if we can add an option in the SSIM in `kornia` to be able to remove padding. 

The related colab link is as following: https://colab.research.google.com/drive/1JCDQn_OWbm52hyDadKiU1UpwB3glGY5f?usp=sharing

A lot of thanks for this wonderful project!




Discussion:
@wendy-xiaozong thanks for reporting this. I believe we need to modify this part of the code in `filter2d`
https://github.com/kornia/kornia/blob/f56eec6a8f2dddfbb9a918f979a9f7e83b289ec1/kornia/filters/filter.py#L96-L98

Any thoughts here @ducha-aiki ? possibly we need to add a `apply_padding` param
I believe, that can be done a bit simpler - pass padding type to the SSIM and then crop out the part of "valid" convolution. 
@ducha-aiki @edgarriba If no one is working on this right now, I would be happy to implement this by adding an option to crop out the part of "valid", and set the default option as `False` for that.
Definitely-go ahead

Resolving patch:
diff --git a/kornia/losses/ssim.py b/kornia/losses/ssim.py
--- a/kornia/losses/ssim.py
+++ b/kornia/losses/ssim.py
@@ -11,6 +11,7 @@ def ssim_loss(
     max_val: float = 1.0,
     eps: float = 1e-12,
     reduction: str = 'mean',
+    padding: str = 'same',
 ) -> torch.Tensor:
     r"""Function that computes a loss based on the SSIM measurement.
 
@@ -32,6 +33,8 @@ def ssim_loss(
          output: ``'none'`` | ``'mean'`` | ``'sum'``. ``'none'``: no reduction will be applied,
          ``'mean'``: the sum of the output will be divided by the number of elements
          in the output, ``'sum'``: the output will be summed.
+        padding: ``'same'`` | ``'valid'``. Whether to only use the "valid" convolution
+         area to compute SSIM to match the MATLAB implementation of original SSIM paper.
 
     Returns:
         The loss based on the ssim index.
@@ -42,7 +45,7 @@ def ssim_loss(
         >>> loss = ssim_loss(input1, input2, 5)
     """
     # compute the ssim map
-    ssim_map: torch.Tensor = metrics.ssim(img1, img2, window_size, max_val, eps)
+    ssim_map: torch.Tensor = metrics.ssim(img1, img2, window_size, max_val, eps, padding)
 
     # compute and reduce the loss
     loss = torch.clamp((1.0 - ssim_map) / 2, min=0, max=1)
@@ -75,6 +78,8 @@ class SSIMLoss(nn.Module):
          output: ``'none'`` | ``'mean'`` | ``'sum'``. ``'none'``: no reduction will be applied,
          ``'mean'``: the sum of the output will be divided by the number of elements
          in the output, ``'sum'``: the output will be summed.
+        padding: ``'same'`` | ``'valid'``. Whether to only use the "valid" convolution
+         area to compute SSIM to match the MATLAB implementation of original SSIM paper.
 
     Returns:
         The loss based on the ssim index.
@@ -86,12 +91,15 @@ class SSIMLoss(nn.Module):
         >>> loss = criterion(input1, input2)
     """
 
-    def __init__(self, window_size: int, max_val: float = 1.0, eps: float = 1e-12, reduction: str = 'mean') -> None:
+    def __init__(
+        self, window_size: int, max_val: float = 1.0, eps: float = 1e-12, reduction: str = 'mean', padding: str = 'same'
+    ) -> None:
         super().__init__()
         self.window_size: int = window_size
         self.max_val: float = max_val
         self.eps: float = eps
         self.reduction: str = reduction
+        self.padding: str = padding
 
     def forward(self, img1: torch.Tensor, img2: torch.Tensor) -> torch.Tensor:
-        return ssim_loss(img1, img2, self.window_size, self.max_val, self.eps, self.reduction)
+        return ssim_loss(img1, img2, self.window_size, self.max_val, self.eps, self.reduction, self.padding)
diff --git a/kornia/metrics/ssim.py b/kornia/metrics/ssim.py
--- a/kornia/metrics/ssim.py
+++ b/kornia/metrics/ssim.py
@@ -1,11 +1,26 @@
+from typing import List
+
 import torch
 import torch.nn as nn
 
 from kornia.filters import filter2d, get_gaussian_kernel2d
+from kornia.filters.filter import _compute_padding
+
+
+def _crop(img: torch.Tensor, cropping_shape: List[int]) -> torch.Tensor:
+    """Crop out the part of "valid" convolution area."""
+    return torch.nn.functional.pad(
+        img, (-cropping_shape[2], -cropping_shape[3], -cropping_shape[0], -cropping_shape[1])
+    )
 
 
 def ssim(
-    img1: torch.Tensor, img2: torch.Tensor, window_size: int, max_val: float = 1.0, eps: float = 1e-12
+    img1: torch.Tensor,
+    img2: torch.Tensor,
+    window_size: int,
+    max_val: float = 1.0,
+    eps: float = 1e-12,
+    padding: str = 'same',
 ) -> torch.Tensor:
     r"""Function that computes the Structural Similarity (SSIM) index map between two images.
 
@@ -30,6 +45,8 @@ def ssim(
         window_size: the size of the gaussian kernel to smooth the images.
         max_val: the dynamic range of the images.
         eps: Small value for numerically stability when dividing.
+        padding: ``'same'`` | ``'valid'``. Whether to only use the "valid" convolution
+         area to compute SSIM to match the MATLAB implementation of original SSIM paper.
 
     Returns:
        The ssim index map with shape :math:`(B, C, H, W)`.
@@ -68,14 +85,34 @@ def ssim(
     mu1: torch.Tensor = filter2d(img1, kernel)
     mu2: torch.Tensor = filter2d(img2, kernel)
 
+    cropping_shape: List[int] = []
+    if padding == 'valid':
+        height, width = kernel.shape[-2:]
+        cropping_shape = _compute_padding([height, width])
+        mu1 = _crop(mu1, cropping_shape)
+        mu2 = _crop(mu2, cropping_shape)
+    elif padding == 'same':
+        pass
+
     mu1_sq = mu1 ** 2
     mu2_sq = mu2 ** 2
     mu1_mu2 = mu1 * mu2
 
+    mu_img1_sq = filter2d(img1 ** 2, kernel)
+    mu_img2_sq = filter2d(img2 ** 2, kernel)
+    mu_img1_img2 = filter2d(img1 * img2, kernel)
+
+    if padding == 'valid':
+        mu_img1_sq = _crop(mu_img1_sq, cropping_shape)
+        mu_img2_sq = _crop(mu_img2_sq, cropping_shape)
+        mu_img1_img2 = _crop(mu_img1_img2, cropping_shape)
+    elif padding == 'same':
+        pass
+
     # compute local sigma per channel
-    sigma1_sq = filter2d(img1 ** 2, kernel) - mu1_sq
-    sigma2_sq = filter2d(img2 ** 2, kernel) - mu2_sq
-    sigma12 = filter2d(img1 * img2, kernel) - mu1_mu2
+    sigma1_sq = mu_img1_sq - mu1_sq
+    sigma2_sq = mu_img2_sq - mu2_sq
+    sigma12 = mu_img1_img2 - mu1_mu2
 
     # compute the similarity index map
     num: torch.Tensor = (2.0 * mu1_mu2 + C1) * (2.0 * sigma12 + C2)
@@ -106,6 +143,8 @@ class SSIM(nn.Module):
         window_size: the size of the gaussian kernel to smooth the images.
         max_val: the dynamic range of the images.
         eps: Small value for numerically stability when dividing.
+        padding: ``'same'`` | ``'valid'``. Whether to only use the "valid" convolution
+         area to compute SSIM to match the MATLAB implementation of original SSIM paper.
 
     Shape:
         - Input: :math:`(B, C, H, W)`.
@@ -119,11 +158,12 @@ class SSIM(nn.Module):
         >>> ssim_map = ssim(input1, input2)  # 1x4x5x5
     """
 
-    def __init__(self, window_size: int, max_val: float = 1.0, eps: float = 1e-12) -> None:
+    def __init__(self, window_size: int, max_val: float = 1.0, eps: float = 1e-12, padding: str = 'same') -> None:
         super().__init__()
         self.window_size: int = window_size
         self.max_val: float = max_val
         self.eps = eps
+        self.padding = padding
 
     def forward(self, img1: torch.Tensor, img2: torch.Tensor) -> torch.Tensor:
-        return ssim(img1, img2, self.window_size, self.max_val, self.eps)
+        return ssim(img1, img2, self.window_size, self.max_val, self.eps, self.padding)


Test patch: