codecsplat / src /evaluation /metrics.py
multimodalart's picture
multimodalart HF Staff
CodecSplat demo
7405e85 verified
Raw History Blame Contribute Delete
4.41 kB
from functools import cache
import torch
import torch.nn.functional as F
from einops import reduce
from jaxtyping import Float
from lpips import LPIPS
from torch import Tensor
@torch.no_grad()
def compute_psnr(
ground_truth: Float[Tensor, "batch channel height width"],
predicted: Float[Tensor, "batch channel height width"],
) -> Float[Tensor, " batch"]:
with torch.autocast(device_type=predicted.device.type, enabled=False):
ground_truth = ground_truth.float().clip(min=0, max=1)
predicted = predicted.float().clip(min=0, max=1)
mse = reduce((ground_truth - predicted) ** 2, "b c h w -> b", "mean")
return -10 * mse.log10()
@cache
def get_lpips(device: torch.device) -> LPIPS:
model = LPIPS(net="vgg").eval().requires_grad_(False)
return model.to(device=device, dtype=torch.float32)
@torch.no_grad()
def compute_lpips(
ground_truth: Float[Tensor, "batch channel height width"],
predicted: Float[Tensor, "batch channel height width"],
) -> Float[Tensor, " batch"]:
with torch.autocast(device_type=predicted.device.type, enabled=False):
value = get_lpips(predicted.device).forward(
ground_truth.float(),
predicted.float(),
normalize=True,
)
return value[:, 0, 0, 0].float()
@torch.no_grad()
def compute_ssim(
ground_truth: Float[Tensor, "batch channel height width"],
predicted: Float[Tensor, "batch channel height width"],
*,
win_size: int = 11,
gaussian_weights: bool = True,
data_range: float = 1.0,
use_sample_covariance: bool = True,
sigma: float = 1.5,
k1: float = 0.01,
k2: float = 0.03,
) -> Float[Tensor, " batch"]:
if ground_truth.shape != predicted.shape or ground_truth.ndim != 4:
raise ValueError("SSIM inputs must have the same BCHW shape.")
if win_size <= 0 or win_size % 2 == 0:
raise ValueError("win_size must be a positive odd integer.")
if min(ground_truth.shape[-2:]) < win_size:
raise ValueError("win_size must not exceed the image dimensions.")
if gaussian_weights and sigma <= 0:
raise ValueError("sigma must be positive when using Gaussian weights.")
if data_range <= 0 or k1 < 0 or k2 < 0:
raise ValueError("data_range must be positive and k1/k2 non-negative.")
with torch.autocast(device_type=predicted.device.type, enabled=False):
ground_truth = ground_truth.float()
predicted = predicted.float()
if gaussian_weights:
coordinates = torch.arange(
win_size,
dtype=torch.float32,
device=predicted.device,
)
coordinates -= (win_size - 1) / 2
kernel_1d = torch.exp(-(coordinates.square()) / (2 * sigma**2))
kernel_1d /= kernel_1d.sum()
kernel = torch.outer(kernel_1d, kernel_1d)
else:
kernel = torch.full(
(win_size, win_size),
1 / win_size**2,
dtype=torch.float32,
device=predicted.device,
)
channels = predicted.shape[1]
moments = torch.cat(
(
ground_truth,
predicted,
ground_truth.square(),
predicted.square(),
ground_truth * predicted,
),
dim=1,
)
kernels = kernel[None, None].expand(5 * channels, 1, -1, -1)
ux, uy, uxx, uyy, uxy = F.conv2d(
moments,
kernels,
groups=5 * channels,
).chunk(5, dim=1)
covariance_normalization = 1.0
if use_sample_covariance:
num_pixels = win_size**2
if num_pixels == 1:
raise ValueError(
"Sample covariance requires a window larger than one pixel."
)
covariance_normalization = num_pixels / (num_pixels - 1)
vx = covariance_normalization * (uxx - ux.square())
vy = covariance_normalization * (uyy - uy.square())
vxy = covariance_normalization * (uxy - ux * uy)
c1 = (k1 * data_range) ** 2
c2 = (k2 * data_range) ** 2
ssim = (
(2 * ux * uy + c1)
* (2 * vxy + c2)
/ ((ux.square() + uy.square() + c1) * (vx + vy + c2))
)
ssim = ssim.mean(dim=(1, 2, 3))
return ssim