Skip to content

[proto] Added GaussianBlur transform and tests #6273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions test/test_prototype_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,3 +644,58 @@ def test_forward(self, padding, pad_if_needed, fill, padding_mode, mocker):
else:
# vfdev-5: I do not know how to mock and test this case
pass


class TestGaussianBlur:
def test_assertions(self):
with pytest.raises(ValueError, match="Kernel size should be a tuple/list of two integers"):
transforms.GaussianBlur([10, 12, 14])

with pytest.raises(ValueError, match="Kernel size value should be an odd and positive number"):
transforms.GaussianBlur(4)

with pytest.raises(TypeError, match="sigma should be a single float or a list/tuple with length 2"):
transforms.GaussianBlur(3, sigma=[1, 2, 3])

with pytest.raises(ValueError, match="If sigma is a single number, it must be positive"):
transforms.GaussianBlur(3, sigma=-1.0)

with pytest.raises(ValueError, match="sigma values should be positive and of the form"):
transforms.GaussianBlur(3, sigma=[2.0, 1.0])

@pytest.mark.parametrize("sigma", [10.0, [10.0, 12.0]])
def test__get_params(self, sigma):
transform = transforms.GaussianBlur(3, sigma=sigma)
params = transform._get_params(None)

if isinstance(sigma, float):
assert params["sigma"][0] == params["sigma"][1] == 10
else:
assert sigma[0] <= params["sigma"][0] <= sigma[1]
assert sigma[0] <= params["sigma"][1] <= sigma[1]

@pytest.mark.parametrize("kernel_size", [3, [3, 5], (5, 3)])
@pytest.mark.parametrize("sigma", [2.0, [2.0, 3.0]])
def test__transform(self, kernel_size, sigma, mocker):
transform = transforms.GaussianBlur(kernel_size=kernel_size, sigma=sigma)

if isinstance(kernel_size, (tuple, list)):
assert transform.kernel_size == kernel_size
else:
assert transform.kernel_size == (kernel_size, kernel_size)

if isinstance(sigma, (tuple, list)):
assert transform.sigma == sigma
else:
assert transform.sigma == (sigma, sigma)

fn = mocker.patch("torchvision.prototype.transforms.functional.gaussian_blur")
inpt = features.Image(torch.rand(1, 3, 32, 32))
# vfdev-5, Feature Request: let's store params as Transform attribute
# This could be also helpful for users
torch.manual_seed(12)
_ = transform(inpt)
torch.manual_seed(12)
params = transform._get_params(inpt)

fn.assert_called_once_with(inpt, **params)
67 changes: 67 additions & 0 deletions test/test_prototype_transforms_functional.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import functools
import itertools
import math
import os

import numpy as np
import pytest
Expand Down Expand Up @@ -495,6 +496,7 @@ def center_crop_bounding_box():
)


