From b24de4cea88aef0c3390ea2abda96e448a58cbd1 Mon Sep 17 00:00:00 2001 From: Federico Pozzi Date: Mon, 7 Mar 2022 21:53:12 +0100 Subject: [PATCH 1/6] refactor: port RandomHorizontalFlip to prototype API (#5523) --- test/test_prototype_transforms.py | 1 + torchvision/prototype/transforms/__init__.py | 11 ++++++++++- torchvision/prototype/transforms/_geometry.py | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index 7bc83d9ffe1..6b074505aab 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -70,6 +70,7 @@ class TestSmoke: transforms.Resize([16, 16]), transforms.CenterCrop([16, 16]), transforms.ConvertImageDtype(), + transforms.RandomHorizontalFlip(), ) def test_common(self, transform, input): transform(input) diff --git a/torchvision/prototype/transforms/__init__.py b/torchvision/prototype/transforms/__init__.py index 16369428e47..380ede8b551 100644 --- a/torchvision/prototype/transforms/__init__.py +++ b/torchvision/prototype/transforms/__init__.py @@ -7,7 +7,16 @@ from ._augment import RandomErasing, RandomMixup, RandomCutmix from ._auto_augment import RandAugment, TrivialAugmentWide, AutoAugment, AugMix from ._container import Compose, RandomApply, RandomChoice, RandomOrder -from ._geometry import HorizontalFlip, Resize, CenterCrop, RandomResizedCrop, FiveCrop, TenCrop, BatchMultiCrop +from ._geometry import ( + HorizontalFlip, + Resize, + CenterCrop, + RandomResizedCrop, + FiveCrop, + TenCrop, + BatchMultiCrop, + RandomHorizontalFlip, +) from ._meta import ConvertBoundingBoxFormat, ConvertImageDtype, ConvertImageColorSpace from ._misc import Identity, Normalize, ToDtype, Lambda from ._presets import ( diff --git a/torchvision/prototype/transforms/_geometry.py b/torchvision/prototype/transforms/_geometry.py index e04e9f819f3..4e17a32dc2e 100644 --- a/torchvision/prototype/transforms/_geometry.py +++ b/torchvision/prototype/transforms/_geometry.py @@ -256,3 +256,16 @@ def apply_recursively(obj: Any) -> Any: return obj return apply_recursively(inputs if len(inputs) > 1 else inputs[0]) + + +class RandomHorizontalFlip(HorizontalFlip): + def __init__(self, p: float = 0.5): + super().__init__() + self.p = p + + def forward(self, *inputs: Any) -> Any: + sample = inputs if len(inputs) > 1 else inputs[0] + if torch.rand(1) >= self.p: + return sample + + return super().forward(sample) From 02f243b39562033215fbc63800cb5a15e2f9627e Mon Sep 17 00:00:00 2001 From: Federico Pozzi Date: Tue, 8 Mar 2022 22:31:45 +0100 Subject: [PATCH 2/6] refactor: merge HorizontalFlip and RandomHorizontalFlip Add unit tests for RandomHorizontalFlip --- test/test_prototype_transforms.py | 49 ++++++++++++++++++- torchvision/prototype/transforms/__init__.py | 1 - torchvision/prototype/transforms/_geometry.py | 29 +++++------ 3 files changed, 62 insertions(+), 17 deletions(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index 6b074505aab..7695d78fe42 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -4,7 +4,7 @@ import torch from test_prototype_transforms_functional import make_images, make_bounding_boxes, make_one_hot_labels from torchvision.prototype import transforms, features -from torchvision.transforms.functional import to_pil_image +from torchvision.transforms.functional import to_pil_image, pil_to_tensor def make_vanilla_tensor_images(*args, **kwargs): @@ -66,7 +66,6 @@ def parametrize_from_transforms(*transforms): class TestSmoke: @parametrize_from_transforms( transforms.RandomErasing(p=1.0), - transforms.HorizontalFlip(), transforms.Resize([16, 16]), transforms.CenterCrop([16, 16]), transforms.ConvertImageDtype(), @@ -153,3 +152,49 @@ def test_normalize(self, transform, input): ) def test_random_resized_crop(self, transform, input): transform(input) + + +class TestRandomHorizontalFlip: + def input_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: + return torch.tensor([[[0, 1], [0, 1]], [[1, 0], [1, 0]]], dtype=dtype) + + def expected_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: + return torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) + + def test_simple_tensor_p1(self): + input = self.input_tensor() + + actual = transforms.RandomHorizontalFlip(p=1.0)(input) + + assert torch.equal(self.expected_tensor(), actual) + + def test_pil_image_p1(self): + input = to_pil_image(self.input_tensor(dtype=torch.uint8)) + + actual = transforms.RandomHorizontalFlip(p=1.0)(input) + + assert torch.equal(self.expected_tensor(dtype=torch.uint8), pil_to_tensor(actual)) + + def test_features_image_p1(self): + input = features.Image(self.input_tensor()) + + actual = transforms.RandomHorizontalFlip(p=1.0)(input) + + assert torch.equal(features.Image(self.expected_tensor()), actual) + + def test_features_segmentation_mask_p1(self): + input = features.SegmentationMask(self.input_tensor()) + + actual = transforms.RandomHorizontalFlip(p=1.0)(input) + + assert torch.equal(features.SegmentationMask(self.expected_tensor()), actual) + + def test_features_bounding_box_p1(self): + bbox_format = features.BoundingBoxFormat.XYXY + image_size = (10, 10) + input = features.BoundingBox(torch.tensor([0, 0, 5, 5]), format=bbox_format, image_size=image_size) + + actual = transforms.RandomHorizontalFlip(p=1.0)(input) + + expected = features.BoundingBox(torch.tensor([5, 0, 10, 5]), format=bbox_format, image_size=image_size) + assert torch.equal(expected, actual) diff --git a/torchvision/prototype/transforms/__init__.py b/torchvision/prototype/transforms/__init__.py index 380ede8b551..ee996b474cf 100644 --- a/torchvision/prototype/transforms/__init__.py +++ b/torchvision/prototype/transforms/__init__.py @@ -8,7 +8,6 @@ from ._auto_augment import RandAugment, TrivialAugmentWide, AutoAugment, AugMix from ._container import Compose, RandomApply, RandomChoice, RandomOrder from ._geometry import ( - HorizontalFlip, Resize, CenterCrop, RandomResizedCrop, diff --git a/torchvision/prototype/transforms/_geometry.py b/torchvision/prototype/transforms/_geometry.py index 4e17a32dc2e..aa92942675b 100644 --- a/torchvision/prototype/transforms/_geometry.py +++ b/torchvision/prototype/transforms/_geometry.py @@ -13,11 +13,25 @@ from ._utils import query_image, get_image_dimensions, has_any, is_simple_tensor -class HorizontalFlip(Transform): +class RandomHorizontalFlip(Transform): + def __init__(self, p: float = 0.5): + super().__init__() + self.p = p + + def forward(self, *inputs: Any) -> Any: + sample = inputs if len(inputs) > 1 else inputs[0] + if torch.rand(1) >= self.p: + return sample + + return super().forward(sample) + def _transform(self, input: Any, params: Dict[str, Any]) -> Any: if isinstance(input, features.Image): output = F.horizontal_flip_image_tensor(input) return features.Image.new_like(input, output) + elif isinstance(input, features.SegmentationMask): + output = F.horizontal_flip_image_tensor(input) + return features.SegmentationMask.new_like(input, output) elif isinstance(input, features.BoundingBox): output = F.horizontal_flip_bounding_box(input, format=input.format, image_size=input.image_size) return features.BoundingBox.new_like(input, output) @@ -256,16 +270,3 @@ def apply_recursively(obj: Any) -> Any: return obj return apply_recursively(inputs if len(inputs) > 1 else inputs[0]) - - -class RandomHorizontalFlip(HorizontalFlip): - def __init__(self, p: float = 0.5): - super().__init__() - self.p = p - - def forward(self, *inputs: Any) -> Any: - sample = inputs if len(inputs) > 1 else inputs[0] - if torch.rand(1) >= self.p: - return sample - - return super().forward(sample) From 28b2e2c568470a00633942d11131b5537f7b9b9d Mon Sep 17 00:00:00 2001 From: Federico Pozzi Date: Thu, 10 Mar 2022 21:53:47 +0100 Subject: [PATCH 3/6] test: RandomHorizontalFlip with p=0 --- test/test_prototype_transforms.py | 50 +++++++++++-------- torchvision/prototype/transforms/_geometry.py | 4 +- .../transforms/functional/__init__.py | 1 + .../transforms/functional/_geometry.py | 4 ++ 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index 7695d78fe42..84ddec68537 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -2,6 +2,7 @@ import pytest import torch +from common_utils import assert_equal from test_prototype_transforms_functional import make_images, make_bounding_boxes, make_one_hot_labels from torchvision.prototype import transforms, features from torchvision.transforms.functional import to_pil_image, pil_to_tensor @@ -161,40 +162,47 @@ def input_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: def expected_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: return torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) - def test_simple_tensor_p1(self): + @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) + def test_simple_tensor(self, p: float): input = self.input_tensor() - actual = transforms.RandomHorizontalFlip(p=1.0)(input) + actual = transforms.RandomHorizontalFlip(p=p)(input) - assert torch.equal(self.expected_tensor(), actual) + expected = self.expected_tensor() if p == 1.0 else input + assert_equal(expected, actual) - def test_pil_image_p1(self): - input = to_pil_image(self.input_tensor(dtype=torch.uint8)) + @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) + def test_pil_image(self, p: float): + input = self.input_tensor(dtype=torch.uint8) - actual = transforms.RandomHorizontalFlip(p=1.0)(input) + actual = transforms.RandomHorizontalFlip(p=p)(to_pil_image(input)) - assert torch.equal(self.expected_tensor(dtype=torch.uint8), pil_to_tensor(actual)) + expected = self.expected_tensor(dtype=torch.uint8) if p == 1.0 else input + assert_equal(expected, pil_to_tensor(actual)) - def test_features_image_p1(self): - input = features.Image(self.input_tensor()) + @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) + def test_features_image(self, p: float): + input = self.input_tensor() - actual = transforms.RandomHorizontalFlip(p=1.0)(input) + actual = transforms.RandomHorizontalFlip(p=p)(features.Image(input)) - assert torch.equal(features.Image(self.expected_tensor()), actual) + expected = self.expected_tensor() if p == 1.0 else input + assert_equal(features.Image(expected), actual) - def test_features_segmentation_mask_p1(self): + @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) + def test_features_segmentation_mask(self, p: float): input = features.SegmentationMask(self.input_tensor()) - actual = transforms.RandomHorizontalFlip(p=1.0)(input) + actual = transforms.RandomHorizontalFlip(p=p)(input) - assert torch.equal(features.SegmentationMask(self.expected_tensor()), actual) + expected = self.expected_tensor() if p == 1.0 else input + assert_equal(features.SegmentationMask(expected), actual) - def test_features_bounding_box_p1(self): - bbox_format = features.BoundingBoxFormat.XYXY - image_size = (10, 10) - input = features.BoundingBox(torch.tensor([0, 0, 5, 5]), format=bbox_format, image_size=image_size) + @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) + def test_features_bounding_box(self, p: float): + input = features.BoundingBox([0, 0, 5, 5], format=features.BoundingBoxFormat.XYXY, image_size=(10, 10)) - actual = transforms.RandomHorizontalFlip(p=1.0)(input) + actual = transforms.RandomHorizontalFlip(p=p)(input) - expected = features.BoundingBox(torch.tensor([5, 0, 10, 5]), format=bbox_format, image_size=image_size) - assert torch.equal(expected, actual) + expected = torch.tensor([5, 0, 10, 5]) if p == 1.0 else input + assert_equal(features.BoundingBox.new_like(input, expected), actual) diff --git a/torchvision/prototype/transforms/_geometry.py b/torchvision/prototype/transforms/_geometry.py index aa92942675b..04340e3f8df 100644 --- a/torchvision/prototype/transforms/_geometry.py +++ b/torchvision/prototype/transforms/_geometry.py @@ -14,7 +14,7 @@ class RandomHorizontalFlip(Transform): - def __init__(self, p: float = 0.5): + def __init__(self, p: float = 0.5) -> None: super().__init__() self.p = p @@ -30,7 +30,7 @@ def _transform(self, input: Any, params: Dict[str, Any]) -> Any: output = F.horizontal_flip_image_tensor(input) return features.Image.new_like(input, output) elif isinstance(input, features.SegmentationMask): - output = F.horizontal_flip_image_tensor(input) + output = F.horizontal_flip_segmentation_mask(input) return features.SegmentationMask.new_like(input, output) elif isinstance(input, features.BoundingBox): output = F.horizontal_flip_bounding_box(input, format=input.format, image_size=input.image_size) diff --git a/torchvision/prototype/transforms/functional/__init__.py b/torchvision/prototype/transforms/functional/__init__.py index c0825784f66..d7698ac67e6 100644 --- a/torchvision/prototype/transforms/functional/__init__.py +++ b/torchvision/prototype/transforms/functional/__init__.py @@ -40,6 +40,7 @@ horizontal_flip_bounding_box, horizontal_flip_image_tensor, horizontal_flip_image_pil, + horizontal_flip_segmentation_mask, resize_bounding_box, resize_image_tensor, resize_image_pil, diff --git a/torchvision/prototype/transforms/functional/_geometry.py b/torchvision/prototype/transforms/functional/_geometry.py index 6c9309749af..452f3a74a90 100644 --- a/torchvision/prototype/transforms/functional/_geometry.py +++ b/torchvision/prototype/transforms/functional/_geometry.py @@ -15,6 +15,10 @@ horizontal_flip_image_pil = _FP.hflip +def horizontal_flip_segmentation_mask(segmentation_mask: torch.Tensor) -> torch.Tensor: + return horizontal_flip_image_tensor(segmentation_mask) + + def horizontal_flip_bounding_box( bounding_box: torch.Tensor, format: features.BoundingBoxFormat, image_size: Tuple[int, int] ) -> torch.Tensor: From 825c4f8f752ac9ac79645532c02d9484ad29059e Mon Sep 17 00:00:00 2001 From: Federico Pozzi Date: Sat, 12 Mar 2022 03:46:21 +0100 Subject: [PATCH 4/6] refactor: remove type annotations from tests --- test/test_prototype_transforms.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index 84ddec68537..df75ba0b97d 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -156,14 +156,14 @@ def test_random_resized_crop(self, transform, input): class TestRandomHorizontalFlip: - def input_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: + def input_tensor(self, dtype=torch.float32): return torch.tensor([[[0, 1], [0, 1]], [[1, 0], [1, 0]]], dtype=dtype) - def expected_tensor(self, dtype: torch.dtype = torch.float32) -> torch.Tensor: + def expected_tensor(self, dtype=torch.float32): return torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) - def test_simple_tensor(self, p: float): + def test_simple_tensor(self, p): input = self.input_tensor() actual = transforms.RandomHorizontalFlip(p=p)(input) @@ -172,7 +172,7 @@ def test_simple_tensor(self, p: float): assert_equal(expected, actual) @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) - def test_pil_image(self, p: float): + def test_pil_image(self, p): input = self.input_tensor(dtype=torch.uint8) actual = transforms.RandomHorizontalFlip(p=p)(to_pil_image(input)) @@ -181,7 +181,7 @@ def test_pil_image(self, p: float): assert_equal(expected, pil_to_tensor(actual)) @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) - def test_features_image(self, p: float): + def test_features_image(self, p): input = self.input_tensor() actual = transforms.RandomHorizontalFlip(p=p)(features.Image(input)) @@ -190,7 +190,7 @@ def test_features_image(self, p: float): assert_equal(features.Image(expected), actual) @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) - def test_features_segmentation_mask(self, p: float): + def test_features_segmentation_mask(self, p): input = features.SegmentationMask(self.input_tensor()) actual = transforms.RandomHorizontalFlip(p=p)(input) @@ -199,7 +199,7 @@ def test_features_segmentation_mask(self, p: float): assert_equal(features.SegmentationMask(expected), actual) @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) - def test_features_bounding_box(self, p: float): + def test_features_bounding_box(self, p): input = features.BoundingBox([0, 0, 5, 5], format=features.BoundingBoxFormat.XYXY, image_size=(10, 10)) actual = transforms.RandomHorizontalFlip(p=p)(input) From 2b7cae6e94e0545a4ebd7e0c938b1a594f06a9de Mon Sep 17 00:00:00 2001 From: Federico Pozzi Date: Mon, 14 Mar 2022 21:26:42 +0100 Subject: [PATCH 5/6] refactor: improve tests --- test/test_prototype_transforms.py | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index b42179cf2b9..2b95b291c18 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -191,54 +191,56 @@ def test_convert_image_color_space(self, transform, input): transform(input) +@pytest.mark.parametrize("p", [0.0, 1.0]) class TestRandomHorizontalFlip: - def input_tensor(self, dtype=torch.float32): - return torch.tensor([[[0, 1], [0, 1]], [[1, 0], [1, 0]]], dtype=dtype) + def input_expected_image_tensor(self, p, dtype=torch.float32): + input = torch.tensor([[[0, 1], [0, 1]], [[1, 0], [1, 0]]], dtype=dtype) + expected = torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) - def expected_tensor(self, dtype=torch.float32): - return torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) + if p == 1.0: + return input, expected + return input, input - @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) def test_simple_tensor(self, p): - input = self.input_tensor() + input, expected = self.input_expected_image_tensor(p) + transform = transforms.RandomHorizontalFlip(p=p) - actual = transforms.RandomHorizontalFlip(p=p)(input) + actual = transform(input) - expected = self.expected_tensor() if p == 1.0 else input assert_equal(expected, actual) - @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) def test_pil_image(self, p): - input = self.input_tensor(dtype=torch.uint8) + input, expected = self.input_expected_image_tensor(p, dtype=torch.uint8) + transform = transforms.RandomHorizontalFlip(p=p) - actual = transforms.RandomHorizontalFlip(p=p)(to_pil_image(input)) + actual = transform(to_pil_image(input)) - expected = self.expected_tensor(dtype=torch.uint8) if p == 1.0 else input assert_equal(expected, pil_to_tensor(actual)) - @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) def test_features_image(self, p): - input = self.input_tensor() + input, expected = self.input_expected_image_tensor(p) + transform = transforms.RandomHorizontalFlip(p=p) - actual = transforms.RandomHorizontalFlip(p=p)(features.Image(input)) + actual = transform(features.Image(input)) - expected = self.expected_tensor() if p == 1.0 else input assert_equal(features.Image(expected), actual) - @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) def test_features_segmentation_mask(self, p): - input = features.SegmentationMask(self.input_tensor()) + input, expected = self.input_expected_image_tensor(p) + transform = transforms.RandomHorizontalFlip(p=p) - actual = transforms.RandomHorizontalFlip(p=p)(input) + actual = transform(features.SegmentationMask(input)) - expected = self.expected_tensor() if p == 1.0 else input assert_equal(features.SegmentationMask(expected), actual) - @pytest.mark.parametrize("p", [0.0, 1.0], ids=["p=0", "p=1"]) def test_features_bounding_box(self, p): input = features.BoundingBox([0, 0, 5, 5], format=features.BoundingBoxFormat.XYXY, image_size=(10, 10)) + transform = transforms.RandomHorizontalFlip(p=p) - actual = transforms.RandomHorizontalFlip(p=p)(input) + actual = transform(input) - expected = torch.tensor([5, 0, 10, 5]) if p == 1.0 else input - assert_equal(features.BoundingBox.new_like(input, expected), actual) + expected_image_tensor = torch.tensor([5, 0, 10, 5]) if p == 1.0 else input + expected = features.BoundingBox.new_like(input, data=expected_image_tensor) + assert_equal(expected, actual) + assert actual.format == expected.format + assert actual.image_size == expected.image_size From 6e84c7aecd1fe668d583de49b1e94bdb554e2579 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 14 Mar 2022 22:55:53 +0100 Subject: [PATCH 6/6] Update test/test_prototype_transforms.py --- test/test_prototype_transforms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_prototype_transforms.py b/test/test_prototype_transforms.py index 2b95b291c18..5b0693a2e78 100644 --- a/test/test_prototype_transforms.py +++ b/test/test_prototype_transforms.py @@ -197,9 +197,7 @@ def input_expected_image_tensor(self, p, dtype=torch.float32): input = torch.tensor([[[0, 1], [0, 1]], [[1, 0], [1, 0]]], dtype=dtype) expected = torch.tensor([[[1, 0], [1, 0]], [[0, 1], [0, 1]]], dtype=dtype) - if p == 1.0: - return input, expected - return input, input + return input, expected if p == 1 else input def test_simple_tensor(self, p): input, expected = self.input_expected_image_tensor(p)