Skip to content

Commit bb2f4e1

Browse files
authored
[proto] Added GaussianBlur transform and tests (#6273)
* Added GaussianBlur transform and tests * Fixing code format * Copied correctness test
1 parent 615b175 commit bb2f4e1

File tree

8 files changed

+197
-4
lines changed

8 files changed

+197
-4
lines changed

test/test_prototype_transforms.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,58 @@ def test_forward(self, padding, pad_if_needed, fill, padding_mode, mocker):
644644
else:
645645
# vfdev-5: I do not know how to mock and test this case
646646
pass
647+
648+
649+
class TestGaussianBlur:
650+
def test_assertions(self):
651+
with pytest.raises(ValueError, match="Kernel size should be a tuple/list of two integers"):
652+
transforms.GaussianBlur([10, 12, 14])
653+
654+
with pytest.raises(ValueError, match="Kernel size value should be an odd and positive number"):
655+
transforms.GaussianBlur(4)
656+
657+
with pytest.raises(TypeError, match="sigma should be a single float or a list/tuple with length 2"):
658+
transforms.GaussianBlur(3, sigma=[1, 2, 3])
659+
660+
with pytest.raises(ValueError, match="If sigma is a single number, it must be positive"):
661+
transforms.GaussianBlur(3, sigma=-1.0)
662+
663+
with pytest.raises(ValueError, match="sigma values should be positive and of the form"):
664+
transforms.GaussianBlur(3, sigma=[2.0, 1.0])
665+
666+
@pytest.mark.parametrize("sigma", [10.0, [10.0, 12.0]])
667+
def test__get_params(self, sigma):
668+
transform = transforms.GaussianBlur(3, sigma=sigma)
669+
params = transform._get_params(None)
670+
671+
if isinstance(sigma, float):
672+
assert params["sigma"][0] == params["sigma"][1] == 10
673+
else:
674+
assert sigma[0] <= params["sigma"][0] <= sigma[1]
675+
assert sigma[0] <= params["sigma"][1] <= sigma[1]
676+
677+
@pytest.mark.parametrize("kernel_size", [3, [3, 5], (5, 3)])
678+
@pytest.mark.parametrize("sigma", [2.0, [2.0, 3.0]])
679+
def test__transform(self, kernel_size, sigma, mocker):
680+
transform = transforms.GaussianBlur(kernel_size=kernel_size, sigma=sigma)
681+
682+
if isinstance(kernel_size, (tuple, list)):
683+
assert transform.kernel_size == kernel_size
684+
else:
685+
assert transform.kernel_size == (kernel_size, kernel_size)
686+
687+
if isinstance(sigma, (tuple, list)):
688+
assert transform.sigma == sigma
689+
else:
690+
assert transform.sigma == (sigma, sigma)
691+
692+
fn = mocker.patch("torchvision.prototype.transforms.functional.gaussian_blur")
693+
inpt = features.Image(torch.rand(1, 3, 32, 32))
694+
# vfdev-5, Feature Request: let's store params as Transform attribute
695+
# This could be also helpful for users
696+
torch.manual_seed(12)
697+
_ = transform(inpt)
698+
torch.manual_seed(12)
699+
params = transform._get_params(inpt)
700+
701+
fn.assert_called_once_with(inpt, **params)

test/test_prototype_transforms_functional.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import itertools
33
import math
4+
import os
45

56
import numpy as np
67
import pytest
@@ -495,6 +496,7 @@ def center_crop_bounding_box():
495496
)
496497

497498

