"""Contains transform functions.""" import cv2 import numpy as np import PIL.Image import torch import torch.nn as nn import torch.nn.functional as F __all__ = [ 'crop_resize_image', 'progressive_resize_image', 'resize_image', 'normalize_image', 'normalize_latent_code', 'ImageResizing', 'ImageNormalization', 'LatentCodeNormalization', ] def crop_resize_image(image, size): """Crops a square patch and then resizes it to the given size. Args: image: The input image to crop and resize. size: An integer, indicating the target size. Returns: An image with target size. Raises: TypeError: If the input `image` is not with type `numpy.ndarray`. ValueError: If the input `image` is not with shape [H, W, C]. """ if not isinstance(image, np.ndarray): raise TypeError(f'Input image should be with type `numpy.ndarray`, ' f'but `{type(image)}` is received!') if image.ndim != 3: raise ValueError(f'Input image should be with shape [H, W, C], ' f'but `{image.shape}` is received!') height, width, channel = image.shape short_side = min(height, width) image = image[(height - short_side) // 2:(height + short_side) // 2, (width - short_side) // 2:(width + short_side) // 2] pil_image = PIL.Image.fromarray(image) pil_image = pil_image.resize((size, size), PIL.Image.ANTIALIAS) image = np.asarray(pil_image) assert image.shape == (size, size, channel) return image def progressive_resize_image(image, size): """Resizes image to target size progressively. Different from normal resize, this function will reduce the image size progressively. In each step, the maximum reduce factor is 2. NOTE: This function can only handle square images, and can only be used for downsampling. Args: image: The input (square) image to resize. size: An integer, indicating the target size. Returns: An image with target size. Raises: TypeError: If the input `image` is not with type `numpy.ndarray`. ValueError: If the input `image` is not with shape [H, W, C]. """ if not isinstance(image, np.ndarray): raise TypeError(f'Input image should be with type `numpy.ndarray`, ' f'but `{type(image)}` is received!') if image.ndim != 3: raise ValueError(f'Input image should be with shape [H, W, C], ' f'but `{image.shape}` is received!') height, width, channel = image.shape assert height == width assert height >= size num_iters = int(np.log2(height) - np.log2(size)) for _ in range(num_iters): height = max(height // 2, size) image = cv2.resize(image, (height, height), interpolation=cv2.INTER_LINEAR) assert image.shape == (size, size, channel) return image def resize_image(image, size): """Resizes image to target size. NOTE: We use adaptive average pooing for image resizing. Instead of bilinear interpolation, average pooling is able to acquire information from more pixels, such that the resized results can be with higher quality. Args: image: The input image tensor, with shape [C, H, W], to resize. size: An integer or a tuple of integer, indicating the target size. Returns: An image tensor with target size. Raises: TypeError: If the input `image` is not with type `torch.Tensor`. ValueError: If the input `image` is not with shape [C, H, W]. """ if not isinstance(image, torch.Tensor): raise TypeError(f'Input image should be with type `torch.Tensor`, ' f'but `{type(image)}` is received!') if image.ndim != 3: raise ValueError(f'Input image should be with shape [C, H, W], ' f'but `{image.shape}` is received!') image = F.adaptive_avg_pool2d(image.unsqueeze(0), size).squeeze(0) return image def normalize_image(image, mean=127.5, std=127.5): """Normalizes image by subtracting mean and dividing std. Args: image: The input image tensor to normalize. mean: The mean value to subtract from the input tensor. (default: 127.5) std: The standard deviation to normalize the input tensor. (default: 127.5) Returns: A normalized image tensor. Raises: TypeError: If the input `image` is not with type `torch.Tensor`. """ if not isinstance(image, torch.Tensor): raise TypeError(f'Input image should be with type `torch.Tensor`, ' f'but `{type(image)}` is received!') out = (image - mean) / std return out def normalize_latent_code(latent_code, adjust_norm=True): """Normalizes latent code. NOTE: The latent code will always be normalized along the last axis. Meanwhile, if `adjust_norm` is set as `True`, the norm of the result will be adjusted to `sqrt(latent_code.shape[-1])` in order to avoid too small value. Args: latent_code: The input latent code tensor to normalize. adjust_norm: Whether to adjust the norm of the output. (default: True) Returns: A normalized latent code tensor. Raises: TypeError: If the input `latent_code` is not with type `torch.Tensor`. """ if not isinstance(latent_code, torch.Tensor): raise TypeError(f'Input latent code should be with type ' f'`torch.Tensor`, but `{type(latent_code)}` is ' f'received!') dim = latent_code.shape[-1] norm = latent_code.pow(2).sum(-1, keepdim=True).pow(0.5) out = latent_code / norm if adjust_norm: out = out * (dim ** 0.5) return out class ImageResizing(nn.Module): """Implements the image resizing layer.""" def __init__(self, size): super().__init__() self.size = size def forward(self, image): return resize_image(image, self.size) class ImageNormalization(nn.Module): """Implements the image normalization layer.""" def __init__(self, mean=127.5, std=127.5): super().__init__() self.mean = mean self.std = std def forward(self, image): return normalize_image(image, self.mean, self.std) class LatentCodeNormalization(nn.Module): """Implements the latent code normalization layer.""" def __init__(self, adjust_norm=True): super().__init__() self.adjust_norm = adjust_norm def forward(self, latent_code): return normalize_latent_code(latent_code, self.adjust_norm)