Repository: kornia/kornia
Pull request: Add 3D-SSIM loss
Hi team, this PR aims to add SSIM Loss and Metrics for 3D images. Based on the following comment - https://github.com/kornia/kornia/issues/1158#issuecomment-880569245 and the Monai Implementation. Will have to skim through the paper once more to follow up on this - https://github.com/kornia/kornia/issues/1158#issuecomment-880605633

Also, I just checked, the failing doctests are because of the recent changes in filters for gaussian kernels. Will make changes as per the new format.

Fixes #1158 


#### Type of change
<!-- Please delete options that are not relevant. -->
- [ ] 📚  Documentation Update
- [ ] 🧪 Tests Cases
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
- [x] 🔬 New feature (non-breaking change which adds functionality)
- [ ] 🚨 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] 📝 This change requires a documentation update


#### Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] Did you update CHANGELOG in case of a major change?


Problem statement:
[Feature request] Add 3D-SSIM
## 🚀 Feature
3D-SSIM

## Motivation
SSIM and SSIMLoss are currently only supported in 2D in kornia. While it is common in medical imaging to compute SSIM on 3D volumes. In addition, 3D-SSIM is useful for video.

## Pitch
Adding 3D-SSIM

## Alternatives
N/A

## Additional context
N/A

Hints from issue discussion:
@wendy-xiaozong that would be a nice feature. Do you have any implementation reference ?
Hey @edgarriba,