499+
@register_kernel_info_from_sample_inputs_fn
498500
def center_crop_segmentation_mask():
499501
for mask, output_size in itertools.product(
500502
make_segmentation_masks(image_sizes=((16, 16), (7, 33), (31, 9))),
@@ -503,6 +505,16 @@ def center_crop_segmentation_mask():
503505
yield SampleInput(mask, output_size)
504506

505507

508+
@register_kernel_info_from_sample_inputs_fn
509+
def gaussian_blur_image_tensor():
510+
for image, kernel_size, sigma in itertools.product(
511+
make_images(extra_dims=((4,),)),
512+
[[3, 3]],
513+
[None, [3.0, 3.0]],
514+
):
515+
yield SampleInput(image, kernel_size=kernel_size, sigma=sigma)
516+
517+
506518
@pytest.mark.parametrize(
507519
"kernel",
508520
[
@@ -1555,3 +1567,58 @@ def _compute_expected_segmentation_mask(mask, output_size):
15551567

15561568
expected = _compute_expected_segmentation_mask(mask, output_size)
15571569
torch.testing.assert_close(expected, actual)
1570+
1571+
1572+
# Copied from test/test_functional_tensor.py
1573+
@pytest.mark.parametrize("device", cpu_and_gpu())
1574+
@pytest.mark.parametrize("image_size", ("small", "large"))
1575+
@pytest.mark.parametrize("dt", [None, torch.float32, torch.float64, torch.float16])
1576+
@pytest.mark.parametrize("ksize", [(3, 3), [3, 5], (23, 23)])
1577+
@pytest.mark.parametrize("sigma", [[0.5, 0.5], (0.5, 0.5), (0.8, 0.8), (1.7, 1.7)])
1578+
def test_correctness_gaussian_blur_image_tensor(device, image_size, dt, ksize, sigma):
1579+
fn = F.gaussian_blur_image_tensor
1580+
1581+
# true_cv2_results = {
1582+
# # np_img = np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3))
1583+
# # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.8)
1584+
# "3_3_0.8": ...
1585+
# # cv2.GaussianBlur(np_img, ksize=(3, 3), sigmaX=0.5)
1586+
# "3_3_0.5": ...
1587+
# # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.8)
1588+
# "3_5_0.8": ...
1589+
# # cv2.GaussianBlur(np_img, ksize=(3, 5), sigmaX=0.5)
1590+
# "3_5_0.5": ...
1591+
# # np_img2 = np.arange(26 * 28, dtype="uint8").reshape((26, 28))
1592+
# # cv2.GaussianBlur(np_img2, ksize=(23, 23), sigmaX=1.7)
1593+
# "23_23_1.7": ...
1594+
# }
1595+
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "gaussian_blur_opencv_results.pt")
1596+
true_cv2_results = torch.load(p)
1597+
1598+
if image_size == "small":
1599+
tensor = (
1600+
torch.from_numpy(np.arange(3 * 10 * 12, dtype="uint8").reshape((10, 12, 3))).permute(2, 0, 1).to(device)
1601+
)
1602+
else:
1603+
tensor = torch.from_numpy(np.arange(26 * 28, dtype="uint8").reshape((1, 26, 28))).to(device)
1604+
1605+
if dt == torch.float16 and device == "cpu":
1606+
# skip float16 on CPU case
1607+
return
1608+
1609+
if dt is not None:
1610+
tensor = tensor.to(dtype=dt)
1611+
1612+
_ksize = (ksize, ksize) if isinstance(ksize, int) else ksize
1613+
_sigma = sigma[0] if sigma is not None else None
1614+
shape = tensor.shape
1615+
gt_key = f"{shape[-2]}_{shape[-1]}_{shape[-3]}__{_ksize[0]}_{_ksize[1]}_{_sigma}"
1616+
if gt_key not in true_cv2_results:
1617+
return
1618+
1619+
true_out = (
1620+
torch.tensor(true_cv2_results[gt_key]).reshape(shape[-2], shape[-1], shape[-3]).permute(2, 0, 1).to(tensor)
1621+
)
1622+
1623+
out = fn(tensor, kernel_size=ksize, sigma=sigma)
1624+
torch.testing.assert_close(out, true_out, rtol=0.0, atol=1.0, msg=f"{ksize}, {sigma}")

torchvision/prototype/features/_feature.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,6 @@ def equalize(self) -> Any:
189189

190190
def invert(self) -> Any:
191191
return self
192+
193+
def gaussian_blur(self, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Any:
194+
return self

torchvision/prototype/features/_image.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,9 @@ def invert(self) -> Image:
309309

310310
output = _F.invert_image_tensor(self)
311311
return Image.new_like(self, output)
312+
313+
def gaussian_blur(self, kernel_size: List[int], sigma: Optional[List[float]] = None) -> Image:
314+
from torchvision.prototype.transforms import functional as _F
315+
316+
output = _F.gaussian_blur_image_tensor(self, kernel_size=kernel_size, sigma=sigma)
317+
return Image.new_like(self, output)

torchvision/prototype/transforms/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
RandomAffine,
2323
)
2424
from ._meta import ConvertBoundingBoxFormat, ConvertImageDtype, ConvertImageColorSpace
25-
from ._misc import Identity, Normalize, ToDtype, Lambda
25+
from ._misc import Identity, GaussianBlur, Normalize, ToDtype, Lambda
2626
from ._type_conversion import DecodeImage, LabelToOneHot
2727

2828
from ._deprecated import Grayscale, RandomGrayscale, ToTensor, ToPILImage, PILToTensor # usort: skip
29+
30+
# TODO: add RandomPerspective, RandomInvert, RandomPosterize, RandomSolarize,
31+
# RandomAdjustSharpness, RandomAutocontrast, ElasticTransform

torchvision/prototype/transforms/_misc.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import functools
2-
from typing import Any, List, Type, Callable, Dict
2+
from typing import Any, List, Type, Callable, Dict, Sequence, Union
33

44
import torch
55
from torchvision.prototype.transforms import Transform, functional as F
6+
from torchvision.transforms.transforms import _setup_size
67

78

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

4849

50+
class GaussianBlur(Transform):
51+
def __init__(
52+
self, kernel_size: Union[int, Sequence[int]], sigma: Union[float, Sequence[float]] = (0.1, 2.0)
53+
) -> None:
54+
super().__init__()
55+
self.kernel_size = _setup_size(kernel_size, "Kernel size should be a tuple/list of two integers")
56+
for ks in self.kernel_size:
57+
if ks <= 0 or ks % 2 == 0:
58+
raise ValueError("Kernel size value should be an odd and positive number.")
59+
60+
if isinstance(sigma, float):
61+
if sigma <= 0:
62+
raise ValueError("If sigma is a single number, it must be positive.")
63+
sigma = (sigma, sigma)
64+
elif isinstance(sigma, Sequence) and len(sigma) == 2:
65+
if not 0.0 < sigma[0] <= sigma[1]:
66+
raise ValueError("sigma values should be positive and of the form (min, max).")
67+
else:
68+
raise TypeError("sigma should be a single float or a list/tuple with length 2 floats.")
69+
70+
self.sigma = sigma
71+
72+
def _get_params(self, sample: Any) -> Dict[str, Any]:
73+
sigma = torch.empty(1).uniform_(self.sigma[0], self.sigma[1]).item()
74+
return dict(sigma=[sigma, sigma])
75+
76+
def _transform(self, inpt: Any, params: Dict[str, Any]) -> Any:
77+
return F.gaussian_blur(inpt, **params)
78+
79+
4980
class ToDtype(Lambda):
5081
def __init__(self, dtype: torch.dtype, *types: Type) -> None:
5182
self.dtype = dtype

torchvision/prototype/transforms/functional/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@
9999
ten_crop_image_tensor,
100100
ten_crop_image_pil,
101101
)
102-
from ._misc import normalize_image_tensor, gaussian_blur_image_tensor
102+
from ._misc import (
103+
normalize_image_tensor,
104+
gaussian_blur,
105+
gaussian_blur_image_tensor,
106+
gaussian_blur_image_pil,
107+
)
103108
from ._type_conversion import (
104109
decode_image_with_pil,
105110
decode_video_with_av,

torchvision/prototype/transforms/functional/_misc.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1-
from typing import Optional, List
1+
from typing import Optional, List, Union
22

33
import PIL.Image
44
import torch
5+
from torchvision.prototype import features
56
from torchvision.transforms import functional_tensor as _FT
67
from torchvision.transforms.functional import pil_to_tensor, to_pil_image
78

89

10+
# shortcut type
11+
DType = Union[torch.Tensor, PIL.Image.Image, features._Feature]
12+
13+
914
normalize_image_tensor = _FT.normalize
1015

1116

17+
def normalize(inpt: DType, mean: List[float], std: List[float], inplace: bool = False) -> DType:
18+
if isinstance(inpt, features.Image):
19+
return normalize_image_tensor(inpt, mean=mean, std=std, inplace=inplace)
20+
elif type(inpt) == torch.Tensor:
21+
return normalize_image_tensor(inpt, mean=mean, std=std, inplace=inplace)
22+
else:
23+
raise TypeError("Unsupported input type")
24+
25+
1226
def gaussian_blur_image_tensor(
1327
img: torch.Tensor, kernel_size: List[int], sigma: Optional[List[float]] = None
1428
) -> torch.Tensor:
@@ -42,3 +56,12 @@ def gaussian_blur_image_pil(img: PIL.Image, kernel_size: List[int], sigma: Optio
4256
t_img = pil_to_tensor(img)
4357
output = gaussian_blur_image_tensor(t_img, kernel_size=kernel_size, sigma=sigma)
4458
return to_pil_image(output, mode=img.mode)
59+
60+
61+
def gaussian_blur(inpt: DType, kernel_size: List[int], sigma: Optional[List[float]] = None) -> DType:
62+
if isinstance(inpt, features._Feature):
63+
return inpt.gaussian_blur(kernel_size=kernel_size, sigma=sigma)
64+
elif isinstance(inpt, PIL.Image.Image):
65+
return gaussian_blur_image_pil(inpt, kernel_size=kernel_size, sigma=sigma)
66+
else:
67+
return gaussian_blur_image_tensor(inpt, kernel_size=kernel_size, sigma=sigma)

0 commit comments

Comments
 (0)