File size: 11,833 Bytes
7667a87 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | r"""Code is adapted from https://github.com/Lightning-AI/torchmetrics/blob/54a06013cdac4895bf8e85b583c5f220388ebc1d/src/torchmetrics/image/fid.py#L127-L300"""
from copy import deepcopy
from typing import Any, List, Optional, Union, Literal
import math
import os
from einops import rearrange
import numpy as np
import torch
from torch import Tensor
from torch.nn import Module, functional as F
from torchmetrics.metric import Metric
from torchmetrics.image.fid import _compute_fid
from utils.optim import disable_train
from utils.path import (
default_pretrained_metrics_dir,
pretrained_i3d_400_name,pretrained_i3d_600_name
)
# if _TORCH_FIDELITY_AVAILABLE:
# from torch_fidelity.feature_extractor_inceptionv3 import FeatureExtractorInceptionV3
# else:
# class FeatureExtractorInceptionV3(Module): # type: ignore
# pass
#
# __doctest_skip__ = ["FrechetInceptionDistance", "FID"]
def load_i3d_pretrained(device=torch.device('cpu'), channels:Literal[400,600]=400):
assert channels in [400,600], f"Only 400 and 600 channels are supported, got {channels}."
filename = pretrained_i3d_400_name if channels==400 else pretrained_i3d_600_name
from .pytorch_i3d import InceptionI3d
i3d = InceptionI3d(channels, in_channels=3).to(device)
filepath = os.path.join(default_pretrained_metrics_dir, filename)
assert os.path.exists(filepath), f"Pretrained Evaluation Model {filename} not found in directory {filepath}"
i3d.load_state_dict(torch.load(filepath, map_location=device))
i3d.eval()
return i3d
class I3DWrapper(Module):
def __init__(self, channels=400):
super().__init__()
self.channels = channels
self.i3d = load_i3d_pretrained(channels=channels, device=torch.device("cpu"))
@staticmethod
def preprocess(video, target_resolution=224):
r"""
Parameters
----------
video: torch.Tensor
shape = (b, t, 3, h, w)
value range fomr 0 to 1
target_resolution: int
224 by default
Returns
-------
"""
b, t, c, h, w = video.shape
# scale shorter side to resolution
scale = target_resolution / min(h, w)
if h < w:
target_size = (target_resolution, math.ceil(w * scale))
else:
target_size = (math.ceil(h * scale), target_resolution)
video = rearrange(video, "b t c h w -> (b t) c h w")
video = F.interpolate(video, size=target_size, mode='bilinear',
align_corners=False)
# center crop
_, _, h, w = video.shape
w_start = (w - target_resolution) // 2
h_start = (h - target_resolution) // 2
video = video[:, :, h_start:h_start + target_resolution, w_start:w_start + target_resolution]
video = rearrange(video, "(b t) c h w -> b c t h w", b=b, t=t).contiguous() # CTHW
video -= 0.5
return video * 2 # value range from -1 to 1
def forward(self, video):
r"""
Parameters
----------
video: torch.Tensor
shape = (b, t, c, h, w)
value from 0 to 1
Returns
-------
logits: torch.Tensor
shape = (b, self.channels)
"""
processed_video = self.preprocess(video=video)
return self.i3d(processed_video)
class FrechetVideoDistance(Metric):
r"""Calculates Fréchet video distance (FVD) which is used to access the quality of generated images. Given
by.
.. math::
FVD = |\mu - \mu_w| + tr(\Sigma + \Sigma_w - 2(\Sigma \Sigma_w)^{\frac{1}{2}})
As input to ``forward`` and ``update`` the metric accepts the following input
- ``videos`` (:class:`~torch.Tensor`): tensor with videos feed to the feature extractor with
- ``real`` (:class:`~bool`): bool indicating if ``imgs`` belong to the real or the fake distribution
As output of `forward` and `compute` the metric returns the following output
- ``fvd`` (:class:`~torch.Tensor`): float scalar tensor with mean FVD value over samples
Args:
feature:
Either an integer or ``nn.Module``:
- an integer will indicate the i3d feature layer to choose. Can be one of the following:
400, 600
- an ``nn.Module`` for using a custom feature extractor. Expects that its forward method returns
an ``(N,d)`` matrix where ``N`` is the batch size and ``d`` is the feature size.
reset_real_features: Whether to also reset the real features. Since in many cases the real dataset does not
change, the features can cached them to avoid recomputing them which is costly. Set this to ``False`` if
your dataset does not change.
kwargs: Additional keyword arguments, see :ref:`Metric kwargs` for more info.
Raises:
ValueError:
If ``feature`` is set to an ``int`` not in [400, 600]
TypeError:
If ``feature`` is not an ``str``, ``int`` or ``torch.nn.Module``
ValueError:
If ``reset_real_features`` is not an ``bool``
Example:
>>> _ = torch.manual_seed(123)
>>> fvd = FrechetVideoDistance(feature=400)
>>> # generate two slightly overlapping image intensity distributions
>>> imgs_dist1 = torch.randint(0, 200, (100, 10, 3, 224, 224), dtype=torch.uint8)
>>> imgs_dist2 = torch.randint(100, 255, (100, 10, 3, 224, 224), dtype=torch.uint8)
>>> fvd.update(imgs_dist1, real=True)
>>> fvd.update(imgs_dist2, real=False)
>>> fvd.compute()
tensor(12.7202)
"""
higher_is_better: bool = False
is_differentiable: bool = False
full_state_update: bool = False
real_features_sum: Tensor
real_features_cov_sum: Tensor
real_features_num_samples: Tensor
fake_features_sum: Tensor
fake_features_cov_sum: Tensor
fake_features_num_samples: Tensor
default_layout = "NTCHW"
einops_default_layout = "N T C H W"
default_t_axis = 1
min_t = 9
def __init__(
self,
feature: Union[int, Module] = 400,
layout: str = "NTCHW",
reset_real_features: bool = True,
normalize: bool = False,
auto_t: bool = False,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.layout = layout
if isinstance(feature, int):
num_features = feature
# if not _TORCH_FIDELITY_AVAILABLE:
# raise ModuleNotFoundError(
# "FrechetInceptionDistance metric requires that `Torch-fidelity` is installed."
# " Either install as `pip install torchmetrics[image]` or `pip install torch-fidelity`."
# )
valid_int_input = [400, 600]
if feature not in valid_int_input:
raise ValueError(
f"Integer input to argument `feature` must be one of {valid_int_input}, but got {feature}."
)
self.inception = I3DWrapper(channels=feature)
elif isinstance(feature, Module):
self.inception = feature
dummy_image = torch.randint(0, 255, (1, 9, 3, 299, 299), dtype=torch.uint8)
num_features = self.inception(dummy_image).shape[-1]
else:
raise TypeError("Got unknown input to argument `feature`")
if not isinstance(reset_real_features, bool):
raise ValueError("Argument `reset_real_features` expected to be a bool")
self.reset_real_features = reset_real_features
if not isinstance(normalize, bool):
raise ValueError("Argument `normalize` expected to be a bool")
self.normalize = normalize
self.auto_t = auto_t
mx_nb_feets = (num_features, num_features)
self.add_state("real_features_sum", torch.zeros(num_features).double(), dist_reduce_fx="sum")
self.add_state("real_features_cov_sum", torch.zeros(mx_nb_feets).double(), dist_reduce_fx="sum")
self.add_state("real_features_num_samples", torch.tensor(0).long(), dist_reduce_fx="sum")
self.add_state("fake_features_sum", torch.zeros(num_features).double(), dist_reduce_fx="sum")
self.add_state("fake_features_cov_sum", torch.zeros(mx_nb_feets).double(), dist_reduce_fx="sum")
self.add_state("fake_features_num_samples", torch.tensor(0).long(), dist_reduce_fx="sum")
disable_train(self)
@property
def einops_layout(self):
if not hasattr(self, "_einops_layout"):
self._einops_layout = " ".join(self.layout)
return self._einops_layout
def update(self, videos: Tensor, real: bool) -> None: # type: ignore
r"""
Update the state with extracted features.
Parameters
----------
videos: torch.Tensor
shape = (b, t, c, h, w), t >= 9, c = 3 or 1
value from 0 to 255 if self.normalize else 0 to 1
real: bool
"""
videos = rearrange(videos, f"{self.einops_layout} -> {self.einops_default_layout}")
if videos.shape[1] < self.min_t:
if self.auto_t:
videos = torch.repeat_interleave(videos, repeats=2, dim=self.default_t_axis)
else:
raise ValueError(f"The temporal length of the input is smaller than the minimal requirement:"
f" videos.shape[1] = {videos.shape[1]} < {self.min_t}.")
videos = videos / 255.0 if self.normalize else videos
c = videos.shape[2]
if c == 1: # see discussion:https://github.com/google/compare_gan/issues/13 and reference implementation: https://github.com/google/compare_gan/blob/560697ee213f91048c6b4231ab79fcdd9bf20381/compare_gan/src/eval_gan_lib.py#L786-L791
videos = videos.repeat(1, 1, 3, 1, 1)
features = self.inception(videos)
self.orig_dtype = features.dtype
features = features.double()
if features.dim() == 1:
features = features.unsqueeze(0)
if real:
self.real_features_sum += features.sum(dim=0)
self.real_features_cov_sum += features.t().mm(features)
self.real_features_num_samples += videos.shape[0]
else:
self.fake_features_sum += features.sum(dim=0)
self.fake_features_cov_sum += features.t().mm(features)
self.fake_features_num_samples += videos.shape[0]
def compute(self) -> Tensor:
"""Calculate FID score based on accumulated extracted features from the two distributions."""
mean_real = (self.real_features_sum / self.real_features_num_samples).unsqueeze(0)
mean_fake = (self.fake_features_sum / self.fake_features_num_samples).unsqueeze(0)
cov_real_num = self.real_features_cov_sum - self.real_features_num_samples * mean_real.t().mm(mean_real)
cov_real = cov_real_num / (self.real_features_num_samples - 1)
cov_fake_num = self.fake_features_cov_sum - self.fake_features_num_samples * mean_fake.t().mm(mean_fake)
cov_fake = cov_fake_num / (self.fake_features_num_samples - 1)
return _compute_fid(mean_real.squeeze(0), cov_real, mean_fake.squeeze(0), cov_fake).to(self.orig_dtype)
def reset(self) -> None:
if not self.reset_real_features:
real_features_sum = deepcopy(self.real_features_sum)
real_features_cov_sum = deepcopy(self.real_features_cov_sum)
real_features_num_samples = deepcopy(self.real_features_num_samples)
super().reset()
self.real_features_sum = real_features_sum
self.real_features_cov_sum = real_features_cov_sum
self.real_features_num_samples = real_features_num_samples
else:
super().reset()
|