# Copyright (c) 2025 Hansheng Chen import os import sys import logging import pickle import warnings import numpy as np import torch import torch.distributed as dist import torch.nn.functional as F import mmcv import hashlib from copy import deepcopy from contextlib import contextmanager, redirect_stdout, nullcontext from scipy import linalg from scipy.stats import entropy from torchvision import models from mmcv.runner import get_dist_info, load_checkpoint from mmgen.utils import get_root_logger from mmgen.core.registry import METRICS from mmgen.core.evaluation.metrics import ( Metric, TERO_INCEPTION_URL, _load_inception_torch, MMGEN_CACHE_DIR) from mmgen.core.evaluation.metrics import FID as _FID from mmgen.core.evaluation.metrics import PR as _PR from open_clip import get_tokenizer, create_model from lakonlab.utils.io_utils import download_from_huggingface, download_from_url # Global caches for model loading _inception_cache = {} _hpsv2_cache = {} _clip_cache = {} def _argv_ctx(argv): class _Argv: def __enter__(self): self._old = sys.argv sys.argv = argv def __exit__(self, exc_type, exc, tb): sys.argv = self._old return _Argv() def _redirect_stdout(to_buf): return redirect_stdout(to_buf) if to_buf is not None else nullcontext() @contextmanager def _quarantine_openclip_logging(): """ Guard against open_clip (and friends) mutating global logging. Snapshots root handlers/level, runs the block, then removes any NEW handlers and restores the level. Also disables propagation for the open_clip logger so logs don’t bubble to root. """ root = logging.getLogger() before_handlers = tuple(root.handlers) # snapshot by identity before_ids = {id(h) for h in before_handlers} before_level = root.level try: yield finally: # Remove only handlers that were added during the block for h in list(root.handlers): if id(h) not in before_ids: root.removeHandler(h) try: h.close() except Exception: pass root.setLevel(before_level) # Clamp open_clip logger so it won’t re-emit to root oc = logging.getLogger("open_clip") oc.propagate = False oc.handlers.clear() def _load_inception_from_path(inception_path, map_location=None): mmcv.print_log( 'Try to load Tero\'s Inception Model from ' f'\'{inception_path}\'.', 'mmgen') try: model = torch.jit.load(inception_path, map_location=map_location) mmcv.print_log('Load Tero\'s Inception Model successfully.', 'mmgen') except Exception as e: model = None mmcv.print_log( 'Load Tero\'s Inception Model failed. ' f'\'{e}\' occurs.', 'mmgen') return model def _load_inception_from_url(inception_url, map_location=None): """ Fix multi-node downloading issue in MMGen. """ inception_url = inception_url if inception_url else TERO_INCEPTION_URL mmcv.print_log(f'Try to download Inception Model from {inception_url}...', 'mmgen') try: path = download_from_url(inception_url, dest_dir=MMGEN_CACHE_DIR) mmcv.print_log('Download Finished.') return _load_inception_from_path(path, map_location=map_location) except Exception as e: mmcv.print_log(f'Download Failed. {e} occurs.') return None def load_inception(inception_args, metric, map_location=None): """ Fix multi-node downloading issue in MMGen. """ if not isinstance(inception_args, dict): raise TypeError('Receive invalid \'inception_args\': ' f'\'{inception_args}\'') # Create cache key from arguments cache_key = hashlib.md5(str(sorted(inception_args.items())).encode()).hexdigest() cache_key += f"_{metric}" # Check if model is already cached if cache_key in _inception_cache: return _inception_cache[cache_key] _inception_args = deepcopy(inception_args) inceptoin_type = _inception_args.pop('type', None) if torch.__version__ < '1.6.0': mmcv.print_log( 'Current Pytorch Version not support script module, load ' 'Inception Model from torch model zoo. If you want to use ' 'Tero\' script model, please update your Pytorch higher ' f'than \'1.6\' (now is {torch.__version__})', 'mmgen') result = _load_inception_torch(_inception_args, metric), 'pytorch' _inception_cache[cache_key] = result return result # load pytorch version is specific if inceptoin_type != 'StyleGAN': result = _load_inception_torch(_inception_args, metric), 'pytorch' _inception_cache[cache_key] = result return result # try to load Tero's version path = _inception_args.get('inception_path', TERO_INCEPTION_URL) # try to parse `path` as web url and download if 'http' not in path: model = _load_inception_from_path(path, map_location=map_location) if isinstance(model, torch.nn.Module): result = model, 'StyleGAN' _inception_cache[cache_key] = result return result # try to parse `path` as path on disk model = _load_inception_from_url(path, map_location=map_location) if isinstance(model, torch.nn.Module): result = model, 'StyleGAN' _inception_cache[cache_key] = result return result raise RuntimeError('Cannot Load Inception Model, please check the input ' f'`inception_args`: {inception_args}') def load_hpsv2(hps_version, device='cpu', precision='fp16'): assert hps_version in ['v2', 'v2.1'] # Create cache key from arguments cache_key = f"{hps_version}_{device}_{precision}" # Check if model is already cached if cache_key in _hpsv2_cache: return _hpsv2_cache[cache_key] with _quarantine_openclip_logging(): model = create_model( 'ViT-H-14-quickgelu', precision=precision, device=device, output_dict=True) model.requires_grad_(False) tokenizer = get_tokenizer('ViT-H-14') load_checkpoint( model, f'huggingface://xswu/HPSv2/HPS_{hps_version}_compressed.pt', map_location='cpu', strict=True) result = model, tokenizer _hpsv2_cache[cache_key] = result return result def load_openclip( model_name='ViT-L-14-336-quickgelu', pretrained='openai', device='cpu', precision='fp16'): cache_key = f'{model_name}_{pretrained}_{device}_{precision}' if cache_key in _clip_cache: return _clip_cache[cache_key] with _quarantine_openclip_logging(): model = create_model( model_name, pretrained=pretrained, precision=precision, device=device, output_dict=True) model.requires_grad_(False) tokenizer = get_tokenizer(model_name) _clip_cache[cache_key] = (model, tokenizer) return _clip_cache[cache_key] def compute_pr_distances(row_features, col_features, col_batch_size=10000): dist_batches = [] for col_batch in col_features.split(col_batch_size): dist_batch = torch.cdist( row_features.unsqueeze(0), col_batch.unsqueeze(0))[0] dist_batches.append(dist_batch.cpu()) return torch.cat(dist_batches, dim=1) @METRICS.register_module(force=True) class PR(_PR): def __init__( self, num_images=None, image_shape=None, feats_pkl=None, k=3, bgr2rgb=True, vgg16_script=None, inception_args=None, row_batch_size=10000, col_batch_size=10000): super(_PR, self).__init__(num_images, image_shape) self.feats_pkl = feats_pkl self.vgg16 = self.inception_net = None self.device = 'cpu' if vgg16_script is not None: mmcv.print_log('loading vgg16 for improved precision and recall...', 'mmgen') if os.path.isfile(vgg16_script): self.vgg16 = torch.jit.load('work_dirs/cache/vgg16.pt', map_location=self.device).eval() self.use_tero_scirpt = True else: mmcv.print_log( 'Cannot load Tero\'s script module. Use official ' 'vgg16 instead', 'mmgen') self.vgg16 = models.vgg16(pretrained=True).eval() self.use_tero_scirpt = False elif inception_args is not None: self.inception_net, self.inception_style = load_inception( inception_args, 'FID') else: raise ValueError('Please provide either vgg16_script or inception_args') self.k = k self.bgr2rgb = bgr2rgb self.row_batch_size = row_batch_size self.col_batch_size = col_batch_size def prepare(self): self.features_of_reals = [] self.features_of_fakes = [] if self.feats_pkl is not None: assert mmcv.is_filepath(self.feats_pkl) with open(self.feats_pkl, 'rb') as f: reference = pickle.load(f) self.features_of_reals = [torch.from_numpy(feat) for feat in reference['features_of_reals']] self.num_real_feeded = reference['num_real_feeded'] mmcv.print_log( f'Load reference inception pkl from {self.feats_pkl}', 'mmgen') def extract_features(self, batch): if self.vgg16 is not None: if self.use_tero_scirpt: batch = (batch * 127.5 + 128).clamp(0, 255).to(torch.uint8) feat = self.vgg16(batch, return_features=True) else: batch = F.interpolate(batch, size=(224, 224)) before_fc = self.vgg16.features(batch) before_fc = before_fc.view(-1, 7 * 7 * 512) feat = self.vgg16.classifier[:4](before_fc) else: if self.inception_style == 'StyleGAN': batch = (batch * 127.5 + 128).clamp(0, 255).to(torch.uint8) feat = self.inception_net(batch, return_features=True) else: feat = self.inception_net(batch)[0].view(batch.shape[0], -1) return feat @torch.no_grad() def feed_op(self, batch, mode): batch = batch.to(self.device) if self.bgr2rgb: batch = batch[:, [2, 1, 0]] feat = self.extract_features(batch) if dist.is_initialized(): ws = dist.get_world_size() placeholder = [torch.zeros_like(feat) for _ in range(ws)] dist.all_gather(placeholder, feat) feat = torch.stack(placeholder, dim=1).reshape(feat.size(0) * ws, *feat.shape[1:]) if (dist.is_initialized() and dist.get_rank() == 0) or not dist.is_initialized(): if mode == 'reals': self.features_of_reals.append(feat) elif mode == 'fakes': self.features_of_fakes.append(feat) else: raise ValueError(f'{mode} is not a implemented feed mode.') def feed(self, batch, mode): if self.num_images is not None: return super().feed(batch, mode) else: self.feed_op(batch, mode) @torch.no_grad() def summary(self): gen_features = torch.cat(self.features_of_fakes) real_features = torch.cat(self.features_of_reals).to(device=gen_features.device) if self.num_images is not None: assert gen_features.shape[0] >= self.num_images gen_features = gen_features[:self.num_images] if self.feats_pkl is None: # real feats not pre-calculated assert real_features.shape[0] >= self.num_images real_features = real_features[:self.num_images] self._result_dict = {} for name, manifold, probes in [ ('precision', real_features, gen_features), ('recall', gen_features, real_features) ]: kth = [] for manifold_batch in manifold.split(self.row_batch_size): distance = compute_pr_distances( row_features=manifold_batch, col_features=manifold, col_batch_size=self.col_batch_size) kth.append( distance.to(torch.float32).kthvalue(self.k + 1).values.to(torch.float16)) kth = torch.cat(kth) pred = [] for probes_batch in probes.split(self.row_batch_size): distance = compute_pr_distances( row_features=probes_batch, col_features=manifold, col_batch_size=self.col_batch_size) pred.append((distance <= kth).any(dim=1)) self._result_dict[name] = float(torch.cat(pred).to(torch.float32).mean()) precision = self._result_dict['precision'] recall = self._result_dict['recall'] self._result_str = f'precision: {precision}, recall:{recall}' return self._result_dict def clear_fake_data(self): self.features_of_fakes = [] self.num_fake_feeded = 0 def clear(self, clear_reals=False): self.clear_fake_data() if clear_reals: self.features_of_reals = [] self.num_real_feeded = 0 def load_to_gpu(self): """Move models to GPU.""" if torch.cuda.is_available(): if self.vgg16 is not None: self.vgg16 = self.vgg16.cuda() elif self.inception_net is not None: self.inception_net.cuda() self.device = 'cuda' def offload_to_cpu(self): """Move models to CPU.""" if self.vgg16 is not None: self.vgg16 = self.vgg16.cpu() elif self.inception_net is not None: self.inception_net.cpu() self.device = 'cpu' @METRICS.register_module(force=True) class FID(_FID): def __init__(self, num_images=None, image_shape=None, inception_pkl=None, bgr2rgb=True, inception_args=dict(normalize_input=False)): super().__init__( num_images, image_shape=image_shape, inception_pkl=inception_pkl, bgr2rgb=bgr2rgb, inception_args=inception_args) def prepare(self): if self.inception_pkl is not None: assert mmcv.is_filepath(self.inception_pkl) if self.inception_pkl.startswith('huggingface://'): self.inception_pkl = download_from_huggingface(self.inception_pkl) elif self.inception_pkl.startswith(('http://', 'https://')): self.inception_pkl = download_from_url(self.inception_pkl) with open(self.inception_pkl, 'rb') as f: reference = pickle.load(f) self.real_mean = reference['mean'] self.real_cov = reference['cov'] mmcv.print_log( f'Load reference inception pkl from {self.inception_pkl}', 'mmgen') self.num_real_feeded = self.num_images @torch.no_grad() def summary(self): # calculate reference inception stat if self.real_mean is None: feats = torch.cat(self.real_feats, dim=0) if self.num_images is not None: assert feats.shape[0] >= self.num_images feats = feats[:self.num_images] feats_np = feats.numpy() self.real_mean = np.mean(feats_np, 0) self.real_cov = np.cov(feats_np, rowvar=False) # calculate fake inception stat fake_feats = torch.cat(self.fake_feats, dim=0) if self.num_images is not None: assert fake_feats.shape[0] >= self.num_images fake_feats = fake_feats[:self.num_images] fake_feats_np = fake_feats.numpy() fake_mean = np.mean(fake_feats_np, 0) fake_cov = np.cov(fake_feats_np, rowvar=False) # calculate distance between real and fake statistics fid, mean, cov = self._calc_fid(fake_mean, fake_cov, self.real_mean, self.real_cov) # results for print/table self._result_str = (f'{fid:.4f} ({mean:.5f}/{cov:.5f})') # results for log_buffer self._result_dict = dict(fid=fid, fid_mean=mean, fid_cov=cov) return fid, mean, cov def feed(self, batch, mode): if self.num_images is not None: return super().feed(batch, mode) else: self.feed_op(batch, mode) @METRICS.register_module() class FIDKID(FID): name = 'FIDKID' def __init__(self, num_images=None, num_subsets=100, max_subset_size=1000, **kwargs): super().__init__(num_images=num_images, **kwargs) self.num_subsets = num_subsets self.max_subset_size = max_subset_size self.real_feats_np = None def prepare(self): if self.inception_pkl is not None: assert mmcv.is_filepath(self.inception_pkl) with open(self.inception_pkl, 'rb') as f: reference = pickle.load(f) self.real_mean = reference['mean'] self.real_cov = reference['cov'] self.real_feats_np = reference['feats_np'] mmcv.print_log( f'Load reference inception pkl from {self.inception_pkl}', 'mmgen') self.num_real_feeded = self.num_images @staticmethod def _calc_kid(real_feat, fake_feat, num_subsets, max_subset_size): """Refer to the implementation from: https://github.com/NVlabs/stylegan2-ada-pytorch/blob/main/metrics/kernel_inception_distance.py#L18 # noqa Args: real_feat (np.array): Features of the real samples. fake_feat (np.array): Features of the fake samples. num_subsets (int): Number of subsets to calculate KID. max_subset_size (int): The max size of each subset. Returns: float: The calculated kid metric. """ n = real_feat.shape[1] m = min(min(real_feat.shape[0], fake_feat.shape[0]), max_subset_size) t = 0 for _ in range(num_subsets): x = fake_feat[np.random.choice( fake_feat.shape[0], m, replace=False)] y = real_feat[np.random.choice( real_feat.shape[0], m, replace=False)] a = (x @ x.T / n + 1)**3 + (y @ y.T / n + 1)**3 b = (x @ y.T / n + 1)**3 t += (a.sum() - np.diag(a).sum()) / (m - 1) - b.sum() * 2 / m kid = t / num_subsets / m return float(kid) @torch.no_grad() def summary(self): if self.real_feats_np is None: feats = torch.cat(self.real_feats, dim=0) if self.num_images is not None: assert feats.shape[0] >= self.num_images feats = feats[:self.num_images] feats_np = feats.numpy() self.real_feats_np = feats_np self.real_mean = np.mean(feats_np, 0) self.real_cov = np.cov(feats_np, rowvar=False) fake_feats = torch.cat(self.fake_feats, dim=0) if self.num_images is not None: assert fake_feats.shape[0] >= self.num_images fake_feats = fake_feats[:self.num_images] fake_feats_np = fake_feats.numpy() fake_mean = np.mean(fake_feats_np, 0) fake_cov = np.cov(fake_feats_np, rowvar=False) fid, mean, cov = self._calc_fid(fake_mean, fake_cov, self.real_mean, self.real_cov) kid = self._calc_kid(self.real_feats_np, fake_feats_np, self.num_subsets, self.max_subset_size) * 1000 self._result_str = f'{fid:.4f} ({mean:.5f}/{cov:.5f}), {kid:.4f}' self._result_dict = dict(fid=fid, fid_mean=mean, fid_cov=cov, kid=kid) return fid, mean, cov, kid @METRICS.register_module() class InceptionMetrics(Metric): name = 'InceptionMetrics' def __init__(self, num_images=None, reference_pkl=None, bgr2rgb=False, center_crop=False, # SDXL-Lightning patch FID resize=True, inception_args=dict( type='StyleGAN', inception_path=TERO_INCEPTION_URL), use_kid=False, use_pr=True, use_is=True, kid_num_subsets=100, kid_max_subset_size=1000, pr_k=3, pr_row_batch_size=10000, pr_col_batch_size=10000, is_splits=10, prefix=''): super().__init__(num_images) self.reference_pkl = reference_pkl self.real_feats = [] self.fake_feats = [] self.preds = [] self.real_mean = None self.real_cov = None self.bgr2rgb = bgr2rgb self.center_crop = center_crop self.resize = resize self.device = 'cpu' if self.center_crop and self.resize: warnings.warn('`center_crop` is set to True, `resize` will be ignored.') logger = get_root_logger() ori_level = logger.level logger.setLevel('ERROR') self.inception_net, self.inception_style = load_inception( inception_args, 'FID', map_location=self.device) logger.setLevel(ori_level) self.inception_net.eval() self.use_kid = use_kid self.use_pr = use_pr self.use_is = use_is self.kid_num_subsets = kid_num_subsets self.kid_max_subset_size = kid_max_subset_size self.real_feats_np = None self.pr_k = pr_k self.pr_row_batch_size = pr_row_batch_size self.pr_col_batch_size = pr_col_batch_size self.is_splits = is_splits self.prefix = prefix def prepare(self): self.real_feats = [] self.real_feats_np = None self.fake_feats = [] self.preds = [] if self.reference_pkl is not None: assert mmcv.is_filepath(self.reference_pkl) if self.reference_pkl.startswith('huggingface://'): self.reference_pkl = download_from_huggingface(self.reference_pkl) elif self.reference_pkl.startswith(('http://', 'https://')): self.reference_pkl = download_from_url(self.reference_pkl) with open(self.reference_pkl, 'rb') as f: reference = pickle.load(f) self.real_mean = reference['mean'] self.real_cov = reference['cov'] self.real_feats_np = reference['real_feats_np'] self.real_feats = [torch.from_numpy(reference['real_feats_np'])] self.num_real_feeded = reference['num_real_feeded'] @staticmethod def _calc_fid(sample_mean, sample_cov, real_mean, real_cov, eps=1e-6): """Refer to the implementation from: https://github.com/rosinality/stylegan2-pytorch/blob/master/fid.py#L34 """ cov_sqrt, _ = linalg.sqrtm(sample_cov @ real_cov, disp=False) if not np.isfinite(cov_sqrt).all(): print('product of cov matrices is singular') offset = np.eye(sample_cov.shape[0]) * eps cov_sqrt = linalg.sqrtm( (sample_cov + offset) @ (real_cov + offset)) if np.iscomplexobj(cov_sqrt): if not np.allclose(np.diagonal(cov_sqrt).imag, 0, atol=1e-3): m = np.max(np.abs(cov_sqrt.imag)) raise ValueError(f'Imaginary component {m}') cov_sqrt = cov_sqrt.real mean_diff = sample_mean - real_mean mean_norm = mean_diff @ mean_diff trace = np.trace(sample_cov) + np.trace( real_cov) - 2 * np.trace(cov_sqrt) fid = mean_norm + trace return fid, mean_norm, trace @staticmethod def _calc_kid(real_feat, fake_feat, num_subsets, max_subset_size): """Refer to the implementation from: https://github.com/NVlabs/stylegan2-ada-pytorch/blob/main/metrics/kernel_inception_distance.py#L18 # noqa Args: real_feat (np.array): Features of the real samples. fake_feat (np.array): Features of the fake samples. num_subsets (int): Number of subsets to calculate KID. max_subset_size (int): The max size of each subset. Returns: float: The calculated kid metric. """ n = real_feat.shape[1] m = min(min(real_feat.shape[0], fake_feat.shape[0]), max_subset_size) t = 0 for _ in range(num_subsets): x = fake_feat[np.random.choice( fake_feat.shape[0], m, replace=False)] y = real_feat[np.random.choice( real_feat.shape[0], m, replace=False)] a = (x @ x.T / n + 1)**3 + (y @ y.T / n + 1)**3 b = (x @ y.T / n + 1)**3 t += (a.sum() - np.diag(a).sum()) / (m - 1) - b.sum() * 2 / m kid = t / num_subsets / m return float(kid) def extract_features(self, batch): if self.center_crop: crop_size = 299 h, w = batch.shape[2], batch.shape[3] assert h >= crop_size and w >= crop_size h_offset = (h - crop_size) // 2 w_offset = (w - crop_size) // 2 batch = batch[:, :, h_offset:h_offset + crop_size, w_offset:w_offset + crop_size] elif self.resize: batch = F.interpolate( batch, size=(299, 299), mode='bicubic', align_corners=False, antialias=True).clamp(min=-1, max=1) assert self.inception_style == 'StyleGAN' batch = (batch * 127.5 + 128).clamp(0, 255).to(torch.uint8) feat = self.inception_net(batch, return_features=True) pred = F.linear(feat, self.inception_net.output.weight).softmax(dim=1) return feat, pred @torch.no_grad() def feed_op(self, batch, mode): if self.bgr2rgb: batch = batch[:, [2, 1, 0]] batch = batch.to(self.device) feat, pred = self.extract_features(batch) if dist.is_initialized(): ws = dist.get_world_size() placeholder = [torch.zeros_like(feat) for _ in range(ws)] dist.all_gather(placeholder, feat) feat = torch.stack(placeholder, dim=1).reshape(feat.size(0) * ws, *feat.shape[1:]) if mode == 'fakes': placeholder = [torch.zeros_like(pred) for _ in range(ws)] dist.all_gather(placeholder, pred) pred = torch.stack(placeholder, dim=1).reshape(pred.size(0) * ws, *pred.shape[1:]) # in distributed training, we only collect features at rank-0. if (dist.is_initialized() and dist.get_rank() == 0) or not dist.is_initialized(): if mode == 'reals': self.real_feats.append(feat.cpu()) elif mode == 'fakes': self.fake_feats.append(feat.cpu()) self.preds.append(pred.cpu().numpy()) else: raise ValueError( f"The expected mode should be set to 'reals' or 'fakes,\ but got '{mode}'") def feed(self, batch, mode): if self.num_images is None: self.feed_op(batch, mode) else: _, ws = get_dist_info() if mode == 'reals': if self.num_real_feeded == self.num_real_need: return 0 if isinstance(batch, dict): batch_size = len(list(batch.values())[0]) end = min(batch_size, self.num_real_need - self.num_real_feeded) batch_to_feed = {k: v[:end] for k, v in batch.items()} else: batch_size = batch.shape[0] end = min(batch_size, self.num_real_need - self.num_real_feeded) batch_to_feed = batch[:end] global_end = min(batch_size * ws, self.num_real_need - self.num_real_feeded) self.feed_op(batch_to_feed, mode) self.num_real_feeded += global_end return end elif mode == 'fakes': if self.num_fake_feeded == self.num_fake_need: return 0 if isinstance(batch, dict): batch_size = len(list(batch.values())[0]) end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = {k: v[:end] for k, v in batch.items()} else: batch_size = batch.shape[0] end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = batch[:end] global_end = min(batch_size * ws, self.num_fake_need - self.num_fake_feeded) self.feed_op(batch_to_feed, mode) self.num_fake_feeded += global_end return end else: raise ValueError( 'The expected mode should be set to \'reals\' or \'fakes\',' f'but got \'{mode}\'') @torch.no_grad() def summary(self): real_feats = torch.cat(self.real_feats, dim=0) fake_feats = torch.cat(self.fake_feats, dim=0) if self.num_images is not None: assert fake_feats.shape[0] >= self.num_images fake_feats = fake_feats[:self.num_images] if self.reference_pkl is None: # real feats not pre-calculated assert real_feats.shape[0] >= self.num_images real_feats = real_feats[:self.num_images] if self.real_feats_np is None: real_feats_np = real_feats.numpy() self.real_feats_np = real_feats_np self.real_mean = np.mean(real_feats_np, 0) self.real_cov = np.cov(real_feats_np, rowvar=False) self._result_dict = dict() prefix = self.prefix + '_' if len(self.prefix) > 0 else '' # FID fake_feats_np = fake_feats.numpy() fake_mean = np.mean(fake_feats_np, 0) fake_cov = np.cov(fake_feats_np, rowvar=False) fid, mean, cov = self._calc_fid(fake_mean, fake_cov, self.real_mean, self.real_cov) self._result_dict.update({f'{prefix}fid': fid}) _result_str = f'{prefix}FID: {fid:.4f} ({mean:.4f}/{cov:.4f})' # KID if self.use_kid: kid = self._calc_kid(self.real_feats_np, fake_feats_np, self.kid_num_subsets, self.kid_max_subset_size) * 1000 self._result_dict.update({f'{prefix}kid': kid}) _result_str += f', {prefix}KID: {kid:.4f}' else: kid = None # PR if self.use_pr: for name, manifold, probes in [ (f'{prefix}precision', real_feats, fake_feats), (f'{prefix}recall', fake_feats, real_feats) ]: kth = [] for manifold_batch in manifold.split(self.pr_row_batch_size): distance = compute_pr_distances( row_features=manifold_batch, col_features=manifold, col_batch_size=self.pr_col_batch_size) kth.append( distance.to(torch.float32).kthvalue(self.pr_k + 1).values.to(torch.float16)) kth = torch.cat(kth) pred = [] for probes_batch in probes.split(self.pr_row_batch_size): distance = compute_pr_distances( row_features=probes_batch, col_features=manifold, col_batch_size=self.pr_col_batch_size) pred.append((distance <= kth).any(dim=1)) self._result_dict[name] = float(torch.cat(pred).to(torch.float32).mean()) precision = self._result_dict[f'{prefix}precision'] recall = self._result_dict[f'{prefix}recall'] _result_str += f', {prefix}Precision: {precision:.5f}, {prefix}Recall:{recall:.5f}' else: precision = recall = None # IS if self.use_is: split_scores = [] self.preds = np.concatenate(self.preds, axis=0) if self.num_images is not None: assert self.preds.shape[0] >= self.num_images self.preds = self.preds[:self.num_images] num_preds = self.preds.shape[0] for k in range(self.is_splits): part = self.preds[k * (num_preds // self.is_splits):(k + 1) * (num_preds // self.is_splits), :] py = np.mean(part, axis=0) scores = [] for i in range(part.shape[0]): pyx = part[i, :] scores.append(entropy(pyx, py)) split_scores.append(np.exp(np.mean(scores))) is_mean = np.mean(split_scores) self._result_dict.update({f'{prefix}is': is_mean}) _result_str += f', {prefix}IS: {is_mean:.2f}' else: is_mean = None self._result_str = _result_str return fid, kid, precision, recall, is_mean def clear_fake_data(self): self.fake_feats = [] self.preds = [] self.num_fake_feeded = 0 def clear(self, clear_reals=False): self.clear_fake_data() if clear_reals: self.real_feats = [] self.real_feats_np = None self.num_real_feeded = 0 def load_to_gpu(self): """Move models to GPU.""" if torch.cuda.is_available(): self.inception_net.cuda() self.device = 'cuda' def offload_to_cpu(self): """Move models to CPU.""" self.inception_net.cpu() self.device = 'cpu' @METRICS.register_module() class ColorStats(Metric): name = 'ColorStats' def __init__(self, num_images=None): super().__init__(num_images) def prepare(self): self.stats = [] @staticmethod def srgb_to_linear(c): threshold = 0.04045 below = c <= threshold out = torch.where( below, c / 12.92, ((c + 0.055) / 1.055) ** 2.4) return out @staticmethod def linear_to_srgb(c): threshold = 0.0031308 below = c <= threshold out = torch.where( below, 12.92 * c, 1.055 * c ** (1.0 / 2.4) - 0.055) return out def rgb_to_grayscale_srgb(self, img_srgb): img_lin = self.srgb_to_linear(img_srgb) R_lin, G_lin, B_lin = img_lin.unbind(dim=1) Y_lin = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin gray_srgb = self.linear_to_srgb(Y_lin) return gray_srgb @staticmethod def srgb_to_hsv_saturation(img_srgb): c_max = torch.amax(img_srgb, dim=1) c_min = torch.amin(img_srgb, dim=1) delta = c_max - c_min sat = delta / c_max.clamp(min=1e-5) return sat def compute_stats(self, batch): batch = (batch / 2 + 0.5).clamp(0, 1) gray = self.rgb_to_grayscale_srgb(batch).flatten(1) contrast, brightness = torch.std_mean(gray, dim=1) saturation = self.srgb_to_hsv_saturation(batch).flatten(1).mean(dim=1) return torch.stack([brightness, contrast, saturation], dim=-1) @torch.no_grad() def feed_op(self, batch, mode): stats = self.compute_stats(batch) if dist.is_initialized(): ws = dist.get_world_size() placeholder = [torch.zeros_like(stats) for _ in range(ws)] dist.all_gather(placeholder, stats) stats = torch.stack(placeholder, dim=1).reshape(stats.size(0) * ws, *stats.shape[1:]) # in distributed training, we only collect features at rank-0. if (dist.is_initialized() and dist.get_rank() == 0) or not dist.is_initialized(): self.stats.append(stats.cpu()) def feed(self, batch, mode): if mode == 'reals': return 0 if self.num_images is None: self.feed_op(batch, mode) else: _, ws = get_dist_info() if self.num_fake_feeded == self.num_fake_need: return 0 if isinstance(batch, dict): batch_size = len(list(batch.values())[0]) end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = {k: v[:end] for k, v in batch.items()} else: batch_size = batch.shape[0] end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = batch[:end] global_end = min(batch_size * ws, self.num_fake_need - self.num_fake_feeded) self.feed_op(batch_to_feed, mode) self.num_fake_feeded += global_end return end @torch.no_grad() def summary(self): stats = torch.cat(self.stats, dim=0) if self.num_images is not None: assert stats.shape[0] >= self.num_images stats = stats[:self.num_images] stats = stats.mean(dim=0) brightness, contrast, saturation = stats.tolist() self._result_dict = dict( brightness=brightness, contrast=contrast, saturation=saturation) self._result_str = f'Brightness: {brightness:.4f}, Contrast: {contrast:.4f}, Saturation: {saturation:.4f}' return brightness, contrast, saturation def clear_fake_data(self): self.stats = [] self.num_fake_feeded = 0 def clear(self, clear_reals=False): self.clear_fake_data() @METRICS.register_module() class HPSv2(Metric): name = 'HPSv2' requires_prompt = True def __init__(self, num_images=None, hps_version='v2.1'): super().__init__(num_images) self.hps_version = hps_version self.device = 'cpu' # Initialize on CPU self.dtype = torch.float16 self.model, self.tokenizer = load_hpsv2(hps_version, device=self.device, precision='fp16') self.model.eval() image_size = self.model.visual.image_size if isinstance(image_size, tuple): assert len(image_size) == 2 and image_size[0] == image_size[1] image_size = image_size[0] self.image_size = image_size self.image_mean = torch.tensor(self.model.visual.image_mean, device=self.device).view(3, 1, 1) self.image_std = torch.tensor(self.model.visual.image_std, device=self.device).view(3, 1, 1) def prepare(self): self.scores = [] def resize(self, imgs): h, w = imgs.shape[2:] scale = self.image_size / float(max(h, w)) if scale != 1.0: h = int(round(h * scale)) w = int(round(w * scale)) imgs = F.interpolate(imgs, size=(h, w), mode='bicubic', align_corners=False, antialias=True).clamp(0, 1) if h != w: pad_h = self.image_size - h pad_w = self.image_size - w imgs = F.pad( imgs, (pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2), mode='constant', value=0) return imgs @torch.no_grad() def feed_op(self, batch, mode): imgs = batch['imgs'] prompts = batch['prompts'] imgs = (imgs.to(device=self.device, dtype=torch.float32) / 2 + 0.5).clamp(0, 1) imgs = ((self.resize(imgs) - self.image_mean) / self.image_std).to(dtype=self.dtype) prompts = self.tokenizer(prompts).to(device=self.device) outputs = self.model(imgs, prompts) image_features, text_features = outputs['image_features'], outputs['text_features'] hps_scores = (image_features * text_features).sum(dim=-1) # (bs, ) if dist.is_initialized(): ws = dist.get_world_size() placeholder = [torch.empty_like(hps_scores) for _ in range(ws)] dist.all_gather(placeholder, hps_scores) hps_scores = torch.stack(placeholder, dim=1).reshape(hps_scores.size(0) * ws) if (dist.is_initialized() and dist.get_rank() == 0) or not dist.is_initialized(): self.scores.append(hps_scores.float().cpu()) def feed(self, batch, mode): if mode == 'reals': return 0 if self.num_images is None: self.feed_op(batch, mode) else: _, ws = get_dist_info() if self.num_fake_feeded == self.num_fake_need: return 0 if isinstance(batch, dict): batch_size = len(list(batch.values())[0]) end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = {k: v[:end] for k, v in batch.items()} else: batch_size = batch.shape[0] end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = batch[:end] global_end = min(batch_size * ws, self.num_fake_need - self.num_fake_feeded) self.feed_op(batch_to_feed, mode) self.num_fake_feeded += global_end return end @torch.no_grad() def summary(self): scores = torch.cat(self.scores, dim=0) if self.num_images is not None: assert scores.shape[0] >= self.num_images scores = scores[:self.num_images] mean_score = scores.mean().item() self._result_dict = dict(hpsv2=mean_score) self._result_str = f'HPSv2: {mean_score:.4f}' return mean_score def clear_fake_data(self): self.scores = [] self.num_fake_feeded = 0 def clear(self, clear_reals=False): self.clear_fake_data() def load_to_gpu(self): if torch.cuda.is_available(): self.model.cuda() self.image_mean = self.image_mean.cuda() self.image_std = self.image_std.cuda() self.device = 'cuda' def offload_to_cpu(self): self.model.cpu() self.image_mean = self.image_mean.cpu() self.image_std = self.image_std.cpu() self.device = 'cpu' @METRICS.register_module() class CLIPSimilarity(Metric): """ Average image–text CLIP cosine similarity (↑ better). Preprocess emulates OpenAI CLIP for ViT-L/14@336: - Resize so min(H, W) = 336 (bicubic, antialias), keep aspect ratio - Center crop to 336x336 - Normalize with model.visual.image_mean/std Expects batch = {'imgs': (B,3,H,W) in [-1,1], 'prompts': List[str]} """ name = 'CLIPSimilarity' requires_prompt = True def __init__( self, num_images=None, model_name='ViT-L-14-336-quickgelu', pretrained='openai', precision='fp16', # 'fp16' | 'fp32' | 'bf16' ): super().__init__(num_images) self.model_name = model_name self.pretrained = pretrained self.precision = precision self.device = 'cpu' self.dtype = { 'fp16': torch.float16, 'bf16': torch.bfloat16, 'fp32': torch.float32 }.get(precision, torch.float16) self.model, self.tokenizer = load_openclip( model_name=model_name, pretrained=pretrained, device=self.device, precision=precision, ) self.model.eval() # OpenAI ViT-L/14@336 uses square 336 input image_size = self.model.visual.image_size if isinstance(image_size, tuple): assert len(image_size) == 2 and image_size[0] == image_size[1] image_size = image_size[0] self.image_size = int(image_size) # 336 # Use the model's own stats for normalization self.image_mean = torch.tensor(self.model.visual.image_mean, device=self.device).view(3, 1, 1) self.image_std = torch.tensor(self.model.visual.image_std, device=self.device).view(3, 1, 1) def prepare(self): self.scores = [] def _resize_min_side_then_center_crop(self, imgs): """ imgs: (B,3,H,W) in [0,1], float32, on self.device 1) Resize so min(H,W) == self.image_size, preserve AR (bicubic, antialias) 2) Center-crop to (self.image_size, self.image_size) 3) Normalize with model mean/std 4) Cast to self.dtype """ _, _, H, W = imgs.shape target = self.image_size # Scale factor so that the shorter side becomes 'target' short, long = (H, W) if H < W else (W, H) if short == 0: raise ValueError("Invalid image with zero dimension.") scale = target / float(short) new_h = max(1, int(round(H * scale))) new_w = max(1, int(round(W * scale))) if new_h != H or new_w != W: imgs = F.interpolate( imgs, size=(new_h, new_w), mode='bicubic', align_corners=False, antialias=True ).clamp(0, 1) # Center crop to target x target top = max(0, (new_h - target) // 2) left = max(0, (new_w - target) // 2) imgs = imgs[:, :, top:top + target, left:left + target] imgs = (imgs - self.image_mean) / self.image_std return imgs.to(dtype=self.dtype) @torch.no_grad() def feed_op(self, batch, mode): if mode == 'reals': return 0 imgs = batch['imgs'] prompts = batch['prompts'] # [-1,1] -> [0,1] imgs = (imgs.to(device=self.device, dtype=torch.float32) / 2 + 0.5).clamp(0, 1) imgs = self._resize_min_side_then_center_crop(imgs) # Tokenize on device text = self.tokenizer(prompts).to(device=self.device) # Forward (create_model(..., output_dict=True)) => dict w/ features out = self.model(imgs, text) if isinstance(out, dict) and ('image_features' in out and 'text_features' in out): img_feat = out['image_features'] txt_feat = out['text_features'] else: img_feat = self.model.encode_image(imgs) txt_feat = self.model.encode_text(text) # Cosine similarity per pair img_feat = F.normalize(img_feat, dim=-1) txt_feat = F.normalize(txt_feat, dim=-1) sim = (img_feat * txt_feat).sum(dim=-1).to(torch.float32) # (B,) # DDP gather if dist.is_initialized(): ws = dist.get_world_size() bucket = [torch.empty_like(sim) for _ in range(ws)] dist.all_gather(bucket, sim) sim = torch.stack(bucket, dim=1).reshape(sim.size(0) * ws) if (dist.is_initialized() and dist.get_rank() == 0) or not dist.is_initialized(): self.scores.append(sim.cpu()) def feed(self, batch, mode): if mode == 'reals': return 0 if self.num_images is None: self.feed_op(batch, mode) else: _, ws = get_dist_info() if self.num_fake_feeded == self.num_fake_need: return 0 if isinstance(batch, dict): batch_size = len(list(batch.values())[0]) end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = {k: v[:end] for k, v in batch.items()} else: batch_size = batch.shape[0] end = min(batch_size, self.num_fake_need - self.num_fake_feeded) batch_to_feed = batch[:end] global_end = min(batch_size * ws, self.num_fake_need - self.num_fake_feeded) self.feed_op(batch_to_feed, mode) self.num_fake_feeded += global_end return end @torch.no_grad() def summary(self): sims = torch.cat(self.scores, dim=0) if self.num_images is not None: assert sims.shape[0] >= self.num_images sims = sims[:self.num_images] mean_sim = sims.mean().item() self._result_dict = dict(clipsim=mean_sim) # raw cosine in [-1,1] self._result_str = f'CLIPSim: {mean_sim:.4f}' return mean_sim def clear_fake_data(self): self.scores = [] self.num_fake_feeded = 0 def clear(self, clear_reals=False): self.clear_fake_data() def load_to_gpu(self): if torch.cuda.is_available(): self.model.cuda() self.image_mean = self.image_mean.cuda() self.image_std = self.image_std.cuda() self.device = 'cuda' def offload_to_cpu(self): self.model.cpu() self.image_mean = self.image_mean.cpu() self.image_std = self.image_std.cpu() self.device = 'cpu'