# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib import paddle import warnings import math import numpy as np from PIL import Image from typing import Union, Optional, List, Tuple, Text, BinaryIO @paddle.no_grad() def make_grid(tensor: Union[paddle.Tensor, List[paddle.Tensor]], nrow: int=8, padding: int=2, normalize: bool=False, value_range: Optional[Tuple[int, int]]=None, scale_each: bool=False, pad_value: int=0, **kwargs) -> paddle.Tensor: """Make a grid of images. Args: tensor (Tensor or list): 4D mini-batch Tensor of shape (B x C x H x W) or a list of images all of the same size. nrow (int, optional): Number of images displayed in each row of the grid. The final grid size is ``(B / nrow, nrow)``. Default: ``8``. padding (int, optional): amount of padding. Default: ``2``. normalize (bool, optional): If True, shift the image to the range (0, 1), by the min and max values specified by :attr:`range`. Default: ``False``. value_range (tuple, optional): tuple (min, max) where min and max are numbers, then these numbers are used to normalize the image. By default, min and max are computed from the tensor. scale_each (bool, optional): If ``True``, scale each image in the batch of images separately rather than the (min, max) over all images. Default: ``False``. pad_value (float, optional): Value for the padded pixels. Default: ``0``. Example: See this notebook `here `_ """ if not (isinstance(tensor, paddle.Tensor) or (isinstance(tensor, list) and all( isinstance(t, paddle.Tensor) for t in tensor))): raise TypeError( f'tensor or list of tensors expected, got {type(tensor)}') if "range" in kwargs.keys(): warning = "range will be deprecated, please use value_range instead." warnings.warn(warning) value_range = kwargs["range"] # if list of tensors, convert to a 4D mini-batch Tensor if isinstance(tensor, list): tensor = paddle.stack(tensor, axis=0) if tensor.dim() == 2: # single image H x W tensor = tensor.unsqueeze(0) if tensor.dim() == 3: # single image if tensor.size(0) == 1: # if single-channel, convert to 3-channel tensor = paddle.concat((tensor, tensor, tensor), 0) tensor = tensor.unsqueeze(0) if tensor.dim() == 4 and tensor.size(1) == 1: # single-channel images tensor = paddle.concat((tensor, tensor, tensor), 1) if normalize is True: if value_range is not None: assert isinstance(value_range, tuple), \ "value_range has to be a tuple (min, max) if specified. min and max are numbers" def norm_ip(img, low, high): img.clip(min=low, max=high) img = img - low img = img / max(high - low, 1e-5) def norm_range(t, value_range): if value_range is not None: norm_ip(t, value_range[0], value_range[1]) else: norm_ip(t, float(t.min()), float(t.max())) if scale_each is True: for t in tensor: # loop over mini-batch dimension norm_range(t, value_range) else: norm_range(tensor, value_range) if tensor.size(0) == 1: return tensor.squeeze(0) # make the mini-batch of images into a grid nmaps = tensor.size(0) xmaps = min(nrow, nmaps) ymaps = int(math.ceil(float(nmaps) / xmaps)) height, width = int(tensor.shape[2] + padding), int(tensor.shape[3] + padding) num_channels = tensor.shape[1] grid = paddle.full((num_channels, height * ymaps + padding, width * xmaps + padding), pad_value) k = 0 for y in range(ymaps): for x in range(xmaps): if k >= nmaps: break grid[:, y * height + padding:(y + 1) * height, x * width + padding:( x + 1) * width] = tensor[k] k = k + 1 return grid @paddle.no_grad() def save_image(tensor: Union[paddle.Tensor, List[paddle.Tensor]], fp: Union[Text, pathlib.Path, BinaryIO], format: Optional[str]=None, **kwargs) -> None: """Save a given Tensor into an image file. Args: tensor (Tensor or list): Image to be saved. If given a mini-batch tensor, saves the tensor as a grid of images by calling ``make_grid``. fp (string or file object): A filename or a file object format(Optional): If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used. **kwargs: Other arguments are documented in ``make_grid``. """ grid = make_grid(tensor, **kwargs) # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer ndarr = paddle.clip(grid * 255 + 0.5, 0, 255).transpose( [1, 2, 0]).cast("uint8").numpy() im = Image.fromarray(ndarr) im.save(fp, format=format)