I did find a paper on 3D-SSIM ([3D-SSIM FOR VIDEO QUALITY ASSESSMENT](https://ieeexplore.ieee.org/document/6466936)) from the same author of the SSIM paper. To compute it, it appears that they are simply converting the 2D blocks to 3D.

I would be very happy to implement it because I need to use it anyway.
I found there are already some implementations on that topic, like [this](https://github.com/jinh0park/pytorch-ssim-3D). Probably, we can test against [scipy](https://stackoverflow.com/questions/53933528/ssim-for-3d-image-volume).

I do not have the access to the full paper now, but @wendy-xiaozong can you evaluate those implementations? It looks like the paper you mentioned has some more advanced features.
Hey @shijianjian, Thanks for finding those references! I will evaluate the 3D SSIM code with scipy.

For the paper I mentioned, I find [a pdf version](https://www.researchgate.net/profile/Kai-Zeng-9/publication/261386970_3D-SSIM_for_video_quality_assessment/links/5c9cebfd45851506d730632f/3D-SSIM-for-video-quality-assessment.pdf) online. So, they also use a width-adapted exponential weighting function applied upon sorted 3D block index instead of averaging it. But I cannot find the source code, although they did say in the paper there is a Matlab implementation online. I will just leave that alone.
Thanks for the paper. While I think we should have a proper design generic API that allows different weighting method. Please go for a PR if you are willing to do so!
I could give this a shot.
Hi Priya, I found MONAI has implemented the 3D SSIM loss recently. It might be another good reference to start with: https://docs.monai.io/en/stable/losses.html#monai.losses.ssim_loss.SSIMLoss

Good luck!

Jueqi
I like how Monai has only one exposed wrapper for both 2D and 3D data. Do we want to follow the same? Or have a different  SSIM3DLoss class?
I would start with different, later we think of merging if makes sense for us

Commit message: [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Patch to review:
diff --git a/kornia/filters/__init__.py b/kornia/filters/__init__.py
index c399e2829b..ea9754cf18 100644
--- a/kornia/filters/__init__.py
+++ b/kornia/filters/__init__.py
@@ -15,6 +15,7 @@
     get_gaussian_kernel1d_t,
     get_gaussian_kernel2d,
     get_gaussian_kernel2d_t,
+    get_gaussian_kernel3d,
     get_hanning_kernel1d,
     get_hanning_kernel2d,
     get_laplacian_kernel1d,
@@ -40,6 +41,7 @@
     "get_gaussian_discrete_kernel1d",
     "get_gaussian_erf_kernel1d",
     "get_gaussian_kernel2d",
+    "get_gaussian_kernel3d",
     "get_gaussian_kernel2d_t",
     "get_hanning_kernel1d",
     "get_hanning_kernel2d",
diff --git a/kornia/filters/kernels.py b/kornia/filters/kernels.py
index 7a17c8da4c..39d60a9731 100644
--- a/kornia/filters/kernels.py
+++ b/kornia/filters/kernels.py
@@ -783,3 +783,49 @@ def get_hanning_kernel2d(kernel_size: Tuple[int, int], device=torch.device('cpu'
     kx: Tensor = get_hanning_kernel1d(kernel_size[1], device, dtype)[None]
     kernel2d = ky @ kx
     return kernel2d
+
+
+def get_gaussian_kernel3d(kernel_size: Tuple[int, int], sigma: Tuple[float, float], force_even: bool = False) -> Tensor:
+    r"""Function that returns Gaussian filter matrix coefficients.
+
+    Args:
+        kernel_size: filter sizes in the x, y and z direction.
+         Sizes should be odd and positive.
+        sigma: gaussian standard deviation in the x, y and z
+         direction.
+        force_even: overrides requirement for odd kernel size.
+
+    Returns:
+        3D tensor with gaussian filter matrix coefficients.
+
+    Shape:
+        - Output: :math:`(\text{kernel_size}_x, \text{kernel_size}_y,  \text{kernel_size}_z)`
+
+    Examples:
+        >>> get_gaussian_kernel3d((3, 3, 3), (1.5, 1.5, 1.5))
+            tensor([[[0.0292, 0.0364, 0.0292],
+                    [0.0364, 0.0455, 0.0364],
+                    [0.0292, 0.0364, 0.0292]],
+
+                    [[0.0364, 0.0455, 0.0364],
+                    [0.0455, 0.0568, 0.0455],
+                    [0.0364, 0.0455, 0.0364]],
+
+                    [[0.0292, 0.0364, 0.0292],
+                    [0.0364, 0.0455, 0.0364],
+                    [0.0292, 0.0364, 0.0292]]])
+        >>> get_gaussian_kernel3d((3, 3, 3), (1.5, 1.5, 1.5)).sum()
+            tensor(1.)
+    """
+    if not isinstance(kernel_size, tuple) or len(kernel_size) != 3:
+        raise TypeError(f"kernel_size must be a tuple of length three. Got {kernel_size}")
+    if not isinstance(sigma, tuple) or len(sigma) != 3:
+        raise TypeError(f"sigma must be a tuple of length three. Got {sigma}")
+    ksize_x, ksize_y, ksize_z = kernel_size
+    sigma_x, sigma_y, sigma_z = sigma
+    kernel_x: Tensor = get_gaussian_kernel1d(ksize_x, sigma_x, force_even)
+    kernel_y: Tensor = get_gaussian_kernel1d(ksize_y, sigma_y, force_even)
+    kernel_z: Tensor = get_gaussian_kernel1d(ksize_z, sigma_z, force_even)
+    kernel_2d: Tensor = torch.matmul(kernel_x.unsqueeze(-1), kernel_y.unsqueeze(-1).t())
+    kernel_3d: Tensor = torch.matmul(kernel_z.unsqueeze(1), kernel_2d.reshape(1, -1)).reshape(kernel_size)
+    return kernel_3d
diff --git a/kornia/losses/__init__.py b/kornia/losses/__init__.py
index f21e0a72d8..e773bac0f0 100644
--- a/kornia/losses/__init__.py
+++ b/kornia/losses/__init__.py
@@ -8,6 +8,7 @@
 from .ms_ssim import MS_SSIMLoss
 from .psnr import PSNRLoss, psnr_loss
 from .ssim import SSIMLoss, ssim_loss
+from .ssim3d import SSIM3DLoss, ssim3d_loss
 from .total_variation import TotalVariation, total_variation
 from .tversky import TverskyLoss, tversky_loss
 
@@ -28,6 +29,8 @@
     "PSNRLoss",
     "ssim_loss",
     "SSIMLoss",
+    "ssim3d_loss",
+    "SSIM3DLoss",
     "total_variation",
     "TotalVariation",
     "tversky_loss",
diff --git a/kornia/losses/ssim3d.py b/kornia/losses/ssim3d.py
new file mode 100644
index 0000000000..6516b6723c
--- /dev/null
+++ b/kornia/losses/ssim3d.py
@@ -0,0 +1,105 @@
+import torch
+import torch.nn as nn
+
+import kornia.metrics as metrics
+
+
+def ssim3d_loss(
+    img1: torch.Tensor,
+    img2: torch.Tensor,
+    window_size: int,
+    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.
+
+    The loss, or the Structural dissimilarity (DSSIM) is described as:
+
+    .. math::
+
+      \text{loss}(x, y) = \frac{1 - \text{SSIM}(x, y)}{2}
+
+    See :meth:`~kornia.losses.ssim` for details about SSIM.
+
+    Args:
+        img1: the first input image with shape :math:`(B, C, D, H, W)`.
+        img2: the second input image with shape :math:`(B, C, D, H, W)`.
+        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.
+        reduction : Specifies the reduction to apply to the
+         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.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> loss = ssim_loss(input1, input2, 5)
+    """
+    # compute the ssim map
+    ssim_map: torch.Tensor = metrics.ssim3d(img1, img2, window_size, max_val, eps, padding)
+
+    # compute and reduce the loss
+    loss = 1.0 - ssim_map
+
+    if reduction == "mean":
+        loss = torch.mean(loss)
+    elif reduction == "sum":
+        loss = torch.sum(loss)
+    elif reduction == "none":
+        pass
+    return loss
+
+
+class SSIM3DLoss(nn.Module):
+    r"""Create a criterion that computes a loss based on the SSIM measurement.
+
+    The loss, or the Structural dissimilarity (DSSIM) is described as:
+
+    .. math::
+
+      \text{loss}(x, y) = \frac{1 - \text{SSIM}(x, y)}{2}
+
+    See :meth:`~kornia.losses.ssim_loss` for details about SSIM.
+
+    Args:
+        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.
+        reduction : Specifies the reduction to apply to the
+         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.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> criterion = SSIM3DLoss(5)
+        >>> loss = criterion(input1, input2)
+    """
+
+    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 ssim3d_loss(img1, img2, self.window_size, self.max_val, self.eps, self.reduction, self.padding)
diff --git a/kornia/metrics/__init__.py b/kornia/metrics/__init__.py
index 319acccd97..6682889b53 100644
--- a/kornia/metrics/__init__.py
+++ b/kornia/metrics/__init__.py
@@ -5,6 +5,7 @@
 from .mean_iou import mean_iou, mean_iou_bbox
 from .psnr import psnr
 from .ssim import SSIM, ssim
+from .ssim3d import SSIM3D, ssim3d
 
 __all__ = [
     "accuracy",
@@ -15,5 +16,7 @@
     "mean_average_precision",
     "psnr",
     "ssim",
+    "ssim3d",
     "SSIM",
+    "SSIM3D",
 ]
diff --git a/kornia/metrics/ssim3d.py b/kornia/metrics/ssim3d.py
new file mode 100644
index 0000000000..ee5b5103c4
--- /dev/null
+++ b/kornia/metrics/ssim3d.py
@@ -0,0 +1,177 @@
+from typing import List
+
+import torch
+import torch.nn as nn
+
+from kornia.filters import filter3d, get_gaussian_kernel3d
+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[4],
+            -cropping_shape[5],
+            -cropping_shape[2],
+            -cropping_shape[3],
+            -cropping_shape[0],
+            -cropping_shape[1],
+        ),
+    )
+
+
+def ssim3d(
+    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.
+
+    Measures the (SSIM) index between each element in the input `x` and target `y`.
+
+    The index can be described as:
+
+    .. math::
+
+      \text{SSIM}(x, y) = \frac{(2\mu_x\mu_y+c_1)(2\sigma_{xy}+c_2)}
+      {(\mu_x^2+\mu_y^2+c_1)(\sigma_x^2+\sigma_y^2+c_2)}
+
+    where:
+      - :math:`c_1=(k_1 L)^2` and :math:`c_2=(k_2 L)^2` are two variables to
+        stabilize the division with weak denominator.
+      - :math:`L` is the dynamic range of the pixel-values (typically this is
+        :math:`2^{\#\text{bits per pixel}}-1`).
+
+    Args:
+        img1: the first input image with shape :math:`(B, C, D, H, W)`.
+        img2: the second input image with shape :math:`(B, C, D, H, W)`.
+        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, D, H, W)`.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> ssim_map = ssim(input1, input2, 5)  # 1x4x5x5x5
+    """
+    if not isinstance(img1, torch.Tensor):
+        raise TypeError(f"Input img1 type is not a torch.Tensor. Got {type(img1)}")
+
+    if not isinstance(img2, torch.Tensor):
+        raise TypeError(f"Input img2 type is not a torch.Tensor. Got {type(img2)}")
+
+    if not isinstance(max_val, float):
+        raise TypeError(f"Input max_val type is not a float. Got {type(max_val)}")
+
+    if not len(img1.shape) == 5:
+        raise ValueError(f"Invalid img1 shape, we expect BxCxDxHxW. Got: {img1.shape}")
+
+    if not len(img2.shape) == 5:
+        raise ValueError(f"Invalid img2 shape, we expect BxCxDxHxW. Got: {img2.shape}")
+
+    if not img1.shape == img2.shape:
+        raise ValueError(f"img1 and img2 shapes must be the same. Got: {img1.shape} and {img2.shape}")
+
+    # prepare kernel
+    kernel: torch.Tensor = get_gaussian_kernel3d((window_size, window_size, window_size), (1.5, 1.5, 1.5)).unsqueeze(0)
+
+    # compute coefficients
+    C1: float = (0.01 * max_val) ** 2
+    C2: float = (0.03 * max_val) ** 2
+
+    # compute local mean per channel
+    mu1: torch.Tensor = filter3d(img1, kernel)
+    mu2: torch.Tensor = filter3d(img2, kernel)
+
+    cropping_shape: List[int] = []
+    if padding == 'valid':
+        depth, height, width = kernel.shape[-3:]
+        cropping_shape = _compute_padding([depth, 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 = filter3d(img1**2, kernel)
+    mu_img2_sq = filter3d(img2**2, kernel)
+    mu_img1_img2 = filter3d(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 = 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)
+    den: torch.Tensor = (mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)
+
+    return num / (den + eps)
+
+
+class SSIM3D(nn.Module):
+    r"""Create a module that computes the Structural Similarity (SSIM) index between two 3D images.
+
+    Measures the (SSIM) index between each element in the input `x` and target `y`.
+
+    The index can be described as:
+
+    .. math::
+
+      \text{SSIM}(x, y) = \frac{(2\mu_x\mu_y+c_1)(2\sigma_{xy}+c_2)}
+      {(\mu_x^2+\mu_y^2+c_1)(\sigma_x^2+\sigma_y^2+c_2)}
+
+    where:
+      - :math:`c_1=(k_1 L)^2` and :math:`c_2=(k_2 L)^2` are two variables to
+        stabilize the division with weak denominator.
+      - :math:`L` is the dynamic range of the pixel-values (typically this is
+        :math:`2^{\#\text{bits per pixel}}-1`).
+
+    Args:
+        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, D, H, W)`.
+        - Target :math:`(B, C, D, H, W)`.
+        - Output: :math:`(B, C, D, H, W)`.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> ssim = SSIM(5)
+        >>> ssim_map = ssim(input1, input2)  # 1x4x5x5x5
+    """
+
+    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 ssim3d(img1, img2, self.window_size, self.max_val, self.eps, self.padding)


Code review comments:
diff_hunk: @@ -0,0 +1,105 @@
+import torch
+import torch.nn as nn
+
+import kornia.metrics as metrics
+
+
+def ssim3d_loss(
+    img1: torch.Tensor,
+    img2: torch.Tensor,
+    window_size: int,
+    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.
+
+    The loss, or the Structural dissimilarity (DSSIM) is described as:
+
+    .. math::
+
+      \text{loss}(x, y) = \frac{1 - \text{SSIM}(x, y)}{2}
+
+    See :meth:`~kornia.losses.ssim` for details about SSIM.
+
+    Args:
+        img1: the first input image with shape :math:`(B, C, D, H, W)`.
+        img2: the second input image with shape :math:`(B, C, D, H, W)`.
+        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.
+        reduction : Specifies the reduction to apply to the
+         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.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> loss = ssim_loss(input1, input2, 5)
+    """
+    # compute the ssim map
+    ssim_map: torch.Tensor = metrics.ssim3d(img1, img2, window_size, max_val, eps, padding)
+
+    # compute and reduce the loss
+    loss = 1.0 - ssim_map
+
+    if reduction == "mean":
+        loss = torch.mean(loss)
+    elif reduction == "sum":
+        loss = torch.sum(loss)
+    elif reduction == "none":
+        pass
+    return loss
+
+
+class SSIM3DLoss(nn.Module):
path: kornia/losses/ssim3d.py
text: @user1:
```suggestion
class SSIM3DLoss(Module):
```
import fomr kornia core

diff_hunk: @@ -0,0 +1,177 @@
+from typing import List
+
+import torch
+import torch.nn as nn
+
+from kornia.filters import filter3d, get_gaussian_kernel3d
+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[4],
+            -cropping_shape[5],
+            -cropping_shape[2],
+            -cropping_shape[3],
+            -cropping_shape[0],
+            -cropping_shape[1],
+        ),
+    )
+
+
+def ssim3d(
+    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.
+
+    Measures the (SSIM) index between each element in the input `x` and target `y`.
+
+    The index can be described as:
+
+    .. math::
+
+      \text{SSIM}(x, y) = \frac{(2\mu_x\mu_y+c_1)(2\sigma_{xy}+c_2)}
+      {(\mu_x^2+\mu_y^2+c_1)(\sigma_x^2+\sigma_y^2+c_2)}
+
+    where:
+      - :math:`c_1=(k_1 L)^2` and :math:`c_2=(k_2 L)^2` are two variables to
+        stabilize the division with weak denominator.
+      - :math:`L` is the dynamic range of the pixel-values (typically this is
+        :math:`2^{\#\text{bits per pixel}}-1`).
+
+    Args:
+        img1: the first input image with shape :math:`(B, C, D, H, W)`.
+        img2: the second input image with shape :math:`(B, C, D, H, W)`.
+        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, D, H, W)`.
+
+    Examples:
+        >>> input1 = torch.rand(1, 4, 5, 5, 5)
+        >>> input2 = torch.rand(1, 4, 5, 5, 5)
+        >>> ssim_map = ssim(input1, input2, 5)  # 1x4x5x5x5
+    """
+    if not isinstance(img1, torch.Tensor):
path: kornia/metrics/ssim3d.py
text: @user1:
use KORNIA_CHECK functions

diff_hunk: @@ -783,3 +783,49 @@ def get_hanning_kernel2d(kernel_size: Tuple[int, int], device=torch.device('cpu'
     kx: Tensor = get_hanning_kernel1d(kernel_size[1], device, dtype)[None]
     kernel2d = ky @ kx
     return kernel2d
+
+
+def get_gaussian_kernel3d(kernel_size: Tuple[int, int], sigma: Tuple[float, float], force_even: bool = False) -> Tensor:
+    r"""Function that returns Gaussian filter matrix coefficients.
+
+    Args:
+        kernel_size: filter sizes in the x, y and z direction.
+         Sizes should be odd and positive.
+        sigma: gaussian standard deviation in the x, y and z
+         direction.
+        force_even: overrides requirement for odd kernel size.
+
+    Returns:
+        3D tensor with gaussian filter matrix coefficients.
+
+    Shape:
+        - Output: :math:`(\text{kernel_size}_x, \text{kernel_size}_y,  \text{kernel_size}_z)`
+
+    Examples:
+        >>> get_gaussian_kernel3d((3, 3, 3), (1.5, 1.5, 1.5))
+            tensor([[[0.0292, 0.0364, 0.0292],
+                    [0.0364, 0.0455, 0.0364],
+                    [0.0292, 0.0364, 0.0292]],
+
+                    [[0.0364, 0.0455, 0.0364],
+                    [0.0455, 0.0568, 0.0455],
+                    [0.0364, 0.0455, 0.0364]],
+
+                    [[0.0292, 0.0364, 0.0292],
+                    [0.0364, 0.0455, 0.0364],
+                    [0.0292, 0.0364, 0.0292]]])
+        >>> get_gaussian_kernel3d((3, 3, 3), (1.5, 1.5, 1.5)).sum()
+            tensor(1.)
+    """
+    if not isinstance(kernel_size, tuple) or len(kernel_size) != 3:
+        raise TypeError(f"kernel_size must be a tuple of length three. Got {kernel_size}")
+    if not isinstance(sigma, tuple) or len(sigma) != 3:
+        raise TypeError(f"sigma must be a tuple of length three. Got {sigma}")
+    ksize_x, ksize_y, ksize_z = kernel_size
+    sigma_x, sigma_y, sigma_z = sigma
+    kernel_x: Tensor = get_gaussian_kernel1d(ksize_x, sigma_x, force_even)
+    kernel_y: Tensor = get_gaussian_kernel1d(ksize_y, sigma_y, force_even)
+    kernel_z: Tensor = get_gaussian_kernel1d(ksize_z, sigma_z, force_even)
+    kernel_2d: Tensor = torch.matmul(kernel_x.unsqueeze(-1), kernel_y.unsqueeze(-1).t())
path: kornia/filters/kernels.py
text: @user1:
```suggestion
    kernel_2d: Tensor = kernel_x[..., None] @ kernel_y[..., None].t()
```
more pythonic. same below

diff_hunk: @@ -0,0 +1,105 @@
+import torch
+import torch.nn as nn
+
+import kornia.metrics as metrics
+
+
+def ssim3d_loss(
+    img1: torch.Tensor,
path: kornia/losses/ssim3d.py
text: @user1:
```suggestion
    img1: Tensor,
```
`from kornia.core import Tensor`