def forward(self, x: torch.Tensor) -> Tuple[ # type: ignore List, List, List]: bs, ch, h, w = x.size() pixel_distance = 1.0 cur_sigma = 0.5 if self.double_image: x = F.interpolate(x, scale_factor=2.0, mode='bilinear', align_corners=False) pixel_distance = 0.5 cur_sigma *= 2.0 if self.init_sigma > cur_sigma: sigma = math.sqrt(self.init_sigma**2 - cur_sigma**2) cur_sigma = self.init_sigma ksize = self.get_kernel_size(sigma) cur_level = gaussian_blur2d(x, (ksize, ksize), (sigma, sigma)) else: cur_level = x sigmas = [ cur_sigma * torch.ones(bs, self.n_levels).to(x.device).to(x.dtype) ] pixel_dists = [ pixel_distance * torch.ones(bs, self.n_levels).to(x.device).to(x.dtype) ] pyr = [[cur_level.unsqueeze(1)]] oct_idx = 0 while True: cur_level = pyr[-1][0].squeeze(1) for level_idx in range(1, self.n_levels): sigma = cur_sigma * math.sqrt(self.sigma_step**2 - 1.0) cur_level = gaussian_blur2d(cur_level, (ksize, ksize), (sigma, sigma)) cur_sigma *= self.sigma_step pyr[-1].append(cur_level.unsqueeze(1)) sigmas[-1][:, level_idx] = cur_sigma pixel_dists[-1][:, level_idx] = pixel_distance nextOctaveFirstLevel = F.interpolate(pyr[-1][-1].squeeze(1), scale_factor=0.5, mode='bilinear', align_corners=False) pixel_distance *= 2.0 cur_sigma = self.init_sigma if (min(nextOctaveFirstLevel.size(2), nextOctaveFirstLevel.size(3)) <= self.min_size): break pyr.append([nextOctaveFirstLevel.unsqueeze(1)]) sigmas.append(cur_sigma * torch.ones(bs, self.n_levels).to(x.device)) pixel_dists.append(pixel_distance * torch.ones(bs, self.n_levels).to(x.device)) oct_idx += 1 for i in range(len(pyr)): pyr[i] = torch.cat(pyr[i], dim=1) # type: ignore return pyr, sigmas, pixel_dists
def unsharp_mask(input: torch.Tensor, kernel_size: Tuple[int, int], sigma: Tuple[float, float], border_type: str = 'reflect') -> torch.Tensor: r"""Creates an operator that blurs a tensor using the existing Gaussian filter available with the Kornia library. Arguments: input (torch.Tensor): the input tensor with shape :math:`(B,C,H,W)`. kernel_size (Tuple[int, int]): the size of the kernel. sigma (Tuple[float, float]): the standard deviation of the kernel. border_type (str): the padding mode to be applied before convolving. The expected modes are: ``'constant'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. Default: ``'reflect'``. Returns: torch.Tensor: the blurred tensor with shape :math:`(B,C,H,W)`. Examples: >>> input = torch.rand(2, 4, 5, 5) >>> output = unsharp_mask(input, (3, 3), (1.5, 1.5)) >>> output.shape torch.Size([2, 4, 5, 5]) """ data_blur: torch.Tensor = gaussian_blur2d(input, kernel_size, sigma) data_sharpened: torch.Tensor = input + (input - data_blur) return data_sharpened
def unsharp_mask(input: torch.Tensor, kernel_size: Tuple[int, int], sigma: Tuple[float, float], border_type: str = 'reflect') -> torch.Tensor: r"""Create an operator that sharpens a tensor by applying operation out = 2 * image - gaussian_blur2d(image). .. image:: _static/img/unsharp_mask.png Args: input: the input tensor with shape :math:`(B,C,H,W)`. kernel_size: the size of the kernel. sigma: the standard deviation of the kernel. border_type: the padding mode to be applied before convolving. The expected modes are: ``'constant'``, ``'reflect'``, ``'replicate'`` or ``'circular'``. Returns: the blurred tensor with shape :math:`(B,C,H,W)`. Examples: >>> input = torch.rand(2, 4, 5, 5) >>> output = unsharp_mask(input, (3, 3), (1.5, 1.5)) >>> output.shape torch.Size([2, 4, 5, 5]) """ data_blur: torch.Tensor = gaussian_blur2d(input, kernel_size, sigma, border_type) data_sharpened: torch.Tensor = input + (input - data_blur) return data_sharpened
def apply_transform(self, input: Tensor, params: Dict[str, Tensor], transform: Optional[Tensor] = None) -> Tensor: return gaussian_blur2d(input, self.flags["kernel_size"], self.flags["sigma"], self.flags["border_type"].name.lower())
def __call__(self, image): random_kernel = random.randrange(25, 35, 2) random_kernel = (random_kernel, random_kernel) random_sigma = random.uniform(5, 10) random_sigma = (random_sigma, random_sigma) image = torch.unsqueeze(kornia.image_to_tensor(image).float(), dim=0) image = gaussian_blur2d(image, random_kernel, random_sigma) return kornia.tensor_to_image(image)
def forward(self, x): gau = [] y = x # gaussian for i in range(self.levels): gau.append(y) y = gaussian_blur2d(y, kernel_size=(3, 3), sigma=(2, 2)) y = self.pool(y) return gau
def defocus_blur(inp, disk_radius, alias_blur): kernel_size = (3, 3) if disk_radius <= 8 else (5, 5) mesh_range = ch.arange(-max(8, disk_radius), max(8, disk_radius) + 1) X, Y = ch.meshgrid(mesh_range, mesh_range) aliased_disk = ((X.pow(2) + Y.pow(2)) <= disk_radius**2).float() aliased_disk /= aliased_disk.sum() kernel = filters.gaussian_blur2d(aliased_disk[None, None, ...], kernel_size, (alias_blur, alias_blur))[0] return ch.clamp(filters.filter2D(inp, kernel), 0, 1)
def forward(self, x: torch.Tensor) -> Tuple[List, List, List]: # type: ignore bs, ch, h, w = x.size() cur_level, cur_sigma, pixel_distance = self.get_first_level(x) sigmas = [ cur_sigma * torch.ones(bs, self.n_levels + self.extra_levels).to( x.device).to(x.dtype) ] pixel_dists = [ pixel_distance * torch.ones( bs, self.n_levels + self.extra_levels).to(x.device).to(x.dtype) ] pyr = [[cur_level]] oct_idx = 0 while True: cur_level = pyr[-1][0] for level_idx in range(1, self.n_levels + self.extra_levels): sigma = cur_sigma * math.sqrt(self.sigma_step**2 - 1.0) ksize = self.get_kernel_size(sigma) # Hack, because PyTorch does not allow to pad more than original size. # But for the huge sigmas, one needs huge kernel and padding... ksize = min(ksize, min(cur_level.size(2), cur_level.size(3))) if ksize % 2 == 0: ksize += 1 cur_level = gaussian_blur2d(cur_level, (ksize, ksize), (sigma, sigma)) cur_sigma *= self.sigma_step pyr[-1].append(cur_level) sigmas[-1][:, level_idx] = cur_sigma pixel_dists[-1][:, level_idx] = pixel_distance _pyr = pyr[-1][-self.extra_levels] nextOctaveFirstLevel = F.interpolate( _pyr, size=(_pyr.size(-2) // 2, _pyr.size(-1) // 2), mode='nearest') # Nearest matches OpenCV SIFT pixel_distance *= 2.0 cur_sigma = self.init_sigma if min(nextOctaveFirstLevel.size(2), nextOctaveFirstLevel.size(3)) <= self.min_size: break pyr.append([nextOctaveFirstLevel]) sigmas.append( cur_sigma * torch.ones(bs, self.n_levels + self.extra_levels).to(x.device)) pixel_dists.append( pixel_distance * torch.ones(bs, self.n_levels + self.extra_levels).to(x.device)) oct_idx += 1 for i in range(len(pyr)): pyr[i] = torch.stack(pyr[i], dim=2) # type: ignore return pyr, sigmas, pixel_dists
def get_normal_anlges(image, eps=EPS): """Calculate the normal direction of edges. Ref: https://github.com/nv-tlabs/STEAL/blob/master/utils/edges_nms.m """ first_grads = spatial_gradient(gaussian_blur2d(image, (5, 5), (2, 2))) second_grad_x = spatial_gradient(first_grads[:, :, 0, :, :].squeeze_(2)) second_grad_y = spatial_gradient(first_grads[:, :, 1, :, :].squeeze_(2)) grad_xx = second_grad_x[:, :, 0, :, :].squeeze_() grad_xy = second_grad_y[:, :, 0, :, :].squeeze_() grad_yy = second_grad_y[:, :, 1, :, :].squeeze_() angle = torch.atan(grad_yy * torch.sign(-(grad_xy + eps)) / (grad_xx + eps)) return angle
def get_first_level(self, input): pixel_distance = 1.0 cur_sigma = 0.5 # Same as in OpenCV up to interpolation difference if self.double_image: x = F.interpolate(input, scale_factor=2.0, mode='bilinear', align_corners=False) pixel_distance = 0.5 cur_sigma *= 2.0 else: x = input if self.init_sigma > cur_sigma: sigma = max(math.sqrt(self.init_sigma**2 - cur_sigma**2), 0.01) ksize = self.get_kernel_size(sigma) cur_level = gaussian_blur2d(x, (ksize, ksize), (sigma, sigma)) cur_sigma = self.init_sigma else: cur_level = x return cur_level, cur_sigma, pixel_distance
def gaussian_blur(inp, blur_std): kernel_size = math.ceil((2 * blur_std) * 2) if kernel_size % 2 == 0: kernel_size += 1 return filters.gaussian_blur2d(inp, (kernel_size, kernel_size), (blur_std, blur_std))
def harris_response( input: torch.Tensor, k: Union[torch.Tensor, float] = 0.04, grads_mode: str = 'sobel', sigmas: Optional[torch.Tensor] = None, ) -> torch.Tensor: r"""Computes the Harris cornerness function. Function does not do any normalization or nms. The response map is computed according the following formulation: .. math:: R = max(0, det(M) - k \cdot trace(M)^2) where: .. math:: M = \sum_{(x,y) \in W} \begin{bmatrix} I^{2}_x & I_x I_y \\ I_x I_y & I^{2}_y \\ \end{bmatrix} and :math:`k` is an empirically determined constant :math:`k ∈ [ 0.04 , 0.06 ]` Args: input: input image with shape :math:`(B, C, H, W)`. k: the Harris detector free parameter. grads_mode: can be ``'sobel'`` for standalone use or ``'diff'`` for use on Gaussian pyramid. sigmas: coefficients to be multiplied by multichannel response. Should be shape of :math:`(B)` It is necessary for performing non-maxima-suppression across different scale pyramid levels. See `vlfeat <https://github.com/vlfeat/vlfeat/blob/master/vl/covdet.c#L874>`_. Return: the response map per channel with shape :math:`(B, C, H, W)`. Example: >>> input = torch.tensor([[[ ... [0., 0., 0., 0., 0., 0., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 0., 0., 0., 0., 0., 0.], ... ]]]) # 1x1x7x7 >>> # compute the response map harris_response(input, 0.04) tensor([[[[0.0012, 0.0039, 0.0020, 0.0000, 0.0020, 0.0039, 0.0012], [0.0039, 0.0065, 0.0040, 0.0000, 0.0040, 0.0065, 0.0039], [0.0020, 0.0040, 0.0029, 0.0000, 0.0029, 0.0040, 0.0020], [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], [0.0020, 0.0040, 0.0029, 0.0000, 0.0029, 0.0040, 0.0020], [0.0039, 0.0065, 0.0040, 0.0000, 0.0040, 0.0065, 0.0039], [0.0012, 0.0039, 0.0020, 0.0000, 0.0020, 0.0039, 0.0012]]]]) """ # TODO: Recompute doctest if not isinstance(input, torch.Tensor): raise TypeError("Input type is not a torch.Tensor. Got {}".format( type(input))) if not len(input.shape) == 4: raise ValueError( "Invalid input shape, we expect BxCxHxW. Got: {}".format( input.shape)) if sigmas is not None: if not isinstance(sigmas, torch.Tensor): raise TypeError("sigmas type is not a torch.Tensor. Got {}".format( type(sigmas))) if (not len(sigmas.shape) == 1) or (sigmas.size(0) != input.size(0)): raise ValueError( "Invalid sigmas shape, we expect B == input.size(0). Got: {}". format(sigmas.shape)) gradients: torch.Tensor = spatial_gradient(input, grads_mode) dx: torch.Tensor = gradients[:, :, 0] dy: torch.Tensor = gradients[:, :, 1] # compute the structure tensor M elements dx2: torch.Tensor = gaussian_blur2d(dx**2, (7, 7), (1.0, 1.0)) dy2: torch.Tensor = gaussian_blur2d(dy**2, (7, 7), (1.0, 1.0)) dxy: torch.Tensor = gaussian_blur2d(dx * dy, (7, 7), (1.0, 1.0)) det_m: torch.Tensor = dx2 * dy2 - dxy * dxy trace_m: torch.Tensor = dx2 + dy2 # compute the response map scores: torch.Tensor = det_m - k * (trace_m**2) if sigmas is not None: scores = scores * sigmas.pow(4).view(-1, 1, 1, 1) return scores
def gftt_response(input: torch.Tensor, grads_mode: str = 'sobel', sigmas: Optional[torch.Tensor] = None) -> torch.Tensor: r"""Computes the Shi-Tomasi cornerness function. Function does not do any normalization or nms. The response map is computed according the following formulation: .. math:: R = min(eig(M)) where: .. math:: M = \sum_{(x,y) \in W} \begin{bmatrix} I^{2}_x & I_x I_y \\ I_x I_y & I^{2}_y \\ \end{bmatrix} Args: input: input image with shape :math:`(B, C, H, W)`. grads_mode: can be ``'sobel'`` for standalone use or ``'diff'`` for use on Gaussian pyramid. sigmas: coefficients to be multiplied by multichannel response. Should be shape of :math:`(B)` It is necessary for performing non-maxima-suppression across different scale pyramid levels. See `vlfeat <https://github.com/vlfeat/vlfeat/blob/master/vl/covdet.c#L874>`_. Return: the response map per channel with shape :math:`(B, C, H, W)`. Example: >>> input = torch.tensor([[[ ... [0., 0., 0., 0., 0., 0., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 1., 1., 1., 1., 1., 0.], ... [0., 0., 0., 0., 0., 0., 0.], ... ]]]) # 1x1x7x7 >>> # compute the response map gftt_response(input) tensor([[[[0.0155, 0.0334, 0.0194, 0.0000, 0.0194, 0.0334, 0.0155], [0.0334, 0.0575, 0.0339, 0.0000, 0.0339, 0.0575, 0.0334], [0.0194, 0.0339, 0.0497, 0.0000, 0.0497, 0.0339, 0.0194], [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], [0.0194, 0.0339, 0.0497, 0.0000, 0.0497, 0.0339, 0.0194], [0.0334, 0.0575, 0.0339, 0.0000, 0.0339, 0.0575, 0.0334], [0.0155, 0.0334, 0.0194, 0.0000, 0.0194, 0.0334, 0.0155]]]]) """ # TODO: Recompute doctest if not isinstance(input, torch.Tensor): raise TypeError("Input type is not a torch.Tensor. Got {}".format( type(input))) if not len(input.shape) == 4: raise ValueError( "Invalid input shape, we expect BxCxHxW. Got: {}".format( input.shape)) gradients: torch.Tensor = spatial_gradient(input, grads_mode) dx: torch.Tensor = gradients[:, :, 0] dy: torch.Tensor = gradients[:, :, 1] dx2: torch.Tensor = gaussian_blur2d(dx**2, (7, 7), (1.0, 1.0)) dy2: torch.Tensor = gaussian_blur2d(dy**2, (7, 7), (1.0, 1.0)) dxy: torch.Tensor = gaussian_blur2d(dx * dy, (7, 7), (1.0, 1.0)) det_m: torch.Tensor = dx2 * dy2 - dxy * dxy trace_m: torch.Tensor = dx2 + dy2 e1: torch.Tensor = 0.5 * (trace_m + torch.sqrt( (trace_m**2 - 4 * det_m).abs())) e2: torch.Tensor = 0.5 * (trace_m - torch.sqrt( (trace_m**2 - 4 * det_m).abs())) scores: torch.Tensor = torch.min(e1, e2) if sigmas is not None: scores = scores * sigmas.pow(4).view(-1, 1, 1, 1) return scores
def forward(self, img): if self.p > random(): sigma = (self.max_sigma - self.min_sigma) * random() + self.min_sigma return F.gaussian_blur2d(img, kernel_size=self.kernel_size, sigma=(sigma, sigma)) else: return img
def g(x): return gaussian_blur2d(x, (3, 3), (1., 1.))
def canny( input: torch.Tensor, low_threshold: float = 0.1, high_threshold: float = 0.2, kernel_size: Tuple[int, int] = (5, 5), sigma: Tuple[float, float] = (1, 1), hysteresis: bool = True, eps: float = 1e-6, ) -> Tuple[torch.Tensor, torch.Tensor]: r"""Finds edges of the input image and filters them using the Canny algorithm. Args: input (torch.Tensor): input image tensor with shape :math:`(B,C,H,W)`. low_threshold (float): lower threshold for the hysteresis procedure. Default: 0.1. high_threshold (float): upper threshold for the hysteresis procedure. Default: 0.1. kernel_size (Tuple[int, int]): the size of the kernel for the gaussian blur. sigma (Tuple[float, float]): the standard deviation of the kernel for the gaussian blur. hysteresis (bool): if True, applies the hysteresis edge tracking. Otherwise, the edges are divided between weak (0.5) and strong (1) edges. eps (float): regularization number to avoid NaN during backprop. Default: 1e-6. Returns: Tuple[torch.Tensor, torch.Tensor]: - the canny edge magnitudes map, shape of :math:`(B,1,H,W)`. - the canny edge detection filtered by thresholds and hysteresis, shape of :math:`(B,1,H,W)`. Example: >>> input = torch.rand(5, 3, 4, 4) >>> magnitude, edges = canny(input) # 5x3x4x4 >>> magnitude.shape torch.Size([5, 1, 4, 4]) >>> edges.shape torch.Size([5, 1, 4, 4]) """ if not isinstance(input, torch.Tensor): raise TypeError("Input type is not a torch.Tensor. Got {}".format(type(input))) if not len(input.shape) == 4: raise ValueError("Invalid input shape, we expect BxCxHxW. Got: {}".format(input.shape)) if low_threshold > high_threshold: raise ValueError( "Invalid input thresholds. low_threshold should be smaller than the high_threshold. Got: {}>{}".format( low_threshold, high_threshold ) ) if low_threshold < 0 and low_threshold > 1: raise ValueError( "Invalid input threshold. low_threshold should be in range (0,1). Got: {}".format(low_threshold) ) if high_threshold < 0 and high_threshold > 1: raise ValueError( "Invalid input threshold. high_threshold should be in range (0,1). Got: {}".format(high_threshold) ) device: torch.device = input.device dtype: torch.dtype = input.dtype # To Grayscale if input.shape[1] == 3: input = rgb_to_grayscale(input) # Gaussian filter blurred: torch.Tensor = gaussian_blur2d(input, kernel_size, sigma) # Compute the gradients gradients: torch.Tensor = spatial_gradient(blurred, normalized=False) # Unpack the edges gx: torch.Tensor = gradients[:, :, 0] gy: torch.Tensor = gradients[:, :, 1] # Compute gradient magnitude and angle magnitude: torch.Tensor = torch.sqrt(gx * gx + gy * gy + eps) angle: torch.Tensor = torch.atan2(gy, gx) # Radians to Degrees angle = rad2deg(angle) # Round angle to the nearest 45 degree angle = torch.round(angle / 45) * 45 # Non-maximal suppression nms_kernels: torch.Tensor = get_canny_nms_kernel(device, dtype) nms_magnitude: torch.Tensor = F.conv2d(magnitude, nms_kernels, padding=nms_kernels.shape[-1] // 2) # Get the indices for both directions positive_idx: torch.Tensor = (angle / 45) % 8 positive_idx = positive_idx.long() negative_idx: torch.Tensor = ((angle / 45) + 4) % 8 negative_idx = negative_idx.long() # Apply the non-maximum suppresion to the different directions channel_select_filtered_positive: torch.Tensor = torch.gather(nms_magnitude, 1, positive_idx) channel_select_filtered_negative: torch.Tensor = torch.gather(nms_magnitude, 1, negative_idx) channel_select_filtered: torch.Tensor = torch.stack( [channel_select_filtered_positive, channel_select_filtered_negative], 1 ) is_max: torch.Tensor = channel_select_filtered.min(dim=1)[0] > 0.0 magnitude = magnitude * is_max # Threshold edges: torch.Tensor = F.threshold(magnitude, low_threshold, 0.0) low: torch.Tensor = magnitude > low_threshold high: torch.Tensor = magnitude > high_threshold edges = low * 0.5 + high * 0.5 edges = edges.to(dtype) # Hysteresis if hysteresis: edges_old: torch.Tensor = -torch.ones(edges.shape, device=edges.device, dtype=dtype) hysteresis_kernels: torch.Tensor = get_hysteresis_kernel(device, dtype) while ((edges_old - edges).abs() != 0).any(): weak: torch.Tensor = (edges == 0.5).float() strong: torch.Tensor = (edges == 1).float() hysteresis_magnitude: torch.Tensor = F.conv2d( edges, hysteresis_kernels, padding=hysteresis_kernels.shape[-1] // 2 ) hysteresis_magnitude = (hysteresis_magnitude == 1).any(1, keepdim=True).to(dtype) hysteresis_magnitude = hysteresis_magnitude * weak + strong edges_old = edges.clone() edges = hysteresis_magnitude + (hysteresis_magnitude == 0) * weak * 0.5 edges = hysteresis_magnitude return magnitude, edges
def g(x): return gaussian_blur2d(x, (7, 7), (1., 1.))
def resize( input: torch.Tensor, size: Union[int, Tuple[int, int]], interpolation: str = 'bilinear', align_corners: Optional[bool] = None, side: str = "short", antialias: bool = False, ) -> torch.Tensor: r"""Resize the input torch.Tensor to the given size. .. image:: _static/img/resize.png Args: tensor: The image tensor to be skewed with shape of :math:`(..., H, W)`. `...` means there can be any number of dimensions. size: Desired output size. If size is a sequence like (h, w), output size will be matched to this. If size is an int, smaller edge of the image will be matched to this number. i.e, if height > width, then image will be rescaled to (size * height / width, size) interpolation: algorithm used for upsampling: ``'nearest'`` | ``'linear'`` | ``'bilinear'`` | 'bicubic' | 'trilinear' | 'area'. align_corners: interpolation flag. side: Corresponding side if ``size`` is an integer. Can be one of ``'short'``, ``'long'``, ``'vert'``, or ``'horz'``. antialias: if True, then image will be filtered with Gaussian before downscaling. No effect for upscaling. Returns: The resized tensor with the shape as the specified size. Example: >>> img = torch.rand(1, 3, 4, 4) >>> out = resize(img, (6, 8)) >>> print(out.shape) torch.Size([1, 3, 6, 8]) """ if not isinstance(input, torch.Tensor): raise TypeError( f"Input tensor type is not a torch.Tensor. Got {type(input)}") if len(input.shape) < 2: raise ValueError( f'Input tensor must have at least two dimensions. Got {len(input.shape)}' ) input_size = h, w = input.shape[-2:] if isinstance(size, int): aspect_ratio = w / h size = _side_to_image_size(size, aspect_ratio, side) if size == input_size: return input factors = (h / size[0], w / size[1]) # We do bluring only for downscaling antialias = antialias and (max(factors) > 1) if antialias: # First, we have to determine sigma # Taken from skimage: https://github.com/scikit-image/scikit-image/blob/v0.19.2/skimage/transform/_warps.py#L171 sigmas = (max((factors[0] - 1.0) / 2.0, 0.001), max((factors[1] - 1.0) / 2.0, 0.001)) # Now kernel size. Good results are for 3 sigma, but that is kind of slow. Pillow uses 1 sigma # https://github.com/python-pillow/Pillow/blob/master/src/libImaging/Resample.c#L206 # But they do it in the 2 passes, which gives better results. Let's try 2 sigmas for now ks = int(max(2.0 * 2 * sigmas[0], 3)), int(max(2.0 * 2 * sigmas[1], 3)) # Make sure it is odd if (ks[0] % 2) == 0: ks = ks[0] + 1, ks[1] if (ks[1] % 2) == 0: ks = ks[0], ks[1] + 1 input = gaussian_blur2d(input, ks, sigmas) output = torch.nn.functional.interpolate(input, size=size, mode=interpolation, align_corners=align_corners) return output