@register_kernel_info_from_sample_inputs_fn
def center_crop_segmentation_mask():
for mask, output_size in itertools.product(
make_segmentation_masks(image_sizes=((16, 16), (7, 33), (31, 9))),
Expand All @@ -503,6 +505,16 @@ def center_crop_segmentation_mask():
yield SampleInput(mask, output_size)


@register_kernel_info_from_sample_inputs_fn
def gaussian_blur_image_tensor():
for image, kernel_size, sigma in itertools.product(
make_images(extra_dims=((4,),)),
[[3, 3]],
[None, [3.0, 3.0]],
):
yield SampleInput(image, kernel_size=kernel_size, sigma=sigma)


@pytest.mark.parametrize(
"kernel",
[
Expand Down Expand Up @@ -1555,3 +1567,58 @@ def _compute_expected_segmentation_mask(mask, output_size):

expected = _compute_expected_segmentation_mask(mask, output_size)
torch.testing.assert_close(expected, actual)


# Copied from test/test_functional_tensor.py
@pytest.mark.parametrize("device", cpu_and_gpu())
@pytest.mark.parametrize("image_size", ("small", "large"))
@pytest.mark.parametrize("dt", [None, torch.float32, torch.float64, torch.float16])
@pytest.mark.parametrize("ksize", [(3, 3), [3, 5], (23, 23)])
@pytest.mark.parametrize("sigma", [[0.5, 0.5], (0.5, 0.5), (0.8, 0.8), (1.7, 1.7)])
def test_correctness_gaussian_blur_image_tensor(device, image_size, dt, ksize, sigma):
fn = F.gaussian_blur_image_tensor

# 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)

if image_size == "small":
tensor = (
torch.from_numpy(np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3))).permute(2, 0, 1).to(device)
)
else:
tensor = torch.from_numpy(np.arange(26 * 28, dtype="uint8").reshape((1, 26, 28))).to(device)

if dt == torch.float16 and device == "cpu":
# skip float16 on CPU case
return

if dt is not None:
tensor = tensor.to(dtype=dt)

_ksize = (ksize, ksize) if isinstance(ksize, int) else ksize
_sigma = sigma[0] if sigma is not None else None
shape = tensor.shape
gt_key = f"{shape[-2]}_{shape[-1]}_{shape[-3]}__{_ksize[0]}_{_ksize[1]}_{_sigma}"
if gt_key not in true_cv2_results:
return

true_out = (
torch.tensor(true_cv2_results[gt_key]).reshape(shape[-2], shape[-1], shape[-3]).permute(2, 0, 1).to(tensor)
)

out = fn(tensor, kernel_size=ksize, sigma=sigma)
torch.testing.assert_close(out, true_out, rtol=0.0, atol=1.0, msg=f"{ksize}, {sigma}")
3 changes: 3 additions & 0 deletions torchvision/prototype/features/_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,6 @@ def equalize(self) -> Any:

def invert(self) -> Any:
return self

def gaussian_blur(self, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Any:
return self
6 changes: 6 additions & 0 deletions torchvision/prototype/features/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,9 @@ def invert(self) -> Image:

output = _F.invert_image_tensor(self)
return Image.new_like(self, output)

def gaussian_blur(self, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Image:
from torchvision.prototype.transforms import functional as _F

output = _F.gaussian_blur_image_tensor(self, kernel_size=kernel_size, sigma=sigma)
return Image.new_like(self, output)
5 changes: 4 additions & 1 deletion torchvision/prototype/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
RandomAffine,
)
from ._meta import ConvertBoundingBoxFormat, ConvertImageDtype, ConvertImageColorSpace
from ._misc import Identity, Normalize, ToDtype, Lambda
from ._misc import Identity, GaussianBlur, Normalize, ToDtype, Lambda
from ._type_conversion import DecodeImage, LabelToOneHot

from ._deprecated import Grayscale, RandomGrayscale, ToTensor, ToPILImage, PILToTensor # usort: skip

# TODO: add RandomPerspective, RandomInvert, RandomPosterize, RandomSolarize,
# RandomAdjustSharpness, RandomAutocontrast, ElasticTransform
33 changes: 32 additions & 1 deletion torchvision/prototype/transforms/_misc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import functools
from typing import Any, List, Type, Callable, Dict
from typing import Any, List, Type, Callable, Dict, Sequence, Union

import torch
from torchvision.prototype.transforms import Transform, functional as F
from torchvision.transforms.transforms import _setup_size


class Identity(Transform):
Expand Down Expand Up @@ -46,6 +47,36 @@ def _transform(self, input: Any, params: Dict[str, Any]) -> Any:
return input


class GaussianBlur(Transform):
def __init__(
self, kernel_size: Union[int, Sequence[int]], sigma: Union[float, Sequence[float]] = (0.1, 2.0)
) -> None:
super().__init__()
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.")

if isinstance(sigma, float):
if sigma <= 0:
raise ValueError("If sigma is a single number, it must be positive.")
sigma = (sigma, sigma)
elif isinstance(sigma, Sequence) and len(sigma) == 2:
if not 0.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 float or a list/tuple with length 2 floats.")

self.sigma = sigma

def _get_params(self, sample: Any) -> Dict[str, Any]:
sigma = torch.empty(1).uniform_(self.sigma[0], self.sigma[1]).item()
return dict(sigma=[sigma, sigma])

def _transform(self, inpt: Any, params: Dict[str, Any]) -> Any:
return F.gaussian_blur(inpt, **params)


class ToDtype(Lambda):
def __init__(self, dtype: torch.dtype, *types: Type) -> None:
self.dtype = dtype
Expand Down
7 changes: 6 additions & 1 deletion torchvision/prototype/transforms/functional/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@
ten_crop_image_tensor,
ten_crop_image_pil,
)
from ._misc import normalize_image_tensor, gaussian_blur_image_tensor
from ._misc import (
normalize_image_tensor,
gaussian_blur,
gaussian_blur_image_tensor,
gaussian_blur_image_pil,
)
from ._type_conversion import (
decode_image_with_pil,
decode_video_with_av,
Expand Down
25 changes: 24 additions & 1 deletion torchvision/prototype/transforms/functional/_misc.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
from typing import Optional, List
from typing import Optional, List, Union

import PIL.Image
import torch
from torchvision.prototype import features
from torchvision.transforms import functional_tensor as _FT
from torchvision.transforms.functional import pil_to_tensor, to_pil_image


# shortcut type
DType = Union[torch.Tensor, PIL.Image.Image, features._Feature]


normalize_image_tensor = _FT.normalize


def normalize(inpt: DType, mean: List[float], std: List[float], inplace: bool = False) -> DType:
if isinstance(inpt, features.Image):
return normalize_image_tensor(inpt, mean=mean, std=std, inplace=inplace)
elif type(inpt) == torch.Tensor:
return normalize_image_tensor(inpt, mean=mean, std=std, inplace=inplace)
else:
raise TypeError("Unsupported input type")


def gaussian_blur_image_tensor(
img: torch.Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None
) -> torch.Tensor:
Expand Down Expand Up @@ -42,3 +56,12 @@ def gaussian_blur_image_pil(img: PIL.Image, kernel_size: List[int], sigma: Optio
t_img = pil_to_tensor(img)
output = gaussian_blur_image_tensor(t_img, kernel_size=kernel_size, sigma=sigma)
return to_pil_image(output, mode=img.mode)


def gaussian_blur(inpt: DType, kernel_size: List[int], sigma: Optional[List[float]] = None) -> DType:
if isinstance(inpt, features._Feature):
return inpt.gaussian_blur(kernel_size=kernel_size, sigma=sigma)
elif isinstance(inpt, PIL.Image.Image):
return gaussian_blur_image_pil(inpt, kernel_size=kernel_size, sigma=sigma)
else:
return gaussian_blur_image_tensor(inpt, kernel_size=kernel_size, sigma=sigma)