From 653d2fd4f071ce594b0fdc7429cfa0b37bbe9eca Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Sun, 9 Aug 2020 14:26:28 -0400 Subject: [PATCH 01/16] adjust_hue now supports inputs of type Tensor --- torchvision/transforms/functional.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index 4d607400b48..867d476fe99 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -729,7 +729,7 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: .. _Hue: https://en.wikipedia.org/wiki/Hue Args: - img (PIL Image): PIL Image to be adjusted. + img (PIL Image or Tensor): Image to be adjusted. hue_factor (float): How much to shift the hue channel. Should be in [-0.5, 0.5]. 0.5 and -0.5 give complete reversal of hue channel in HSV space in positive and negative direction respectively. @@ -737,12 +737,12 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: with complementary colors while 0 gives the original image. Returns: - PIL Image: Hue adjusted image. + PIL Image or Tensor: Hue adjusted image. """ if not isinstance(img, torch.Tensor): return F_pil.adjust_hue(img, hue_factor) - raise TypeError('img should be PIL Image. Got {}'.format(type(img))) + return F_t.adjust_hue(img, hue_factor) def adjust_gamma(img: Tensor, gamma: float, gain: float = 1) -> Tensor: From ff93f7856d2c704cf63fe7be7f59624f2ce03499 Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Mon, 10 Aug 2020 20:36:36 -0400 Subject: [PATCH 02/16] Added comparison between original adjust_hue and its Tensor and torch.jit.script versions. --- test/test_functional_tensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index f4edc0f7f07..d7e2a78444d 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -130,10 +130,12 @@ def test_adjustments(self): script_adjust_brightness = torch.jit.script(F_t.adjust_brightness) script_adjust_contrast = torch.jit.script(F_t.adjust_contrast) script_adjust_saturation = torch.jit.script(F_t.adjust_saturation) + script_adjust_hue = torch.jit.script(F_t.adjust_hue) fns = ((F.adjust_brightness, F_t.adjust_brightness, script_adjust_brightness), (F.adjust_contrast, F_t.adjust_contrast, script_adjust_contrast), - (F.adjust_saturation, F_t.adjust_saturation, script_adjust_saturation)) + (F.adjust_saturation, F_t.adjust_saturation, script_adjust_saturation), + (F.adjust_hue, F_t.adjust_hue, script_adjust_hue)) for _ in range(20): channels = 3 From b565af5a6c9ebe553328ce51af0a1aed3ca0325e Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Mon, 10 Aug 2020 22:06:43 -0400 Subject: [PATCH 03/16] Added a few type checkings related to adjust_hue in functional_tensor.py in hopes to make F_t.adjust_hue scriptable...but to no avail. --- torchvision/transforms/functional_tensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 433575ac6e2..fe2f0905b09 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -128,8 +128,7 @@ def adjust_contrast(img: Tensor, contrast_factor: float) -> Tensor: return _blend(img, mean, contrast_factor) - -def adjust_hue(img, hue_factor): +def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and @@ -164,7 +163,7 @@ def adjust_hue(img, hue_factor): if img.dtype == torch.uint8: img = img.to(dtype=torch.float32) / 255.0 - img = _rgb2hsv(img) + img: Tensor = _rgb2hsv(img) h, s, v = img.unbind(0) h += hue_factor h = h % 1.0 @@ -344,7 +343,7 @@ def _blend(img1: Tensor, img2: Tensor, ratio: float) -> Tensor: return (ratio * img1 + (1 - ratio) * img2).clamp(0, bound).to(img1.dtype) -def _rgb2hsv(img): +def _rgb2hsv(img: Tensor) -> Tensor: r, g, b = img.unbind(0) maxc = torch.max(img, dim=0).values @@ -362,7 +361,7 @@ def _rgb2hsv(img): cr = maxc - minc # Since `eqc => cr = 0`, replacing denominator with 1 when `eqc` is fine. - s = cr / torch.where(eqc, maxc.new_ones(()), maxc) + s: Tensor = cr / torch.where(eqc, maxc.new_ones(()), maxc) # Note that `eqc => maxc = minc = r = g = b`. So the following calculation # of `h` would reduce to `bc - gc + 2 + rc - bc + 4 + rc - bc = 6` so it # would not matter what values `rc`, `gc`, and `bc` have here, and thus @@ -380,7 +379,7 @@ def _rgb2hsv(img): return torch.stack((h, s, maxc)) -def _hsv2rgb(img): +def _hsv2rgb(img: Tensor) -> Tensor: h, s, v = img.unbind(0) i = torch.floor(h * 6.0) f = (h * 6.0) - i From 535d1de8be4bdbf95d936973f2ef69a4406bb04f Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Wed, 12 Aug 2020 18:14:00 -0400 Subject: [PATCH 04/16] Changed implementation of _rgb2hsv and removed useless type declaration according to PR's review. --- torchvision/transforms/functional_tensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index fe2f0905b09..73a84ae88ca 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -163,7 +163,7 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: if img.dtype == torch.uint8: img = img.to(dtype=torch.float32) / 255.0 - img: Tensor = _rgb2hsv(img) + img = _rgb2hsv(img) h, s, v = img.unbind(0) h += hue_factor h = h % 1.0 @@ -344,6 +344,8 @@ def _blend(img1: Tensor, img2: Tensor, ratio: float) -> Tensor: def _rgb2hsv(img: Tensor) -> Tensor: + if not isinstance(img, torch.Tensor): + raise TypeError("img should be of type torch.Tensor. Got {}".format(type(img))) r, g, b = img.unbind(0) maxc = torch.max(img, dim=0).values @@ -361,12 +363,13 @@ def _rgb2hsv(img: Tensor) -> Tensor: cr = maxc - minc # Since `eqc => cr = 0`, replacing denominator with 1 when `eqc` is fine. - s: Tensor = cr / torch.where(eqc, maxc.new_ones(()), maxc) + ones = torch.ones_like(maxc) + s = cr / torch.where(eqc, ones, maxc) # Note that `eqc => maxc = minc = r = g = b`. So the following calculation # of `h` would reduce to `bc - gc + 2 + rc - bc + 4 + rc - bc = 6` so it # would not matter what values `rc`, `gc`, and `bc` have here, and thus # replacing denominator with 1 when `eqc` is fine. - cr_divisor = torch.where(eqc, maxc.new_ones(()), cr) + cr_divisor = torch.where(eqc, ones, cr) rc = (maxc - r) / cr_divisor gc = (maxc - g) / cr_divisor bc = (maxc - b) / cr_divisor @@ -380,6 +383,8 @@ def _rgb2hsv(img: Tensor) -> Tensor: def _hsv2rgb(img: Tensor) -> Tensor: + if not isinstance(img, torch.Tensor): + raise TypeError("img should be of type torch.Tensor. Got {}".format(type(img))) h, s, v = img.unbind(0) i = torch.floor(h * 6.0) f = (h * 6.0) - i From 5f9ba2734303619d265f2abf04131fb20c69652e Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Wed, 12 Aug 2020 21:43:25 -0400 Subject: [PATCH 05/16] Handled the range of hue_factor in the assertions and temporarily increased the assertLess bound to make sure that no other test fails. --- test/test_functional_tensor.py | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index d7e2a78444d..5af85680553 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -148,25 +148,37 @@ def test_adjustments(self): img = torch.randint(0, 256, shape, dtype=torch.uint8) factor = 3 * torch.rand(1) + hue_factor = torch.add(torch.rand(1), -0.5) img_clone = img.clone() - for f, ft, sft in fns: + for i, (f, ft, sft) in enumerate(fns): + if i == 3: + ft_img = ft(img, hue_factor) + sft_img = sft(img, hue_factor) + if not img.dtype.is_floating_point: + ft_img = ft_img.to(torch.float) / 255 + sft_img = sft_img.to(torch.float) / 255 + + img_pil = transforms.ToPILImage()(img) + f_img_pil = f(img_pil, hue_factor) + f_img = transforms.ToTensor()(f_img_pil) + else: + ft_img = ft(img, factor) + sft_img = sft(img, factor) + if not img.dtype.is_floating_point: + ft_img = ft_img.to(torch.float) / 255 + sft_img = sft_img.to(torch.float) / 255 + + img_pil = transforms.ToPILImage()(img) + f_img_pil = f(img_pil, factor) + f_img = transforms.ToTensor()(f_img_pil) - ft_img = ft(img, factor) - sft_img = sft(img, factor) - if not img.dtype.is_floating_point: - ft_img = ft_img.to(torch.float) / 255 - sft_img = sft_img.to(torch.float) / 255 - - img_pil = transforms.ToPILImage()(img) - f_img_pil = f(img_pil, factor) - f_img = transforms.ToTensor()(f_img_pil) # F uses uint8 and F_t uses float, so there is a small # difference in values caused by (at most 5) truncations. max_diff = (ft_img - f_img).abs().max() max_diff_scripted = (sft_img - f_img).abs().max() - self.assertLess(max_diff, 5 / 255 + 1e-5) - self.assertLess(max_diff_scripted, 5 / 255 + 1e-5) + self.assertLess(max_diff, 5 + 1e-5) # TODO: 5 / 255, not 5 + self.assertLess(max_diff_scripted, 5 + 1e-5) # TODO: idem self.assertTrue(torch.equal(img, img_clone)) # test for class interface From 3f6c5a5b18864338510ab77344d724b29bcd068b Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Wed, 12 Aug 2020 22:03:27 -0400 Subject: [PATCH 06/16] Fixed some lint issues with CircleCI and added type hints in functional_pil.py as well. --- test/test_functional_tensor.py | 1 - torchvision/transforms/functional_pil.py | 2 +- torchvision/transforms/functional_tensor.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 5af85680553..c1feb00c17d 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -172,7 +172,6 @@ def test_adjustments(self): f_img_pil = f(img_pil, factor) f_img = transforms.ToTensor()(f_img_pil) - # F uses uint8 and F_t uses float, so there is a small # difference in values caused by (at most 5) truncations. max_diff = (ft_img - f_img).abs().max() diff --git a/torchvision/transforms/functional_pil.py b/torchvision/transforms/functional_pil.py index f1e8504f874..e94068949f0 100644 --- a/torchvision/transforms/functional_pil.py +++ b/torchvision/transforms/functional_pil.py @@ -118,7 +118,7 @@ def adjust_saturation(img, saturation_factor): @torch.jit.unused -def adjust_hue(img, hue_factor): +def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 73a84ae88ca..ae73f138323 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -128,6 +128,7 @@ def adjust_contrast(img: Tensor, contrast_factor: float) -> Tensor: return _blend(img, mean, contrast_factor) + def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: """Adjust hue of an image. From 270e02ecaf6341269f27523b92c1b72d117aacf4 Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Wed, 12 Aug 2020 22:26:54 -0400 Subject: [PATCH 07/16] Corrected type hint mistakes. --- torchvision/transforms/functional.py | 2 +- torchvision/transforms/functional_pil.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index 867d476fe99..489a70d6ce3 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -714,7 +714,7 @@ def adjust_saturation(img: Tensor, saturation_factor: float) -> Tensor: return F_t.adjust_saturation(img, saturation_factor) -def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: +def adjust_hue(img, hue_factor: float): """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and diff --git a/torchvision/transforms/functional_pil.py b/torchvision/transforms/functional_pil.py index e94068949f0..1a0d6c264f0 100644 --- a/torchvision/transforms/functional_pil.py +++ b/torchvision/transforms/functional_pil.py @@ -118,7 +118,7 @@ def adjust_saturation(img, saturation_factor): @torch.jit.unused -def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: +def adjust_hue(img, hue_factor: float): """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and From 3b072db32aa99e7b2f1b586dda821e801e2f1c12 Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Thu, 13 Aug 2020 20:53:24 -0400 Subject: [PATCH 08/16] Followed PR review recommendations and added test for class interface with hue. --- test/test_functional_tensor.py | 48 ++++++++++----------- torchvision/transforms/functional.py | 2 +- torchvision/transforms/functional_pil.py | 2 +- torchvision/transforms/functional_tensor.py | 12 ++---- 4 files changed, 28 insertions(+), 36 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index c1feb00c17d..1c7d32e02af 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -147,30 +147,22 @@ def test_adjustments(self): else: img = torch.randint(0, 256, shape, dtype=torch.uint8) - factor = 3 * torch.rand(1) - hue_factor = torch.add(torch.rand(1), -0.5) + bcs_factor = 3 * torch.rand(1).item() + hue_factor = torch.rand(1).item() - 0.5 + factor = bcs_factor img_clone = img.clone() - for i, (f, ft, sft) in enumerate(fns): - if i == 3: - ft_img = ft(img, hue_factor) - sft_img = sft(img, hue_factor) - if not img.dtype.is_floating_point: - ft_img = ft_img.to(torch.float) / 255 - sft_img = sft_img.to(torch.float) / 255 - - img_pil = transforms.ToPILImage()(img) - f_img_pil = f(img_pil, hue_factor) - f_img = transforms.ToTensor()(f_img_pil) - else: - ft_img = ft(img, factor) - sft_img = sft(img, factor) - if not img.dtype.is_floating_point: - ft_img = ft_img.to(torch.float) / 255 - sft_img = sft_img.to(torch.float) / 255 - - img_pil = transforms.ToPILImage()(img) - f_img_pil = f(img_pil, factor) - f_img = transforms.ToTensor()(f_img_pil) + for f, ft, sft in fns: + if f == F.adjust_hue: + factor = hue_factor + ft_img = ft(img, factor) + sft_img = sft(img, factor) + if not img.dtype.is_floating_point: + ft_img = ft_img.to(torch.float) / 255 + sft_img = sft_img.to(torch.float) / 255 + + img_pil = transforms.ToPILImage()(img) + f_img_pil = f(img_pil, factor) + f_img = transforms.ToTensor()(f_img_pil) # F uses uint8 and F_t uses float, so there is a small # difference in values caused by (at most 5) truncations. @@ -181,15 +173,19 @@ def test_adjustments(self): self.assertTrue(torch.equal(img, img_clone)) # test for class interface - f = transforms.ColorJitter(brightness=factor.item()) + f = transforms.ColorJitter(brightness=bcs_factor) scripted_fn = torch.jit.script(f) scripted_fn(img) - f = transforms.ColorJitter(contrast=factor.item()) + f = transforms.ColorJitter(contrast=bcs_factor) scripted_fn = torch.jit.script(f) scripted_fn(img) - f = transforms.ColorJitter(saturation=factor.item()) + f = transforms.ColorJitter(saturation=bcs_factor) + scripted_fn = torch.jit.script(f) + scripted_fn(img) + + f = transforms.ColorJitter(hue=abs(hue_factor)) scripted_fn = torch.jit.script(f) scripted_fn(img) diff --git a/torchvision/transforms/functional.py b/torchvision/transforms/functional.py index 489a70d6ce3..867d476fe99 100644 --- a/torchvision/transforms/functional.py +++ b/torchvision/transforms/functional.py @@ -714,7 +714,7 @@ def adjust_saturation(img: Tensor, saturation_factor: float) -> Tensor: return F_t.adjust_saturation(img, saturation_factor) -def adjust_hue(img, hue_factor: float): +def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and diff --git a/torchvision/transforms/functional_pil.py b/torchvision/transforms/functional_pil.py index 1a0d6c264f0..f1e8504f874 100644 --- a/torchvision/transforms/functional_pil.py +++ b/torchvision/transforms/functional_pil.py @@ -118,7 +118,7 @@ def adjust_saturation(img, saturation_factor): @torch.jit.unused -def adjust_hue(img, hue_factor: float): +def adjust_hue(img, hue_factor): """Adjust hue of an image. The image hue is adjusted by converting the image to HSV and diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index ae73f138323..5a17dab1937 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -157,8 +157,8 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: if not (-0.5 <= hue_factor <= 0.5): raise ValueError('hue_factor ({}) is not in [-0.5, 0.5].'.format(hue_factor)) - if not _is_tensor_a_torch_image(img): - raise TypeError('tensor is not a torch image.') + if not (isinstance(img, torch.Tensor) and _is_tensor_a_torch_image(img)): + raise TypeError('img should be Tensor image. Got {}'.format(type(img))) orig_dtype = img.dtype if img.dtype == torch.uint8: @@ -344,9 +344,7 @@ def _blend(img1: Tensor, img2: Tensor, ratio: float) -> Tensor: return (ratio * img1 + (1 - ratio) * img2).clamp(0, bound).to(img1.dtype) -def _rgb2hsv(img: Tensor) -> Tensor: - if not isinstance(img, torch.Tensor): - raise TypeError("img should be of type torch.Tensor. Got {}".format(type(img))) +def _rgb2hsv(img): r, g, b = img.unbind(0) maxc = torch.max(img, dim=0).values @@ -383,9 +381,7 @@ def _rgb2hsv(img: Tensor) -> Tensor: return torch.stack((h, s, maxc)) -def _hsv2rgb(img: Tensor) -> Tensor: - if not isinstance(img, torch.Tensor): - raise TypeError("img should be of type torch.Tensor. Got {}".format(type(img))) +def _hsv2rgb(img): h, s, v = img.unbind(0) i = torch.floor(h * 6.0) f = (h * 6.0) - i From bdae1ccb898b4603f0f0078ec3a65a95df75ce8e Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Fri, 14 Aug 2020 22:52:32 -0400 Subject: [PATCH 09/16] Refactored test_functional_tensor.py to match vfdev-5's d016cab branch by simple copy/paste and added the test_adjust_hue and ColorJitter class interface test in the same style (class interface test was removed in vfdev-5's branch for some reason). --- test/test_functional_tensor.py | 310 +++++++++++++++------------------ 1 file changed, 143 insertions(+), 167 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 1c7d32e02af..52dcaabe4fa 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -1,5 +1,4 @@ import unittest -import random import colorsys import math @@ -23,9 +22,11 @@ def _create_data(self, height=3, width=3, channels=3): return tensor, pil_img def compareTensorToPIL(self, tensor, pil_image, msg=None): - pil_tensor = torch.as_tensor(np.array(pil_image).transpose((2, 0, 1))) - if msg is None: - msg = "tensor:\n{} \ndid not equal PIL tensor:\n{}".format(tensor, pil_tensor) + np_pil_image = np.array(pil_image) + if np_pil_image.ndim == 2: + np_pil_image = np_pil_image[:, :, None] + pil_tensor = torch.as_tensor(np_pil_image.transpose((2, 0, 1))) + msg = "{}: tensor:\n{} \ndid not equal PIL tensor:\n{}".format(msg, tensor, pil_tensor) self.assertTrue(tensor.equal(pil_tensor), msg) def approxEqualTensorToPIL(self, tensor, pil_image, tol=1e-5, msg=None): @@ -64,22 +65,24 @@ def test_hflip(self): def test_crop(self): script_crop = torch.jit.script(F_t.crop) - img_tensor = torch.randint(0, 255, (3, 16, 16), dtype=torch.uint8) - img_tensor_clone = img_tensor.clone() - top = random.randint(0, 15) - left = random.randint(0, 15) - height = random.randint(1, 16 - top) - width = random.randint(1, 16 - left) - img_cropped = F_t.crop(img_tensor, top, left, height, width) - img_PIL = transforms.ToPILImage()(img_tensor) - img_PIL_cropped = F.crop(img_PIL, top, left, height, width) - img_cropped_GT = transforms.ToTensor()(img_PIL_cropped) - self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) - self.assertTrue(torch.equal(img_cropped, (img_cropped_GT * 255).to(torch.uint8)), - "functional_tensor crop not working") - # scriptable function test - cropped_img_script = script_crop(img_tensor, top, left, height, width) - self.assertTrue(torch.equal(img_cropped, cropped_img_script)) + + img_tensor, pil_img = self._create_data(16, 18) + + test_configs = [ + (1, 2, 4, 5), # crop inside top-left corner + (2, 12, 3, 4), # crop inside top-right corner + (8, 3, 5, 6), # crop inside bottom-left corner + (8, 11, 4, 3), # crop inside bottom-right corner + ] + + for top, left, height, width in test_configs: + pil_img_cropped = F.crop(pil_img, top, left, height, width) + + img_tensor_cropped = F.crop(img_tensor, top, left, height, width) + self.compareTensorToPIL(img_tensor_cropped, pil_img_cropped) + + img_tensor_cropped = script_crop(img_tensor, top, left, height, width) + self.compareTensorToPIL(img_tensor_cropped, pil_img_cropped) def test_hsv2rgb(self): shape = (3, 100, 150) @@ -126,152 +129,65 @@ def test_rgb2hsv(self): self.assertLess(max_diff, 1e-5) - def test_adjustments(self): - script_adjust_brightness = torch.jit.script(F_t.adjust_brightness) - script_adjust_contrast = torch.jit.script(F_t.adjust_contrast) - script_adjust_saturation = torch.jit.script(F_t.adjust_saturation) - script_adjust_hue = torch.jit.script(F_t.adjust_hue) + def test_rgb_to_grayscale(self): + script_rgb_to_grayscale = torch.jit.script(F.rgb_to_grayscale) - fns = ((F.adjust_brightness, F_t.adjust_brightness, script_adjust_brightness), - (F.adjust_contrast, F_t.adjust_contrast, script_adjust_contrast), - (F.adjust_saturation, F_t.adjust_saturation, script_adjust_saturation), - (F.adjust_hue, F_t.adjust_hue, script_adjust_hue)) + img_tensor, pil_img = self._create_data(32, 34) - for _ in range(20): - channels = 3 - dims = torch.randint(1, 50, (2,)) - shape = (channels, dims[0], dims[1]) + for num_output_channels in (3, 1): + gray_pil_image = F.rgb_to_grayscale(pil_img, num_output_channels=num_output_channels) + gray_tensor = F.rgb_to_grayscale(img_tensor, num_output_channels=num_output_channels) - if torch.randint(0, 2, (1,)) == 0: - img = torch.rand(*shape, dtype=torch.float) - else: - img = torch.randint(0, 256, shape, dtype=torch.uint8) - - bcs_factor = 3 * torch.rand(1).item() - hue_factor = torch.rand(1).item() - 0.5 - factor = bcs_factor - img_clone = img.clone() - for f, ft, sft in fns: - if f == F.adjust_hue: - factor = hue_factor - ft_img = ft(img, factor) - sft_img = sft(img, factor) - if not img.dtype.is_floating_point: - ft_img = ft_img.to(torch.float) / 255 - sft_img = sft_img.to(torch.float) / 255 - - img_pil = transforms.ToPILImage()(img) - f_img_pil = f(img_pil, factor) - f_img = transforms.ToTensor()(f_img_pil) - - # F uses uint8 and F_t uses float, so there is a small - # difference in values caused by (at most 5) truncations. - max_diff = (ft_img - f_img).abs().max() - max_diff_scripted = (sft_img - f_img).abs().max() - self.assertLess(max_diff, 5 + 1e-5) # TODO: 5 / 255, not 5 - self.assertLess(max_diff_scripted, 5 + 1e-5) # TODO: idem - self.assertTrue(torch.equal(img, img_clone)) - - # test for class interface - f = transforms.ColorJitter(brightness=bcs_factor) - scripted_fn = torch.jit.script(f) - scripted_fn(img) + if num_output_channels == 1: + print(gray_tensor.shape) - f = transforms.ColorJitter(contrast=bcs_factor) - scripted_fn = torch.jit.script(f) - scripted_fn(img) + self.compareTensorToPIL(gray_tensor, gray_pil_image) - f = transforms.ColorJitter(saturation=bcs_factor) - scripted_fn = torch.jit.script(f) - scripted_fn(img) + s_gray_tensor = script_rgb_to_grayscale(img_tensor, num_output_channels=num_output_channels) + self.assertTrue(s_gray_tensor.equal(gray_tensor)) - f = transforms.ColorJitter(hue=abs(hue_factor)) - scripted_fn = torch.jit.script(f) - scripted_fn(img) + def test_center_crop(self): + script_center_crop = torch.jit.script(F.center_crop) - f = transforms.ColorJitter(brightness=1) - scripted_fn = torch.jit.script(f) - scripted_fn(img) + img_tensor, pil_img = self._create_data(32, 34) - def test_rgb_to_grayscale(self): - script_rgb_to_grayscale = torch.jit.script(F_t.rgb_to_grayscale) - img_tensor = torch.randint(0, 255, (3, 16, 16), dtype=torch.uint8) - img_tensor_clone = img_tensor.clone() - grayscale_tensor = F_t.rgb_to_grayscale(img_tensor).to(int) - grayscale_pil_img = torch.tensor(np.array(F.to_grayscale(F.to_pil_image(img_tensor)))).to(int) - max_diff = (grayscale_tensor - grayscale_pil_img).abs().max() - self.assertLess(max_diff, 1.0001) - self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) - # scriptable function test - grayscale_script = script_rgb_to_grayscale(img_tensor).to(int) - self.assertTrue(torch.equal(grayscale_script, grayscale_tensor)) + cropped_pil_image = F.center_crop(pil_img, [10, 11]) - def test_center_crop(self): - script_center_crop = torch.jit.script(F_t.center_crop) - img_tensor = torch.randint(0, 255, (1, 32, 32), dtype=torch.uint8) - img_tensor_clone = img_tensor.clone() - cropped_tensor = F_t.center_crop(img_tensor, [10, 10]) - cropped_pil_image = F.center_crop(transforms.ToPILImage()(img_tensor), [10, 10]) - cropped_pil_tensor = (transforms.ToTensor()(cropped_pil_image) * 255).to(torch.uint8) - self.assertTrue(torch.equal(cropped_tensor, cropped_pil_tensor)) - self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) - # scriptable function test - cropped_script = script_center_crop(img_tensor, [10, 10]) - self.assertTrue(torch.equal(cropped_script, cropped_tensor)) + cropped_tensor = F.center_crop(img_tensor, [10, 11]) + self.compareTensorToPIL(cropped_tensor, cropped_pil_image) + + cropped_tensor = script_center_crop(img_tensor, [10, 11]) + self.compareTensorToPIL(cropped_tensor, cropped_pil_image) def test_five_crop(self): - script_five_crop = torch.jit.script(F_t.five_crop) - img_tensor = torch.randint(0, 255, (1, 32, 32), dtype=torch.uint8) - img_tensor_clone = img_tensor.clone() - cropped_tensor = F_t.five_crop(img_tensor, [10, 10]) - cropped_pil_image = F.five_crop(transforms.ToPILImage()(img_tensor), [10, 10]) - self.assertTrue(torch.equal(cropped_tensor[0], - (transforms.ToTensor()(cropped_pil_image[0]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[1], - (transforms.ToTensor()(cropped_pil_image[2]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[2], - (transforms.ToTensor()(cropped_pil_image[1]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[3], - (transforms.ToTensor()(cropped_pil_image[3]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[4], - (transforms.ToTensor()(cropped_pil_image[4]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) - # scriptable function test - cropped_script = script_five_crop(img_tensor, [10, 10]) - for cropped_script_img, cropped_tensor_img in zip(cropped_script, cropped_tensor): - self.assertTrue(torch.equal(cropped_script_img, cropped_tensor_img)) + script_five_crop = torch.jit.script(F.five_crop) + + img_tensor, pil_img = self._create_data(32, 34) + + cropped_pil_images = F.five_crop(pil_img, [10, 11]) + + cropped_tensors = F.five_crop(img_tensor, [10, 11]) + for i in range(5): + self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) + + cropped_tensors = script_five_crop(img_tensor, [10, 11]) + for i in range(5): + self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) def test_ten_crop(self): - script_ten_crop = torch.jit.script(F_t.ten_crop) - img_tensor = torch.randint(0, 255, (1, 32, 32), dtype=torch.uint8) - img_tensor_clone = img_tensor.clone() - cropped_tensor = F_t.ten_crop(img_tensor, [10, 10]) - cropped_pil_image = F.ten_crop(transforms.ToPILImage()(img_tensor), [10, 10]) - self.assertTrue(torch.equal(cropped_tensor[0], - (transforms.ToTensor()(cropped_pil_image[0]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[1], - (transforms.ToTensor()(cropped_pil_image[2]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[2], - (transforms.ToTensor()(cropped_pil_image[1]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[3], - (transforms.ToTensor()(cropped_pil_image[3]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[4], - (transforms.ToTensor()(cropped_pil_image[4]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[5], - (transforms.ToTensor()(cropped_pil_image[5]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[6], - (transforms.ToTensor()(cropped_pil_image[7]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[7], - (transforms.ToTensor()(cropped_pil_image[6]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[8], - (transforms.ToTensor()(cropped_pil_image[8]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(cropped_tensor[9], - (transforms.ToTensor()(cropped_pil_image[9]) * 255).to(torch.uint8))) - self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) - # scriptable function test - cropped_script = script_ten_crop(img_tensor, [10, 10]) - for cropped_script_img, cropped_tensor_img in zip(cropped_script, cropped_tensor): - self.assertTrue(torch.equal(cropped_script_img, cropped_tensor_img)) + script_ten_crop = torch.jit.script(F.ten_crop) + + img_tensor, pil_img = self._create_data(32, 34) + + cropped_pil_images = F.ten_crop(pil_img, [10, 11]) + + cropped_tensors = F.ten_crop(img_tensor, [10, 11]) + for i in range(10): + self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) + + cropped_tensors = script_ten_crop(img_tensor, [10, 11]) + for i in range(10): + self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) def test_pad(self): script_fn = torch.jit.script(F_t.pad) @@ -311,32 +227,92 @@ def test_pad(self): with self.assertRaises(ValueError, msg="Padding can not be negative for symmetric padding_mode"): F_t.pad(tensor, (-2, -3), padding_mode="symmetric") - def test_adjust_gamma(self): - script_fn = torch.jit.script(F_t.adjust_gamma) - tensor, pil_img = self._create_data(26, 36) + def _test_adjust_fn(self, fn, fn_pil, fn_t, configs): + script_fn = torch.jit.script(fn) + + torch.manual_seed(15) - for dt in [torch.float64, torch.float32, None]: + tensor, pil_img = self._create_data(26, 34) + + for dt in [None, torch.float32, torch.float64]: if dt is not None: tensor = F.convert_image_dtype(tensor, dt) - gammas = [0.8, 1.0, 1.2] - gains = [0.7, 1.0, 1.3] - for gamma, gain in zip(gammas, gains): + for config in configs: - adjusted_tensor = F_t.adjust_gamma(tensor, gamma, gain) - adjusted_pil = F_pil.adjust_gamma(pil_img, gamma, gain) - scripted_result = script_fn(tensor, gamma, gain) - self.assertEqual(adjusted_tensor.dtype, scripted_result.dtype) - self.assertEqual(adjusted_tensor.size()[1:], adjusted_pil.size[::-1]) + adjusted_tensor = fn_t(tensor, **config) + adjusted_pil = fn_pil(pil_img, **config) + scripted_result = script_fn(tensor, **config) + msg = "{}, {}".format(dt, config) + self.assertEqual(adjusted_tensor.dtype, scripted_result.dtype, msg=msg) + self.assertEqual(adjusted_tensor.size()[1:], adjusted_pil.size[::-1], msg=msg) rbg_tensor = adjusted_tensor + if adjusted_tensor.dtype != torch.uint8: rbg_tensor = F.convert_image_dtype(adjusted_tensor, torch.uint8) - self.compareTensorToPIL(rbg_tensor, adjusted_pil) + # Check that max difference does not exceed 1 in [0, 255] range + # Exact matching is not possible due to incompatibility convert_image_dtype and PIL results + rbg_tensor = rbg_tensor.float() + adjusted_pil_tensor = torch.as_tensor(np.array(adjusted_pil).transpose((2, 0, 1))).to(rbg_tensor) + max_diff = torch.abs(rbg_tensor - adjusted_pil_tensor).max().item() + self.assertLessEqual( + max_diff, + 1.0, + msg="{}: tensor:\n{} \ndid not equal PIL tensor:\n{}".format(msg, rbg_tensor, adjusted_pil_tensor) + ) + + self.assertTrue(adjusted_tensor.equal(scripted_result), msg=msg) + + def test_adjust_brightness(self): + self._test_adjust_fn( + F.adjust_brightness, + F_pil.adjust_brightness, + F_t.adjust_brightness, + [{"brightness_factor": f} for f in [0.1, 0.5, 1.0, 1.34, 2.5]] + ) + + def test_adjust_contrast(self): + self._test_adjust_fn( + F.adjust_contrast, + F_pil.adjust_contrast, + F_t.adjust_contrast, + [{"contrast_factor": f} for f in [0.2, 0.5, 1.0, 1.5, 2.0]] + ) + + def test_adjust_saturation(self): + self._test_adjust_fn( + F.adjust_saturation, + F_pil.adjust_saturation, + F_t.adjust_saturation, + [{"saturation_factor": f} for f in [0.5, 0.75, 1.0, 1.25, 1.5]] + ) + + def test_adjust_hue(self): + self._test_adjust_fn( + F.adjust_hue, + F_pil.adjust_hue, + F_t.adjust_hue, + [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]] + ) - self.assertTrue(adjusted_tensor.equal(scripted_result)) + def test_class_interface(self): + tensor, _ = self._create_data(26, 34) + configs = [{"brightness": 0.1}, {"contrast": 0.2}, {"saturation": 0.5}, {"hue": 0.5}] + for config in configs: + f = transforms.ColorJitter(**config) + scripted_fn = torch.jit.script(f) + scripted_fn(tensor) + + def test_adjust_gamma(self): + self._test_adjust_fn( + F.adjust_gamma, + F_pil.adjust_gamma, + F_t.adjust_gamma, + [{"gamma": g1, "gain": g2} for g1, g2 in zip([0.8, 1.0, 1.2], [0.7, 1.0, 1.3])] + ) def test_resize(self): script_fn = torch.jit.script(F_t.resize) From 70cff26a5a1ada2bcb0a998d75f6851b3565a0bd Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Sat, 15 Aug 2020 11:11:56 -0400 Subject: [PATCH 10/16] Removed test_adjustments from test_transforms_tensor.py and moved the ColorJitter class interface test in test_transforms_tensor.py. --- test/test_functional_tensor.py | 8 -------- test/test_transforms_tensor.py | 36 +++++++++++----------------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 52dcaabe4fa..2c11a0db2f2 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -298,14 +298,6 @@ def test_adjust_hue(self): [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]] ) - def test_class_interface(self): - tensor, _ = self._create_data(26, 34) - configs = [{"brightness": 0.1}, {"contrast": 0.2}, {"saturation": 0.5}, {"hue": 0.5}] - for config in configs: - f = transforms.ColorJitter(**config) - scripted_fn = torch.jit.script(f) - scripted_fn(tensor) - def test_adjust_gamma(self): self._test_adjust_fn( F.adjust_gamma, diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 4f7b8a8094a..fd556ba10db 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -57,31 +57,17 @@ def test_random_horizontal_flip(self): def test_random_vertical_flip(self): self._test_geom_op('vflip', 'RandomVerticalFlip') - def test_adjustments(self): - fns = ['adjust_brightness', 'adjust_contrast', 'adjust_saturation'] - for _ in range(20): - factor = 3 * torch.rand(1).item() - tensor, _ = self._create_data() - pil_img = T.ToPILImage()(tensor) - - for func in fns: - adjusted_tensor = getattr(F, func)(tensor, factor) - adjusted_pil_img = getattr(F, func)(pil_img, factor) - - adjusted_pil_tensor = T.ToTensor()(adjusted_pil_img) - scripted_fn = torch.jit.script(getattr(F, func)) - adjusted_tensor_script = scripted_fn(tensor, factor) - - if not tensor.dtype.is_floating_point: - adjusted_tensor = adjusted_tensor.to(torch.float) / 255 - adjusted_tensor_script = adjusted_tensor_script.to(torch.float) / 255 - - # F uses uint8 and F_t uses float, so there is a small - # difference in values caused by (at most 5) truncations. - max_diff = (adjusted_tensor - adjusted_pil_tensor).abs().max() - max_diff_scripted = (adjusted_tensor - adjusted_tensor_script).abs().max() - self.assertLess(max_diff, 5 / 255 + 1e-5) - self.assertLess(max_diff_scripted, 5 / 255 + 1e-5) + def test_class_interface(self): + tensor, _ = self._create_data(26, 34) + configs = [{"brightness": 0.1}, {"contrast": 0.2}, {"saturation": 0.5}, {"hue": 0.5}] + for config in configs: # One parameter at the time + f = T.ColorJitter(**config) + scripted_fn = torch.jit.script(f) + scripted_fn(tensor) + + f = T.ColorJitter(brightness=0.1, contrast=0.2, saturation=0.5, hue=0.5) # All 4 parameters together + scripted_fn = torch.jit.script(f) + scripted_fn(tensor) def test_pad(self): From bb7ec8c3af4283e3e959ee50a40ebb7a27070a77 Mon Sep 17 00:00:00 2001 From: Dragos Cristian Manta Date: Fri, 21 Aug 2020 19:03:00 -0400 Subject: [PATCH 11/16] Added cuda test cases for test_adjustments and tried to fix conflict. --- test/test_functional_tensor.py | 384 ++++++++++++++++++++++----------- 1 file changed, 259 insertions(+), 125 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 2c11a0db2f2..65136d4cb5f 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -1,4 +1,5 @@ import unittest +import random import colorsys import math @@ -16,18 +17,16 @@ class Tester(unittest.TestCase): - def _create_data(self, height=3, width=3, channels=3): - tensor = torch.randint(0, 255, (channels, height, width), dtype=torch.uint8) - pil_img = Image.fromarray(tensor.permute(1, 2, 0).contiguous().numpy()) + def _create_data(self, height=3, width=3, channels=3, device="cpu"): + tensor = torch.randint(0, 255, (channels, height, width), dtype=torch.uint8, device=device) + pil_img = Image.fromarray(tensor.permute(1, 2, 0).contiguous().cpu().numpy()) return tensor, pil_img def compareTensorToPIL(self, tensor, pil_image, msg=None): - np_pil_image = np.array(pil_image) - if np_pil_image.ndim == 2: - np_pil_image = np_pil_image[:, :, None] - pil_tensor = torch.as_tensor(np_pil_image.transpose((2, 0, 1))) - msg = "{}: tensor:\n{} \ndid not equal PIL tensor:\n{}".format(msg, tensor, pil_tensor) - self.assertTrue(tensor.equal(pil_tensor), msg) + pil_tensor = torch.as_tensor(np.array(pil_image).transpose((2, 0, 1))) + if msg is None: + msg = "tensor:\n{} \ndid not equal PIL tensor:\n{}".format(tensor, pil_tensor) + self.assertTrue(tensor.cpu().equal(pil_tensor), msg) def approxEqualTensorToPIL(self, tensor, pil_image, tol=1e-5, msg=None): pil_tensor = torch.as_tensor(np.array(pil_image).transpose((2, 0, 1))).to(tensor) @@ -37,9 +36,9 @@ def approxEqualTensorToPIL(self, tensor, pil_image, tol=1e-5, msg=None): msg="{}: mae={}, tol={}: \n{}\nvs\n{}".format(msg, mae, tol, tensor[0, :10, :10], pil_tensor[0, :10, :10]) ) - def test_vflip(self): + def _test_vflip(self, device): script_vflip = torch.jit.script(F_t.vflip) - img_tensor = torch.randn(3, 16, 16) + img_tensor = torch.randn(3, 16, 16, device=device) img_tensor_clone = img_tensor.clone() vflipped_img = F_t.vflip(img_tensor) vflipped_img_again = F_t.vflip(vflipped_img) @@ -50,9 +49,16 @@ def test_vflip(self): vflipped_img_script = script_vflip(img_tensor) self.assertTrue(torch.equal(vflipped_img, vflipped_img_script)) - def test_hflip(self): + def test_vflip_cpu(self): + self._test_vflip("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_vflip_cuda(self): + self._test_vflip("cuda") + + def _test_hflip(self, device): script_hflip = torch.jit.script(F_t.hflip) - img_tensor = torch.randn(3, 16, 16) + img_tensor = torch.randn(3, 16, 16, device=device) img_tensor_clone = img_tensor.clone() hflipped_img = F_t.hflip(img_tensor) hflipped_img_again = F_t.hflip(hflipped_img) @@ -63,10 +69,17 @@ def test_hflip(self): hflipped_img_script = script_hflip(img_tensor) self.assertTrue(torch.equal(hflipped_img, hflipped_img_script)) - def test_crop(self): - script_crop = torch.jit.script(F_t.crop) + def test_hflip_cpu(self): + self._test_hflip("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_hflip_cuda(self): + self._test_hflip("cuda") - img_tensor, pil_img = self._create_data(16, 18) + def _test_crop(self, device): + script_crop = torch.jit.script(F.crop) + + img_tensor, pil_img = self._create_data(16, 18, device=device) test_configs = [ (1, 2, 4, 5), # crop inside top-left corner @@ -84,6 +97,13 @@ def test_crop(self): img_tensor_cropped = script_crop(img_tensor, top, left, height, width) self.compareTensorToPIL(img_tensor_cropped, pil_img_cropped) + def test_crop_cpu(self): + self._test_crop("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_crop_cuda(self): + self._test_crop("cuda") + def test_hsv2rgb(self): shape = (3, 100, 150) for _ in range(20): @@ -129,27 +149,112 @@ def test_rgb2hsv(self): self.assertLess(max_diff, 1e-5) - def test_rgb_to_grayscale(self): - script_rgb_to_grayscale = torch.jit.script(F.rgb_to_grayscale) + def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, device="cpu"): + script_fn = torch.jit.script(fn) - img_tensor, pil_img = self._create_data(32, 34) + torch.manual_seed(15) - for num_output_channels in (3, 1): - gray_pil_image = F.rgb_to_grayscale(pil_img, num_output_channels=num_output_channels) - gray_tensor = F.rgb_to_grayscale(img_tensor, num_output_channels=num_output_channels) + tensor, pil_img = self._create_data(26, 34, device=device) - if num_output_channels == 1: - print(gray_tensor.shape) + for dt in [None, torch.float32, torch.float64]: - self.compareTensorToPIL(gray_tensor, gray_pil_image) + if dt is not None: + tensor = F.convert_image_dtype(tensor, dt) - s_gray_tensor = script_rgb_to_grayscale(img_tensor, num_output_channels=num_output_channels) - self.assertTrue(s_gray_tensor.equal(gray_tensor)) + for config in configs: - def test_center_crop(self): + adjusted_tensor = fn_t(tensor, **config) + adjusted_pil = fn_pil(pil_img, **config) + scripted_result = script_fn(tensor, **config) + msg = "{}, {}".format(dt, config) + self.assertEqual(adjusted_tensor.dtype, scripted_result.dtype, msg=msg) + self.assertEqual(adjusted_tensor.size()[1:], adjusted_pil.size[::-1], msg=msg) + + rbg_tensor = adjusted_tensor + + if adjusted_tensor.dtype != torch.uint8: + rbg_tensor = F.convert_image_dtype(adjusted_tensor, torch.uint8) + + # Check that max difference does not exceed 1 in [0, 255] range + # Exact matching is not possible due to incompatibility convert_image_dtype and PIL results + rbg_tensor = rbg_tensor.float() + adjusted_pil_tensor = torch.as_tensor(np.array(adjusted_pil).transpose((2, 0, 1)), + device=torch.device(device)).to(rbg_tensor) + max_diff = torch.abs(rbg_tensor - adjusted_pil_tensor).max().item() + self.assertLessEqual( + max_diff, + 1.0, + msg="{}: tensor:\n{} \ndid not equal PIL tensor:\n{}".format(msg, rbg_tensor, adjusted_pil_tensor) + ) + + self.assertTrue(adjusted_tensor.equal(scripted_result), msg=msg) + + def _test_adjust_brightness(self, device): + self._test_adjust_fn( + F.adjust_brightness, + F_pil.adjust_brightness, + F_t.adjust_brightness, + [{"brightness_factor": f} for f in [0.1, 0.5, 1.0, 1.34, 2.5]], + device=device + ) + + def _test_adjust_contrast(self, device): + self._test_adjust_fn( + F.adjust_contrast, + F_pil.adjust_contrast, + F_t.adjust_contrast, + [{"contrast_factor": f} for f in [0.2, 0.5, 1.0, 1.5, 2.0]], + device=device + ) + + def _test_adjust_saturation(self, device): + self._test_adjust_fn( + F.adjust_saturation, + F_pil.adjust_saturation, + F_t.adjust_saturation, + [{"saturation_factor": f} for f in [0.5, 0.75, 1.0, 1.25, 1.5]], + device=device + ) + + def _test_adjust_hue(self, device): + self._test_adjust_fn( + F.adjust_hue, + F_pil.adjust_hue, + F_t.adjust_hue, + [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]], + device=device + ) + + def test_adjustments(self): + self._test_adjust_brightness("cpu") + self._test_adjust_contrast("cpu") + self._test_adjust_saturation("cpu") + self._test_adjust_hue("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_adjustments_cuda(self): + self._test_adjust_brightness("cuda") + self._test_adjust_contrast("cuda") + self._test_adjust_saturation("cuda") + self._test_adjust_hue("cuda") + + def test_rgb_to_grayscale(self): + script_rgb_to_grayscale = torch.jit.script(F_t.rgb_to_grayscale) + img_tensor = torch.randint(0, 255, (3, 16, 16), dtype=torch.uint8) + img_tensor_clone = img_tensor.clone() + grayscale_tensor = F_t.rgb_to_grayscale(img_tensor).to(int) + grayscale_pil_img = torch.tensor(np.array(F.to_grayscale(F.to_pil_image(img_tensor)))).to(int) + max_diff = (grayscale_tensor - grayscale_pil_img).abs().max() + self.assertLess(max_diff, 1.0001) + self.assertTrue(torch.equal(img_tensor, img_tensor_clone)) + # scriptable function test + grayscale_script = script_rgb_to_grayscale(img_tensor).to(int) + self.assertTrue(torch.equal(grayscale_script, grayscale_tensor)) + + def _test_center_crop(self, device): script_center_crop = torch.jit.script(F.center_crop) - img_tensor, pil_img = self._create_data(32, 34) + img_tensor, pil_img = self._create_data(32, 34, device=device) cropped_pil_image = F.center_crop(pil_img, [10, 11]) @@ -159,10 +264,17 @@ def test_center_crop(self): cropped_tensor = script_center_crop(img_tensor, [10, 11]) self.compareTensorToPIL(cropped_tensor, cropped_pil_image) - def test_five_crop(self): + def test_center_crop(self): + self._test_center_crop("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_center_crop_cuda(self): + self._test_center_crop("cuda") + + def _test_five_crop(self, device): script_five_crop = torch.jit.script(F.five_crop) - img_tensor, pil_img = self._create_data(32, 34) + img_tensor, pil_img = self._create_data(32, 34, device=device) cropped_pil_images = F.five_crop(pil_img, [10, 11]) @@ -174,10 +286,17 @@ def test_five_crop(self): for i in range(5): self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) - def test_ten_crop(self): + def test_five_crop(self): + self._test_five_crop("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_five_crop_cuda(self): + self._test_five_crop("cuda") + + def _test_ten_crop(self, device): script_ten_crop = torch.jit.script(F.ten_crop) - img_tensor, pil_img = self._create_data(32, 34) + img_tensor, pil_img = self._create_data(32, 34, device=device) cropped_pil_images = F.ten_crop(pil_img, [10, 11]) @@ -189,9 +308,16 @@ def test_ten_crop(self): for i in range(10): self.compareTensorToPIL(cropped_tensors[i], cropped_pil_images[i]) - def test_pad(self): + def test_ten_crop(self): + self._test_ten_crop("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_ten_crop_cuda(self): + self._test_ten_crop("cuda") + + def _test_pad(self, device): script_fn = torch.jit.script(F_t.pad) - tensor, pil_img = self._create_data(7, 8) + tensor, pil_img = self._create_data(7, 8, device=device) for dt in [None, torch.float32, torch.float64]: if dt is not None: @@ -227,88 +353,50 @@ def test_pad(self): with self.assertRaises(ValueError, msg="Padding can not be negative for symmetric padding_mode"): F_t.pad(tensor, (-2, -3), padding_mode="symmetric") - def _test_adjust_fn(self, fn, fn_pil, fn_t, configs): - script_fn = torch.jit.script(fn) + def test_pad_cpu(self): + self._test_pad("cpu") - torch.manual_seed(15) + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_pad_cuda(self): + self._test_pad("cuda") - tensor, pil_img = self._create_data(26, 34) + def _test_adjust_gamma(self, device): + script_fn = torch.jit.script(F.adjust_gamma) + tensor, pil_img = self._create_data(26, 36, device=device) - for dt in [None, torch.float32, torch.float64]: + for dt in [torch.float64, torch.float32, None]: if dt is not None: tensor = F.convert_image_dtype(tensor, dt) - for config in configs: + gammas = [0.8, 1.0, 1.2] + gains = [0.7, 1.0, 1.3] + for gamma, gain in zip(gammas, gains): - adjusted_tensor = fn_t(tensor, **config) - adjusted_pil = fn_pil(pil_img, **config) - scripted_result = script_fn(tensor, **config) - msg = "{}, {}".format(dt, config) - self.assertEqual(adjusted_tensor.dtype, scripted_result.dtype, msg=msg) - self.assertEqual(adjusted_tensor.size()[1:], adjusted_pil.size[::-1], msg=msg) + adjusted_tensor = F.adjust_gamma(tensor, gamma, gain) + adjusted_pil = F.adjust_gamma(pil_img, gamma, gain) + scripted_result = script_fn(tensor, gamma, gain) + self.assertEqual(adjusted_tensor.dtype, scripted_result.dtype) + self.assertEqual(adjusted_tensor.size()[1:], adjusted_pil.size[::-1]) rbg_tensor = adjusted_tensor - if adjusted_tensor.dtype != torch.uint8: rbg_tensor = F.convert_image_dtype(adjusted_tensor, torch.uint8) - # Check that max difference does not exceed 1 in [0, 255] range - # Exact matching is not possible due to incompatibility convert_image_dtype and PIL results - rbg_tensor = rbg_tensor.float() - adjusted_pil_tensor = torch.as_tensor(np.array(adjusted_pil).transpose((2, 0, 1))).to(rbg_tensor) - max_diff = torch.abs(rbg_tensor - adjusted_pil_tensor).max().item() - self.assertLessEqual( - max_diff, - 1.0, - msg="{}: tensor:\n{} \ndid not equal PIL tensor:\n{}".format(msg, rbg_tensor, adjusted_pil_tensor) - ) - - self.assertTrue(adjusted_tensor.equal(scripted_result), msg=msg) - - def test_adjust_brightness(self): - self._test_adjust_fn( - F.adjust_brightness, - F_pil.adjust_brightness, - F_t.adjust_brightness, - [{"brightness_factor": f} for f in [0.1, 0.5, 1.0, 1.34, 2.5]] - ) - - def test_adjust_contrast(self): - self._test_adjust_fn( - F.adjust_contrast, - F_pil.adjust_contrast, - F_t.adjust_contrast, - [{"contrast_factor": f} for f in [0.2, 0.5, 1.0, 1.5, 2.0]] - ) + self.compareTensorToPIL(rbg_tensor, adjusted_pil) - def test_adjust_saturation(self): - self._test_adjust_fn( - F.adjust_saturation, - F_pil.adjust_saturation, - F_t.adjust_saturation, - [{"saturation_factor": f} for f in [0.5, 0.75, 1.0, 1.25, 1.5]] - ) + self.assertTrue(adjusted_tensor.allclose(scripted_result)) - def test_adjust_hue(self): - self._test_adjust_fn( - F.adjust_hue, - F_pil.adjust_hue, - F_t.adjust_hue, - [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]] - ) + def test_adjust_gamma_cpu(self): + self._test_adjust_gamma("cpu") - def test_adjust_gamma(self): - self._test_adjust_fn( - F.adjust_gamma, - F_pil.adjust_gamma, - F_t.adjust_gamma, - [{"gamma": g1, "gain": g2} for g1, g2 in zip([0.8, 1.0, 1.2], [0.7, 1.0, 1.3])] - ) + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_adjust_gamma_cuda(self): + self._test_adjust_gamma("cuda") - def test_resize(self): + def _test_resize(self, device): script_fn = torch.jit.script(F_t.resize) - tensor, pil_img = self._create_data(26, 36) + tensor, pil_img = self._create_data(26, 36, device=device) for dt in [None, torch.float32, torch.float64]: if dt is not None: @@ -344,16 +432,23 @@ def test_resize(self): resize_result = script_fn(tensor, size=script_size, interpolation=interpolation) self.assertTrue(resized_tensor.equal(resize_result), msg="{}, {}".format(size, interpolation)) - def test_resized_crop(self): + def test_resize_cpu(self): + self._test_resize("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_resize_cuda(self): + self._test_resize("cuda") + + def _test_resized_crop(self, device): # test values of F.resized_crop in several cases: # 1) resize to the same size, crop to the same size => should be identity - tensor, _ = self._create_data(26, 36) + tensor, _ = self._create_data(26, 36, device=device) for i in [0, 2, 3]: out_tensor = F.resized_crop(tensor, top=0, left=0, height=26, width=36, size=[26, 36], interpolation=i) self.assertTrue(tensor.equal(out_tensor), msg="{} vs {}".format(out_tensor[0, :5, :5], tensor[0, :5, :5])) # 2) resize by half and crop a TL corner - tensor, _ = self._create_data(26, 36) + tensor, _ = self._create_data(26, 36, device=device) out_tensor = F.resized_crop(tensor, top=0, left=0, height=20, width=30, size=[10, 15], interpolation=0) expected_out_tensor = tensor[:, :20:2, :30:2] self.assertTrue( @@ -361,11 +456,18 @@ def test_resized_crop(self): msg="{} vs {}".format(expected_out_tensor[0, :10, :10], out_tensor[0, :10, :10]) ) - def test_affine(self): + def test_resized_crop_cpu(self): + self._test_resized_crop("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_resized_crop_cuda(self): + self._test_resized_crop("cuda") + + def _test_affine(self, device): # Tests on square and rectangular images scripted_affine = torch.jit.script(F.affine) - for tensor, pil_img in [self._create_data(26, 26), self._create_data(32, 26)]: + for tensor, pil_img in [self._create_data(26, 26, device=device), self._create_data(32, 26, device=device)]: # 1) identity map out_tensor = F.affine(tensor, angle=0, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0) @@ -389,8 +491,16 @@ def test_affine(self): (180, torch.rot90(tensor, k=2, dims=(-1, -2))), ] for a, true_tensor in test_configs: + + out_pil_img = F.affine( + pil_img, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 + ) + out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))).to(device) + for fn in [F.affine, scripted_affine]: - out_tensor = fn(tensor, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0) + out_tensor = fn( + tensor, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 + ) if true_tensor is not None: self.assertTrue( true_tensor.equal(out_tensor), @@ -399,11 +509,6 @@ def test_affine(self): else: true_tensor = out_tensor - out_pil_img = F.affine( - pil_img, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 - ) - out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) - num_diff_pixels = (true_tensor != out_pil_tensor).sum().item() / 3.0 ratio_diff_pixels = num_diff_pixels / true_tensor.shape[-1] / true_tensor.shape[-2] # Tolerance : less than 6% of different pixels @@ -419,12 +524,16 @@ def test_affine(self): 90, 45, 15, -30, -60, -120 ] for a in test_configs: + + out_pil_img = F.affine( + pil_img, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 + ) + out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) + for fn in [F.affine, scripted_affine]: - out_tensor = fn(tensor, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0) - out_pil_img = F.affine( - pil_img, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 - ) - out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) + out_tensor = fn( + tensor, angle=a, translate=[0, 0], scale=1.0, shear=[0.0, 0.0], resample=0 + ).cpu() num_diff_pixels = (out_tensor != out_pil_tensor).sum().item() / 3.0 ratio_diff_pixels = num_diff_pixels / out_tensor.shape[-1] / out_tensor.shape[-2] @@ -442,9 +551,12 @@ def test_affine(self): [10, 12], (-12, -13) ] for t in test_configs: + + out_pil_img = F.affine(pil_img, angle=0, translate=t, scale=1.0, shear=[0.0, 0.0], resample=0) + for fn in [F.affine, scripted_affine]: out_tensor = fn(tensor, angle=0, translate=t, scale=1.0, shear=[0.0, 0.0], resample=0) - out_pil_img = F.affine(pil_img, angle=0, translate=t, scale=1.0, shear=[0.0, 0.0], resample=0) + self.compareTensorToPIL(out_tensor, out_pil_img) # 3) Test rotation + translation + scale + share @@ -466,23 +578,31 @@ def test_affine(self): out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) for fn in [F.affine, scripted_affine]: - out_tensor = fn(tensor, angle=a, translate=t, scale=s, shear=sh, resample=r) + out_tensor = fn(tensor, angle=a, translate=t, scale=s, shear=sh, resample=r).cpu() num_diff_pixels = (out_tensor != out_pil_tensor).sum().item() / 3.0 ratio_diff_pixels = num_diff_pixels / out_tensor.shape[-1] / out_tensor.shape[-2] - # Tolerance : less than 5% of different pixels + # Tolerance : less than 5% (cpu), 6% (cuda) of different pixels + tol = 0.06 if device == "cuda" else 0.05 self.assertLess( ratio_diff_pixels, - 0.05, + tol, msg="{}: {}\n{} vs \n{}".format( (r, a, t, s, sh), ratio_diff_pixels, out_tensor[0, :7, :7], out_pil_tensor[0, :7, :7] ) ) - def test_rotate(self): + def test_affine_cpu(self): + self._test_affine("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_affine_cuda(self): + self._test_affine("cuda") + + def _test_rotate(self, device): # Tests on square image scripted_rotate = torch.jit.script(F.rotate) - for tensor, pil_img in [self._create_data(26, 26), self._create_data(32, 26)]: + for tensor, pil_img in [self._create_data(26, 26, device=device), self._create_data(32, 26, device=device)]: img_size = pil_img.size centers = [ @@ -499,7 +619,7 @@ def test_rotate(self): out_pil_img = F.rotate(pil_img, angle=a, resample=r, expand=e, center=c) out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) for fn in [F.rotate, scripted_rotate]: - out_tensor = fn(tensor, angle=a, resample=r, expand=e, center=c) + out_tensor = fn(tensor, angle=a, resample=r, expand=e, center=c).cpu() self.assertEqual( out_tensor.shape, @@ -522,11 +642,18 @@ def test_rotate(self): ) ) - def test_perspective(self): + def test_rotate_cpu(self): + self._test_rotate("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_rotate_cuda(self): + self._test_rotate("cuda") + + def _test_perspective(self, device): from torchvision.transforms import RandomPerspective - for tensor, pil_img in [self._create_data(26, 34), self._create_data(26, 26)]: + for tensor, pil_img in [self._create_data(26, 34, device=device), self._create_data(26, 26, device=device)]: scripted_tranform = torch.jit.script(F.perspective) @@ -546,7 +673,7 @@ def test_perspective(self): out_pil_tensor = torch.from_numpy(np.array(out_pil_img).transpose((2, 0, 1))) for fn in [F.perspective, scripted_tranform]: - out_tensor = fn(tensor, startpoints=spoints, endpoints=epoints, interpolation=r) + out_tensor = fn(tensor, startpoints=spoints, endpoints=epoints, interpolation=r).cpu() num_diff_pixels = (out_tensor != out_pil_tensor).sum().item() / 3.0 ratio_diff_pixels = num_diff_pixels / out_tensor.shape[-1] / out_tensor.shape[-2] @@ -562,6 +689,13 @@ def test_perspective(self): ) ) + def test_perspective_cpu(self): + self._test_perspective("cpu") + + @unittest.skipIf(not torch.cuda.is_available(), reason="Skip if no CUDA device") + def test_perspective_cuda(self): + self._test_perspective("cuda") + if __name__ == '__main__': unittest.main() From d4dd848348efae90e8bae624eee8929e1ac2c9f3 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Tue, 1 Sep 2020 23:14:40 +0200 Subject: [PATCH 12/16] Updated tests - adjust hue - color jitter --- test/test_functional_tensor.py | 9 +++++---- test/test_transforms_tensor.py | 17 ++++++----------- torchvision/transforms/functional_tensor.py | 2 ++ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index a98087116b3..7e63d01341c 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -209,7 +209,7 @@ def test_pad(self): with self.assertRaises(ValueError, msg="Padding can not be negative for symmetric padding_mode"): F_t.pad(tensor, (-2, -3), padding_mode="symmetric") - def _test_adjust_fn(self, fn, fn_pil, fn_t, configs): + def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, tol=2.0+1e-10, agg_method="max"): script_fn = torch.jit.script(fn) torch.manual_seed(15) @@ -237,8 +237,7 @@ def _test_adjust_fn(self, fn, fn_pil, fn_t, configs): # Check that max difference does not exceed 2 in [0, 255] range # Exact matching is not possible due to incompatibility convert_image_dtype and PIL results - tol = 2.0 + 1e-10 - self.approxEqualTensorToPIL(rbg_tensor.float(), adjusted_pil, tol, msg=msg, agg_method="max") + self.approxEqualTensorToPIL(rbg_tensor.float(), adjusted_pil, tol=tol, msg=msg, agg_method=agg_method) self.assertTrue(adjusted_tensor.allclose(scripted_result), msg=msg) def test_adjust_brightness(self): @@ -270,7 +269,9 @@ def test_adjust_hue(self): F.adjust_hue, F_pil.adjust_hue, F_t.adjust_hue, - [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]] + [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]], + tol=0.1, + agg_method="mean" ) def test_adjust_gamma(self): diff --git a/test/test_transforms_tensor.py b/test/test_transforms_tensor.py index 46f1ab53675..d82c4f6b309 100644 --- a/test/test_transforms_tensor.py +++ b/test/test_transforms_tensor.py @@ -60,39 +60,34 @@ def test_random_vertical_flip(self): def test_color_jitter(self): tol = 1.0 + 1e-10 - # TODO: add tuple/list confs - for f in [0.1, 0.5, 1.0, 1.34]: + for f in [0.1, 0.5, 1.0, 1.34, (0.3, 0.7), [0.4, 0.5]]: meth_kwargs = {"brightness": f} self._test_class_op( "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=tol, agg_method="max" ) - # TODO: add tuple/list confs - for f in [0.2, 0.5, 1.0, 1.5]: + for f in [0.2, 0.5, 1.0, 1.5, (0.3, 0.7), [0.4, 0.5]]: meth_kwargs = {"contrast": f} self._test_class_op( "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=tol, agg_method="max" ) - # TODO: add tuple/list confs - for f in [0.5, 0.75, 1.0, 1.25]: + for f in [0.5, 0.75, 1.0, 1.25, (0.3, 0.7), [0.3, 0.4]]: meth_kwargs = {"saturation": f} self._test_class_op( "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=tol, agg_method="max" ) - # TODO: add tuple/list confs - for f in [0.2, 0.5]: + for f in [0.2, 0.5, (-0.2, 0.3), [-0.4, 0.5]]: meth_kwargs = {"hue": f} self._test_class_op( - "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=tol, agg_method="max" + "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=0.1, agg_method="mean" ) # All 4 parameters together - # TODO: add tuple/list confs meth_kwargs = {"brightness": 0.2, "contrast": 0.2, "saturation": 0.2, "hue": 0.2} self._test_class_op( - "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=tol, agg_method="max" + "ColorJitter", meth_kwargs=meth_kwargs, test_exact_match=False, tol=0.1, agg_method="mean" ) def test_pad(self): diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 8b38f926472..84b197cd9c5 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -408,6 +408,8 @@ def _blend(img1: Tensor, img2: Tensor, ratio: float) -> Tensor: def _rgb2hsv(img): r, g, b = img.unbind(0) + # Implementation is based on https://github.com/python-pillow/Pillow/blob/4174d4267616897df3746d315d5a2d0f82c656ee/ + # src/libImaging/Convert.c#L330 maxc = torch.max(img, dim=0).values minc = torch.min(img, dim=0).values From 8551011ed2a4a1525f28daf985a9ec00faa9efb0 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Tue, 1 Sep 2020 22:24:08 +0000 Subject: [PATCH 13/16] Fixes incompatible devices --- torchvision/transforms/functional_tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index 84b197cd9c5..ff4c870e5bb 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -455,7 +455,7 @@ def _hsv2rgb(img): t = torch.clamp((v * (1.0 - s * (1.0 - f))), 0.0, 1.0) i = i % 6 - mask = i == torch.arange(6)[:, None, None] + mask = i == torch.arange(6, device=i.device)[:, None, None] a1 = torch.stack((v, q, p, p, t, v)) a2 = torch.stack((t, v, v, q, p, p)) From 71185bd29a6c47d09e58262416a7f2028d6763aa Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Wed, 2 Sep 2020 08:38:44 +0000 Subject: [PATCH 14/16] Increased tol for cuda tests --- test/test_functional_tensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_functional_tensor.py b/test/test_functional_tensor.py index 7e63d01341c..fcadcd59119 100644 --- a/test/test_functional_tensor.py +++ b/test/test_functional_tensor.py @@ -209,11 +209,9 @@ def test_pad(self): with self.assertRaises(ValueError, msg="Padding can not be negative for symmetric padding_mode"): F_t.pad(tensor, (-2, -3), padding_mode="symmetric") - def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, tol=2.0+1e-10, agg_method="max"): + def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, tol=2.0 + 1e-10, agg_method="max"): script_fn = torch.jit.script(fn) - torch.manual_seed(15) - tensor, pil_img = self._create_data(26, 34, device=self.device) for dt in [None, torch.float32, torch.float64]: @@ -222,7 +220,6 @@ def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, tol=2.0+1e-10, agg_method=" tensor = F.convert_image_dtype(tensor, dt) for config in configs: - adjusted_tensor = fn_t(tensor, **config) adjusted_pil = fn_pil(pil_img, **config) scripted_result = script_fn(tensor, **config) @@ -238,7 +235,11 @@ def _test_adjust_fn(self, fn, fn_pil, fn_t, configs, tol=2.0+1e-10, agg_method=" # Check that max difference does not exceed 2 in [0, 255] range # Exact matching is not possible due to incompatibility convert_image_dtype and PIL results self.approxEqualTensorToPIL(rbg_tensor.float(), adjusted_pil, tol=tol, msg=msg, agg_method=agg_method) - self.assertTrue(adjusted_tensor.allclose(scripted_result), msg=msg) + + atol = 1e-6 + if adjusted_tensor.dtype == torch.uint8 and "cuda" in torch.device(self.device).type: + atol = 1.0 + self.assertTrue(adjusted_tensor.allclose(scripted_result, atol=atol), msg=msg) def test_adjust_brightness(self): self._test_adjust_fn( @@ -269,7 +270,7 @@ def test_adjust_hue(self): F.adjust_hue, F_pil.adjust_hue, F_t.adjust_hue, - [{"hue_factor": f} for f in [-0.5, -0.25, 0.0, 0.25, 0.5]], + [{"hue_factor": f} for f in [-0.45, -0.25, 0.0, 0.25, 0.45]], tol=0.1, agg_method="mean" ) From c58d1514b9a5ee9a3b8d48a1a56e282ccf9cf070 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Wed, 2 Sep 2020 11:38:34 +0000 Subject: [PATCH 15/16] Fixes potential issue with inplace op - fixes irreproducible failing test on Travis CI --- torchvision/transforms/functional_tensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index ff4c870e5bb..d1a509bf24b 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -194,8 +194,7 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: img = _rgb2hsv(img) h, s, v = img.unbind(0) - h += hue_factor - h = h % 1.0 + h = torch.fmod(h + hue_factor, 1.0) img = torch.stack((h, s, v)) img_hue_adj = _hsv2rgb(img) From a143835ff36113097f5a85284f614c4518a2ea95 Mon Sep 17 00:00:00 2001 From: vfdev-5 Date: Wed, 2 Sep 2020 13:34:57 +0000 Subject: [PATCH 16/16] Reverted fmod -> % --- torchvision/transforms/functional_tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchvision/transforms/functional_tensor.py b/torchvision/transforms/functional_tensor.py index d1a509bf24b..73aa020b637 100644 --- a/torchvision/transforms/functional_tensor.py +++ b/torchvision/transforms/functional_tensor.py @@ -194,7 +194,7 @@ def adjust_hue(img: Tensor, hue_factor: float) -> Tensor: img = _rgb2hsv(img) h, s, v = img.unbind(0) - h = torch.fmod(h + hue_factor, 1.0) + h = (h + hue_factor) % 1.0 img = torch.stack((h, s, v)) img_hue_adj = _hsv2rgb(img)