Spaces:
Running on Zero
Running on Zero
| import os | |
| from typing import Iterable, Union | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from torchvision import transforms | |
| from torchvision.models import inception_v3 | |
| from torchvision.models.inception import InceptionA, InceptionC, InceptionE | |
| ImageInput = Union[str, os.PathLike, Image.Image] | |
| IMAGE_EXTENSIONS = {".bmp", ".jpg", ".jpeg", ".pgm", ".png", ".ppm", ".tif", ".tiff", ".webp"} | |
| def _image_files(path: Union[str, os.PathLike]): | |
| path = os.fspath(path) | |
| if os.path.isfile(path): | |
| if os.path.splitext(path)[1].lower() not in IMAGE_EXTENSIONS: | |
| raise ValueError(f"Unsupported image extension for FID: {path}") | |
| return [path] | |
| if not os.path.exists(path): | |
| raise FileNotFoundError(f"FID path does not exist: {path}") | |
| files = [] | |
| for root, dirs, names in os.walk(path): | |
| dirs.sort() | |
| for name in sorted(names): | |
| if os.path.splitext(name)[1].lower() in IMAGE_EXTENSIONS: | |
| files.append(os.path.join(root, name)) | |
| if not files: | |
| raise ValueError(f"No images found under {path}.") | |
| return files | |
| class _ImageDataset(torch.utils.data.Dataset): | |
| def __init__(self, images: Iterable[ImageInput], transform): | |
| self.images = list(images) | |
| self.transform = transform | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, index): | |
| image = self.images[index] | |
| if isinstance(image, (str, os.PathLike)): | |
| image = Image.open(image) | |
| if not isinstance(image, Image.Image): | |
| raise TypeError(f"FID expects PIL images or image paths, but received {type(image)}.") | |
| return self.transform(image.convert("RGB")) | |
| class FIDInceptionModel(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.model = _fid_inception_v3() | |
| def forward(self, images): | |
| images = 2 * images - 1 | |
| return self.model(images) | |
| def _fid_inception_v3(weights_path: str = None): | |
| model = inception_v3(weights=None, aux_logits=False, num_classes=1008, init_weights=False) | |
| model.Mixed_5b = _FIDInceptionA(192, pool_features=32) | |
| model.Mixed_5c = _FIDInceptionA(256, pool_features=64) | |
| model.Mixed_5d = _FIDInceptionA(288, pool_features=64) | |
| model.Mixed_6b = _FIDInceptionC(768, channels_7x7=128) | |
| model.Mixed_6c = _FIDInceptionC(768, channels_7x7=160) | |
| model.Mixed_6d = _FIDInceptionC(768, channels_7x7=160) | |
| model.Mixed_6e = _FIDInceptionC(768, channels_7x7=192) | |
| model.Mixed_7b = _FIDInceptionE1(1280) | |
| model.Mixed_7c = _FIDInceptionE2(2048) | |
| if weights_path is not None: | |
| model.load_state_dict(torch.load(weights_path, map_location="cpu")) | |
| model.fc = nn.Identity() | |
| return model | |
| class _FIDInceptionA(InceptionA): | |
| def forward(self, x): | |
| branch1x1 = self.branch1x1(x) | |
| branch5x5 = self.branch5x5_1(x) | |
| branch5x5 = self.branch5x5_2(branch5x5) | |
| branch3x3dbl = self.branch3x3dbl_1(x) | |
| branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) | |
| branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl) | |
| branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False) | |
| branch_pool = self.branch_pool(branch_pool) | |
| return torch.cat([branch1x1, branch5x5, branch3x3dbl, branch_pool], 1) | |
| class _FIDInceptionC(InceptionC): | |
| def forward(self, x): | |
| branch1x1 = self.branch1x1(x) | |
| branch7x7 = self.branch7x7_1(x) | |
| branch7x7 = self.branch7x7_2(branch7x7) | |
| branch7x7 = self.branch7x7_3(branch7x7) | |
| branch7x7dbl = self.branch7x7dbl_1(x) | |
| branch7x7dbl = self.branch7x7dbl_2(branch7x7dbl) | |
| branch7x7dbl = self.branch7x7dbl_3(branch7x7dbl) | |
| branch7x7dbl = self.branch7x7dbl_4(branch7x7dbl) | |
| branch7x7dbl = self.branch7x7dbl_5(branch7x7dbl) | |
| branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False) | |
| branch_pool = self.branch_pool(branch_pool) | |
| return torch.cat([branch1x1, branch7x7, branch7x7dbl, branch_pool], 1) | |
| class _FIDInceptionE1(InceptionE): | |
| def forward(self, x): | |
| branch1x1 = self.branch1x1(x) | |
| branch3x3 = self.branch3x3_1(x) | |
| branch3x3 = torch.cat([self.branch3x3_2a(branch3x3), self.branch3x3_2b(branch3x3)], 1) | |
| branch3x3dbl = self.branch3x3dbl_1(x) | |
| branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) | |
| branch3x3dbl = torch.cat([self.branch3x3dbl_3a(branch3x3dbl), self.branch3x3dbl_3b(branch3x3dbl)], 1) | |
| branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1, count_include_pad=False) | |
| branch_pool = self.branch_pool(branch_pool) | |
| return torch.cat([branch1x1, branch3x3, branch3x3dbl, branch_pool], 1) | |
| class _FIDInceptionE2(InceptionE): | |
| def forward(self, x): | |
| branch1x1 = self.branch1x1(x) | |
| branch3x3 = self.branch3x3_1(x) | |
| branch3x3 = torch.cat([self.branch3x3_2a(branch3x3), self.branch3x3_2b(branch3x3)], 1) | |
| branch3x3dbl = self.branch3x3dbl_1(x) | |
| branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl) | |
| branch3x3dbl = torch.cat([self.branch3x3dbl_3a(branch3x3dbl), self.branch3x3dbl_3b(branch3x3dbl)], 1) | |
| branch_pool = F.max_pool2d(x, kernel_size=3, stride=1, padding=1) | |
| branch_pool = self.branch_pool(branch_pool) | |
| return torch.cat([branch1x1, branch3x3, branch3x3dbl, branch_pool], 1) | |
| class FIDModel(torch.nn.Module): | |
| def __init__(self, model: torch.nn.Module, device: Union[str, torch.device] = "cpu", batch_size: int = 50, num_workers: int = 0): | |
| super().__init__() | |
| self.model = model | |
| self.batch_size = batch_size | |
| self.num_workers = num_workers | |
| self.transform = transforms.Compose( | |
| [ | |
| transforms.Resize((299, 299), interpolation=transforms.InterpolationMode.BICUBIC), | |
| transforms.ToTensor(), | |
| ] | |
| ) | |
| self.to(device) | |
| def device(self): | |
| try: | |
| return next(self.model.parameters()).device | |
| except StopIteration: | |
| return torch.device("cpu") | |
| def _as_images(self, images): | |
| if isinstance(images, (str, os.PathLike)): | |
| return _image_files(images) | |
| if isinstance(images, Image.Image): | |
| return [images] | |
| return list(images) | |
| def get_activations(self, images, batch_size: int = None, num_workers: int = None): | |
| images = self._as_images(images) | |
| batch_size = self.batch_size if batch_size is None else batch_size | |
| num_workers = self.num_workers if num_workers is None else num_workers | |
| dataset = _ImageDataset(images, transform=self.transform) | |
| dataloader = torch.utils.data.DataLoader(dataset, batch_size=min(batch_size, len(dataset)), shuffle=False, num_workers=num_workers) | |
| activations = [] | |
| self.model.eval() | |
| for batch in dataloader: | |
| batch = batch.to(self.device) | |
| features = self.model(batch) | |
| if isinstance(features, tuple): | |
| features = features[0] | |
| if features.ndim == 4: | |
| features = F.adaptive_avg_pool2d(features, output_size=(1, 1)).flatten(1) | |
| activations.append(features.detach().cpu().to(torch.float64)) | |
| return torch.cat(activations, dim=0) | |
| def statistics(self, images, batch_size: int = None, num_workers: int = None): | |
| activations = self.get_activations(images, batch_size=batch_size, num_workers=num_workers) | |
| return self.activation_statistics(activations) | |
| def activation_statistics(activations): | |
| activations = activations.to(torch.float64) | |
| mean = activations.mean(dim=0) | |
| centered = activations - mean | |
| if activations.shape[0] <= 1: | |
| covariance = torch.zeros((activations.shape[1], activations.shape[1]), dtype=torch.float64) | |
| else: | |
| covariance = centered.T @ centered / (activations.shape[0] - 1) | |
| return mean, covariance | |
| def _sqrtm_psd(matrix, eps: float = 1e-10): | |
| matrix = (matrix + matrix.T) * 0.5 | |
| eigenvalues, eigenvectors = torch.linalg.eigh(matrix) | |
| eigenvalues = eigenvalues.clamp_min(eps).sqrt() | |
| return (eigenvectors * eigenvalues.unsqueeze(0)) @ eigenvectors.T | |
| def frechet_distance(cls, mean1, covariance1, mean2, covariance2, eps: float = 1e-6): | |
| mean1 = mean1.to(torch.float64) | |
| covariance1 = covariance1.to(torch.float64) | |
| mean2 = mean2.to(torch.float64) | |
| covariance2 = covariance2.to(torch.float64) | |
| diff = mean1 - mean2 | |
| offset = torch.eye(covariance1.shape[0], dtype=torch.float64) * eps | |
| sqrt_cov1 = cls._sqrtm_psd(covariance1 + offset) | |
| covmean = cls._sqrtm_psd(sqrt_cov1 @ (covariance2 + offset) @ sqrt_cov1) | |
| distance = diff.dot(diff) + torch.trace(covariance1) + torch.trace(covariance2) - 2 * torch.trace(covmean) | |
| return distance.clamp_min(0) | |
| def compute(self, reference_images, generated_images, batch_size: int = None, num_workers: int = None): | |
| mean1, covariance1 = self.statistics(reference_images, batch_size=batch_size, num_workers=num_workers) | |
| mean2, covariance2 = self.statistics(generated_images, batch_size=batch_size, num_workers=num_workers) | |
| return self.frechet_distance(mean1, covariance1, mean2, covariance2) | |
| def forward(self, reference_images, generated_images): | |
| return self.compute(reference_images, generated_images) | |