Repository: Lightning-AI/torchmetrics

Issue:
The `divided by zeros` problem in low precision when use `uqi`, `sdi`, `ssim` and `ms-ssim`
## 🐛 Bug

Hi, guys! Thanks for your fast response and fix! I really appreciate that.
When I tried to compute the `uqi` function, I found that there is still some situation that the quotients will be zeros.
Accroding to [https://en.wikipedia.org/wiki/Variance](url), I know that
![Variance Equation](https://wikimedia.org/api/rest_v1/media/math/render/svg/4ad35c4161b9cf52868e879d457d8d796094ff02)
And I tried to debug the code for understanding why I got NaN values. I noticed that 
https://github.com/Lightning-AI/torchmetrics/blob/62adb4022b30e0fe5c47589d9ecd82d3ff041117/src/torchmetrics/functional/image/uqi.py#L105-L106
produces very small negative values around 5e-7. It seems that the results give the "negative variance" values due to float deviations, which is impossible for math. Although there is a eps to avoid zero, the absolute value of "negative variance" is much greater than eps, and the  "negative variance" finally leads to this NaN or "divided by zeros" problems. `sdi` uses the `uqi` results so it produced the same NaN. Besides, `ssim` and `ms-ssim` use C1 and C2 to prevent this thing but in fact they also suffer the same situation.

### To Reproduce

I'm sorry that I can't give the original data due to the upload limitations. Each file is 1.15GB. It's very tricky to reproduce the problem. If there are some other methods to upload data, I’m glad to provide my data. 😊

### Expected behavior

I'm not sure how to solve the problem. `torch.unfold` can compute as the variance defination but will cost huge amount of memory which is not acceptable. I think it's better to add a check of the variance to prevent negetive ones join the quotients. However, I don't know which is better between using the absolute values of negetive results and just leaving them as zeros.

### Environment

Name | Version | Build | Channel
-- | -- | -- | --
_anaconda_depends | 2023.07 | py311_0 | https://repo.anaconda.com/pkgs/main
conda | 23.9.0 | py311haa95532_0 | https://repo.anaconda.com/pkgs/main
python | 3.10.13 | he1021f5_0 | defaults
pytorch | 2.1.0 | py3.10_cuda12.1_cudnn8_0 | pytorch
pytorch-cuda | 12.1 | hde6ce7c_5 | pytorch
pytorch-mutex | 1.0 | cuda | pytorch
torchaudio | 2.1.0 | pypi_0 | pypi
torchmetrics | 1.2.1 | pyhd8ed1ab_0 | conda-forge
torchvision | 0.16.0 | pypi_0 | pypi




Discussion:
To solve this issue, one approach can be using Fractions instead of floats to represent numbers with high denominators. Since tensors only work with floats and integers, you can create two seperate tensors for representing numerators and denominators.

Resolving patch:
diff --git a/src/torchmetrics/functional/image/ssim.py b/src/torchmetrics/functional/image/ssim.py
--- a/src/torchmetrics/functional/image/ssim.py
+++ b/src/torchmetrics/functional/image/ssim.py
@@ -154,8 +154,9 @@ def _ssim_update(
     mu_target_sq = output_list[1].pow(2)
     mu_pred_target = output_list[0] * output_list[1]
 
-    sigma_pred_sq = output_list[2] - mu_pred_sq
-    sigma_target_sq = output_list[3] - mu_target_sq
+    # Calculate the variance of the predicted and target images, should be non-negative
+    sigma_pred_sq = torch.clamp(output_list[2] - mu_pred_sq, min=0.0)
+    sigma_target_sq = torch.clamp(output_list[3] - mu_target_sq, min=0.0)
     sigma_pred_target = output_list[4] - mu_pred_target
 
     upper = 2 * sigma_pred_target.to(dtype) + c2
diff --git a/src/torchmetrics/functional/image/uqi.py b/src/torchmetrics/functional/image/uqi.py
--- a/src/torchmetrics/functional/image/uqi.py
+++ b/src/torchmetrics/functional/image/uqi.py
@@ -102,8 +102,9 @@ def _uqi_compute(
     mu_target_sq = output_list[1].pow(2)
     mu_pred_target = output_list[0] * output_list[1]
 
-    sigma_pred_sq = output_list[2] - mu_pred_sq
-    sigma_target_sq = output_list[3] - mu_target_sq
+    # Calculate the variance of the predicted and target images, should be non-negative
+    sigma_pred_sq = torch.clamp(output_list[2] - mu_pred_sq, min=0.0)
+    sigma_target_sq = torch.clamp(output_list[3] - mu_target_sq, min=0.0)
     sigma_pred_target = output_list[4] - mu_pred_target
 
     upper = 2 * sigma_pred_target


Test patch: