From e9ece08d666eaf608960f7bf185c7fafe57d41ea Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Thu, 3 Sep 2020 16:13:19 +0530 Subject: [PATCH 01/12] Added GaussianBlur transform --- torchvision/transforms/functional.py | 16 ++++ torchvision/transforms/functional_pil.py | 21 ++++- torchvision/transforms/functional_tensor.py | 92 ++++++++++++++++++++- torchvision/transforms/transforms.py | 62 +++++++++++++- 4 files changed, 187 insertions(+), 4 deletions(-) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index 4a36e0b05e6..cac1c62dd57 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -1026,3 +1026,19 @@ def erase(img: Tensor, i: int, j: int, h: int, w: int, v: Tensor, inplace: bool img[:, i:i + h, j:j + w] = v return img + + +def gaussian_blur(img: Tensor, radius: float) -> Tensor: + """Performs Gaussian blurring on the img by given kernel. + + Args: + img (PIL Image or Tensor): Image to be blurred + radius (float): Blur radius + + Returns: + PIL Image or Tensor: Gaussian Blurred version of the image. + """ + if not isinstance(img, torch.Tensor): + return F_pil.gaussian_blur(img, radius) + + return F_t.gaussian_blur(img, radius) diff --git a/torchvision/transforms/functional_pil.py b/torchvision/transforms/functional_pil.py index ba620ab9d9c..d9466d251a8 100644 --- a/torchvision/transforms/functional_pil.py +++ b/torchvision/transforms/functional_pil.py @@ -3,7 +3,7 @@ import numpy as np import torch -from PIL import Image, ImageOps, ImageEnhance, __version__ as PILLOW_VERSION +from PIL import Image, ImageOps, ImageEnhance, ImageFilter, __version__ as PILLOW_VERSION try: import accimage @@ -517,3 +517,22 @@ def to_grayscale(img, num_output_channels): raise ValueError('num_output_channels should be either 1 or 3') return img + + +@torch.jit.unused +def gaussian_blur(img: Image.Image, radius: float) -> Image.Image: + """Performs Gaussian blurring on the img by given kernel. + + Args: + img (Tensor): Image to be blurred + radius (float): Blur radius + + Returns: + Tensor: An image that is blurred using kernel of given radius + """ + if not _is_pil_image(img): + raise TypeError('img should be PIL Image. Got {}'.format(type(img))) + + blurred_img = img.filter(ImageFilter.GaussianBlur(radius)) + + return blurred_img diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 6b581abd8d9..3c75e47052b 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -3,7 +3,7 @@ import torch from torch import Tensor -from torch.nn.functional import grid_sample +from torch.nn.functional import grid_sample, conv2d from torch.jit.annotations import List, BroadcastingList2 @@ -462,6 +462,29 @@ def _hsv2rgb(img): return torch.einsum("ijk, xijk -> xjk", mask.to(dtype=img.dtype), a4) +def _compute_padding(kernel_size: List[int]) -> List[int]: + """Computes padding tuple.""" + # 4 ints: (padding_left, padding_top, padding_right, padding_bottom) + + # This function is modified from kornia: + # https://github.com/kornia/kornia/blob/85661d982e6349ffac237051164cf38cc604454b/kornia/filters/filter.py#L11 + + assert len(kernel_size) >= 2, kernel_size + padding_tmp= [k // 2 for k in kernel_size] + + # for even kernels we need to do asymetric padding :( + + out_padding = [0, 0] * len(kernel_size) + + for i, (ksize, pad_tmp) in enumerate(zip(kernel_size, padding_tmp)): + padding = pad_tmp if ksize % 2 else pad_tmp - 1 + + out_padding[i] = padding + out_padding[i+2] = pad_tmp + + return out_padding + + def _pad_symmetric(img: Tensor, padding: List[int]) -> Tensor: # padding is left, right, top, bottom in_sizes = img.size() @@ -917,3 +940,70 @@ def perspective( mode = _interpolation_modes[interpolation] return _apply_grid_transform(img, grid, mode) + + +def _get_kernel(radius: float, passes: int): + sigma2 = torch.Tensor([radius ** 2 / passes]) + + kernel_rad = (torch.sqrt(12. * sigma2 + 1.) - 1.) / 2. + + kernel_rad_int = kernel_rad.long().item() + + kernel_rad_float = (2 * kernel_rad_int + 1) * (kernel_rad_int * (kernel_rad_int + 1) - 3 * sigma2) + kernel_rad_float /= 6 * (sigma2 - (kernel_rad_int + 1) * (kernel_rad_int + 1)) + kernel_rad_float = kernel_rad_float.item() + + kernel_rad = kernel_rad_int + kernel_rad_float + + ksize = 2 * kernel_rad_int + 1 + 2 * (kernel_rad_float > 0) + kernel1d = torch.ones(ksize) / (2 * kernel_rad + 1) + + if kernel_rad_float > 0: + kernel1d[[0, -1]] = kernel_rad_float / (2 * kernel_rad + 1) + + kernel2d = torch.mm(kernel1d[:, None], kernel1d[None, :]) + + return kernel2d + + +def gaussian_blur(img: Tensor, radius: float) -> Tensor: + """Performs Gaussian blurring on the img by given kernel. + + Args: + img (Tensor): Image to be blurred + radius (float): Blur radius + + Returns: + Tensor: An image that is blurred using kernel of given radius + """ + if not (isinstance(img, torch.Tensor) and _is_tensor_a_torch_image(img)): + raise TypeError('img should be Tensor Image. Got {}'.format(type(img))) + if not isinstance(radius, (float, int)): + raise TypeError('radius should be either float or int. Got {}'.format(type(radius))) + + radius = float(radius) + passes = 3 + + ndim = img.ndim + if ndim == 2: + img = img.unsqueeze(0) + if ndim == 3: + img = img.unsqueeze(0) + + kernel = _get_kernel(radius, passes) + + padding = _compute_padding(kernel.shape[::-1]) + + kernel = kernel[None, None, :, :].repeat(img.size(-3), 1, 1, 1) + + padded_img = pad(img, padding, padding_mode='edge') + blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) + for n_iter in range(passes-1): + padded_img = pad(blurred_img, padding, padding_mode='edge') + blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) + + if ndim == 2: + return blurred_img[0, 0] + if ndim == 3: + return blurred_img[0] + return blurred_img diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py index b995101c3c7..d10d47642eb 100644 --- a/torchvision/transforms/transforms.py +++ b/torchvision/transforms/transforms.py @@ -16,12 +16,11 @@ from . import functional as F - __all__ = ["Compose", "ToTensor", "PILToTensor", "ConvertImageDtype", "ToPILImage", "Normalize", "Resize", "Scale", "CenterCrop", "Pad", "Lambda", "RandomApply", "RandomChoice", "RandomOrder", "RandomCrop", "RandomHorizontalFlip", "RandomVerticalFlip", "RandomResizedCrop", "RandomSizedCrop", "FiveCrop", "TenCrop", "LinearTransformation", "ColorJitter", "RandomRotation", "RandomAffine", "Grayscale", "RandomGrayscale", - "RandomPerspective", "RandomErasing"] + "RandomPerspective", "RandomErasing", "GaussianBlur"] _pil_interpolation_to_str = { Image.NEAREST: 'PIL.Image.NEAREST', @@ -1545,3 +1544,62 @@ def forward(self, img): x, y, h, w, v = self.get_params(img, scale=self.scale, ratio=self.ratio, value=value) return F.erase(img, x, y, h, w, v, self.inplace) return img + + +class GaussianBlur(torch.nn.Module): + """Blurs image with randomly chosen Gaussian blur. + The image can be a PIL Image or a Tensor, in which case it is expected + to have [..., 3, H, W] shape, where ... means an arbitrary number of leading + dimensions + + Args: + rad_min (float): Minimum radius that can be chosen for blurring kernel. + rad_max (float): Maximum radius that can be chosen for blurring kernel. + + Returns: + PIL Image or Tensor: Gaussian blurred version of the input image. + + """ + + def __init__(self, rad_min=0.1, rad_max=2.0): + super().__init__() + + if rad_min < 0: + raise ValueError("Random Gaussian Blur minimum radius should be between non-negative") + if rad_max < 0: + raise ValueError("Random Gaussian Blur maximum radius should be between non-negative") + + if rad_min > rad_max: + warnings.warn("minimum radius should be <= maximum radius. For now, their values will be swapped.") + rad_min, rad_max = rad_max, rad_min + + self.rad_min = rad_min + self.rad_max = rad_max + + @staticmethod + def get_params(rad_min: float, rad_max: float): + """Choose radius for ``gaussian_blur`` for random gaussian blurring. + + Args: + rad_min (float): Minimum radius that can be chosen for blurring kernel. + rad_max (float): Maximum radius that can be chosen for blurring kernel. + + Returns: + float: radius be passed to ``gaussian_blur`` for gaussian blurring. + """ + radius = torch.rand(1).item() * (rad_max - rad_min) + rad_min + return radius + + def forward(self, img): + """ + Args: + img (PIL Image or Tensor): image of size (C, H, W) to be blurred. + + Returns: + PIL Image or Tensor: Gaussian blurred image + """ + radius = self.get_params(self.rad_min, self.rad_max) + return F.gaussian_blur(img, radius) + + def __repr__(self): + return self.__class__.__name__ + '(rad_min={0}, rad_max={1})'.format(self.rad_min, self.rad_max) From 2b7ced5f755482e00541039a92d3c735d7929346 Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Wed, 9 Sep 2020 14:10:12 +0530 Subject: [PATCH 02/12] fixed linting --- torchvision/transforms/functional_tensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 3c75e47052b..cb9c3a7cd7a 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -470,7 +470,7 @@ def _compute_padding(kernel_size: List[int]) -> List[int]: # https://github.com/kornia/kornia/blob/85661d982e6349ffac237051164cf38cc604454b/kornia/filters/filter.py#L11 assert len(kernel_size) >= 2, kernel_size - padding_tmp= [k // 2 for k in kernel_size] + padding_tmp = [k // 2 for k in kernel_size] # for even kernels we need to do asymetric padding :( @@ -479,8 +479,8 @@ def _compute_padding(kernel_size: List[int]) -> List[int]: for i, (ksize, pad_tmp) in enumerate(zip(kernel_size, padding_tmp)): padding = pad_tmp if ksize % 2 else pad_tmp - 1 - out_padding[i] = padding - out_padding[i+2] = pad_tmp + out_padding[i] = padding + out_padding[i + 2] = pad_tmp return out_padding @@ -998,8 +998,8 @@ def gaussian_blur(img: Tensor, radius: float) -> Tensor: padded_img = pad(img, padding, padding_mode='edge') blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) - for n_iter in range(passes-1): - padded_img = pad(blurred_img, padding, padding_mode='edge') + for _ in range(passes - 1): + padded_img = pad(blurred_img, padding, padding_mode='edge') blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) if ndim == 2: From 6cd6595964068ae701b81ef531fe9705835080fd Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Thu, 10 Sep 2020 12:19:29 +0530 Subject: [PATCH 03/12] supports fixed radius for kernel --- torchvision/transforms/transforms.py | 30 +++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py index d10d47642eb..ad67735c64c 100644 --- a/torchvision/transforms/transforms.py +++ b/torchvision/transforms/transforms.py @@ -1553,28 +1553,30 @@ class GaussianBlur(torch.nn.Module): dimensions Args: - rad_min (float): Minimum radius that can be chosen for blurring kernel. - rad_max (float): Maximum radius that can be chosen for blurring kernel. + radius (float or tuple of float (min, max)): Radius to be used for creating + kernel to perform blurring. If float, radius is fixed. If it is tuple of + float (min, max), kernel radius is chosen uniformly at random to lie in the + given range. Returns: PIL Image or Tensor: Gaussian blurred version of the input image. """ - def __init__(self, rad_min=0.1, rad_max=2.0): + def __init__(self, radius=(0.1, 2.0)): super().__init__() - if rad_min < 0: - raise ValueError("Random Gaussian Blur minimum radius should be between non-negative") - if rad_max < 0: - raise ValueError("Random Gaussian Blur maximum radius should be between non-negative") - - if rad_min > rad_max: - warnings.warn("minimum radius should be <= maximum radius. For now, their values will be swapped.") - rad_min, rad_max = rad_max, rad_min + if isinstance(radius, numbers.Number): + if radius <= 0: + raise ValueError("If radius is a single number, it must be positive.") + radius = (radius, radius) + elif isinstance(radius, (tuple, list)) and len(radius) == 2: + if not 0. < radius[0] <= radius[1]: + raise ValueError("radius values should be positive and of the form (min, max)") + else: + raise TypeError("radius should be a single number or a list/tuple with length 2.") - self.rad_min = rad_min - self.rad_max = rad_max + self.rad_min, self.rad_max = radius @staticmethod def get_params(rad_min: float, rad_max: float): @@ -1587,7 +1589,7 @@ def get_params(rad_min: float, rad_max: float): Returns: float: radius be passed to ``gaussian_blur`` for gaussian blurring. """ - radius = torch.rand(1).item() * (rad_max - rad_min) + rad_min + radius = random.uniform(rad_min, rad_max) return radius def forward(self, img): From d2ccb49f9059ccf6c1a6042b1f62f2525ce0bc41 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Fri, 18 Sep 2020 10:31:06 +0000 Subject: [PATCH 04/12] [WIP] New API for gaussian_blur --- torchvision/transforms/functional.py | 35 ++++++++++++++++++++---- torchvision/transforms/functional_pil.py | 19 ------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index cac1c62dd57..b159ebed934 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -115,7 +115,7 @@ def pil_to_tensor(pic): Returns: Tensor: Converted image. """ - if not(F_pil._is_pil_image(pic)): + if not F_pil._is_pil_image(pic): raise TypeError('pic should be PIL Image. Got {}'.format(type(pic))) if accimage is not None and isinstance(pic, accimage.Image): @@ -335,7 +335,7 @@ def resize(img: Tensor, size: List[int], interpolation: int = Image.BILINEAR) -> the smaller edge of the image will be matched to this number maintaining the aspect ratio. i.e, if height > width, then image will be rescaled to :math:`\left(\text{size} \times \frac{\text{height}}{\text{width}}, \text{size}\right)`. - In torchscript mode padding as single int is not supported, use a tuple or + In torchscript mode size as single int is not supported, use a tuple or list of length 1: ``[size, ]``. interpolation (int, optional): Desired interpolation enum defined by `filters`_. Default is ``PIL.Image.BILINEAR``. If input is Tensor, only ``PIL.Image.NEAREST``, ``PIL.Image.BILINEAR`` @@ -1028,17 +1028,40 @@ def erase(img: Tensor, i: int, j: int, h: int, w: int, v: Tensor, inplace: bool return img -def gaussian_blur(img: Tensor, radius: float) -> Tensor: +def gaussian_blur(img: Tensor, kernel_size: int, sigma: float = None) -> Tensor: """Performs Gaussian blurring on the img by given kernel. + The image can be a PIL Image or a Tensor, in which case it is expected + to have [..., H, W] shape, where ... means an arbitrary number of leading dimensions Args: img (PIL Image or Tensor): Image to be blurred - radius (float): Blur radius + kernel_size (sequence or int): Gaussian kernel size. Can be a sequence of integers + like ``(kx, ky)`` or a single integer for square kernels. + In torchscript mode kernel_size as single int is not supported, use a tuple or + list of length 1: ``[size, ]``. + sigma (sequence or float, optional): Gaussian kernel standard deviation. Can be a + sequence of floats like ``(sigma_x, sigma_y)`` or a single float to define the + same sigma in both X/Y directions. If None, then it is computed using + ``kernel_size`` as ``sigma = 0.3 * ((kernel_size - 1) * 0.5 - 1) + 0.8``. + Default, None. In torchscript mode sigma as single float is + not supported, use a tuple or list of length 1: ``[sigma, ]``. Returns: PIL Image or Tensor: Gaussian Blurred version of the image. """ + is_pil_image = False + t_img = img if not isinstance(img, torch.Tensor): - return F_pil.gaussian_blur(img, radius) + if not F_pil._is_pil_image(img): + raise TypeError('img should be PIL Image or Tensor. Got {}'.format(type(img))) - return F_t.gaussian_blur(img, radius) + is_pil_image = True + t_img = pil_to_tensor(img) + + output = F_t.gaussian_blur(t_img, kernel_size, sigma) + + if is_pil_image: + output = output.permute((1, 2, 0)) + output = Image.fromarray(output.numpy()) + + return output diff --git a/torchvision/transforms/functional_pil.py b/torchvision/transforms/functional_pil.py index d9466d251a8..799ba8b5cca 100644 --- a/torchvision/transforms/functional_pil.py +++ b/torchvision/transforms/functional_pil.py @@ -517,22 +517,3 @@ def to_grayscale(img, num_output_channels): raise ValueError('num_output_channels should be either 1 or 3') return img - - -@torch.jit.unused -def gaussian_blur(img: Image.Image, radius: float) -> Image.Image: - """Performs Gaussian blurring on the img by given kernel. - - Args: - img (Tensor): Image to be blurred - radius (float): Blur radius - - Returns: - Tensor: An image that is blurred using kernel of given radius - """ - if not _is_pil_image(img): - raise TypeError('img should be PIL Image. Got {}'.format(type(img))) - - blurred_img = img.filter(ImageFilter.GaussianBlur(radius)) - - return blurred_img From 38f3c9c61b63b5c091771ac14c22d9248baa2602 Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Mon, 21 Sep 2020 01:44:15 +0530 Subject: [PATCH 05/12] Gaussian blur with kernelsize and sigma API --- torchvision/transforms/functional.py | 12 ++-- torchvision/transforms/functional_tensor.py | 69 ++++++++++++--------- torchvision/transforms/transforms.py | 56 ++++++++++------- 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index b159ebed934..53847438108 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -1028,18 +1028,18 @@ def erase(img: Tensor, i: int, j: int, h: int, w: int, v: Tensor, inplace: bool return img -def gaussian_blur(img: Tensor, kernel_size: int, sigma: float = None) -> Tensor: +def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Tensor: """Performs Gaussian blurring on the img by given kernel. The image can be a PIL Image or a Tensor, in which case it is expected to have [..., H, W] shape, where ... means an arbitrary number of leading dimensions Args: img (PIL Image or Tensor): Image to be blurred - kernel_size (sequence or int): Gaussian kernel size. Can be a sequence of integers + kernel_size (sequence of ints or int): Gaussian kernel size. Can be a sequence of integers like ``(kx, ky)`` or a single integer for square kernels. In torchscript mode kernel_size as single int is not supported, use a tuple or list of length 1: ``[size, ]``. - sigma (sequence or float, optional): Gaussian kernel standard deviation. Can be a + sigma (sequence of floats or float or None, optional): Gaussian kernel standard deviation. Can be a sequence of floats like ``(sigma_x, sigma_y)`` or a single float to define the same sigma in both X/Y directions. If None, then it is computed using ``kernel_size`` as ``sigma = 0.3 * ((kernel_size - 1) * 0.5 - 1) + 0.8``. @@ -1056,12 +1056,10 @@ def gaussian_blur(img: Tensor, kernel_size: int, sigma: float = None) -> Tensor: raise TypeError('img should be PIL Image or Tensor. Got {}'.format(type(img))) is_pil_image = True - t_img = pil_to_tensor(img) + t_img = to_tensor(img) output = F_t.gaussian_blur(t_img, kernel_size, sigma) if is_pil_image: - output = output.permute((1, 2, 0)) - output = Image.fromarray(output.numpy()) - + output = to_pil_image(output) return output diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index cb9c3a7cd7a..5caa9d1c6b8 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -942,47 +942,63 @@ def perspective( return _apply_grid_transform(img, grid, mode) -def _get_kernel(radius: float, passes: int): - sigma2 = torch.Tensor([radius ** 2 / passes]) +def _get_gaussian_kernel1d(kernel_size: int, sigma: float): + ksize_half = (kernel_size - 1) * 0.5 - kernel_rad = (torch.sqrt(12. * sigma2 + 1.) - 1.) / 2. + x = torch.linspace(-ksize_half, ksize_half, steps=kernel_size) + pdf = torch.exp(-0.5 * (x / sigma).pow(2)) + kernel1d = pdf / pdf.sum() - kernel_rad_int = kernel_rad.long().item() + return kernel1d - kernel_rad_float = (2 * kernel_rad_int + 1) * (kernel_rad_int * (kernel_rad_int + 1) - 3 * sigma2) - kernel_rad_float /= 6 * (sigma2 - (kernel_rad_int + 1) * (kernel_rad_int + 1)) - kernel_rad_float = kernel_rad_float.item() - kernel_rad = kernel_rad_int + kernel_rad_float +def _get_gaussian_kernel2d(kernel_size: List[int], sigma: List[float]): + ksize_x, ksize_y = kernel_size + sigma_x, sigma_y = sigma - ksize = 2 * kernel_rad_int + 1 + 2 * (kernel_rad_float > 0) - kernel1d = torch.ones(ksize) / (2 * kernel_rad + 1) + kernel1d_x = _get_gaussian_kernel1d(ksize_x, sigma_x) + kernel1d_y = _get_gaussian_kernel1d(ksize_y, sigma_y) - if kernel_rad_float > 0: - kernel1d[[0, -1]] = kernel_rad_float / (2 * kernel_rad + 1) - - kernel2d = torch.mm(kernel1d[:, None], kernel1d[None, :]) + kernel2d = torch.mm(kernel1d_y[:, None], kernel1d_x[None, :]) return kernel2d -def gaussian_blur(img: Tensor, radius: float) -> Tensor: +def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Tensor: """Performs Gaussian blurring on the img by given kernel. Args: img (Tensor): Image to be blurred - radius (float): Blur radius + kernel_size (sequence of int or int): Kernel size of the Gaussian kernel + sigma (sequence of float or float or None): Standard deviation of the Gaussian kernel Returns: - Tensor: An image that is blurred using kernel of given radius + Tensor: An image that is blurred using gaussian kernel of given parameters """ - if not (isinstance(img, torch.Tensor) and _is_tensor_a_torch_image(img)): + if not (isinstance(img, torch.Tensor) or _is_tensor_a_torch_image(img)): raise TypeError('img should be Tensor Image. Got {}'.format(type(img))) - if not isinstance(radius, (float, int)): - raise TypeError('radius should be either float or int. Got {}'.format(type(radius))) + if not isinstance(kernel_size, (int, list, tuple)): + raise TypeError('kernel_size should be int or a sequence of integers. Got {}'.format(type(kernel_size))) + if not isinstance(sigma, (float, int, list, tuple)) and sigma != None: + raise TypeError('sigma should be either float or int or its sequence. Got {}'.format(type(sigma))) + + if isinstance(kernel_size, int): + kernel_size = [kernel_size] * 2 + if isinstance(sigma, (int, float, None)): + sigma = [sigma] * 2 + + if len(kernel_size) != 2: + raise ValueError('If kernel_size is a sequence its length should be 2. Got {}'.format(len(kernel_size))) + if len(sigma) != 2: + raise ValueError('If sigma is a sequence its length should be 2. Got {}'.format(len(sigma))) + + if any([ksize % 2 == 0 or not isinstance(ksize, int) for ksize in kernel_size]): + raise ValueError('kernel_size should have odd and positive integers. Got {}'.format(kernel_size)) + + sigma = [s if s != None else 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8 for ksize, s in zip(kernel_size, sigma)] - radius = float(radius) - passes = 3 + if any([s <= 0. for s in sigma]): + raise ValueError('sigma should have positive values. Got {}'.format(sigma)) ndim = img.ndim if ndim == 2: @@ -990,17 +1006,14 @@ def gaussian_blur(img: Tensor, radius: float) -> Tensor: if ndim == 3: img = img.unsqueeze(0) - kernel = _get_kernel(radius, passes) + kernel = _get_gaussian_kernel2d(kernel_size, sigma) - padding = _compute_padding(kernel.shape[::-1]) + padding = _compute_padding(kernel_size) kernel = kernel[None, None, :, :].repeat(img.size(-3), 1, 1, 1) - padded_img = pad(img, padding, padding_mode='edge') + padded_img = pad(img, padding, padding_mode='reflect') blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) - for _ in range(passes - 1): - padded_img = pad(blurred_img, padding, padding_mode='edge') - blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) if ndim == 2: return blurred_img[0, 0] diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py index ad67735c64c..c707f6d477c 100644 --- a/torchvision/transforms/transforms.py +++ b/torchvision/transforms/transforms.py @@ -1553,9 +1553,10 @@ class GaussianBlur(torch.nn.Module): dimensions Args: - radius (float or tuple of float (min, max)): Radius to be used for creating - kernel to perform blurring. If float, radius is fixed. If it is tuple of - float (min, max), kernel radius is chosen uniformly at random to lie in the + ksize (int): Size of the Gaussian kernel. + sigma (float or tuple of float (min, max)): Standard deviation to be used for + creating kernel to perform blurring. If float, sigma is fixed. If it is tuple + of float (min, max), sigma is chosen uniformly at random to lie in the given range. Returns: @@ -1563,34 +1564,41 @@ class GaussianBlur(torch.nn.Module): """ - def __init__(self, radius=(0.1, 2.0)): + def __init__(self, ksize, sigma=(0.1, 2.0)): super().__init__() - if isinstance(radius, numbers.Number): - if radius <= 0: - raise ValueError("If radius is a single number, it must be positive.") - radius = (radius, radius) - elif isinstance(radius, (tuple, list)) and len(radius) == 2: - if not 0. < radius[0] <= radius[1]: - raise ValueError("radius values should be positive and of the form (min, max)") + if isinstance(ksize, numbers.Number): + if ksize <= 0 or ksize % 2 == 0: + raise ValueError("ksize should be an odd and positive number.") else: - raise TypeError("radius should be a single number or a list/tuple with length 2.") + raise TypeError("ksize should be a single number.") + + if isinstance(sigma, numbers.Number): + if sigma <= 0: + raise ValueError("If sigma is a single number, it must be positive.") + sigma = (sigma, sigma) + elif isinstance(sigma, (tuple, list)) and len(sigma) == 2: + if not 0. < sigma[0] <= sigma[1]: + raise ValueError("sigma values should be positive and of the form (min, max).") + else: + raise TypeError("sigma should be a single number or a list/tuple with length 2.") - self.rad_min, self.rad_max = radius + self.ksize = ksize + self.sigma_min, self.sigma_max = sigma @staticmethod - def get_params(rad_min: float, rad_max: float): - """Choose radius for ``gaussian_blur`` for random gaussian blurring. + def get_params(sigma_min: float, sigma_max: float): + """Choose sigma for ``gaussian_blur`` for random gaussian blurring. Args: - rad_min (float): Minimum radius that can be chosen for blurring kernel. - rad_max (float): Maximum radius that can be chosen for blurring kernel. + sigma_min (float): Minimum standard deviation that can be chosen for blurring kernel. + sigma_max (float): Maximum standard deviation that can be chosen for blurring kernel. Returns: - float: radius be passed to ``gaussian_blur`` for gaussian blurring. + float: Standard deviation to be passed to calculate kernel for gaussian blurring. """ - radius = random.uniform(rad_min, rad_max) - return radius + sigma = random.uniform(sigma_min, sigma_max) + return sigma def forward(self, img): """ @@ -1600,8 +1608,10 @@ def forward(self, img): Returns: PIL Image or Tensor: Gaussian blurred image """ - radius = self.get_params(self.rad_min, self.rad_max) - return F.gaussian_blur(img, radius) + sigma = self.get_params(self.sigma_min, self.sigma_max) + return F.gaussian_blur(img, self.ksize, sigma) def __repr__(self): - return self.__class__.__name__ + '(rad_min={0}, rad_max={1})'.format(self.rad_min, self.rad_max) + s = 'kernel size={0}, '.format(self.ksize) + s += '(sigma_min={0}, sigma_max={1})'.format(self.sigma_min, self.sigma_max) + return self.__class__.__name__ + s From 02ad333cf5de414a31bfd8358a713c13bdf936f1 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Mon, 28 Sep 2020 12:31:19 +0200 Subject: [PATCH 06/12] Fixed implementation and updated tests --- test/test_functional_tensor.py | 116 +++++++++++++++++++- test/test_transforms_tensor.py | 18 +++ torchvision/transforms/functional.py | 33 +++++- torchvision/transforms/functional_tensor.py | 116 +++++++------------- torchvision/transforms/transforms.py | 36 +++--- 5 files changed, 216 insertions(+), 103 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 87373359e83..e9a425c2065 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -678,14 +678,14 @@ def test_rotate(self): batch_tensors, F.rotate, angle=32, resample=0, expand=True, center=center ) - def _test_perspective(self, tensor, pil_img, scripted_tranform, test_configs): + def _test_perspective(self, tensor, pil_img, scripted_transform, test_configs): dt = tensor.dtype for r in [0, ]: for spoints, epoints in test_configs: out_pil_img = F.perspective(pil_img, startpoints=spoints, endpoints=epoints, interpolation=r) out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) - for fn in [F.perspective, scripted_tranform]: + for fn in [F.perspective, scripted_transform]: out_tensor = fn(tensor, startpoints=spoints, endpoints=epoints, interpolation=r).cpu() if out_tensor.dtype != torch.uint8: @@ -710,7 +710,7 @@ def test_perspective(self): from torchvision.transforms import RandomPerspective data = [self._create_data(26, 34, device=self.device), self._create_data(26, 26, device=self.device)] - scripted_tranform = torch.jit.script(F.perspective) + scripted_transform = torch.jit.script(F.perspective) for tensor, pil_img in data: @@ -733,7 +733,7 @@ def test_perspective(self): if dt is not None: tensor = tensor.to(dtype=dt) - self._test_perspective(tensor, pil_img, scripted_tranform, test_configs) + self._test_perspective(tensor, pil_img, scripted_transform, test_configs) batch_tensors = self._create_data_batch(26, 36, num_samples=4, device=self.device) if dt is not None: @@ -744,6 +744,114 @@ def test_perspective(self): batch_tensors, F.perspective, startpoints=spoints, endpoints=epoints, interpolation=0 ) + def test_gaussian_blur(self): + tensor = torch.from_numpy( + np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3)) + ).permute(2, 0, 1).to(self.device) + + scripted_transform = torch.jit.script(F.gaussian_blur) + + true_cv2_results = { + # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.8) + "3_3_0.8": + [19, 20, 21, 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, 49, 50, 51, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, + 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, + 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, 212, 213, 214, 217, 189, 190, 204, 174, 175, 176, + 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, 187, 188, 189, 192, 130, 131, 162, 93, 94, 95, 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, 89, 90, 91, 94, 66, 67, + 81, 51, 52, 53, 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, 64, 65, 66, 52, 53, 54, 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, 82, 83, 84], + # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.5) + "3_3_0.5": + [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, 40, 41, 42, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, + 105, 106, 109, 110, 111, 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, 140, 141, 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, 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, 212, 213, 214, 217, 212, 213, 216, 196, 197, 198, 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, 221, 222, 223, 226, + 184, 185, 207, 48, 49, 50, 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, 55, 56, 57, 60, 55, 56, 59, 39, 40, 41, 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, 64, 65, 66, 61, 62, 63, 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 + ], + # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.8) + "3_5_0.8": + [21, 22, 23, 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, 51, 52, 53, 39, 40, 41, 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, 69, 70, 71, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, + 142, 145, 146, 147, 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, 176, 177, 178, 181, 180, 181, + 182, 179, 180, 181, 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, 206, 207, 208, 211, 185, 186, 199, 170, 171, 172, + 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, 184, 185, 186, 189, 129, 130, 161, 95, 96, 97, 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, 92, 93, 94, 96, 69, 70, + 83, 54, 55, 56, 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, 68, 69, 70, 62, 57, 58, 60, 55, 56, 57, 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, 80, 81, 82], + # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.5) + "3_5_0.5": + [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, 40, 41, 42, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, + 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, + 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, 212, 213, 214, 217, 212, 213, 216, 196, 197, 198, + 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, 221, 222, 223, 226, 184, 185, 207, 48, 49, 50, 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, 55, 56, 57, 60, 55, 56, + 59, 39, 40, 41, 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, 64, 65, 66, 61, 62, 63, 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] + } + + for dt in [None, torch.float32, torch.float64, torch.float16]: + if dt == torch.float16 and torch.device(self.device).type == "cpu": + # skip float16 on CPU case + continue + + if dt is not None: + tensor = tensor.to(dtype=dt) + + for ksize in [(3, 3), [3, 5]]: + for sigma in [[0.5, 0.5], (0.5, 0.5), (0.8, 0.8)]: + + _ksize = (ksize, ksize) if isinstance(ksize, int) else ksize + _sigma = sigma[0] if sigma is not None else None + true_out = torch.tensor( + true_cv2_results["{}_{}_{}".format(_ksize[0], _ksize[1], _sigma)] + ).reshape(10, 12, 3).permute(2, 0, 1) + + for fn in [F.gaussian_blur, scripted_transform]: + out = fn(tensor, kernel_size=ksize, sigma=sigma) + self.assertEqual(true_out.shape, out.shape, msg="{}, {}".format(ksize, sigma)) + self.assertLessEqual( + torch.max(true_out.float() - out.float()), + 1.0, + msg="{}, {}".format(ksize, sigma) + ) + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") class CUDATester(Tester): diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 74e128d9b1c..561bf4c14cd 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -433,6 +433,24 @@ def test_compose(self): with self.assertRaisesRegex(RuntimeError, r"Could not get name of python class object"): torch.jit.script(t) + def test_gaussian_blur(self): + + tol = 1.0 + 1e-10 + self._test_class_op( + "GaussianBlur", meth_kwargs={"kernel_size": 3, "sigma": 0.75}, + test_exact_match=False, agg_method="max", tol=tol + ) + + self._test_class_op( + "GaussianBlur", meth_kwargs={"kernel_size": 23, "sigma": [0.1, 2.0]}, + test_exact_match=False, agg_method="max", tol=tol + ) + + self._test_class_op( + "GaussianBlur", meth_kwargs={"kernel_size": 23, "sigma": (0.1, 2.0)}, + test_exact_match=False, agg_method="max", tol=tol + ) + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") class CUDATester(Tester): diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index f9a65fb6c4e..bbf4652c874 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -1038,8 +1038,8 @@ def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[floa kernel_size (sequence of ints or int): Gaussian kernel size. Can be a sequence of integers like ``(kx, ky)`` or a single integer for square kernels. In torchscript mode kernel_size as single int is not supported, use a tuple or - list of length 1: ``[size, ]``. - sigma (sequence of floats or float or None, optional): Gaussian kernel standard deviation. Can be a + list of length 1: ``[ksize, ]``. + sigma (sequence of floats or float, optional): Gaussian kernel standard deviation. Can be a sequence of floats like ``(sigma_x, sigma_y)`` or a single float to define the same sigma in both X/Y directions. If None, then it is computed using ``kernel_size`` as ``sigma = 0.3 * ((kernel_size - 1) * 0.5 - 1) + 0.8``. @@ -1049,17 +1049,40 @@ def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[floa Returns: PIL Image or Tensor: Gaussian Blurred version of the image. """ - is_pil_image = False + if not isinstance(kernel_size, (int, list, tuple)): + raise TypeError('kernel_size should be int or a sequence of integers. Got {}'.format(type(kernel_size))) + if isinstance(kernel_size, int): + kernel_size = [kernel_size, kernel_size] + if len(kernel_size) != 2: + raise ValueError('If kernel_size is a sequence its length should be 2. Got {}'.format(len(kernel_size))) + for ksize in kernel_size: + if ksize % 2 == 0 or ksize < 0: + raise ValueError('kernel_size should have odd and positive integers. Got {}'.format(kernel_size)) + + if sigma is None: + sigma = [ksize * 0.15 + 0.35 for ksize in kernel_size] + + if sigma is not None and not isinstance(sigma, (float, list, tuple)): + raise TypeError('sigma should be either float or sequence of floats. Got {}'.format(type(sigma))) + if isinstance(sigma, float): + sigma = [sigma, sigma] + if isinstance(sigma, (list, tuple)) and len(sigma) == 1: + sigma = [sigma[0], sigma[0]] + if len(sigma) != 2: + raise ValueError('If sigma is a sequence, its length should be 2. Got {}'.format(len(sigma))) + for s in sigma: + if s <= 0.: + raise ValueError('sigma should have positive values. Got {}'.format(sigma)) + t_img = img if not isinstance(img, torch.Tensor): if not F_pil._is_pil_image(img): raise TypeError('img should be PIL Image or Tensor. Got {}'.format(type(img))) - is_pil_image = True t_img = to_tensor(img) output = F_t.gaussian_blur(t_img, kernel_size, sigma) - if is_pil_image: + if not isinstance(img, torch.Tensor): output = to_pil_image(output) return output diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 1773c761a21..2683e5d8e08 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -3,7 +3,7 @@ import torch from torch import Tensor -from torch.nn.functional import grid_sample, conv2d +from torch.nn.functional import grid_sample, conv2d, interpolate, pad as torch_pad from torch.jit.annotations import List, BroadcastingList2 @@ -526,29 +526,6 @@ def _hsv2rgb(img): return torch.einsum("...ijk, ...xijk -> ...xjk", mask.to(dtype=img.dtype), a4) -def _compute_padding(kernel_size: List[int]) -> List[int]: - """Computes padding tuple.""" - # 4 ints: (padding_left, padding_top, padding_right, padding_bottom) - - # This function is modified from kornia: - # https://github.com/kornia/kornia/blob/85661d982e6349ffac237051164cf38cc604454b/kornia/filters/filter.py#L11 - - assert len(kernel_size) >= 2, kernel_size - padding_tmp = [k // 2 for k in kernel_size] - - # for even kernels we need to do asymetric padding :( - - out_padding = [0, 0] * len(kernel_size) - - for i, (ksize, pad_tmp) in enumerate(zip(kernel_size, padding_tmp)): - padding = pad_tmp if ksize % 2 else pad_tmp - 1 - - out_padding[i] = padding - out_padding[i + 2] = pad_tmp - - return out_padding - - def _pad_symmetric(img: Tensor, padding: List[int]) -> Tensor: # padding is left, right, top, bottom in_sizes = img.size() @@ -671,7 +648,7 @@ def pad(img: Tensor, padding: List[int], fill: int = 0, padding_mode: str = "con need_cast = True img = img.to(torch.float32) - img = torch.nn.functional.pad(img, p, mode=padding_mode, value=float(fill)) + img = torch_pad(img, p, mode=padding_mode, value=float(fill)) if need_squeeze: img = img.squeeze(dim=0) @@ -764,7 +741,7 @@ def resize(img: Tensor, size: List[int], interpolation: int = 2) -> Tensor: # Define align_corners to avoid warnings align_corners = False if mode in ["bilinear", "bicubic"] else None - img = torch.nn.functional.interpolate(img, size=(size_h, size_w), mode=mode, align_corners=align_corners) + img = interpolate(img, size=[size_h, size_w], mode=mode, align_corners=align_corners) if need_squeeze: img = img.squeeze(dim=0) @@ -1036,7 +1013,7 @@ def perspective( return _apply_grid_transform(img, grid, mode) -def _get_gaussian_kernel1d(kernel_size: int, sigma: float): +def _get_gaussian_kernel1d(kernel_size: int, sigma: float) -> Tensor: ksize_half = (kernel_size - 1) * 0.5 x = torch.linspace(-ksize_half, ksize_half, steps=kernel_size) @@ -1046,71 +1023,60 @@ def _get_gaussian_kernel1d(kernel_size: int, sigma: float): return kernel1d -def _get_gaussian_kernel2d(kernel_size: List[int], sigma: List[float]): - ksize_x, ksize_y = kernel_size - sigma_x, sigma_y = sigma - - kernel1d_x = _get_gaussian_kernel1d(ksize_x, sigma_x) - kernel1d_y = _get_gaussian_kernel1d(ksize_y, sigma_y) +def _get_gaussian_kernel2d( + kernel_size: List[int], sigma: List[float], dtype: torch.dtype, device: torch.device +) -> Tensor: + kernel1d_x = _get_gaussian_kernel1d(kernel_size[1], sigma[0]).to(device, dtype=dtype) + kernel1d_y = _get_gaussian_kernel1d(kernel_size[0], sigma[1]).to(device, dtype=dtype) + kernel2d = torch.mm(kernel1d_x[:, None], kernel1d_y[None, :]) + return kernel2d - kernel2d = torch.mm(kernel1d_y[:, None], kernel1d_x[None, :]) - return kernel2d +def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: List[float]) -> Tensor: + """PRIVATE METHOD. Performs Gaussian blurring on the img by given kernel. + .. warning:: -def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Tensor: - """Performs Gaussian blurring on the img by given kernel. + Module ``transforms.functional_tensor`` is private and should not be used in user application. + Please, consider instead using methods from `transforms.functional` module. Args: img (Tensor): Image to be blurred - kernel_size (sequence of int or int): Kernel size of the Gaussian kernel - sigma (sequence of float or float or None): Standard deviation of the Gaussian kernel + kernel_size (sequence of int or int): Kernel size of the Gaussian kernel ``(kx, ky)``. + sigma (sequence of float or float, optional): Standard deviation of the Gaussian kernel ``(sx, sy)``. Returns: Tensor: An image that is blurred using gaussian kernel of given parameters """ if not (isinstance(img, torch.Tensor) or _is_tensor_a_torch_image(img)): raise TypeError('img should be Tensor Image. Got {}'.format(type(img))) - if not isinstance(kernel_size, (int, list, tuple)): - raise TypeError('kernel_size should be int or a sequence of integers. Got {}'.format(type(kernel_size))) - if not isinstance(sigma, (float, int, list, tuple)) and sigma != None: - raise TypeError('sigma should be either float or int or its sequence. Got {}'.format(type(sigma))) - - if isinstance(kernel_size, int): - kernel_size = [kernel_size] * 2 - if isinstance(sigma, (int, float, None)): - sigma = [sigma] * 2 - - if len(kernel_size) != 2: - raise ValueError('If kernel_size is a sequence its length should be 2. Got {}'.format(len(kernel_size))) - if len(sigma) != 2: - raise ValueError('If sigma is a sequence its length should be 2. Got {}'.format(len(sigma))) - - if any([ksize % 2 == 0 or not isinstance(ksize, int) for ksize in kernel_size]): - raise ValueError('kernel_size should have odd and positive integers. Got {}'.format(kernel_size)) - sigma = [s if s != None else 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8 for ksize, s in zip(kernel_size, sigma)] - - if any([s <= 0. for s in sigma]): - raise ValueError('sigma should have positive values. Got {}'.format(sigma)) + dtype = img.dtype if torch.is_floating_point(img) else torch.float32 + kernel = _get_gaussian_kernel2d(kernel_size, sigma, dtype=dtype, device=img.device) + kernel = kernel.expand(img.shape[-3], 1, kernel.shape[0], kernel.shape[1]) - ndim = img.ndim - if ndim == 2: - img = img.unsqueeze(0) - if ndim == 3: - img = img.unsqueeze(0) + # make image NCHW + need_squeeze = False + if img.ndim < 4: + img = img.unsqueeze(dim=0) + need_squeeze = True - kernel = _get_gaussian_kernel2d(kernel_size, sigma) + out_dtype = img.dtype + need_cast = False + if out_dtype != kernel.dtype: + need_cast = True + img = img.to(kernel) - padding = _compute_padding(kernel_size) + # padding = (left, right, top, bottom) + padding = [kernel_size[0] // 2, kernel_size[0] // 2, kernel_size[1] // 2, kernel_size[1] // 2] + img = torch_pad(img, padding, mode="reflect") + img = conv2d(img, kernel, groups=img.shape[-3]) - kernel = kernel[None, None, :, :].repeat(img.size(-3), 1, 1, 1) + if need_squeeze: + img = img.squeeze(dim=0) - padded_img = pad(img, padding, padding_mode='reflect') - blurred_img = conv2d(padded_img, kernel, groups=img.size(-3)) + if need_cast: + # it is better to round before cast + img = torch.round(img).to(out_dtype) - if ndim == 2: - return blurred_img[0, 0] - if ndim == 3: - return blurred_img[0] - return blurred_img + return img diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py index a6ca8e8e770..e435cbab423 100644 --- a/torchvision/transforms/transforms.py +++ b/torchvision/transforms/transforms.py @@ -3,7 +3,7 @@ import random import warnings from collections.abc import Sequence -from typing import Tuple, List, Optional, Any +from typing import Tuple, List, Optional import torch from PIL import Image @@ -1501,21 +1501,21 @@ class GaussianBlur(torch.nn.Module): dimensions Args: - ksize (int): Size of the Gaussian kernel. + kernel_size (int or sequence): Size of the Gaussian kernel. sigma (float or tuple of float (min, max)): Standard deviation to be used for - creating kernel to perform blurring. If float, sigma is fixed. If it is tuple - of float (min, max), sigma is chosen uniformly at random to lie in the - given range. + creating kernel to perform blurring. If float, sigma is fixed. If it is tuple + of float (min, max), sigma is chosen uniformly at random to lie in the + given range. Returns: PIL Image or Tensor: Gaussian blurred version of the input image. """ - def __init__(self, ksize, sigma=(0.1, 2.0)): + def __init__(self, kernel_size, sigma=(0.1, 2.0)): super().__init__() - self.ksize = _setup_size(ksize, "Kernel size should be a tuple/list of two integers") - for ks in self.ksize: + self.kernel_size = _setup_size(kernel_size, "Kernel size should be a tuple/list of two integers") + for ks in self.kernel_size: if ks <= 0 or ks % 2 == 0: raise ValueError("Kernel size value should be an odd and positive number.") @@ -1523,17 +1523,16 @@ def __init__(self, ksize, sigma=(0.1, 2.0)): if sigma <= 0: raise ValueError("If sigma is a single number, it must be positive.") sigma = (sigma, sigma) - elif isinstance(sigma, (tuple, list)) and len(sigma) == 2: + elif isinstance(sigma, Sequence) and len(sigma) == 2: if not 0. < sigma[0] <= sigma[1]: raise ValueError("sigma values should be positive and of the form (min, max).") else: raise TypeError("sigma should be a single number or a list/tuple with length 2.") - self.ksize = ksize - self.sigma_min, self.sigma_max = sigma + self.sigma = sigma @staticmethod - def get_params(sigma_min: float, sigma_max: float): + def get_params(sigma_min: float, sigma_max: float) -> float: """Choose sigma for ``gaussian_blur`` for random gaussian blurring. Args: @@ -1543,10 +1542,9 @@ def get_params(sigma_min: float, sigma_max: float): Returns: float: Standard deviation to be passed to calculate kernel for gaussian blurring. """ - sigma = random.uniform(sigma_min, sigma_max) - return sigma + return torch.empty(1).uniform_(sigma_min, sigma_max).item() - def forward(self, img): + def forward(self, img: Tensor) -> Tensor: """ Args: img (PIL Image or Tensor): image of size (C, H, W) to be blurred. @@ -1554,12 +1552,12 @@ def forward(self, img): Returns: PIL Image or Tensor: Gaussian blurred image """ - sigma = self.get_params(self.sigma_min, self.sigma_max) - return F.gaussian_blur(img, self.ksize, sigma) + sigma = self.get_params(self.sigma[0], self.sigma[1]) + return F.gaussian_blur(img, self.kernel_size, [sigma, sigma]) def __repr__(self): - s = 'kernel size={0}, '.format(self.ksize) - s += '(sigma_min={0}, sigma_max={1})'.format(self.sigma_min, self.sigma_max) + s = '(kernel size={}, '.format(self.ksize) + s += 'sigma={})'.format(self.sigma) return self.__class__.__name__ + s From 2048863db415d8c11472e7b57b1c1e09c69af4c0 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Mon, 28 Sep 2020 12:13:44 +0000 Subject: [PATCH 07/12] Added large input case and refactored gt into a file --- test/assets/gaussian_blur_opencv_results.pt | Bin 0 -> 49135 bytes test/test_functional_tensor.py | 153 +++++++------------- 2 files changed, 55 insertions(+), 98 deletions(-) create mode 100644 test/assets/gaussian_blur_opencv_results.pt diff --git a/test/assets/gaussian_blur_opencv_results.pt b/test/assets/gaussian_blur_opencv_results.pt new file mode 100644 index 0000000000000000000000000000000000000000..d68f477fb4448794a791109b1efafaa9f351d8b4 GIT binary patch literal 49135 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 literal 0 HcmV?d00001 diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index e9a425c2065..ca25f9db2a6 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -1,3 +1,4 @@ +import os import unittest import colorsys import math @@ -745,112 +746,68 @@ def test_perspective(self): ) def test_gaussian_blur(self): - tensor = torch.from_numpy( + small_image_tensor = torch.from_numpy( np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3)) ).permute(2, 0, 1).to(self.device) + large_image_tensor = torch.from_numpy( + np.arange(26 * 28, dtype="uint8").reshape((1, 26, 28)) + ).to(self.device) + scripted_transform = torch.jit.script(F.gaussian_blur) - true_cv2_results = { - # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.8) - "3_3_0.8": - [19, 20, 21, 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, 49, 50, 51, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, - 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, - 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, 212, 213, 214, 217, 189, 190, 204, 174, 175, 176, - 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, 187, 188, 189, 192, 130, 131, 162, 93, 94, 95, 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, 89, 90, 91, 94, 66, 67, - 81, 51, 52, 53, 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, 64, 65, 66, 52, 53, 54, 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, 82, 83, 84], - # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.5) - "3_3_0.5": - [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, 40, 41, 42, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, - 105, 106, 109, 110, 111, 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, 140, 141, 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, 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, 212, 213, 214, 217, 212, 213, 216, 196, 197, 198, 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, 221, 222, 223, 226, - 184, 185, 207, 48, 49, 50, 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, 55, 56, 57, 60, 55, 56, 59, 39, 40, 41, 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, 64, 65, 66, 61, 62, 63, 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 - ], - # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.8) - "3_5_0.8": - [21, 22, 23, 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, 51, 52, 53, 39, 40, 41, 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, 69, 70, 71, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, - 142, 145, 146, 147, 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, 176, 177, 178, 181, 180, 181, - 182, 179, 180, 181, 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, 206, 207, 208, 211, 185, 186, 199, 170, 171, 172, - 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, 184, 185, 186, 189, 129, 130, 161, 95, 96, 97, 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, 92, 93, 94, 96, 69, 70, - 83, 54, 55, 56, 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, 68, 69, 70, 62, 57, 58, 60, 55, 56, 57, 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, 80, 81, 82], - # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.5) - "3_5_0.5": - [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, 40, 41, 42, 37, 38, 39, 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, 68, 69, 70, 73, 74, 75, 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, 104, 105, 106, 109, 110, 111, 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, 140, 141, - 142, 145, 146, 147, 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, 176, 177, 178, 181, 182, 183, - 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, 212, 213, 214, 217, 212, 213, 216, 196, 197, 198, - 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, 221, 222, 223, 226, 184, 185, 207, 48, 49, 50, 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, 55, 56, 57, 60, 55, 56, - 59, 39, 40, 41, 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, 64, 65, 66, 61, 62, 63, 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] - } + # true_cv2_results = { + # # np_img = np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3)) + # # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.8) + # "3_3_0.8": ... + # # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.5) + # "3_3_0.5": ... + # # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.8) + # "3_5_0.8": ... + # # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.5) + # "3_5_0.5": ... + # # np_img2 = np.arange(26 * 28, dtype="uint8").reshape((26, 28)) + # # cv2.GaussianBlur(np_img2, ksize=(23, 23), sigmaX=1.7) + # "23_23_1.7": ... + # } + p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'gaussian_blur_opencv_results.pt') + true_cv2_results = torch.load(p) + + for tensor in [small_image_tensor, large_image_tensor]: - for dt in [None, torch.float32, torch.float64, torch.float16]: - if dt == torch.float16 and torch.device(self.device).type == "cpu": - # skip float16 on CPU case - continue + for dt in [None, torch.float32, torch.float64, torch.float16]: + if dt == torch.float16 and torch.device(self.device).type == "cpu": + # skip float16 on CPU case + continue - if dt is not None: - tensor = tensor.to(dtype=dt) - - for ksize in [(3, 3), [3, 5]]: - for sigma in [[0.5, 0.5], (0.5, 0.5), (0.8, 0.8)]: - - _ksize = (ksize, ksize) if isinstance(ksize, int) else ksize - _sigma = sigma[0] if sigma is not None else None - true_out = torch.tensor( - true_cv2_results["{}_{}_{}".format(_ksize[0], _ksize[1], _sigma)] - ).reshape(10, 12, 3).permute(2, 0, 1) - - for fn in [F.gaussian_blur, scripted_transform]: - out = fn(tensor, kernel_size=ksize, sigma=sigma) - self.assertEqual(true_out.shape, out.shape, msg="{}, {}".format(ksize, sigma)) - self.assertLessEqual( - torch.max(true_out.float() - out.float()), - 1.0, - msg="{}, {}".format(ksize, sigma) + if dt is not None: + tensor = tensor.to(dtype=dt) + + for ksize in [(3, 3), [3, 5], (23, 23)]: + for sigma in [[0.5, 0.5], (0.5, 0.5), (0.8, 0.8), (1.7, 1.7)]: + + _ksize = (ksize, ksize) if isinstance(ksize, int) else ksize + _sigma = sigma[0] if sigma is not None else None + shape = tensor.shape + gt_key = "{}_{}_{}__{}_{}_{}".format( + shape[-2], shape[-1], shape[-3], + _ksize[0], _ksize[1], _sigma ) + if gt_key not in true_cv2_results: + continue + + true_out = torch.tensor( + true_cv2_results[gt_key] + ).reshape(shape[-2], shape[-1], shape[-3]).permute(2, 0, 1).to(tensor) + + for fn in [F.gaussian_blur, scripted_transform]: + out = fn(tensor, kernel_size=ksize, sigma=sigma) + self.assertEqual(true_out.shape, out.shape, msg="{}, {}".format(ksize, sigma)) + self.assertLessEqual( + torch.max(true_out.float() - out.float()), + 1.0, + msg="{}, {}".format(ksize, sigma) + ) @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") From aed161582156625a8cd89b2f961e2e6292885442 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Mon, 28 Sep 2020 12:22:49 +0000 Subject: [PATCH 08/12] Updated docs --- docs/source/transforms.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index fbda5932735..bd52318783c 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -79,6 +79,8 @@ Transforms on PIL Image .. autoclass:: TenCrop +.. autoclass:: GaussianBlur + Transforms on torch.\*Tensor ---------------------------- From 82d4ee46c8d708771eec66f149fdc33f8f63e394 Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Tue, 29 Sep 2020 18:51:25 +0530 Subject: [PATCH 09/12] fix kernel dimesnions order while creating kernel --- torchvision/transforms/functional_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 2683e5d8e08..866c31faa23 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -1026,8 +1026,8 @@ def _get_gaussian_kernel1d(kernel_size: int, sigma: float) -> Tensor: def _get_gaussian_kernel2d( kernel_size: List[int], sigma: List[float], dtype: torch.dtype, device: torch.device ) -> Tensor: - kernel1d_x = _get_gaussian_kernel1d(kernel_size[1], sigma[0]).to(device, dtype=dtype) - kernel1d_y = _get_gaussian_kernel1d(kernel_size[0], sigma[1]).to(device, dtype=dtype) + kernel1d_x = _get_gaussian_kernel1d(kernel_size[0], sigma[0]).to(device, dtype=dtype) + kernel1d_y = _get_gaussian_kernel1d(kernel_size[1], sigma[1]).to(device, dtype=dtype) kernel2d = torch.mm(kernel1d_x[:, None], kernel1d_y[None, :]) return kernel2d From efc97a11bb51f6e9b9553ab03ecd1f784bf2d29e Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Mon, 5 Oct 2020 15:46:43 +0530 Subject: [PATCH 10/12] added tests for exception handling of gaussian blur --- test/test_transforms.py | 25 +++++++++++++++++++++++++ test/test_transforms_tensor.py | 16 ++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/test/test_transforms.py b/test/test_transforms.py index f3d0f48e4a2..142e9a41629 100644 --- a/test/test_transforms.py +++ b/test/test_transforms.py @@ -1685,6 +1685,31 @@ def test_random_erasing(self): img_re = random_erasing(img) self.assertTrue(torch.equal(img_re, img)) + def test_gaussian_blur(self): + np_img = np.ones((100, 100, 3), dtype=np.uint8) * 255 + img = F.to_pil_image(np_img, "RGB") + + with self.assertRaises(ValueError): + F.gaussian_blur(img, [3]) + with self.assertRaises(ValueError): + F.gaussian_blur(img, [3, 3, 3]) + with self.assertRaises(ValueError): + F.gaussian_blur(img, [4, 4]) + with self.assertRaises(ValueError): + F.gaussian_blur(img, [-3, -3]) + with self.assertRaises(ValueError): + F.gaussian_blur(img, 3, [1,1,1]) + with self.assertRaises(ValueError): + F.gaussian_blur(img, 3, -1) + + with self.assrtRaises(TypeError): + F.gaussian_blur(img, 'kernel_size_string') + with self.assrtRaises(TypeError): + F.gaussian_blur(img, 3, 'sigma_string') + with self.assrtRaises(TypeError): + F.gaussian_blur(np_img, 3, 1) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 561bf4c14cd..67b7d007185 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -451,6 +451,22 @@ def test_gaussian_blur(self): test_exact_match=False, agg_method="max", tol=tol ) + self._test_class_op( + "GaussianBlur", meth_kwargs={"kernel_size": [3, 3], "sigma": (1.0, 1.0)}, + test_exact_match=False, agg_method="max", tol=tol + ) + + self._test_class_op( + "GaussianBlur", fn_kwargs={"kernel_size": (3, 3), "sigma": (0.1, 2.0)}, + test_exact_match=False, agg_method="max", tol=tol + ) + + self._test_class_op( + "GaussianBlur", fn_kwargs={"kernel_size": [23], "sigma": 0.75}, + test_exact_match=False, agg_method="max", tol=tol + ) + + self.assert @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") class CUDATester(Tester): From 436028b8366490f0333d34fc19413e7caa50e25b Mon Sep 17 00:00:00 2001 From: Tejan Karmali Date: Mon, 5 Oct 2020 16:51:27 +0530 Subject: [PATCH 11/12] fix linting, bug in tests --- test/test_transforms.py | 3 +-- test/test_transforms_tensor.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test_transforms.py b/test/test_transforms.py index 71502ab6a59..7cb42cbaa5b 100644 --- a/test/test_transforms.py +++ b/test/test_transforms.py @@ -1708,7 +1708,7 @@ def test_gaussian_blur(self): with self.assertRaises(ValueError): F.gaussian_blur(img, [-3, -3]) with self.assertRaises(ValueError): - F.gaussian_blur(img, 3, [1,1,1]) + F.gaussian_blur(img, 3, [1, 1, 1]) with self.assertRaises(ValueError): F.gaussian_blur(img, 3, -1) @@ -1720,6 +1720,5 @@ def test_gaussian_blur(self): F.gaussian_blur(np_img, 3, 1) - if __name__ == '__main__': unittest.main() diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 05587bf4c87..67cad2bda57 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -491,12 +491,12 @@ def test_gaussian_blur(self): ) self._test_class_op( - "GaussianBlur", fn_kwargs={"kernel_size": (3, 3), "sigma": (0.1, 2.0)}, + "GaussianBlur", meth_kwargs={"kernel_size": (3, 3), "sigma": (0.1, 2.0)}, test_exact_match=False, agg_method="max", tol=tol ) self._test_class_op( - "GaussianBlur", fn_kwargs={"kernel_size": [23], "sigma": 0.75}, + "GaussianBlur", meth_kwargs={"kernel_size": [23], "sigma": 0.75}, test_exact_match=False, agg_method="max", tol=tol ) From 4545d9a156d1c96473692b7ef6d72d0f8e4aa98b Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Tue, 6 Oct 2020 10:37:41 +0000 Subject: [PATCH 12/12] Fixed failing tests, refactored code and other minor fixes --- test/test_transforms.py | 125 +++++++------------- torchvision/transforms/functional.py | 6 +- torchvision/transforms/functional_tensor.py | 49 ++++---- torchvision/transforms/transforms.py | 4 +- 4 files changed, 68 insertions(+), 116 deletions(-) diff --git a/test/test_transforms.py b/test/test_transforms.py index 116fdde75dc..a65d848ec92 100644 --- a/test/test_transforms.py +++ b/test/test_transforms.py @@ -1654,90 +1654,47 @@ def test_random_grayscale(self): # Checking if RandomGrayscale can be printed as string trans3.__repr__() - def test_random_erasing(self): - """Unit tests for random erasing transform""" - for is_scripted in [False, True]: - torch.manual_seed(12) - img = torch.rand(3, 60, 60) - - # Test Set 0: invalid value - random_erasing = transforms.RandomErasing(value=(0.1, 0.2, 0.3, 0.4), p=1.0) - with self.assertRaises(ValueError, msg="If value is a sequence, it should have either a single value or 3"): - img_re = random_erasing(img) - - # Test Set 1: Erasing with int value - random_erasing = transforms.RandomErasing(value=0.2) - if is_scripted: - random_erasing = torch.jit.script(random_erasing) - - i, j, h, w, v = transforms.RandomErasing.get_params( - img, scale=random_erasing.scale, ratio=random_erasing.ratio, value=[random_erasing.value, ] - ) - img_output = F.erase(img, i, j, h, w, v) - self.assertEqual(img_output.size(0), 3) - - # Test Set 2: Check if the unerased region is preserved - true_output = img.clone() - true_output[:, i:i + h, j:j + w] = random_erasing.value - self.assertTrue(torch.equal(true_output, img_output)) - - # Test Set 3: Erasing with random value - random_erasing = transforms.RandomErasing(value="random") - if is_scripted: - random_erasing = torch.jit.script(random_erasing) - img_re = random_erasing(img) - - self.assertEqual(img_re.size(0), 3) - - # Test Set 4: Erasing with tuple value - random_erasing = transforms.RandomErasing(value=(0.2, 0.2, 0.2)) - if is_scripted: - random_erasing = torch.jit.script(random_erasing) - img_re = random_erasing(img) - self.assertEqual(img_re.size(0), 3) - true_output = img.clone() - true_output[:, i:i + h, j:j + w] = torch.tensor(random_erasing.value)[:, None, None] - self.assertTrue(torch.equal(true_output, img_output)) - - # Test Set 5: Testing the inplace behaviour - random_erasing = transforms.RandomErasing(value=(0.2,), inplace=True) - if is_scripted: - random_erasing = torch.jit.script(random_erasing) - - img_re = random_erasing(img) - self.assertTrue(torch.equal(img_re, img)) - - # Test Set 6: Checking when no erased region is selected - img = torch.rand([3, 300, 1]) - random_erasing = transforms.RandomErasing(ratio=(0.1, 0.2), value="random") - if is_scripted: - random_erasing = torch.jit.script(random_erasing) - img_re = random_erasing(img) - self.assertTrue(torch.equal(img_re, img)) - - def test_gaussian_blur(self): - np_img = np.ones((100, 100, 3), dtype=np.uint8) * 255 - img = F.to_pil_image(np_img, "RGB") - - with self.assertRaises(ValueError): - F.gaussian_blur(img, [3]) - with self.assertRaises(ValueError): - F.gaussian_blur(img, [3, 3, 3]) - with self.assertRaises(ValueError): - F.gaussian_blur(img, [4, 4]) - with self.assertRaises(ValueError): - F.gaussian_blur(img, [-3, -3]) - with self.assertRaises(ValueError): - F.gaussian_blur(img, 3, [1, 1, 1]) - with self.assertRaises(ValueError): - F.gaussian_blur(img, 3, -1) - - with self.assrtRaises(TypeError): - F.gaussian_blur(img, 'kernel_size_string') - with self.assrtRaises(TypeError): - F.gaussian_blur(img, 3, 'sigma_string') - with self.assrtRaises(TypeError): - F.gaussian_blur(np_img, 3, 1) + def test_gaussian_blur_asserts(self): + np_img = np.ones((100, 100, 3), dtype=np.uint8) * 255 + img = F.to_pil_image(np_img, "RGB") + + with self.assertRaisesRegex(ValueError, r"If kernel_size is a sequence its length should be 2"): + F.gaussian_blur(img, [3]) + + with self.assertRaisesRegex(ValueError, r"If kernel_size is a sequence its length should be 2"): + F.gaussian_blur(img, [3, 3, 3]) + with self.assertRaisesRegex(ValueError, r"Kernel size should be a tuple/list of two integers"): + transforms.GaussianBlur([3, 3, 3]) + + with self.assertRaisesRegex(ValueError, r"kernel_size should have odd and positive integers"): + F.gaussian_blur(img, [4, 4]) + with self.assertRaisesRegex(ValueError, r"Kernel size value should be an odd and positive number"): + transforms.GaussianBlur([4, 4]) + + with self.assertRaisesRegex(ValueError, r"kernel_size should have odd and positive integers"): + F.gaussian_blur(img, [-3, -3]) + with self.assertRaisesRegex(ValueError, r"Kernel size value should be an odd and positive number"): + transforms.GaussianBlur([-3, -3]) + + with self.assertRaisesRegex(ValueError, r"If sigma is a sequence, its length should be 2"): + F.gaussian_blur(img, 3, [1, 1, 1]) + with self.assertRaisesRegex(ValueError, r"sigma should be a single number or a list/tuple with length 2"): + transforms.GaussianBlur(3, [1, 1, 1]) + + with self.assertRaisesRegex(ValueError, r"sigma should have positive values"): + F.gaussian_blur(img, 3, -1.0) + with self.assertRaisesRegex(ValueError, r"If sigma is a single number, it must be positive"): + transforms.GaussianBlur(3, -1.0) + + with self.assertRaisesRegex(TypeError, r"kernel_size should be int or a sequence of integers"): + F.gaussian_blur(img, "kernel_size_string") + with self.assertRaisesRegex(ValueError, r"Kernel size should be a tuple/list of two integers"): + transforms.GaussianBlur("kernel_size_string") + + with self.assertRaisesRegex(TypeError, r"sigma should be either float or sequence of floats"): + F.gaussian_blur(img, 3, "sigma_string") + with self.assertRaisesRegex(ValueError, r"sigma should be a single number or a list/tuple with length 2"): + transforms.GaussianBlur(3, "sigma_string") if __name__ == '__main__': diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index 2e1e1fd2f52..1e1d048e005 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -1024,10 +1024,10 @@ def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: Optional[List[floa if sigma is None: sigma = [ksize * 0.15 + 0.35 for ksize in kernel_size] - if sigma is not None and not isinstance(sigma, (float, list, tuple)): + if sigma is not None and not isinstance(sigma, (int, float, list, tuple)): raise TypeError('sigma should be either float or sequence of floats. Got {}'.format(type(sigma))) - if isinstance(sigma, float): - sigma = [sigma, sigma] + if isinstance(sigma, (int, float)): + sigma = [float(sigma), float(sigma)] if isinstance(sigma, (list, tuple)) and len(sigma) == 1: sigma = [sigma[0], sigma[0]] if len(sigma) != 2: diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 3862a788d21..74cd0d415fc 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -879,24 +879,22 @@ def _assert_grid_transform_inputs( raise ValueError("Resampling mode '{}' is unsupported with Tensor input".format(resample)) -def _apply_grid_transform(img: Tensor, grid: Tensor, mode: str) -> Tensor: - # make image NCHW +def _cast_squeeze_in(img: Tensor, req_dtype: torch.dtype) -> Tuple[Tensor, bool, bool, torch.dtype]: need_squeeze = False + # make image NCHW if img.ndim < 4: img = img.unsqueeze(dim=0) need_squeeze = True out_dtype = img.dtype need_cast = False - if out_dtype != grid.dtype: + if out_dtype != req_dtype: need_cast = True - img = img.to(grid) + img = img.to(req_dtype) + return img, need_cast, need_squeeze, out_dtype - if img.shape[0] > 1: - # Apply same grid to a batch of images - grid = grid.expand(img.shape[0], grid.shape[1], grid.shape[2], grid.shape[3]) - img = grid_sample(img, grid, mode=mode, padding_mode="zeros", align_corners=False) +def _cast_squeeze_out(img: Tensor, need_cast: bool, need_squeeze: bool, out_dtype: torch.dtype): if need_squeeze: img = img.squeeze(dim=0) @@ -907,6 +905,19 @@ def _apply_grid_transform(img: Tensor, grid: Tensor, mode: str) -> Tensor: return img +def _apply_grid_transform(img: Tensor, grid: Tensor, mode: str) -> Tensor: + + img, need_cast, need_squeeze, out_dtype = _cast_squeeze_in(img, grid.dtype) + + if img.shape[0] > 1: + # Apply same grid to a batch of images + grid = grid.expand(img.shape[0], grid.shape[1], grid.shape[2], grid.shape[3]) + img = grid_sample(img, grid, mode=mode, padding_mode="zeros", align_corners=False) + + img = _cast_squeeze_out(img, need_cast, need_squeeze, out_dtype) + return img + + def _gen_affine_grid( theta: Tensor, w: int, h: int, ow: int, oh: int, ) -> Tensor: @@ -1126,7 +1137,7 @@ def _get_gaussian_kernel2d( ) -> Tensor: kernel1d_x = _get_gaussian_kernel1d(kernel_size[0], sigma[0]).to(device, dtype=dtype) kernel1d_y = _get_gaussian_kernel1d(kernel_size[1], sigma[1]).to(device, dtype=dtype) - kernel2d = torch.mm(kernel1d_x[:, None], kernel1d_y[None, :]) + kernel2d = torch.mm(kernel1d_y[:, None], kernel1d_x[None, :]) return kernel2d @@ -1153,28 +1164,12 @@ def gaussian_blur(img: Tensor, kernel_size: List[int], sigma: List[float]) -> Te kernel = _get_gaussian_kernel2d(kernel_size, sigma, dtype=dtype, device=img.device) kernel = kernel.expand(img.shape[-3], 1, kernel.shape[0], kernel.shape[1]) - # make image NCHW - need_squeeze = False - if img.ndim < 4: - img = img.unsqueeze(dim=0) - need_squeeze = True - - out_dtype = img.dtype - need_cast = False - if out_dtype != kernel.dtype: - need_cast = True - img = img.to(kernel) + img, need_cast, need_squeeze, out_dtype = _cast_squeeze_in(img, kernel.dtype) # padding = (left, right, top, bottom) padding = [kernel_size[0] // 2, kernel_size[0] // 2, kernel_size[1] // 2, kernel_size[1] // 2] img = torch_pad(img, padding, mode="reflect") img = conv2d(img, kernel, groups=img.shape[-3]) - if need_squeeze: - img = img.squeeze(dim=0) - - if need_cast: - # it is better to round before cast - img = torch.round(img).to(out_dtype) - + img = _cast_squeeze_out(img, need_cast, need_squeeze, out_dtype) return img diff --git a/torchvision/transforms/transforms.py b/torchvision/transforms/transforms.py index e443812a2da..e00a50f067a 100644 --- a/torchvision/transforms/transforms.py +++ b/torchvision/transforms/transforms.py @@ -1527,7 +1527,7 @@ def __init__(self, kernel_size, sigma=(0.1, 2.0)): if not 0. < sigma[0] <= sigma[1]: raise ValueError("sigma values should be positive and of the form (min, max).") else: - raise TypeError("sigma should be a single number or a list/tuple with length 2.") + raise ValueError("sigma should be a single number or a list/tuple with length 2.") self.sigma = sigma @@ -1556,7 +1556,7 @@ def forward(self, img: Tensor) -> Tensor: return F.gaussian_blur(img, self.kernel_size, [sigma, sigma]) def __repr__(self): - s = '(kernel size={}, '.format(self.ksize) + s = '(kernel_size={}, '.format(self.kernel_size) s += 'sigma={})'.format(self.sigma) return self.__class__.__name__ + s