From c12991b115173cb7173b92cff1add6deea4d377d Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 8 May 2023 15:12:30 +0200 Subject: [PATCH 01/39] [PoC] refactor transforms v2 tests --- test/test_transforms_v2_refactored.py | 146 ++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 test/test_transforms_v2_refactored.py diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py new file mode 100644 index 00000000000..54142b66493 --- /dev/null +++ b/test/test_transforms_v2_refactored.py @@ -0,0 +1,146 @@ +import pytest +import torch + +from common_utils import cache, cpu_and_gpu +from torch.testing import assert_close +from torch.utils._pytree import tree_map +from torchvision import datapoints + +from torchvision.transforms.v2 import functional as F +from torchvision.transforms.v2.utils import is_simple_tensor + + +def _check_cuda_vs_cpu(kernel, input_cuda, *other_args, **kwargs): + input_cuda = input_cuda.as_subclass(torch.Tensor) + input_cpu = input_cuda.to("cpu") + + actual = kernel(input_cuda, *other_args, **kwargs) + expected = kernel(input_cpu, *other_args, **kwargs) + + assert_close(actual, expected) + + +@cache +def _script(fn): + try: + return torch.jit.script(fn) + except Exception as error: + raise AssertionError(f"Trying to `torch.jit.script` '{fn.__name__}' raised the error above.") from error + + +def _check_scripted_vs_eager(kernel_eager, input, *other_args, **kwargs): + kernel_scripted = _script(kernel_eager) + + input = input.as_subclass(torch.Tensor) + actual = kernel_scripted(input, *other_args, **kwargs) + expected = kernel_eager(input, *other_args, **kwargs) + + assert_close(actual, expected) + + +def _unbatch(batch, *, data_dims): + if isinstance(batch, torch.Tensor): + batched_tensor = batch + metadata = () + else: + batched_tensor, *metadata = batch + + if batched_tensor.ndim == data_dims: + return batch + + return [ + _unbatch(unbatched, data_dims=data_dims) + for unbatched in ( + batched_tensor.unbind(0) if not metadata else [(t, *metadata) for t in batched_tensor.unbind(0)] + ) + ] + + +def _check_batched_vs_single(kernel, batched_input, *other_args, **kwargs): + input_type = datapoints.Image if is_simple_tensor(batched_input) else type(batched_input) + # This dictionary contains the number of rightmost dimensions that contain the actual data. + # Everything to the left is considered a batch dimension. + data_dims = { + datapoints.Image: 3, + datapoints.BoundingBox: 1, + # `Mask`'s are special in the sense that the data dimensions depend on the type of mask. For detection masks + # it is 3 `(*, N, H, W)`, but for segmentation masks it is 2 `(*, H, W)`. Since both a grouped under one + # type all kernels should also work without differentiating between the two. Thus, we go with 2 here as + # common ground. + datapoints.Mask: 2, + datapoints.Video: 4, + }.get(input_type) + if data_dims is None: + raise pytest.UsageError( + f"The number of data dimensions cannot be determined for input of type {input_type.__name__}." + ) from None + elif batched_input.ndim <= data_dims or not all(batched_input.shape[:-data_dims]): + # input is not batched or has a degenerate batch shape + return + + batched_input = batched_input.as_subclass(torch.Tensor) + batched_output = kernel(batched_input, *other_args, **kwargs) + actual = _unbatch(batched_output, data_dims=data_dims) + + single_inputs = _unbatch(batched_input, data_dims=data_dims) + expected = tree_map(lambda single_input: kernel(single_input, *other_args, **kwargs), single_inputs) + + assert_close(actual, expected) + + +def check_kernel( + kernel, + input, + *other_kernel_args, + check_cuda_vs_cpu=True, + check_scripted_vs_eager=True, + check_batched_vs_single=True, + **kernel_kwargs, + # TODO: tolerances! +): + initial_input_version = input._version + output = kernel(input.as_subclass(torch.Tensor), *other_kernel_args, **kernel_kwargs) + + # check that no inplace operation happened + assert input._version == initial_input_version + + assert output.dtype == input.dtype + assert output.device == input.device + + # TODO: we can do better here, by passing the regular output of the kernel instead of multiple times in + # each auxiliary helper + + if check_cuda_vs_cpu and input.device.type == "cuda": + _check_cuda_vs_cpu(kernel, input, *other_kernel_args, **kernel_kwargs) + + if check_scripted_vs_eager: + _check_scripted_vs_eager(kernel, input, *other_kernel_args, **kernel_kwargs) + + if check_batched_vs_single: + _check_batched_vs_single(kernel, input, *other_kernel_args, **kernel_kwargs) + + +def check_dispatcher(): + pass + + +def check_transform(): + pass + + +class TestResize: + @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) + @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("device", cpu_and_gpu()) + def test_resize_image_tensor(self, size, antialias, device): + image = torch.rand((3, 14, 16), dtype=torch.float32, device=device) + check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) + + def test_resize_bounding_box(self): + pass + + def test_resize(self): + pass + + def test_Resize(self): + pass From 9dfe0fb7f981da86f7cfa4ef796647cd301638a3 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 18 May 2023 11:11:21 +0200 Subject: [PATCH 02/39] complete kernel checks --- test/test_transforms_v2_refactored.py | 51 +++++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 54142b66493..4b8dcbdd984 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest import torch @@ -10,7 +12,26 @@ from torchvision.transforms.v2.utils import is_simple_tensor -def _check_cuda_vs_cpu(kernel, input_cuda, *other_args, **kwargs): +def _check_kernel_smoke(kernel, input, *args, **kwargs): + initial_input_version = input._version + + output = kernel(input.as_subclass(torch.Tensor), *args, **kwargs) + + # check that no inplace operation happened + assert input._version == initial_input_version + + assert output.dtype == input.dtype + assert output.device == input.device + + +def _check_logging(fn, *args, **kwargs): + with mock.patch("torch._C._log_api_usage_once", wraps=torch._C._log_api_usage_once) as spy: + fn(*args, **kwargs) + + spy.assert_any_call(f"{fn.__module__}.{fn.__name__}") + + +def _check_kernel_cuda_vs_cpu(kernel, input_cuda, *other_args, **kwargs): input_cuda = input_cuda.as_subclass(torch.Tensor) input_cpu = input_cuda.to("cpu") @@ -28,7 +49,7 @@ def _script(fn): raise AssertionError(f"Trying to `torch.jit.script` '{fn.__name__}' raised the error above.") from error -def _check_scripted_vs_eager(kernel_eager, input, *other_args, **kwargs): +def _check_kernel_scripted_vs_eager(kernel_eager, input, *other_args, **kwargs): kernel_scripted = _script(kernel_eager) input = input.as_subclass(torch.Tensor) @@ -56,7 +77,7 @@ def _unbatch(batch, *, data_dims): ] -def _check_batched_vs_single(kernel, batched_input, *other_args, **kwargs): +def _check_kernel_batched_vs_single(kernel, batched_input, *other_args, **kwargs): input_type = datapoints.Image if is_simple_tensor(batched_input) else type(batched_input) # This dictionary contains the number of rightmost dimensions that contain the actual data. # Everything to the left is considered a batch dimension. @@ -92,32 +113,32 @@ def check_kernel( kernel, input, *other_kernel_args, + # Most kernels don't log because that is done through the dispatcher. Meaning if we set the default to True, + # we'll have to set it to False in almost any test. That would be more explicit though + check_logging=False, check_cuda_vs_cpu=True, check_scripted_vs_eager=True, check_batched_vs_single=True, **kernel_kwargs, # TODO: tolerances! ): - initial_input_version = input._version - output = kernel(input.as_subclass(torch.Tensor), *other_kernel_args, **kernel_kwargs) + # TODO: we can improve performance here by not computing the regular output of the kernel over and over - # check that no inplace operation happened - assert input._version == initial_input_version - - assert output.dtype == input.dtype - assert output.device == input.device + _check_kernel_smoke(kernel, input, *other_kernel_args, **kernel_kwargs) - # TODO: we can do better here, by passing the regular output of the kernel instead of multiple times in - # each auxiliary helper + if check_logging: + # We need to unwrap the input here manually, because `_check_logging` is not only used for kernels and thus + # cannot do this internally + _check_logging(kernel, input.as_subclass(torch.Tensor), *other_kernel_args, **kernel_kwargs) if check_cuda_vs_cpu and input.device.type == "cuda": - _check_cuda_vs_cpu(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_cuda_vs_cpu(kernel, input, *other_kernel_args, **kernel_kwargs) if check_scripted_vs_eager: - _check_scripted_vs_eager(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_scripted_vs_eager(kernel, input, *other_kernel_args, **kernel_kwargs) if check_batched_vs_single: - _check_batched_vs_single(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_batched_vs_single(kernel, input, *other_kernel_args, **kernel_kwargs) def check_dispatcher(): From aa52e9dcfcec1bf8be43255bff4045835c433547 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 18 May 2023 11:17:05 +0200 Subject: [PATCH 03/39] align parameter names --- test/test_transforms_v2_refactored.py | 53 ++++++++++++++------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 4b8dcbdd984..d9979a73380 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -31,12 +31,15 @@ def _check_logging(fn, *args, **kwargs): spy.assert_any_call(f"{fn.__module__}.{fn.__name__}") -def _check_kernel_cuda_vs_cpu(kernel, input_cuda, *other_args, **kwargs): - input_cuda = input_cuda.as_subclass(torch.Tensor) +def _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs): + if input.device.type != "cuda": + return + + input_cuda = input.as_subclass(torch.Tensor) input_cpu = input_cuda.to("cpu") - actual = kernel(input_cuda, *other_args, **kwargs) - expected = kernel(input_cpu, *other_args, **kwargs) + actual = kernel(input_cuda, *args, **kwargs) + expected = kernel(input_cpu, *args, **kwargs) assert_close(actual, expected) @@ -49,12 +52,12 @@ def _script(fn): raise AssertionError(f"Trying to `torch.jit.script` '{fn.__name__}' raised the error above.") from error -def _check_kernel_scripted_vs_eager(kernel_eager, input, *other_args, **kwargs): +def _check_kernel_scripted_vs_eager(kernel_eager, input, *args, **kwargs): kernel_scripted = _script(kernel_eager) input = input.as_subclass(torch.Tensor) - actual = kernel_scripted(input, *other_args, **kwargs) - expected = kernel_eager(input, *other_args, **kwargs) + actual = kernel_scripted(input, *args, **kwargs) + expected = kernel_eager(input, *args, **kwargs) assert_close(actual, expected) @@ -77,8 +80,8 @@ def _unbatch(batch, *, data_dims): ] -def _check_kernel_batched_vs_single(kernel, batched_input, *other_args, **kwargs): - input_type = datapoints.Image if is_simple_tensor(batched_input) else type(batched_input) +def _check_kernel_batched_vs_single(kernel, input, *args, **kwargs): + input_type = datapoints.Image if is_simple_tensor(input) else type(input) # This dictionary contains the number of rightmost dimensions that contain the actual data. # Everything to the left is considered a batch dimension. data_dims = { @@ -95,16 +98,16 @@ def _check_kernel_batched_vs_single(kernel, batched_input, *other_args, **kwargs raise pytest.UsageError( f"The number of data dimensions cannot be determined for input of type {input_type.__name__}." ) from None - elif batched_input.ndim <= data_dims or not all(batched_input.shape[:-data_dims]): + elif input.ndim <= data_dims or not all(input.shape[:-data_dims]): # input is not batched or has a degenerate batch shape return - batched_input = batched_input.as_subclass(torch.Tensor) - batched_output = kernel(batched_input, *other_args, **kwargs) + batched_input = input.as_subclass(torch.Tensor) + batched_output = kernel(batched_input, *args, **kwargs) actual = _unbatch(batched_output, data_dims=data_dims) single_inputs = _unbatch(batched_input, data_dims=data_dims) - expected = tree_map(lambda single_input: kernel(single_input, *other_args, **kwargs), single_inputs) + expected = tree_map(lambda single_input: kernel(single_input, *args, **kwargs), single_inputs) assert_close(actual, expected) @@ -112,33 +115,33 @@ def _check_kernel_batched_vs_single(kernel, batched_input, *other_args, **kwargs def check_kernel( kernel, input, - *other_kernel_args, + *args, # Most kernels don't log because that is done through the dispatcher. Meaning if we set the default to True, # we'll have to set it to False in almost any test. That would be more explicit though check_logging=False, check_cuda_vs_cpu=True, check_scripted_vs_eager=True, check_batched_vs_single=True, - **kernel_kwargs, + **kwargs, # TODO: tolerances! ): # TODO: we can improve performance here by not computing the regular output of the kernel over and over - _check_kernel_smoke(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_smoke(kernel, input, *args, **kwargs) if check_logging: # We need to unwrap the input here manually, because `_check_logging` is not only used for kernels and thus # cannot do this internally - _check_logging(kernel, input.as_subclass(torch.Tensor), *other_kernel_args, **kernel_kwargs) + _check_logging(kernel, input.as_subclass(torch.Tensor), *args, **kwargs) - if check_cuda_vs_cpu and input.device.type == "cuda": - _check_kernel_cuda_vs_cpu(kernel, input, *other_kernel_args, **kernel_kwargs) + if check_cuda_vs_cpu: + _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs) if check_scripted_vs_eager: - _check_kernel_scripted_vs_eager(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_scripted_vs_eager(kernel, input, *args, **kwargs) if check_batched_vs_single: - _check_kernel_batched_vs_single(kernel, input, *other_kernel_args, **kernel_kwargs) + _check_kernel_batched_vs_single(kernel, input, *args, **kwargs) def check_dispatcher(): @@ -153,15 +156,15 @@ class TestResize: @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("antialias", [True, False]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_resize_image_tensor(self, size, antialias, device): + def test_kernel_image_tensor(self, size, antialias, device): image = torch.rand((3, 14, 16), dtype=torch.float32, device=device) check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) - def test_resize_bounding_box(self): + def test_kernel_bounding_box(self): pass - def test_resize(self): + def test_dispatcher(self): pass - def test_Resize(self): + def test_transform(self): pass From d35c381931dc8fee8d94852b22ae463114736d2c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 19 May 2023 14:01:46 +0200 Subject: [PATCH 04/39] add tolerance handling --- test/test_transforms_v2_refactored.py | 42 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index d9979a73380..ff2cb785435 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -3,7 +3,7 @@ import pytest import torch -from common_utils import cache, cpu_and_gpu +from common_utils import cache, cpu_and_gpu, make_image from torch.testing import assert_close from torch.utils._pytree import tree_map from torchvision import datapoints @@ -12,6 +12,22 @@ from torchvision.transforms.v2.utils import is_simple_tensor +def _to_tolerances(maybe_tolerance_dict): + default_tolerances = dict(rtol=None, atol=None) + if not isinstance(maybe_tolerance_dict, dict): + return default_tolerances + + missing = default_tolerances.keys() - maybe_tolerance_dict.keys() + if missing: + raise pytest.UsageError("ADDME") + + extra = maybe_tolerance_dict.keys() - default_tolerances.keys() + if extra: + raise pytest.UsageError("ADDME") + + return maybe_tolerance_dict + + def _check_kernel_smoke(kernel, input, *args, **kwargs): initial_input_version = input._version @@ -31,7 +47,7 @@ def _check_logging(fn, *args, **kwargs): spy.assert_any_call(f"{fn.__module__}.{fn.__name__}") -def _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs): +def _check_kernel_cuda_vs_cpu(kernel, tolerances, input, *args, **kwargs): if input.device.type != "cuda": return @@ -41,7 +57,7 @@ def _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs): actual = kernel(input_cuda, *args, **kwargs) expected = kernel(input_cpu, *args, **kwargs) - assert_close(actual, expected) + assert_close(actual, expected, **tolerances) @cache @@ -52,14 +68,14 @@ def _script(fn): raise AssertionError(f"Trying to `torch.jit.script` '{fn.__name__}' raised the error above.") from error -def _check_kernel_scripted_vs_eager(kernel_eager, input, *args, **kwargs): - kernel_scripted = _script(kernel_eager) +def _check_kernel_scripted_vs_eager(kernel, tolerances, input, *args, **kwargs): + kernel_scripted = _script(kernel) input = input.as_subclass(torch.Tensor) actual = kernel_scripted(input, *args, **kwargs) - expected = kernel_eager(input, *args, **kwargs) + expected = kernel(input, *args, **kwargs) - assert_close(actual, expected) + assert_close(actual, expected, **tolerances) def _unbatch(batch, *, data_dims): @@ -80,7 +96,7 @@ def _unbatch(batch, *, data_dims): ] -def _check_kernel_batched_vs_single(kernel, input, *args, **kwargs): +def _check_kernel_batched_vs_single(kernel, tolerances, input, *args, **kwargs): input_type = datapoints.Image if is_simple_tensor(input) else type(input) # This dictionary contains the number of rightmost dimensions that contain the actual data. # Everything to the left is considered a batch dimension. @@ -109,7 +125,7 @@ def _check_kernel_batched_vs_single(kernel, input, *args, **kwargs): single_inputs = _unbatch(batched_input, data_dims=data_dims) expected = tree_map(lambda single_input: kernel(single_input, *args, **kwargs), single_inputs) - assert_close(actual, expected) + assert_close(actual, expected, **tolerances) def check_kernel( @@ -135,13 +151,13 @@ def check_kernel( _check_logging(kernel, input.as_subclass(torch.Tensor), *args, **kwargs) if check_cuda_vs_cpu: - _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs) + _check_kernel_cuda_vs_cpu(kernel, _to_tolerances(check_cuda_vs_cpu), input, *args, **kwargs) if check_scripted_vs_eager: - _check_kernel_scripted_vs_eager(kernel, input, *args, **kwargs) + _check_kernel_scripted_vs_eager(kernel, _to_tolerances(check_scripted_vs_eager), input, *args, **kwargs) if check_batched_vs_single: - _check_kernel_batched_vs_single(kernel, input, *args, **kwargs) + _check_kernel_batched_vs_single(kernel, _to_tolerances(check_batched_vs_single), input, *args, **kwargs) def check_dispatcher(): @@ -157,7 +173,7 @@ class TestResize: @pytest.mark.parametrize("antialias", [True, False]) @pytest.mark.parametrize("device", cpu_and_gpu()) def test_kernel_image_tensor(self, size, antialias, device): - image = torch.rand((3, 14, 16), dtype=torch.float32, device=device) + image = make_image(size=(14, 16), dtype=torch.float32, device=device) check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) def test_kernel_bounding_box(self): From 3b3edd7b6634f441f1b2cb3e3cdfc26e323cf49c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 22 May 2023 15:24:49 +0200 Subject: [PATCH 05/39] add tolerance handling and dispatcher tests --- test/test_transforms_v2_refactored.py | 220 +++++++++++++++++++++----- 1 file changed, 180 insertions(+), 40 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index ff2cb785435..2f23ff0ac70 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -1,13 +1,16 @@ +import inspect +import re +from typing import get_type_hints from unittest import mock +import PIL.Image import pytest -import torch -from common_utils import cache, cpu_and_gpu, make_image +import torch +from common_utils import cache, cpu_and_gpu, make_bounding_box, make_image from torch.testing import assert_close from torch.utils._pytree import tree_map from torchvision import datapoints - from torchvision.transforms.v2 import functional as F from torchvision.transforms.v2.utils import is_simple_tensor @@ -28,25 +31,6 @@ def _to_tolerances(maybe_tolerance_dict): return maybe_tolerance_dict -def _check_kernel_smoke(kernel, input, *args, **kwargs): - initial_input_version = input._version - - output = kernel(input.as_subclass(torch.Tensor), *args, **kwargs) - - # check that no inplace operation happened - assert input._version == initial_input_version - - assert output.dtype == input.dtype - assert output.device == input.device - - -def _check_logging(fn, *args, **kwargs): - with mock.patch("torch._C._log_api_usage_once", wraps=torch._C._log_api_usage_once) as spy: - fn(*args, **kwargs) - - spy.assert_any_call(f"{fn.__module__}.{fn.__name__}") - - def _check_kernel_cuda_vs_cpu(kernel, tolerances, input, *args, **kwargs): if input.device.type != "cuda": return @@ -132,24 +116,25 @@ def check_kernel( kernel, input, *args, - # Most kernels don't log because that is done through the dispatcher. Meaning if we set the default to True, - # we'll have to set it to False in almost any test. That would be more explicit though - check_logging=False, check_cuda_vs_cpu=True, check_scripted_vs_eager=True, check_batched_vs_single=True, **kwargs, - # TODO: tolerances! ): - # TODO: we can improve performance here by not computing the regular output of the kernel over and over + initial_input_version = input._version - _check_kernel_smoke(kernel, input, *args, **kwargs) + output = kernel(input.as_subclass(torch.Tensor), *args, **kwargs) + # Most kernels just return a tensor, but some also return some additional metadata + if not isinstance(output, torch.Tensor): + output, *_ = output - if check_logging: - # We need to unwrap the input here manually, because `_check_logging` is not only used for kernels and thus - # cannot do this internally - _check_logging(kernel, input.as_subclass(torch.Tensor), *args, **kwargs) + # check that no inplace operation happened + assert input._version == initial_input_version + + assert output.dtype == input.dtype + assert output.device == input.device + # TODO: we can improve performance here by not computing the regular output of the kernel over and over if check_cuda_vs_cpu: _check_kernel_cuda_vs_cpu(kernel, _to_tolerances(check_cuda_vs_cpu), input, *args, **kwargs) @@ -160,8 +145,133 @@ def check_kernel( _check_kernel_batched_vs_single(kernel, _to_tolerances(check_batched_vs_single), input, *args, **kwargs) -def check_dispatcher(): - pass +def _check_kernel_scripted_smoke(dispatcher, input, *args, **kwargs): + dispatcher_scripted = _script(dispatcher) + dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + + +def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): + if not isinstance(input, datapoints.Image): + return + + with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: + output = dispatcher(input.as_subclass(torch.Tensor), *args, **kwargs) + + spy.assert_called_once() + + # We cannot use `isinstance` here since all datapoints are instances of `torch.Tensor` as well + assert type(output) is torch.Tensor + + +def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs): + # Due to our complex dispatch architecture for datapoints, we cannot spy on the kernel directly, + # but rather have to patch the `Datapoint.__F` attribute to contain the spied on kernel. + spy = mock.MagicMock(wraps=kernel) + with mock.patch.object(F, kernel.__name__, spy): + # Due to Python's name mangling, the `Datapoint.__F` attribute is only accessible from inside the class. + # Since that is not the case here, we need to prefix f"_{cls.__name__}" + # See https://docs.python.org/3/tutorial/classes.html#private-variables for details + with mock.patch.object(datapoints._datapoint.Datapoint, "_Datapoint__F", new=F): + output = dispatcher(input, *args, **kwargs) + + spy.assert_called_once() + assert isinstance(output, type(input)) + + if isinstance(input, datapoints.BoundingBox): + assert output.format == input.format + + +def _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs): + if not (isinstance(input, datapoints.Image) and input.dtype is torch.uint8): + return + + kernel = getattr(F, f"{dispatcher.__name__}_image_pil") + + with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: + output = dispatcher(F.to_image_pil(input), *args, **kwargs) + + spy.assert_called_once() + + assert isinstance(output, PIL.Image.Image) + + +def check_dispatcher( + dispatcher, + kernel, + input, + *args, + check_dispatch_simple_tensor=True, + check_dispatch_datapoint=True, + check_dispatch_pil=True, + **kwargs, +): + with mock.patch("torch._C._log_api_usage_once", wraps=torch._C._log_api_usage_once) as spy: + dispatcher(input, *args, **kwargs) + + spy.assert_any_call(f"{dispatcher.__module__}.{dispatcher.__name__}") + + unknown_input = object() + with pytest.raises(TypeError, match=re.escape(str(type(unknown_input)))): + dispatcher(unknown_input, *args, **kwargs) + + if check_dispatch_simple_tensor: + _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) + + if check_dispatch_datapoint: + _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs) + + if check_dispatch_pil: + _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs) + + +def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, datapoint_type): + dispatcher_signature = inspect.signature(dispatcher) + dispatcher_params = list(dispatcher_signature.parameters.values())[1:] + + kernel_signature = inspect.signature(kernel) + kernel_params = list(kernel_signature.parameters.values())[1:] + + # We filter out metadata that is implicitly passed to the dispatcher through the input datapoint, but has to be + # explicit passed to the kernel. + kernel_params = [param for param in kernel_params if param.name not in datapoint_type.__annotations__.keys()] + + dispatcher_params = iter(dispatcher_params) + for dispatcher_param, kernel_param in zip(dispatcher_params, kernel_params): + try: + # In general, the dispatcher parameters are a superset of the kernel parameters. Thus, we filter out + # dispatcher parameters that have no kernel equivalent while keeping the order intact. + while dispatcher_param.name != kernel_param.name: + dispatcher_param = next(dispatcher_params) + except StopIteration: + raise AssertionError( + f"Parameter `{kernel_param.name}` of kernel `{kernel.__name__}` " + f"has no corresponding parameter on the dispatcher `{dispatcher.__name__}`." + ) from None + + assert dispatcher_param == kernel_param + + +def _check_dispatcher_datapoint_signature_match(dispatcher): + dispatcher_signature = inspect.signature(dispatcher) + dispatcher_params = list(dispatcher_signature.parameters.values())[1:] + + datapoint_method = getattr(datapoints._datapoint.Datapoint, dispatcher.__name__) + datapoint_signature = inspect.signature(datapoint_method) + datapoint_params = list(datapoint_signature.parameters.values())[1:] + + # Because we use `from __future__ import annotations` inside the module where `datapoints._datapoint` is + # defined, the annotations are stored as strings. This makes them concrete again, so they can be compared to the + # natively concrete dispatcher annotations. + datapoint_annotations = get_type_hints(datapoint_method) + for param in datapoint_params: + param._annotation = datapoint_annotations[param.name] + + assert dispatcher_params == datapoint_params + + +def check_dispatcher_signatures_match(dispatcher, *, kernel, datapoint_type): + _check_dispatcher_kernel_signature_match(dispatcher, kernel=kernel, datapoint_type=datapoint_type) + _check_dispatcher_datapoint_signature_match(dispatcher) def check_transform(): @@ -171,16 +281,46 @@ def check_transform(): class TestResize: @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_image_tensor(self, size, antialias, device): - image = make_image(size=(14, 16), dtype=torch.float32, device=device) + def test_kernel_image_tensor(self, size, antialias, dtype, device): + image = make_image(size=(14, 16), dtype=dtype, device=device) check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) - def test_kernel_bounding_box(self): - pass + @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) + @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) + @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) + @pytest.mark.parametrize("device", cpu_and_gpu()) + def test_kernel_bounding_box(self, size, format, dtype, device): + spatial_size = (14, 16) + bounding_box = make_bounding_box(format=format, spatial_size=spatial_size, dtype=dtype, device=device) + check_kernel(F.resize_bounding_box, bounding_box, spatial_size=spatial_size, size=size) - def test_dispatcher(self): - pass + @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box]) + @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) + @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8, torch.int64]) + @pytest.mark.parametrize("device", cpu_and_gpu()) + def test_dispatcher(self, kernel, size, antialias, dtype, device): + spatial_size = (14, 16) + if kernel is F.resize_image_tensor: + input = make_image(size=spatial_size, dtype=dtype, device=device) + elif kernel is F.resize_bounding_box: + input = make_bounding_box( + format=datapoints.BoundingBoxFormat.XYXY, spatial_size=spatial_size, dtype=dtype, device=device + ) + + check_dispatcher(F.resize, kernel, input, size=size, antialias=antialias) + + @pytest.mark.parametrize( + ("kernel", "datapoint_type"), + [ + (F.resize_image_tensor, datapoints.Image), + (F.resize_bounding_box, datapoints.BoundingBox), + ], + ) + def test_dispatcher_signature(self, kernel, datapoint_type): + check_dispatcher_signatures_match(F.resize, kernel=kernel, datapoint_type=datapoint_type) def test_transform(self): pass From fde36a2e678cfa78496d136c091c4eb91a3a7522 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 23 May 2023 11:39:58 +0200 Subject: [PATCH 06/39] don't check device in CUDA vs CPU test --- test/test_transforms_v2_refactored.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 2f23ff0ac70..31d739ba038 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -41,7 +41,7 @@ def _check_kernel_cuda_vs_cpu(kernel, tolerances, input, *args, **kwargs): actual = kernel(input_cuda, *args, **kwargs) expected = kernel(input_cpu, *args, **kwargs) - assert_close(actual, expected, **tolerances) + assert_close(actual, expected, **tolerances, check_device=False) @cache From 785d4d905e1152df0849339b511af044acaa3415 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 23 May 2023 13:40:35 +0200 Subject: [PATCH 07/39] address small comments --- test/test_transforms_v2_refactored.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 31d739ba038..a102e89d628 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -97,7 +97,7 @@ def _check_kernel_batched_vs_single(kernel, tolerances, input, *args, **kwargs): if data_dims is None: raise pytest.UsageError( f"The number of data dimensions cannot be determined for input of type {input_type.__name__}." - ) from None + ) elif input.ndim <= data_dims or not all(input.shape[:-data_dims]): # input is not batched or has a degenerate batch shape return @@ -145,11 +145,6 @@ def check_kernel( _check_kernel_batched_vs_single(kernel, _to_tolerances(check_batched_vs_single), input, *args, **kwargs) -def _check_kernel_scripted_smoke(dispatcher, input, *args, **kwargs): - dispatcher_scripted = _script(dispatcher) - dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) - - def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): if not isinstance(input, datapoints.Image): return @@ -214,6 +209,9 @@ def check_dispatcher( with pytest.raises(TypeError, match=re.escape(str(type(unknown_input)))): dispatcher(unknown_input, *args, **kwargs) + dispatcher_scripted = _script(dispatcher) + dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + if check_dispatch_simple_tensor: _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) From c178d26060123999beddc00d07ca4239fa3ca63d Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 23 May 2023 14:46:19 +0200 Subject: [PATCH 08/39] refactor tolerances --- test/test_transforms_v2_refactored.py | 37 ++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index a102e89d628..75ef018199f 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -16,22 +16,12 @@ def _to_tolerances(maybe_tolerance_dict): - default_tolerances = dict(rtol=None, atol=None) if not isinstance(maybe_tolerance_dict, dict): - return default_tolerances + return dict(rtol=None, atol=None) + return dict(rtol=0, atol=0, **maybe_tolerance_dict) - missing = default_tolerances.keys() - maybe_tolerance_dict.keys() - if missing: - raise pytest.UsageError("ADDME") - extra = maybe_tolerance_dict.keys() - default_tolerances.keys() - if extra: - raise pytest.UsageError("ADDME") - - return maybe_tolerance_dict - - -def _check_kernel_cuda_vs_cpu(kernel, tolerances, input, *args, **kwargs): +def _check_kernel_cuda_vs_cpu(kernel, input, *args, rtol, atol, **kwargs): if input.device.type != "cuda": return @@ -41,7 +31,7 @@ def _check_kernel_cuda_vs_cpu(kernel, tolerances, input, *args, **kwargs): actual = kernel(input_cuda, *args, **kwargs) expected = kernel(input_cpu, *args, **kwargs) - assert_close(actual, expected, **tolerances, check_device=False) + assert_close(actual, expected, check_device=False, rtol=rtol, atol=atol) @cache @@ -52,14 +42,14 @@ def _script(fn): raise AssertionError(f"Trying to `torch.jit.script` '{fn.__name__}' raised the error above.") from error -def _check_kernel_scripted_vs_eager(kernel, tolerances, input, *args, **kwargs): +def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): kernel_scripted = _script(kernel) input = input.as_subclass(torch.Tensor) actual = kernel_scripted(input, *args, **kwargs) expected = kernel(input, *args, **kwargs) - assert_close(actual, expected, **tolerances) + assert_close(actual, expected, rtol=rtol, atol=atol) def _unbatch(batch, *, data_dims): @@ -80,7 +70,7 @@ def _unbatch(batch, *, data_dims): ] -def _check_kernel_batched_vs_single(kernel, tolerances, input, *args, **kwargs): +def _check_kernel_batched_vs_single(kernel, input, *args, rtol, atol, **kwargs): input_type = datapoints.Image if is_simple_tensor(input) else type(input) # This dictionary contains the number of rightmost dimensions that contain the actual data. # Everything to the left is considered a batch dimension. @@ -109,7 +99,7 @@ def _check_kernel_batched_vs_single(kernel, tolerances, input, *args, **kwargs): single_inputs = _unbatch(batched_input, data_dims=data_dims) expected = tree_map(lambda single_input: kernel(single_input, *args, **kwargs), single_inputs) - assert_close(actual, expected, **tolerances) + assert_close(actual, expected, rtol=rtol, atol=atol) def check_kernel( @@ -136,13 +126,13 @@ def check_kernel( # TODO: we can improve performance here by not computing the regular output of the kernel over and over if check_cuda_vs_cpu: - _check_kernel_cuda_vs_cpu(kernel, _to_tolerances(check_cuda_vs_cpu), input, *args, **kwargs) + _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs, **_to_tolerances(check_cuda_vs_cpu)) if check_scripted_vs_eager: - _check_kernel_scripted_vs_eager(kernel, _to_tolerances(check_scripted_vs_eager), input, *args, **kwargs) + _check_kernel_scripted_vs_eager(kernel, input, *args, **kwargs, **_to_tolerances(check_scripted_vs_eager)) if check_batched_vs_single: - _check_kernel_batched_vs_single(kernel, _to_tolerances(check_batched_vs_single), input, *args, **kwargs) + _check_kernel_batched_vs_single(kernel, input, *args, **kwargs, **_to_tolerances(check_batched_vs_single)) def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): @@ -209,8 +199,9 @@ def check_dispatcher( with pytest.raises(TypeError, match=re.escape(str(type(unknown_input)))): dispatcher(unknown_input, *args, **kwargs) - dispatcher_scripted = _script(dispatcher) - dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + if isinstance(input, datapoints.Image): + dispatcher_scripted = _script(dispatcher) + dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) if check_dispatch_simple_tensor: _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) From 3ee85ee167c079107c4392c48010a12111eb4fa9 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 24 May 2023 15:51:44 +0200 Subject: [PATCH 09/39] add batch dim parametrization --- test/test_transforms_v2_refactored.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 75ef018199f..683317afc88 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -14,6 +14,10 @@ from torchvision.transforms.v2 import functional as F from torchvision.transforms.v2.utils import is_simple_tensor +VALID_BATCH_DIMS = [(), (4,), (2, 3)] +DEGENERATE_BATCH_DIMS = [(0,), (5, 0), (0, 5)] +DEFAULT_BATCH_DIMS = [*VALID_BATCH_DIMS, *DEGENERATE_BATCH_DIMS] + def _to_tolerances(maybe_tolerance_dict): if not isinstance(maybe_tolerance_dict, dict): @@ -270,19 +274,23 @@ def check_transform(): class TestResize: @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("batch_dims", DEFAULT_BATCH_DIMS) @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_image_tensor(self, size, antialias, dtype, device): - image = make_image(size=(14, 16), dtype=dtype, device=device) + def test_kernel_image_tensor(self, size, antialias, batch_dims, dtype, device): + image = make_image(size=(14, 16), extra_dims=batch_dims, dtype=dtype, device=device) check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) + @pytest.mark.parametrize("batch_dims", DEFAULT_BATCH_DIMS) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_bounding_box(self, size, format, dtype, device): + def test_kernel_bounding_box(self, size, format, batch_dims, dtype, device): spatial_size = (14, 16) - bounding_box = make_bounding_box(format=format, spatial_size=spatial_size, dtype=dtype, device=device) + bounding_box = make_bounding_box( + format=format, spatial_size=spatial_size, extra_dims=batch_dims, dtype=dtype, device=device + ) check_kernel(F.resize_bounding_box, bounding_box, spatial_size=spatial_size, size=size) @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box]) From 045b2379901360a58ad6873988e2e8300d91eb3b Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 31 May 2023 10:40:47 +0200 Subject: [PATCH 10/39] simplify batch checks --- test/test_transforms_v2_refactored.py | 81 +++++++++------------------ 1 file changed, 26 insertions(+), 55 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 683317afc88..0e03296cb4e 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -9,14 +9,8 @@ import torch from common_utils import cache, cpu_and_gpu, make_bounding_box, make_image from torch.testing import assert_close -from torch.utils._pytree import tree_map from torchvision import datapoints from torchvision.transforms.v2 import functional as F -from torchvision.transforms.v2.utils import is_simple_tensor - -VALID_BATCH_DIMS = [(), (4,), (2, 3)] -DEGENERATE_BATCH_DIMS = [(0,), (5, 0), (0, 5)] -DEFAULT_BATCH_DIMS = [*VALID_BATCH_DIMS, *DEGENERATE_BATCH_DIMS] def _to_tolerances(maybe_tolerance_dict): @@ -56,54 +50,35 @@ def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): assert_close(actual, expected, rtol=rtol, atol=atol) -def _unbatch(batch, *, data_dims): - if isinstance(batch, torch.Tensor): - batched_tensor = batch - metadata = () - else: - batched_tensor, *metadata = batch +def _check_kernel_batched_vs_single(kernel, input, *args, rtol, atol, **kwargs): + single_input = input.as_subclass(torch.Tensor) - if batched_tensor.ndim == data_dims: - return batch + for batch_dims in [(4,), (2, 3)]: + repeats = [*batch_dims, *[1] * input.ndim] - return [ - _unbatch(unbatched, data_dims=data_dims) - for unbatched in ( - batched_tensor.unbind(0) if not metadata else [(t, *metadata) for t in batched_tensor.unbind(0)] - ) - ] + actual = kernel(single_input.repeat(repeats), *args, **kwargs) + expected = kernel(single_input, *args, **kwargs) + # We can't directly call `.repeat()` on the output, since some kernel also return some additional metadata + if isinstance(expected, torch.Tensor): + expected = expected.repeat(repeats) + else: + tensor, *metadata = expected + expected = (tensor.repeat(repeats), *metadata) -def _check_kernel_batched_vs_single(kernel, input, *args, rtol, atol, **kwargs): - input_type = datapoints.Image if is_simple_tensor(input) else type(input) - # This dictionary contains the number of rightmost dimensions that contain the actual data. - # Everything to the left is considered a batch dimension. - data_dims = { - datapoints.Image: 3, - datapoints.BoundingBox: 1, - # `Mask`'s are special in the sense that the data dimensions depend on the type of mask. For detection masks - # it is 3 `(*, N, H, W)`, but for segmentation masks it is 2 `(*, H, W)`. Since both a grouped under one - # type all kernels should also work without differentiating between the two. Thus, we go with 2 here as - # common ground. - datapoints.Mask: 2, - datapoints.Video: 4, - }.get(input_type) - if data_dims is None: - raise pytest.UsageError( - f"The number of data dimensions cannot be determined for input of type {input_type.__name__}." - ) - elif input.ndim <= data_dims or not all(input.shape[:-data_dims]): - # input is not batched or has a degenerate batch shape - return + assert_close(actual, expected, rtol=rtol, atol=atol) - batched_input = input.as_subclass(torch.Tensor) - batched_output = kernel(batched_input, *args, **kwargs) - actual = _unbatch(batched_output, data_dims=data_dims) + for degenerate_batch_dims in [(0,), (5, 0), (0, 5)]: + degenerate_batched_input = torch.empty( + degenerate_batch_dims + input.shape, dtype=input.dtype, device=input.device + ) - single_inputs = _unbatch(batched_input, data_dims=data_dims) - expected = tree_map(lambda single_input: kernel(single_input, *args, **kwargs), single_inputs) + output = kernel(degenerate_batched_input, *args, **kwargs) + # Most kernels just return a tensor, but some also return some additional metadata + if not isinstance(output, torch.Tensor): + output, *_ = output - assert_close(actual, expected, rtol=rtol, atol=atol) + assert output.shape[: -input.ndim] == degenerate_batch_dims def check_kernel( @@ -274,23 +249,19 @@ def check_transform(): class TestResize: @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("antialias", [True, False]) - @pytest.mark.parametrize("batch_dims", DEFAULT_BATCH_DIMS) @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_image_tensor(self, size, antialias, batch_dims, dtype, device): - image = make_image(size=(14, 16), extra_dims=batch_dims, dtype=dtype, device=device) + def test_kernel_image_tensor(self, size, antialias, dtype, device): + image = make_image(size=(14, 16), dtype=dtype, device=device) check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) - @pytest.mark.parametrize("batch_dims", DEFAULT_BATCH_DIMS) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_bounding_box(self, size, format, batch_dims, dtype, device): + def test_kernel_bounding_box(self, size, format, dtype, device): spatial_size = (14, 16) - bounding_box = make_bounding_box( - format=format, spatial_size=spatial_size, extra_dims=batch_dims, dtype=dtype, device=device - ) + bounding_box = make_bounding_box(format=format, spatial_size=spatial_size, dtype=dtype, device=device) check_kernel(F.resize_bounding_box, bounding_box, spatial_size=spatial_size, size=size) @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box]) From 0f124862cdabd068c05080d98bddfeacf5538951 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 31 May 2023 10:45:03 +0200 Subject: [PATCH 11/39] reduce dispatcher parametrization --- test/test_transforms_v2_refactored.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 0e03296cb4e..1c5fb7ab489 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -266,19 +266,14 @@ def test_kernel_bounding_box(self, size, format, dtype, device): @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box]) @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) - @pytest.mark.parametrize("antialias", [True, False]) - @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8, torch.int64]) - @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_dispatcher(self, kernel, size, antialias, dtype, device): + def test_dispatcher(self, kernel, size): spatial_size = (14, 16) if kernel is F.resize_image_tensor: - input = make_image(size=spatial_size, dtype=dtype, device=device) + input = make_image(size=spatial_size) elif kernel is F.resize_bounding_box: - input = make_bounding_box( - format=datapoints.BoundingBoxFormat.XYXY, spatial_size=spatial_size, dtype=dtype, device=device - ) + input = make_bounding_box(format=datapoints.BoundingBoxFormat.XYXY, spatial_size=spatial_size) - check_dispatcher(F.resize, kernel, input, size=size, antialias=antialias) + check_dispatcher(F.resize, kernel, input, size=size) @pytest.mark.parametrize( ("kernel", "datapoint_type"), From f6157f31cb53ef9169e406b79416fd48fdeb33eb Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 1 Jun 2023 10:15:53 +0200 Subject: [PATCH 12/39] polish kernel and dispatcher tests --- test/test_transforms_v2_refactored.py | 140 +++++++++++++++++++++----- 1 file changed, 116 insertions(+), 24 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 1c5fb7ab489..8f6390282cb 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -7,7 +7,16 @@ import pytest import torch -from common_utils import cache, cpu_and_gpu, make_bounding_box, make_image +import torchvision.transforms.v2 as transforms +from common_utils import ( + cache, + cpu_and_gpu, + make_bounding_box, + make_detection_mask, + make_image, + make_segmentation_mask, + make_video, +) from torch.testing import assert_close from torchvision import datapoints from torchvision.transforms.v2 import functional as F @@ -41,6 +50,9 @@ def _script(fn): def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): + if input.device.type != "cpu": + return + kernel_scripted = _script(kernel) input = input.as_subclass(torch.Tensor) @@ -103,7 +115,6 @@ def check_kernel( assert output.dtype == input.dtype assert output.device == input.device - # TODO: we can improve performance here by not computing the regular output of the kernel over and over if check_cuda_vs_cpu: _check_kernel_cuda_vs_cpu(kernel, input, *args, **kwargs, **_to_tolerances(check_cuda_vs_cpu)) @@ -114,6 +125,14 @@ def check_kernel( _check_kernel_batched_vs_single(kernel, input, *args, **kwargs, **_to_tolerances(check_batched_vs_single)) +def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): + if not isinstance(input, datapoints.Image): + return + + dispatcher_scripted = _script(dispatcher) + dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + + def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): if not isinstance(input, datapoints.Image): return @@ -164,6 +183,7 @@ def check_dispatcher( kernel, input, *args, + check_scripted_smoke=True, check_dispatch_simple_tensor=True, check_dispatch_datapoint=True, check_dispatch_pil=True, @@ -178,9 +198,8 @@ def check_dispatcher( with pytest.raises(TypeError, match=re.escape(str(type(unknown_input)))): dispatcher(unknown_input, *args, **kwargs) - if isinstance(input, datapoints.Image): - dispatcher_scripted = _script(dispatcher) - dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + if check_scripted_smoke: + _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs) if check_dispatch_simple_tensor: _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) @@ -246,40 +265,113 @@ def check_transform(): pass +# We cannot use `list(transforms.InterpolationMode)` here, since it includes some PIL-only ones as well +INTERPOLATION_MODES = [ + transforms.InterpolationMode.NEAREST, + transforms.InterpolationMode.NEAREST_EXACT, + transforms.InterpolationMode.BILINEAR, + transforms.InterpolationMode.BICUBIC, +] + + class TestResize: - @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) + SPATIAL_SIZE = (17, 11) + SIZES = [17, [17], (17,), [12, 13], (12, 13)] + + def _make_max_size_kwarg(self, *, use_max_size, size): + if use_max_size: + if not (isinstance(size, int) or len(size) == 1): + # This would result in an `ValueError` + return None + + max_size = (size if isinstance(size, int) else size[0]) + 1 + else: + max_size = None + + return dict(max_size=max_size) + + @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) + @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("antialias", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_image_tensor(self, size, antialias, dtype, device): - image = make_image(size=(14, 16), dtype=dtype, device=device) - check_kernel(F.resize_image_tensor, image, size=size, antialias=antialias) + def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, dtype, device): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + image = make_image(size=self.SPATIAL_SIZE, dtype=dtype, device=device) + check_kernel( + F.resize_image_tensor, + image, + size=size, + interpolation=interpolation, + **max_size_kwarg, + antialias=antialias, + check_scripted_vs_eager=not isinstance(size, int), + ) - @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) + @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) @pytest.mark.parametrize("device", cpu_and_gpu()) - def test_kernel_bounding_box(self, size, format, dtype, device): - spatial_size = (14, 16) - bounding_box = make_bounding_box(format=format, spatial_size=spatial_size, dtype=dtype, device=device) - check_kernel(F.resize_bounding_box, bounding_box, spatial_size=spatial_size, size=size) + def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + bounding_box = make_bounding_box(format=format, spatial_size=self.SPATIAL_SIZE, dtype=dtype, device=device) + check_kernel( + F.resize_bounding_box, + bounding_box, + spatial_size=bounding_box.spatial_size, + size=size, + **max_size_kwarg, + check_scripted_vs_eager=not isinstance(size, int), + ) - @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box]) - @pytest.mark.parametrize("size", [(11, 17), (15, 13)]) - def test_dispatcher(self, kernel, size): - spatial_size = (14, 16) + @pytest.mark.parametrize( + "dtype_and_make_mask", [(torch.uint8, make_segmentation_mask), (torch.bool, make_detection_mask)] + ) + def test_kernel_mask(self, dtype_and_make_mask): + dtype, make_mask = dtype_and_make_mask + mask = make_mask(size=self.SPATIAL_SIZE, dtype=dtype) + check_kernel(F.resize_mask, mask, size=self.SIZES[-1]) + + def test_kernel_video(self): + video = make_video(size=self.SPATIAL_SIZE) + check_kernel(F.resize_video, video, size=self.SIZES[-1]) + + def _make_dispatcher_input(self, kernel): if kernel is F.resize_image_tensor: - input = make_image(size=spatial_size) + input = make_image(size=self.SPATIAL_SIZE) elif kernel is F.resize_bounding_box: - input = make_bounding_box(format=datapoints.BoundingBoxFormat.XYXY, spatial_size=spatial_size) + input = make_bounding_box(format=datapoints.BoundingBoxFormat.XYXY, spatial_size=self.SPATIAL_SIZE) + elif kernel is F.resize_mask: + input = make_segmentation_mask(size=self.SPATIAL_SIZE) + elif kernel is F.resize_video: + input = make_video(size=self.SPATIAL_SIZE) - check_dispatcher(F.resize, kernel, input, size=size) + return input + + @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box, F.resize_mask, F.resize_video]) + @pytest.mark.parametrize("size", SIZES) + def test_dispatcher(self, kernel, size): + check_dispatcher( + F.resize, + kernel, + self._make_dispatcher_input(kernel), + size=size, + check_scripted_smoke=not isinstance(size, int), + ) @pytest.mark.parametrize( - ("kernel", "datapoint_type"), + ("datapoint_type", "kernel"), [ - (F.resize_image_tensor, datapoints.Image), - (F.resize_bounding_box, datapoints.BoundingBox), + (datapoints.Image, F.resize_image_tensor), + (datapoints.BoundingBox, F.resize_bounding_box), + (datapoints.Mask, F.resize_mask), + (datapoints.Video, F.resize_video), ], ) def test_dispatcher_signature(self, kernel, datapoint_type): From 43877de3f4db29fa79e1928b6d713cc69e7af64b Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 13:51:32 +0200 Subject: [PATCH 13/39] add transforms tests --- test/test_transforms_v2_refactored.py | 189 +++++++++++++++++++++----- 1 file changed, 156 insertions(+), 33 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 8f6390282cb..e294ca83869 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -125,6 +125,15 @@ def check_kernel( _check_kernel_batched_vs_single(kernel, input, *args, **kwargs, **_to_tolerances(check_batched_vs_single)) +def check_image_kernel_tensor_vs_pil(kernel_tensor, kernel_pil, image_tensor, *args, rtol=None, atol=None, **kwargs): + # FIXME: do we need MAE comparison here? + + actual = kernel_tensor(image_tensor, *args, **kwargs) + expected = F.to_image_tensor(kernel_pil(F.to_image_pil(image_tensor), *args, **kwargs)) + + torch.testing.assert_close(actual, expected, rtol=rtol, atol=atol) + + def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): if not isinstance(input, datapoints.Image): return @@ -211,7 +220,7 @@ def check_dispatcher( _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs) -def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, datapoint_type): +def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): dispatcher_signature = inspect.signature(dispatcher) dispatcher_params = list(dispatcher_signature.parameters.values())[1:] @@ -220,7 +229,7 @@ def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, datapoint_ty # We filter out metadata that is implicitly passed to the dispatcher through the input datapoint, but has to be # explicit passed to the kernel. - kernel_params = [param for param in kernel_params if param.name not in datapoint_type.__annotations__.keys()] + kernel_params = [param for param in kernel_params if param.name not in input_type.__annotations__.keys()] dispatcher_params = iter(dispatcher_params) for dispatcher_param, kernel_param in zip(dispatcher_params, kernel_params): @@ -256,13 +265,31 @@ def _check_dispatcher_datapoint_signature_match(dispatcher): assert dispatcher_params == datapoint_params -def check_dispatcher_signatures_match(dispatcher, *, kernel, datapoint_type): - _check_dispatcher_kernel_signature_match(dispatcher, kernel=kernel, datapoint_type=datapoint_type) +def check_dispatcher_signatures_match(dispatcher, *, kernel, input_type): + _check_dispatcher_kernel_signature_match(dispatcher, kernel=kernel, input_type=input_type) _check_dispatcher_datapoint_signature_match(dispatcher) -def check_transform(): - pass +def _check_transform_v1_compatibility(transform): + if not hasattr(transform, "_v1_transform_cls"): + return + + if hasattr(transform._v1_transform_cls, "get_params"): + assert type(transform).get_params is transform._v1_transform_cls.get_params + + _script(transform) + + +def check_transform(transform_cls, input, *args, **kwargs): + transform = transform_cls(*args, **kwargs) + + output = transform(input) + assert isinstance(output, type(input)) + + if isinstance(input, datapoints.BoundingBox): + assert output.format == input.format + + _check_transform_v1_compatibility(transform) # We cannot use `list(transforms.InterpolationMode)` here, since it includes some PIL-only ones as well @@ -290,6 +317,27 @@ def _make_max_size_kwarg(self, *, use_max_size, size): return dict(max_size=max_size) + def _make_input(self, input_type, *, dtype=None, device="cpu"): + if input_type in {torch.Tensor, PIL.Image.Image, datapoints.Image}: + input = make_image(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + if input_type is torch.Tensor: + input = input.as_subclass(torch.Tensor) + elif input_type is PIL.Image.Image: + input = F.to_image_pil(input) + elif input_type is datapoints.BoundingBox: + input = make_bounding_box( + format=datapoints.BoundingBoxFormat.XYXY, + spatial_size=self.SPATIAL_SIZE, + dtype=dtype or torch.float32, + device=device, + ) + elif input_type is datapoints.Mask: + input = make_segmentation_mask(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + elif input_type is datapoints.Video: + input = make_video(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + + return input + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -300,10 +348,9 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - image = make_image(size=self.SPATIAL_SIZE, dtype=dtype, device=device) check_kernel( F.resize_image_tensor, - image, + self._make_input(datapoints.Image, dtype=dtype, device=device), size=size, interpolation=interpolation, **max_size_kwarg, @@ -311,6 +358,37 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, check_scripted_vs_eager=not isinstance(size, int), ) + @pytest.mark.parametrize("size", SIZES) + # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. + # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` + @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) + @pytest.mark.parametrize("use_max_size", [True, False]) + def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") + + actual = F.resize_image_tensor( + image, + size=size, + interpolation=interpolation, + **max_size_kwarg, + # antialias is always True for PIL + antialias=True, + ) + expected = F.to_image_tensor( + F.resize_image_pil( + F.to_image_pil(image), + size=size, + interpolation=interpolation, + **max_size_kwarg, + ) + ) + + mae = (actual.float() - expected.float()).abs().mean() + assert mae < 1 + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -320,7 +398,7 @@ def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - bounding_box = make_bounding_box(format=format, spatial_size=self.SPATIAL_SIZE, dtype=dtype, device=device) + bounding_box = self._make_input(datapoints.BoundingBox, dtype=dtype, device=device) check_kernel( F.resize_bounding_box, bounding_box, @@ -335,38 +413,33 @@ def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): ) def test_kernel_mask(self, dtype_and_make_mask): dtype, make_mask = dtype_and_make_mask - mask = make_mask(size=self.SPATIAL_SIZE, dtype=dtype) - check_kernel(F.resize_mask, mask, size=self.SIZES[-1]) + check_kernel(F.resize_mask, self._make_input(datapoints.Mask, dtype=dtype), size=self.SIZES[-1]) def test_kernel_video(self): - video = make_video(size=self.SPATIAL_SIZE) - check_kernel(F.resize_video, video, size=self.SIZES[-1]) - - def _make_dispatcher_input(self, kernel): - if kernel is F.resize_image_tensor: - input = make_image(size=self.SPATIAL_SIZE) - elif kernel is F.resize_bounding_box: - input = make_bounding_box(format=datapoints.BoundingBoxFormat.XYXY, spatial_size=self.SPATIAL_SIZE) - elif kernel is F.resize_mask: - input = make_segmentation_mask(size=self.SPATIAL_SIZE) - elif kernel is F.resize_video: - input = make_video(size=self.SPATIAL_SIZE) - - return input + check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.SIZES[-1]) - @pytest.mark.parametrize("kernel", [F.resize_image_tensor, F.resize_bounding_box, F.resize_mask, F.resize_video]) + @pytest.mark.parametrize( + "input_type_and_kernel", + [ + (datapoints.Image, F.resize_image_tensor), + (datapoints.BoundingBox, F.resize_bounding_box), + (datapoints.Mask, F.resize_mask), + (datapoints.Video, F.resize_video), + ], + ) @pytest.mark.parametrize("size", SIZES) - def test_dispatcher(self, kernel, size): + def test_dispatcher(self, input_type_and_kernel, size): + input_type, kernel = input_type_and_kernel check_dispatcher( F.resize, kernel, - self._make_dispatcher_input(kernel), + self._make_input(input_type), size=size, check_scripted_smoke=not isinstance(size, int), ) @pytest.mark.parametrize( - ("datapoint_type", "kernel"), + ("input_type", "kernel"), [ (datapoints.Image, F.resize_image_tensor), (datapoints.BoundingBox, F.resize_bounding_box), @@ -374,8 +447,58 @@ def test_dispatcher(self, kernel, size): (datapoints.Video, F.resize_video), ], ) - def test_dispatcher_signature(self, kernel, datapoint_type): - check_dispatcher_signatures_match(F.resize, kernel=kernel, datapoint_type=datapoint_type) + def test_dispatcher_signature(self, kernel, input_type): + check_dispatcher_signatures_match(F.resize, kernel=kernel, input_type=input_type) + + @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) + @pytest.mark.parametrize("use_max_size", [True, False]) + @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("device", cpu_and_gpu()) + @pytest.mark.parametrize( + "input_type", + [ + # FIXME: should we handle plain tensors and PIL images implicitly as done for the dispatcher? + torch.Tensor, + PIL.Image.Image, + datapoints.Image, + datapoints.BoundingBox, + datapoints.Mask, + datapoints.Video, + ], + ) + def test_transform(self, size, interpolation, use_max_size, antialias, device, input_type): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + input = self._make_input(input_type, device=device) + + check_transform( + transforms.Resize, + input, + size=size, + interpolation=interpolation, + **max_size_kwarg, + antialias=antialias, + ) + + @pytest.mark.parametrize("size", SIZES) + # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. + # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` + @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) + @pytest.mark.parametrize("use_max_size", [True, False]) + def test_transform_image_correctness(self, size, interpolation, use_max_size): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + transform = transforms.Resize(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) + + image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") + + actual = transform(image) + expected = F.to_image_tensor( + F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) + ) - def test_transform(self): - pass + mae = (actual.float() - expected.float()).abs().mean() + assert mae < 1 From 8ab5374e12ed35da8734d0320faa88ddfeff94c5 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 13:57:44 +0200 Subject: [PATCH 14/39] add tests for extra warnings and errors --- test/test_transforms_v2_refactored.py | 97 ++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index e294ca83869..44833ed7631 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -1,3 +1,4 @@ +import contextlib import inspect import re from typing import get_type_hints @@ -9,6 +10,7 @@ import torch import torchvision.transforms.v2 as transforms from common_utils import ( + assert_equal, cache, cpu_and_gpu, make_bounding_box, @@ -301,6 +303,12 @@ def check_transform(transform_cls, input, *args, **kwargs): ] +@contextlib.contextmanager +def assert_warns_antialias_default_value(): + with pytest.warns(UserWarning, match="The default value of the antialias parameter of all the resizing transforms"): + yield + + class TestResize: SPATIAL_SIZE = (17, 11) SIZES = [17, [17], (17,), [12, 13], (12, 13)] @@ -389,6 +397,14 @@ def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size mae = (actual.float() - expected.float()).abs().mean() assert mae < 1 + @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) + def test_pil_interpolation_compat_smoke(self, interpolation): + F.resize_image_pil( + self._make_input(PIL.Image.Image, dtype=torch.uint8, device="cpu"), + size=self.SIZES[0], + interpolation=interpolation, + ) + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -416,7 +432,7 @@ def test_kernel_mask(self, dtype_and_make_mask): check_kernel(F.resize_mask, self._make_input(datapoints.Mask, dtype=dtype), size=self.SIZES[-1]) def test_kernel_video(self): - check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.SIZES[-1]) + check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.SIZES[-1], antialias=True) @pytest.mark.parametrize( "input_type_and_kernel", @@ -435,6 +451,7 @@ def test_dispatcher(self, input_type_and_kernel, size): kernel, self._make_input(input_type), size=size, + antialias=True, check_scripted_smoke=not isinstance(size, int), ) @@ -450,6 +467,75 @@ def test_dispatcher(self, input_type_and_kernel, size): def test_dispatcher_signature(self, kernel, input_type): check_dispatcher_signatures_match(F.resize, kernel=kernel, input_type=input_type) + def test_dispatcher_pil_antialias_warning(self): + with pytest.warns(UserWarning, match="Anti-alias option is always applied for PIL Image input"): + F.resize(self._make_input(PIL.Image.Image), size=self.SIZES[0], antialias=False) + + @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize( + "input_type_and_kernel", + [ + (datapoints.Image, F.resize_image_tensor), + (datapoints.BoundingBox, F.resize_bounding_box), + (datapoints.Mask, F.resize_mask), + (datapoints.Video, F.resize_video), + ], + ) + def test_max_size_error(self, size, input_type_and_kernel): + input_type, kernel = input_type_and_kernel + + if isinstance(size, int) or len(size) == 1: + max_size = (size if isinstance(size, int) else size[0]) - 1 + match = "must be strictly greater than the requested size" + else: + # value can be anything other than None + max_size = -1 + match = "size should be an int or a sequence of length 1" + + with pytest.raises(ValueError, match=match): + F.resize(self._make_input(input_type), size=size, max_size=max_size, antialias=True) + + @pytest.mark.parametrize( + "input_type_and_kernel", + [ + (datapoints.Image, F.resize_image_tensor), + (datapoints.Video, F.resize_video), + ], + ) + def test_antialias_warning(self, input_type_and_kernel): + input_type, kernel = input_type_and_kernel + + with assert_warns_antialias_default_value(): + F.resize(self._make_input(input_type), size=self.SIZES[0]) + + # `InterpolationMode.NEAREST_EXACT` has no proper corresponding integer equivalent. Internally, we map it to `0` to + # be the same as `InterpolationMode.NEAREST` for PIL. However, for the tensor backend there is a difference and thus + # we don't test it here. + @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST_EXACT}) + @pytest.mark.parametrize( + "input_type_and_kernel", + [ + (datapoints.Image, F.resize_image_tensor), + (datapoints.Video, F.resize_video), + ], + ) + def test_interpolation_int(self, interpolation, input_type_and_kernel): + input_type, kernel = input_type_and_kernel + input = self._make_input(input_type) + + expected = F.resize(input, size=self.SIZES[0], interpolation=interpolation) + actual = F.resize( + input, + size=self.SIZES[0], + interpolation={ + transforms.InterpolationMode.NEAREST: 0, + transforms.InterpolationMode.BILINEAR: 2, + transforms.InterpolationMode.BICUBIC: 3, + }[interpolation], + ) + + assert_equal(actual, expected) + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -458,7 +544,6 @@ def test_dispatcher_signature(self, kernel, input_type): @pytest.mark.parametrize( "input_type", [ - # FIXME: should we handle plain tensors and PIL images implicitly as done for the dispatcher? torch.Tensor, PIL.Image.Image, datapoints.Image, @@ -471,6 +556,10 @@ def test_transform(self, size, interpolation, use_max_size, antialias, device, i if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return + if input_type is PIL.Image.Image and antialias is False: + # antialias is always True for PIL + return + input = self._make_input(input_type, device=device) check_transform( @@ -502,3 +591,7 @@ def test_transform_image_correctness(self, size, interpolation, use_max_size): mae = (actual.float() - expected.float()).abs().mean() assert mae < 1 + + def test_transform_unknown_size_error(self): + with pytest.raises(ValueError, match="size can either be an integer or a list or tuple of one or two integers"): + transforms.Resize(size=object()) From 5f1a68b9489f6a94a0ddbcbebce96ed3f356b062 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 15:40:23 +0200 Subject: [PATCH 15/39] fix antialias test --- test/common_utils.py | 10 ++++++++++ test/test_transforms_v2_refactored.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/test/common_utils.py b/test/common_utils.py index 1d0b82a827c..d29e69832af 100644 --- a/test/common_utils.py +++ b/test/common_utils.py @@ -10,6 +10,7 @@ import shutil import sys import tempfile +import warnings from collections import defaultdict from subprocess import CalledProcessError, check_output, STDOUT from typing import Callable, Sequence, Tuple, Union @@ -880,3 +881,12 @@ def assert_run_python_script(source_code): raise RuntimeError(f"script errored with output:\n{e.output.decode()}") if out != b"": raise AssertionError(out.decode()) + + +@contextlib.contextmanager +def assert_no_warnings(): + # The name `catch_warnings` is a misnomer as the context manager does **not** catch any warnings, but rather scopes + # the warning filters. All changes that are made to the filters while in this context, will be reset upon exit. + with warnings.catch_warnings(): + warnings.simplefilter("error") + yield diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 44833ed7631..77d93e9b5ac 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -11,6 +11,7 @@ import torchvision.transforms.v2 as transforms from common_utils import ( assert_equal, + assert_no_warnings, cache, cpu_and_gpu, make_bounding_box, @@ -495,6 +496,7 @@ def test_max_size_error(self, size, input_type_and_kernel): with pytest.raises(ValueError, match=match): F.resize(self._make_input(input_type), size=size, max_size=max_size, antialias=True) + @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize( "input_type_and_kernel", [ @@ -502,11 +504,15 @@ def test_max_size_error(self, size, input_type_and_kernel): (datapoints.Video, F.resize_video), ], ) - def test_antialias_warning(self, input_type_and_kernel): + def test_antialias_warning(self, interpolation, input_type_and_kernel): input_type, kernel = input_type_and_kernel - with assert_warns_antialias_default_value(): - F.resize(self._make_input(input_type), size=self.SIZES[0]) + with ( + assert_warns_antialias_default_value() + if interpolation in {transforms.InterpolationMode.BILINEAR, transforms.InterpolationMode.BICUBIC} + else assert_no_warnings() + ): + F.resize(self._make_input(input_type), size=self.SIZES[0], interpolation=interpolation) # `InterpolationMode.NEAREST_EXACT` has no proper corresponding integer equivalent. Internally, we map it to `0` to # be the same as `InterpolationMode.NEAREST` for PIL. However, for the tensor backend there is a difference and thus From b94d8bcb3f6b2138a3f47e2934d6855f634e4883 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 16:28:01 +0200 Subject: [PATCH 16/39] add output size checks --- test/test_transforms_v2_refactored.py | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 77d93e9b5ac..b7b78fcac2f 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -347,6 +347,35 @@ def _make_input(self, input_type, *, dtype=None, device="cpu"): return input + def _check_size(self, input, output, *, size, max_size): + if isinstance(size, int) or len(size) == 1: + if not isinstance(size, int): + size = size[0] + + old_height, old_width = F.get_spatial_size(input) + ratio = old_width / old_height + if ratio > 1: + new_height = size + new_width = int(ratio * new_height) + else: + new_width = size + new_height = int(new_width / ratio) + + if max_size is not None and max(new_height, new_width) > max_size: + # Need to recompute the aspect ratio, since it might have changed due to rounding + ratio = new_width / new_height + if ratio > 1: + new_width = max_size + new_height = int(new_width / ratio) + else: + new_height = max_size + new_width = int(new_height * ratio) + + else: + new_height, new_width = size + + assert F.get_spatial_size(output) == [new_height, new_width] + @pytest.mark.parametrize("size", SIZES) @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -395,6 +424,8 @@ def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size ) ) + self._check_size(image, actual, size=size, **max_size_kwarg) + mae = (actual.float() - expected.float()).abs().mean() assert mae < 1 @@ -595,6 +626,8 @@ def test_transform_image_correctness(self, size, interpolation, use_max_size): F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) ) + self._check_size(image, actual, size=size, **max_size_kwarg) + mae = (actual.float() - expected.float()).abs().mean() assert mae < 1 From 87ff4ae3566322844c8285854991216348d073c6 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 23:11:56 +0200 Subject: [PATCH 17/39] fix cuda tolerances --- test/test_transforms_v2_refactored.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index b7b78fcac2f..d7fb2186af2 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -28,7 +28,10 @@ def _to_tolerances(maybe_tolerance_dict): if not isinstance(maybe_tolerance_dict, dict): return dict(rtol=None, atol=None) - return dict(rtol=0, atol=0, **maybe_tolerance_dict) + + tolerances = dict(rtol=0, atol=0) + tolerances.update(maybe_tolerance_dict) + return tolerances def _check_kernel_cuda_vs_cpu(kernel, input, *args, rtol, atol, **kwargs): @@ -386,6 +389,17 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return + if device == "cuda": + if dtype.is_floating_point: + check_cuda_vs_cpu_atol = 1 / 255 + elif interpolation is transforms.InterpolationMode.BICUBIC: + check_cuda_vs_cpu_atol = 20 + else: + check_cuda_vs_cpu_atol = 1 + else: + check_cuda_vs_cpu_atol = None + check_cuda_vs_cpu = dict(rtol=0, atol=check_cuda_vs_cpu_atol) + check_kernel( F.resize_image_tensor, self._make_input(datapoints.Image, dtype=dtype, device=device), @@ -393,6 +407,7 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, + check_cuda_vs_cpu=check_cuda_vs_cpu, check_scripted_vs_eager=not isinstance(size, int), ) From 2547acd54701136e27ed61035a6fe1b1a54b9d9c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 23:30:51 +0200 Subject: [PATCH 18/39] address small comments --- test/test_transforms_v2_refactored.py | 112 +++++++++++++------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index d7fb2186af2..06f40f3d43a 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -35,6 +35,7 @@ def _to_tolerances(maybe_tolerance_dict): def _check_kernel_cuda_vs_cpu(kernel, input, *args, rtol, atol, **kwargs): + """Checks if the kernel produces closes results for inputs on GPU and CPU.""" if input.device.type != "cuda": return @@ -56,6 +57,7 @@ def _script(fn): def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): + """Checks if the kernel is scriptable and if the scripted output is close to the eager one.""" if input.device.type != "cpu": return @@ -68,15 +70,16 @@ def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): assert_close(actual, expected, rtol=rtol, atol=atol) -def _check_kernel_batched_vs_single(kernel, input, *args, rtol, atol, **kwargs): - single_input = input.as_subclass(torch.Tensor) +def _check_kernel_batched_vs_unbatched(kernel, input, *args, rtol, atol, **kwargs): + """Checks if the kernel produces close results for batched and unbatched inputs.""" + unbatched_input = input.as_subclass(torch.Tensor) - for batch_dims in [(4,), (2, 3)]: + for batch_dims in [(2,), (2, 1)]: repeats = [*batch_dims, *[1] * input.ndim] - actual = kernel(single_input.repeat(repeats), *args, **kwargs) + actual = kernel(unbatched_input.repeat(repeats), *args, **kwargs) - expected = kernel(single_input, *args, **kwargs) + expected = kernel(unbatched_input, *args, **kwargs) # We can't directly call `.repeat()` on the output, since some kernel also return some additional metadata if isinstance(expected, torch.Tensor): expected = expected.repeat(repeats) @@ -105,7 +108,7 @@ def check_kernel( *args, check_cuda_vs_cpu=True, check_scripted_vs_eager=True, - check_batched_vs_single=True, + check_batched_vs_unbatched=True, **kwargs, ): initial_input_version = input._version @@ -127,20 +130,12 @@ def check_kernel( if check_scripted_vs_eager: _check_kernel_scripted_vs_eager(kernel, input, *args, **kwargs, **_to_tolerances(check_scripted_vs_eager)) - if check_batched_vs_single: - _check_kernel_batched_vs_single(kernel, input, *args, **kwargs, **_to_tolerances(check_batched_vs_single)) - - -def check_image_kernel_tensor_vs_pil(kernel_tensor, kernel_pil, image_tensor, *args, rtol=None, atol=None, **kwargs): - # FIXME: do we need MAE comparison here? - - actual = kernel_tensor(image_tensor, *args, **kwargs) - expected = F.to_image_tensor(kernel_pil(F.to_image_pil(image_tensor), *args, **kwargs)) - - torch.testing.assert_close(actual, expected, rtol=rtol, atol=atol) + if check_batched_vs_unbatched: + _check_kernel_batched_vs_unbatched(kernel, input, *args, **kwargs, **_to_tolerances(check_batched_vs_unbatched)) def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): + """Checks if the dispatcher can be scripted and the scripted version can be called without error.""" if not isinstance(input, datapoints.Image): return @@ -149,6 +144,9 @@ def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): + """Checks if the dispatcher correctly dispatches simple tensors to the ``*_image_tensor`` kernel and the input type + is preserved in doing so. + """ if not isinstance(input, datapoints.Image): return @@ -162,6 +160,9 @@ def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, * def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs): + """Checks if the dispatcher ultimately correctly dispatches datapoints to corresponding kernel and the input type + is preserved in doing so. For bounding boxes also checks that the format is preserved. + """ # Due to our complex dispatch architecture for datapoints, we cannot spy on the kernel directly, # but rather have to patch the `Datapoint.__F` attribute to contain the spied on kernel. spy = mock.MagicMock(wraps=kernel) @@ -180,6 +181,9 @@ def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwa def _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs): + """Checks if the dispatcher correctly dispatches PIL images to the ``*_image_pil`` kernel and the input type + is preserved in doing so. + """ if not (isinstance(input, datapoints.Image) and input.dtype is torch.uint8): return @@ -227,6 +231,7 @@ def check_dispatcher( def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): + """Checks if the signature of the dispatcher matches the kernel signature.""" dispatcher_signature = inspect.signature(dispatcher) dispatcher_params = list(dispatcher_signature.parameters.values())[1:] @@ -254,6 +259,7 @@ def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): def _check_dispatcher_datapoint_signature_match(dispatcher): + """Checks if the signature of the dispatcher matches the corresponding method signature on the Datapoint class.""" dispatcher_signature = inspect.signature(dispatcher) dispatcher_params = list(dispatcher_signature.parameters.values())[1:] @@ -276,14 +282,20 @@ def check_dispatcher_signatures_match(dispatcher, *, kernel, input_type): _check_dispatcher_datapoint_signature_match(dispatcher) -def _check_transform_v1_compatibility(transform): +def _check_transform_v1_compatibility(transform, input): + """If the transform defines the ``_v1_transform_cls`` attribute, checks if the transform has a public, static + ``get_params`` method, is scriptable, and the scripted version can be called without error.""" if not hasattr(transform, "_v1_transform_cls"): return + if type(input) is not torch.Tensor: + return + if hasattr(transform._v1_transform_cls, "get_params"): assert type(transform).get_params is transform._v1_transform_cls.get_params - _script(transform) + scripted_transform = _script(transform) + scripted_transform(input) def check_transform(transform_cls, input, *args, **kwargs): @@ -295,7 +307,7 @@ def check_transform(transform_cls, input, *args, **kwargs): if isinstance(input, datapoints.BoundingBox): assert output.format == input.format - _check_transform_v1_compatibility(transform) + _check_transform_v1_compatibility(transform, input) # We cannot use `list(transforms.InterpolationMode)` here, since it includes some PIL-only ones as well @@ -314,8 +326,8 @@ def assert_warns_antialias_default_value(): class TestResize: - SPATIAL_SIZE = (17, 11) - SIZES = [17, [17], (17,), [12, 13], (12, 13)] + INPUT_SIZE = (17, 11) + OUTPUT_SIZES = [17, [17], (17,), [12, 13], (12, 13)] def _make_max_size_kwarg(self, *, use_max_size, size): if use_max_size: @@ -331,7 +343,7 @@ def _make_max_size_kwarg(self, *, use_max_size, size): def _make_input(self, input_type, *, dtype=None, device="cpu"): if input_type in {torch.Tensor, PIL.Image.Image, datapoints.Image}: - input = make_image(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_image(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) if input_type is torch.Tensor: input = input.as_subclass(torch.Tensor) elif input_type is PIL.Image.Image: @@ -339,14 +351,14 @@ def _make_input(self, input_type, *, dtype=None, device="cpu"): elif input_type is datapoints.BoundingBox: input = make_bounding_box( format=datapoints.BoundingBoxFormat.XYXY, - spatial_size=self.SPATIAL_SIZE, + spatial_size=self.INPUT_SIZE, dtype=dtype or torch.float32, device=device, ) elif input_type is datapoints.Mask: - input = make_segmentation_mask(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_segmentation_mask(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) elif input_type is datapoints.Video: - input = make_video(size=self.SPATIAL_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_video(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) return input @@ -379,7 +391,7 @@ def _check_size(self, input, output, *, size, max_size): assert F.get_spatial_size(output) == [new_height, new_width] - @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("antialias", [True, False]) @@ -393,7 +405,7 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, if dtype.is_floating_point: check_cuda_vs_cpu_atol = 1 / 255 elif interpolation is transforms.InterpolationMode.BICUBIC: - check_cuda_vs_cpu_atol = 20 + check_cuda_vs_cpu_atol = 30 else: check_cuda_vs_cpu_atol = 1 else: @@ -411,7 +423,7 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, check_scripted_vs_eager=not isinstance(size, int), ) - @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("size", OUTPUT_SIZES) # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) @@ -448,11 +460,11 @@ def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size def test_pil_interpolation_compat_smoke(self, interpolation): F.resize_image_pil( self._make_input(PIL.Image.Image, dtype=torch.uint8, device="cpu"), - size=self.SIZES[0], + size=self.OUTPUT_SIZES[0], interpolation=interpolation, ) - @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) @@ -476,11 +488,12 @@ def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): ) def test_kernel_mask(self, dtype_and_make_mask): dtype, make_mask = dtype_and_make_mask - check_kernel(F.resize_mask, self._make_input(datapoints.Mask, dtype=dtype), size=self.SIZES[-1]) + check_kernel(F.resize_mask, self._make_input(datapoints.Mask, dtype=dtype), size=self.OUTPUT_SIZES[-1]) def test_kernel_video(self): - check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.SIZES[-1], antialias=True) + check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.OUTPUT_SIZES[-1], antialias=True) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize( "input_type_and_kernel", [ @@ -490,8 +503,7 @@ def test_kernel_video(self): (datapoints.Video, F.resize_video), ], ) - @pytest.mark.parametrize("size", SIZES) - def test_dispatcher(self, input_type_and_kernel, size): + def test_dispatcher(self, size, input_type_and_kernel): input_type, kernel = input_type_and_kernel check_dispatcher( F.resize, @@ -516,9 +528,9 @@ def test_dispatcher_signature(self, kernel, input_type): def test_dispatcher_pil_antialias_warning(self): with pytest.warns(UserWarning, match="Anti-alias option is always applied for PIL Image input"): - F.resize(self._make_input(PIL.Image.Image), size=self.SIZES[0], antialias=False) + F.resize(self._make_input(PIL.Image.Image), size=self.OUTPUT_SIZES[0], antialias=False) - @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize( "input_type_and_kernel", [ @@ -558,7 +570,7 @@ def test_antialias_warning(self, interpolation, input_type_and_kernel): if interpolation in {transforms.InterpolationMode.BILINEAR, transforms.InterpolationMode.BICUBIC} else assert_no_warnings() ): - F.resize(self._make_input(input_type), size=self.SIZES[0], interpolation=interpolation) + F.resize(self._make_input(input_type), size=self.OUTPUT_SIZES[0], interpolation=interpolation) # `InterpolationMode.NEAREST_EXACT` has no proper corresponding integer equivalent. Internally, we map it to `0` to # be the same as `InterpolationMode.NEAREST` for PIL. However, for the tensor backend there is a difference and thus @@ -575,10 +587,10 @@ def test_interpolation_int(self, interpolation, input_type_and_kernel): input_type, kernel = input_type_and_kernel input = self._make_input(input_type) - expected = F.resize(input, size=self.SIZES[0], interpolation=interpolation) + expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation) actual = F.resize( input, - size=self.SIZES[0], + size=self.OUTPUT_SIZES[0], interpolation={ transforms.InterpolationMode.NEAREST: 0, transforms.InterpolationMode.BILINEAR: 2, @@ -588,10 +600,7 @@ def test_interpolation_int(self, interpolation, input_type_and_kernel): assert_equal(actual, expected) - @pytest.mark.parametrize("size", SIZES) - @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) - @pytest.mark.parametrize("use_max_size", [True, False]) - @pytest.mark.parametrize("antialias", [True, False]) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("device", cpu_and_gpu()) @pytest.mark.parametrize( "input_type", @@ -604,26 +613,17 @@ def test_interpolation_int(self, interpolation, input_type_and_kernel): datapoints.Video, ], ) - def test_transform(self, size, interpolation, use_max_size, antialias, device, input_type): - if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): - return - - if input_type is PIL.Image.Image and antialias is False: - # antialias is always True for PIL - return - + def test_transform(self, size, device, input_type): input = self._make_input(input_type, device=device) check_transform( transforms.Resize, input, size=size, - interpolation=interpolation, - **max_size_kwarg, - antialias=antialias, + antialias=True, ) - @pytest.mark.parametrize("size", SIZES) + @pytest.mark.parametrize("size", OUTPUT_SIZES) # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) From a83f9fbc597e307345a7e824acae7bf781e79181 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Mon, 19 Jun 2023 23:55:17 +0200 Subject: [PATCH 19/39] improve bicubic cuda check --- test/test_transforms_v2_refactored.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 06f40f3d43a..07792b01591 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -401,17 +401,6 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - if device == "cuda": - if dtype.is_floating_point: - check_cuda_vs_cpu_atol = 1 / 255 - elif interpolation is transforms.InterpolationMode.BICUBIC: - check_cuda_vs_cpu_atol = 30 - else: - check_cuda_vs_cpu_atol = 1 - else: - check_cuda_vs_cpu_atol = None - check_cuda_vs_cpu = dict(rtol=0, atol=check_cuda_vs_cpu_atol) - check_kernel( F.resize_image_tensor, self._make_input(datapoints.Image, dtype=dtype, device=device), @@ -419,7 +408,9 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, - check_cuda_vs_cpu=check_cuda_vs_cpu, + check_cuda_vs_cpu=False + if interpolation is transforms.InterpolationMode.BICUBIC + else dict(rtol=0, atol=1 / 255 if dtype.is_floating_point else 1), check_scripted_vs_eager=not isinstance(size, int), ) From 3b102487ac98c964f8f6828ce1a3fa8bd8a2c23c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 00:17:24 +0200 Subject: [PATCH 20/39] properly parametrize over simple tensors and PIL images --- test/test_transforms_v2_refactored.py | 106 ++++++++++++-------------- 1 file changed, 47 insertions(+), 59 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 07792b01591..2ddde8c1a19 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -147,11 +147,11 @@ def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, * """Checks if the dispatcher correctly dispatches simple tensors to the ``*_image_tensor`` kernel and the input type is preserved in doing so. """ - if not isinstance(input, datapoints.Image): + if type(input) is not torch.Tensor: return with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: - output = dispatcher(input.as_subclass(torch.Tensor), *args, **kwargs) + output = dispatcher(input, *args, **kwargs) spy.assert_called_once() @@ -159,10 +159,28 @@ def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, * assert type(output) is torch.Tensor +def _check_dispatcher_dispatch_pil(dispatcher, kernel, input, *args, **kwargs): + """Checks if the dispatcher correctly dispatches PIL images to the ``*_image_pil`` kernel and the input type + is preserved in doing so. + """ + if not isinstance(input, PIL.Image.Image): + return + + with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: + output = dispatcher(input, *args, **kwargs) + + spy.assert_called_once() + + assert isinstance(output, PIL.Image.Image) + + def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs): """Checks if the dispatcher ultimately correctly dispatches datapoints to corresponding kernel and the input type is preserved in doing so. For bounding boxes also checks that the format is preserved. """ + if not isinstance(input, datapoints._datapoint.Datapoint): + return + # Due to our complex dispatch architecture for datapoints, we cannot spy on the kernel directly, # but rather have to patch the `Datapoint.__F` attribute to contain the spied on kernel. spy = mock.MagicMock(wraps=kernel) @@ -180,23 +198,6 @@ def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwa assert output.format == input.format -def _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs): - """Checks if the dispatcher correctly dispatches PIL images to the ``*_image_pil`` kernel and the input type - is preserved in doing so. - """ - if not (isinstance(input, datapoints.Image) and input.dtype is torch.uint8): - return - - kernel = getattr(F, f"{dispatcher.__name__}_image_pil") - - with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: - output = dispatcher(F.to_image_pil(input), *args, **kwargs) - - spy.assert_called_once() - - assert isinstance(output, PIL.Image.Image) - - def check_dispatcher( dispatcher, kernel, @@ -204,8 +205,8 @@ def check_dispatcher( *args, check_scripted_smoke=True, check_dispatch_simple_tensor=True, - check_dispatch_datapoint=True, check_dispatch_pil=True, + check_dispatch_datapoint=True, **kwargs, ): with mock.patch("torch._C._log_api_usage_once", wraps=torch._C._log_api_usage_once) as spy: @@ -223,12 +224,12 @@ def check_dispatcher( if check_dispatch_simple_tensor: _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) + if check_dispatch_pil: + _check_dispatcher_dispatch_pil(dispatcher, kernel, input, *args, **kwargs) + if check_dispatch_datapoint: _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs) - if check_dispatch_pil: - _check_dispatcher_dispatch_pil(dispatcher, input, *args, **kwargs) - def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): """Checks if the signature of the dispatcher matches the kernel signature.""" @@ -238,9 +239,10 @@ def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): kernel_signature = inspect.signature(kernel) kernel_params = list(kernel_signature.parameters.values())[1:] - # We filter out metadata that is implicitly passed to the dispatcher through the input datapoint, but has to be - # explicit passed to the kernel. - kernel_params = [param for param in kernel_params if param.name not in input_type.__annotations__.keys()] + if issubclass(input_type, datapoints._datapoint.Datapoint): + # We filter out metadata that is implicitly passed to the dispatcher through the input datapoint, but has to be + # explicitly passed to the kernel. + kernel_params = [param for param in kernel_params if param.name not in input_type.__annotations__.keys()] dispatcher_params = iter(dispatcher_params) for dispatcher_param, kernel_param in zip(dispatcher_params, kernel_params): @@ -255,6 +257,11 @@ def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): f"has no corresponding parameter on the dispatcher `{dispatcher.__name__}`." ) from None + if issubclass(input_type, PIL.Image.Image): + # PIL kernels often have more correct annotations, since they are not limited by JIT. Thus, we don't check + # them in the first place. + dispatcher_param._annotation = kernel_param._annotation = inspect.Parameter.empty + assert dispatcher_param == kernel_param @@ -488,6 +495,8 @@ def test_kernel_video(self): @pytest.mark.parametrize( "input_type_and_kernel", [ + (torch.Tensor, F.resize_image_tensor), + (PIL.Image.Image, F.resize_image_pil), (datapoints.Image, F.resize_image_tensor), (datapoints.BoundingBox, F.resize_bounding_box), (datapoints.Mask, F.resize_mask), @@ -508,6 +517,8 @@ def test_dispatcher(self, size, input_type_and_kernel): @pytest.mark.parametrize( ("input_type", "kernel"), [ + (torch.Tensor, F.resize_image_tensor), + (PIL.Image.Image, F.resize_image_pil), (datapoints.Image, F.resize_image_tensor), (datapoints.BoundingBox, F.resize_bounding_box), (datapoints.Mask, F.resize_mask), @@ -523,17 +534,10 @@ def test_dispatcher_pil_antialias_warning(self): @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize( - "input_type_and_kernel", - [ - (datapoints.Image, F.resize_image_tensor), - (datapoints.BoundingBox, F.resize_bounding_box), - (datapoints.Mask, F.resize_mask), - (datapoints.Video, F.resize_video), - ], + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], ) - def test_max_size_error(self, size, input_type_and_kernel): - input_type, kernel = input_type_and_kernel - + def test_max_size_error(self, size, input_type): if isinstance(size, int) or len(size) == 1: max_size = (size if isinstance(size, int) else size[0]) - 1 match = "must be strictly greater than the requested size" @@ -547,15 +551,10 @@ def test_max_size_error(self, size, input_type_and_kernel): @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize( - "input_type_and_kernel", - [ - (datapoints.Image, F.resize_image_tensor), - (datapoints.Video, F.resize_video), - ], + "input_type", + [torch.Tensor, datapoints.Image, datapoints.Video], ) - def test_antialias_warning(self, interpolation, input_type_and_kernel): - input_type, kernel = input_type_and_kernel - + def test_antialias_warning(self, interpolation, input_type): with ( assert_warns_antialias_default_value() if interpolation in {transforms.InterpolationMode.BILINEAR, transforms.InterpolationMode.BICUBIC} @@ -568,14 +567,10 @@ def test_antialias_warning(self, interpolation, input_type_and_kernel): # we don't test it here. @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST_EXACT}) @pytest.mark.parametrize( - "input_type_and_kernel", - [ - (datapoints.Image, F.resize_image_tensor), - (datapoints.Video, F.resize_video), - ], + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.Video], ) - def test_interpolation_int(self, interpolation, input_type_and_kernel): - input_type, kernel = input_type_and_kernel + def test_interpolation_int(self, interpolation, input_type): input = self._make_input(input_type) expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation) @@ -595,14 +590,7 @@ def test_interpolation_int(self, interpolation, input_type_and_kernel): @pytest.mark.parametrize("device", cpu_and_gpu()) @pytest.mark.parametrize( "input_type", - [ - torch.Tensor, - PIL.Image.Image, - datapoints.Image, - datapoints.BoundingBox, - datapoints.Mask, - datapoints.Video, - ], + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], ) def test_transform(self, size, device, input_type): input = self._make_input(input_type, device=device) From 50da56101ff551b9b71e2a2ef76ecca780698d7f Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 00:20:20 +0200 Subject: [PATCH 21/39] add bicubic cuda comment --- test/test_transforms_v2_refactored.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 2ddde8c1a19..3dfb2fcf257 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -415,6 +415,8 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, + # The `InterpolationMode.BICUBIO` implementation on CUDA does not match PILs implementation well. Thus, + # instead of testing with an enormous tolerance, we disable the check all together. check_cuda_vs_cpu=False if interpolation is transforms.InterpolationMode.BICUBIC else dict(rtol=0, atol=1 / 255 if dtype.is_floating_point else 1), From b25cef811fe2cd35dd05b052568d987763e6de02 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 08:55:14 +0200 Subject: [PATCH 22/39] reorder tests --- test/test_transforms_v2_refactored.py | 164 +++++++++++++------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 3dfb2fcf257..704e2d0806b 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -415,7 +415,7 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, - # The `InterpolationMode.BICUBIO` implementation on CUDA does not match PILs implementation well. Thus, + # The `InterpolationMode.BICUBIC` implementation on CUDA does not match PILs implementation well. Thus, # instead of testing with an enormous tolerance, we disable the check all together. check_cuda_vs_cpu=False if interpolation is transforms.InterpolationMode.BICUBIC @@ -423,47 +423,6 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, check_scripted_vs_eager=not isinstance(size, int), ) - @pytest.mark.parametrize("size", OUTPUT_SIZES) - # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. - # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` - @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) - @pytest.mark.parametrize("use_max_size", [True, False]) - def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size): - if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): - return - - image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") - - actual = F.resize_image_tensor( - image, - size=size, - interpolation=interpolation, - **max_size_kwarg, - # antialias is always True for PIL - antialias=True, - ) - expected = F.to_image_tensor( - F.resize_image_pil( - F.to_image_pil(image), - size=size, - interpolation=interpolation, - **max_size_kwarg, - ) - ) - - self._check_size(image, actual, size=size, **max_size_kwarg) - - mae = (actual.float() - expected.float()).abs().mean() - assert mae < 1 - - @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) - def test_pil_interpolation_compat_smoke(self, interpolation): - F.resize_image_pil( - self._make_input(PIL.Image.Image, dtype=torch.uint8, device="cpu"), - size=self.OUTPUT_SIZES[0], - interpolation=interpolation, - ) - @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) @pytest.mark.parametrize("use_max_size", [True, False]) @@ -488,7 +447,7 @@ def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): ) def test_kernel_mask(self, dtype_and_make_mask): dtype, make_mask = dtype_and_make_mask - check_kernel(F.resize_mask, self._make_input(datapoints.Mask, dtype=dtype), size=self.OUTPUT_SIZES[-1]) + check_kernel(F.resize_mask, make_mask(dtype=dtype), size=self.OUTPUT_SIZES[-1]) def test_kernel_video(self): check_kernel(F.resize_video, self._make_input(datapoints.Video), size=self.OUTPUT_SIZES[-1], antialias=True) @@ -530,6 +489,86 @@ def test_dispatcher(self, size, input_type_and_kernel): def test_dispatcher_signature(self, kernel, input_type): check_dispatcher_signatures_match(F.resize, kernel=kernel, input_type=input_type) + @pytest.mark.parametrize("size", OUTPUT_SIZES) + @pytest.mark.parametrize("device", cpu_and_gpu()) + @pytest.mark.parametrize( + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], + ) + def test_transform(self, size, device, input_type): + input = self._make_input(input_type, device=device) + + check_transform( + transforms.Resize, + input, + size=size, + antialias=True, + ) + + @pytest.mark.parametrize("size", OUTPUT_SIZES) + # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. + # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` + @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) + @pytest.mark.parametrize("use_max_size", [True, False]) + def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") + + actual = F.resize_image_tensor( + image, + size=size, + interpolation=interpolation, + **max_size_kwarg, + # antialias is always True for PIL + antialias=True, + ) + expected = F.to_image_tensor( + F.resize_image_pil( + F.to_image_pil(image), + size=size, + interpolation=interpolation, + **max_size_kwarg, + ) + ) + + self._check_size(image, actual, size=size, **max_size_kwarg) + + mae = (actual.float() - expected.float()).abs().mean() + assert mae < 1 + + @pytest.mark.parametrize("size", OUTPUT_SIZES) + # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. + # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` + @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) + @pytest.mark.parametrize("use_max_size", [True, False]) + def test_transform_image_correctness(self, size, interpolation, use_max_size): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + transform = transforms.Resize(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) + + image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") + + actual = transform(image) + expected = F.to_image_tensor( + F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) + ) + + self._check_size(image, actual, size=size, **max_size_kwarg) + + mae = (actual.float() - expected.float()).abs().mean() + assert mae < 1 + + @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) + def test_pil_interpolation_compat_smoke(self, interpolation): + F.resize_image_pil( + self._make_input(PIL.Image.Image, dtype=torch.uint8, device="cpu"), + size=self.OUTPUT_SIZES[0], + interpolation=interpolation, + ) + def test_dispatcher_pil_antialias_warning(self): with pytest.warns(UserWarning, match="Anti-alias option is always applied for PIL Image input"): F.resize(self._make_input(PIL.Image.Image), size=self.OUTPUT_SIZES[0], antialias=False) @@ -588,45 +627,6 @@ def test_interpolation_int(self, interpolation, input_type): assert_equal(actual, expected) - @pytest.mark.parametrize("size", OUTPUT_SIZES) - @pytest.mark.parametrize("device", cpu_and_gpu()) - @pytest.mark.parametrize( - "input_type", - [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], - ) - def test_transform(self, size, device, input_type): - input = self._make_input(input_type, device=device) - - check_transform( - transforms.Resize, - input, - size=size, - antialias=True, - ) - - @pytest.mark.parametrize("size", OUTPUT_SIZES) - # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. - # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` - @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) - @pytest.mark.parametrize("use_max_size", [True, False]) - def test_transform_image_correctness(self, size, interpolation, use_max_size): - if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): - return - - transform = transforms.Resize(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) - - image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") - - actual = transform(image) - expected = F.to_image_tensor( - F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) - ) - - self._check_size(image, actual, size=size, **max_size_kwarg) - - mae = (actual.float() - expected.float()).abs().mean() - assert mae < 1 - def test_transform_unknown_size_error(self): with pytest.raises(ValueError, match="size can either be an integer or a list or tuple of one or two integers"): transforms.Resize(size=object()) From 026e295156d3010fef454d1baa652210f78812e6 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 08:57:41 +0200 Subject: [PATCH 23/39] fix int interpolation test --- test/test_transforms_v2_refactored.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 704e2d0806b..c52c8f583ff 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -603,15 +603,18 @@ def test_antialias_warning(self, interpolation, input_type): ): F.resize(self._make_input(input_type), size=self.OUTPUT_SIZES[0], interpolation=interpolation) - # `InterpolationMode.NEAREST_EXACT` has no proper corresponding integer equivalent. Internally, we map it to `0` to - # be the same as `InterpolationMode.NEAREST` for PIL. However, for the tensor backend there is a difference and thus - # we don't test it here. - @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST_EXACT}) + @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @pytest.mark.parametrize( "input_type", [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.Video], ) def test_interpolation_int(self, interpolation, input_type): + # `InterpolationMode.NEAREST_EXACT` has no proper corresponding integer equivalent. Internally, we map it to + # `0` to be the same as `InterpolationMode.NEAREST` for PIL. However, for the tensor backend there is a + # difference and thus we don't test it here. + if issubclass(input_type, torch.Tensor) and interpolation is transforms.InterpolationMode.NEAREST_EXACT: + return + input = self._make_input(input_type) expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation) @@ -622,6 +625,7 @@ def test_interpolation_int(self, interpolation, input_type): transforms.InterpolationMode.NEAREST: 0, transforms.InterpolationMode.BILINEAR: 2, transforms.InterpolationMode.BICUBIC: 3, + transforms.InterpolationMode.NEAREST_EXACT: 0, }[interpolation], ) From 67705961d252317f0896f698a31d90b6fe11d0d3 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 09:32:43 +0200 Subject: [PATCH 24/39] fix warnings --- test/common_utils.py | 12 ++++++++++++ test/test_transforms_v2_refactored.py | 13 +++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/test/common_utils.py b/test/common_utils.py index d29e69832af..69cca6eb239 100644 --- a/test/common_utils.py +++ b/test/common_utils.py @@ -7,6 +7,7 @@ import os import pathlib import random +import re import shutil import sys import tempfile @@ -890,3 +891,14 @@ def assert_no_warnings(): with warnings.catch_warnings(): warnings.simplefilter("error") yield + + +@contextlib.contextmanager +def ignore_jit_no_profile_information_warning(): + # Calling a scripted object often triggers a warning like + # `UserWarning: operator() profile_node %$INT1 : int[] = prim::profile_ivalue($INT2) does not have profile information` + # with varying `INT1` and `INT2`. Since these are uninteresting for us and only clutter the test summary, we ignore + # them. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=re.escape("operator() profile_node %"), category=UserWarning) + yield diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index c52c8f583ff..d9314e35b84 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -14,6 +14,7 @@ assert_no_warnings, cache, cpu_and_gpu, + ignore_jit_no_profile_information_warning, make_bounding_box, make_detection_mask, make_image, @@ -64,7 +65,8 @@ def _check_kernel_scripted_vs_eager(kernel, input, *args, rtol, atol, **kwargs): kernel_scripted = _script(kernel) input = input.as_subclass(torch.Tensor) - actual = kernel_scripted(input, *args, **kwargs) + with ignore_jit_no_profile_information_warning(): + actual = kernel_scripted(input, *args, **kwargs) expected = kernel(input, *args, **kwargs) assert_close(actual, expected, rtol=rtol, atol=atol) @@ -140,7 +142,8 @@ def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): return dispatcher_scripted = _script(dispatcher) - dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) + with ignore_jit_no_profile_information_warning(): + dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): @@ -302,7 +305,8 @@ def _check_transform_v1_compatibility(transform, input): assert type(transform).get_params is transform._v1_transform_cls.get_params scripted_transform = _script(transform) - scripted_transform(input) + with ignore_jit_no_profile_information_warning(): + scripted_transform(input) def check_transform(transform_cls, input, *args, **kwargs): @@ -617,7 +621,7 @@ def test_interpolation_int(self, interpolation, input_type): input = self._make_input(input_type) - expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation) + expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation, antialias=True) actual = F.resize( input, size=self.OUTPUT_SIZES[0], @@ -627,6 +631,7 @@ def test_interpolation_int(self, interpolation, input_type): transforms.InterpolationMode.BICUBIC: 3, transforms.InterpolationMode.NEAREST_EXACT: 0, }[interpolation], + antialias=True, ) assert_equal(actual, expected) From d77baec79a82d6f7e3cfde8f958d146fb6691119 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 10:14:04 +0200 Subject: [PATCH 25/39] add noop test --- test/test_transforms_v2_refactored.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index d9314e35b84..8e9e0034105 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -639,3 +639,25 @@ def test_interpolation_int(self, interpolation, input_type): def test_transform_unknown_size_error(self): with pytest.raises(ValueError, match="size can either be an integer or a list or tuple of one or two integers"): transforms.Resize(size=object()) + + @pytest.mark.parametrize( + "size", [min(INPUT_SIZE), [min(INPUT_SIZE)], (min(INPUT_SIZE),), list(INPUT_SIZE), tuple(INPUT_SIZE)] + ) + @pytest.mark.parametrize( + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], + ) + def test_noop(self, size, input_type): + input = self._make_input(input_type) + + output = F.resize(input, size=size) + + if isinstance(input, datapoints._datapoint.Datapoint): + # We can't test identity directly, since that checks for the identity of the Python object. Since all + # datapoints unwrap before a kernel and wrap again afterwards, the Python object changes. Thus, we just + # check for equality + assert_equal(output, input) + else: + # This identity check is not a requirement. It is here to avoid breaking the behavior by accident. If there + # is a good reason to break this, feel free to downgrade to a equality test as done above. + assert output is input From ad7437d042ec9b2b0b8cb54c7d396dc4f77b73e2 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 10:23:52 +0200 Subject: [PATCH 26/39] add regression test --- test/test_transforms_v2_refactored.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 8e9e0034105..94e0a6948a1 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -661,3 +661,19 @@ def test_noop(self, size, input_type): # This identity check is not a requirement. It is here to avoid breaking the behavior by accident. If there # is a good reason to break this, feel free to downgrade to a equality test as done above. assert output is input + + @pytest.mark.parametrize( + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], + ) + def test_no_regression_5405(self, input_type): + # Checks that `max_size` is not ignored if `size == small_edge_size` + # See https://github.com/pytorch/vision/issues/5405 + + input = self._make_input(input_type) + + size = min(F.get_spatial_size(input)) + max_size = size + 1 + output = F.resize(input, size=size, max_size=max_size) + + assert max(F.get_spatial_size(output)) == max_size From 1ad491dd9f7aee0c3ccc74ca62a2caf28dcee19c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 10:29:26 +0200 Subject: [PATCH 27/39] fix warnings --- test/test_transforms_v2_refactored.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 94e0a6948a1..2bde6e2ec64 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -650,7 +650,7 @@ def test_transform_unknown_size_error(self): def test_noop(self, size, input_type): input = self._make_input(input_type) - output = F.resize(input, size=size) + output = F.resize(input, size=size, antialias=True) if isinstance(input, datapoints._datapoint.Datapoint): # We can't test identity directly, since that checks for the identity of the Python object. Since all @@ -674,6 +674,6 @@ def test_no_regression_5405(self, input_type): size = min(F.get_spatial_size(input)) max_size = size + 1 - output = F.resize(input, size=size, max_size=max_size) + output = F.resize(input, size=size, max_size=max_size, antialias=True) assert max(F.get_spatial_size(output)) == max_size From 6ffeeb45969285d76f66c103f3f213c2293c0be7 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 22:55:34 +0200 Subject: [PATCH 28/39] improve pil compat interpolation test --- test/test_transforms_v2_refactored.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 2bde6e2ec64..fbd3b3b5e00 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -566,12 +566,24 @@ def test_transform_image_correctness(self, size, interpolation, use_max_size): assert mae < 1 @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) - def test_pil_interpolation_compat_smoke(self, interpolation): - F.resize_image_pil( - self._make_input(PIL.Image.Image, dtype=torch.uint8, device="cpu"), - size=self.OUTPUT_SIZES[0], - interpolation=interpolation, - ) + @pytest.mark.parametrize( + "input_type", + [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.Video], + ) + def test_pil_interpolation_compat_smoke(self, interpolation, input_type): + input = self._make_input(input_type) + + with ( + contextlib.nullcontext() + if isinstance(input, PIL.Image.Image) + # This error is triggered in PyTorch core + else pytest.raises(NotImplementedError, match=f"got {interpolation.value.lower()}") + ): + F.resize( + input, + size=self.OUTPUT_SIZES[0], + interpolation=interpolation, + ) def test_dispatcher_pil_antialias_warning(self): with pytest.warns(UserWarning, match="Anti-alias option is always applied for PIL Image input"): From 60c302357bb4785a51f8b8a107a22b0dd31c1ff4 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 22:59:56 +0200 Subject: [PATCH 29/39] fix format not being used in bbox test --- test/test_transforms_v2_refactored.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index fbd3b3b5e00..c79745e4cf2 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -352,24 +352,25 @@ def _make_max_size_kwarg(self, *, use_max_size, size): return dict(max_size=max_size) - def _make_input(self, input_type, *, dtype=None, device="cpu"): + def _make_input(self, input_type, *, dtype=None, device="cpu", **kwargs): if input_type in {torch.Tensor, PIL.Image.Image, datapoints.Image}: - input = make_image(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_image(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device, **kwargs) if input_type is torch.Tensor: input = input.as_subclass(torch.Tensor) elif input_type is PIL.Image.Image: input = F.to_image_pil(input) elif input_type is datapoints.BoundingBox: + kwargs.setdefault("format", datapoints.BoundingBoxFormat.XYXY) input = make_bounding_box( - format=datapoints.BoundingBoxFormat.XYXY, spatial_size=self.INPUT_SIZE, dtype=dtype or torch.float32, device=device, + **kwargs, ) elif input_type is datapoints.Mask: - input = make_segmentation_mask(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_segmentation_mask(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device, **kwargs) elif input_type is datapoints.Video: - input = make_video(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device) + input = make_video(size=self.INPUT_SIZE, dtype=dtype or torch.uint8, device=device, **kwargs) return input @@ -436,7 +437,7 @@ def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - bounding_box = self._make_input(datapoints.BoundingBox, dtype=dtype, device=device) + bounding_box = self._make_input(datapoints.BoundingBox, dtype=dtype, device=device, format=format) check_kernel( F.resize_bounding_box, bounding_box, From dc5b4f1f8c8656d99927fd0ab5ba7249bd8590af Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 23:02:10 +0200 Subject: [PATCH 30/39] use pre-defined PIL interpolation mode mapping --- test/test_transforms_v2_refactored.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index c79745e4cf2..27636acdcd9 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -23,6 +23,7 @@ ) from torch.testing import assert_close from torchvision import datapoints +from torchvision.transforms.functional import pil_modes_mapping from torchvision.transforms.v2 import functional as F @@ -636,15 +637,7 @@ def test_interpolation_int(self, interpolation, input_type): expected = F.resize(input, size=self.OUTPUT_SIZES[0], interpolation=interpolation, antialias=True) actual = F.resize( - input, - size=self.OUTPUT_SIZES[0], - interpolation={ - transforms.InterpolationMode.NEAREST: 0, - transforms.InterpolationMode.BILINEAR: 2, - transforms.InterpolationMode.BICUBIC: 3, - transforms.InterpolationMode.NEAREST_EXACT: 0, - }[interpolation], - antialias=True, + input, size=self.OUTPUT_SIZES[0], interpolation=pil_modes_mapping[interpolation], antialias=True ) assert_equal(actual, expected) From 23b1599c0db9e336790d6ec296cf319372e6080b Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 23:04:08 +0200 Subject: [PATCH 31/39] improve noop test --- test/test_transforms_v2_refactored.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 27636acdcd9..e60f2e28bfd 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -658,14 +658,14 @@ def test_noop(self, size, input_type): output = F.resize(input, size=size, antialias=True) + # This identity check is not a requirement. It is here to avoid breaking the behavior by accident. If there + # is a good reason to break this, feel free to downgrade to an equality check. if isinstance(input, datapoints._datapoint.Datapoint): # We can't test identity directly, since that checks for the identity of the Python object. Since all - # datapoints unwrap before a kernel and wrap again afterwards, the Python object changes. Thus, we just - # check for equality - assert_equal(output, input) + # datapoints unwrap before a kernel and wrap again afterwards, the Python object changes. Thus, we check + # that the underlying storage is the same + assert output.data_ptr() == input.data_ptr() else: - # This identity check is not a requirement. It is here to avoid breaking the behavior by accident. If there - # is a good reason to break this, feel free to downgrade to a equality test as done above. assert output is input @pytest.mark.parametrize( From e66bf4f6be4b54cadccb17369e94140a92f0e3c2 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 23:13:19 +0200 Subject: [PATCH 32/39] unify dispatcher checks --- test/test_transforms_v2_refactored.py | 75 +++++++-------------------- 1 file changed, 20 insertions(+), 55 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index e60f2e28bfd..e967ca7adf2 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -147,55 +147,28 @@ def _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs): dispatcher_scripted(input.as_subclass(torch.Tensor), *args, **kwargs) -def _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs): - """Checks if the dispatcher correctly dispatches simple tensors to the ``*_image_tensor`` kernel and the input type - is preserved in doing so. +def _check_dispatcher_dispatch(dispatcher, kernel, input, *args, **kwargs): + """Checks if the dispatcher correctly dispatches the input to the corresponding kernel and that the input type is + preserved in doing so. For bounding boxes also checks that the format is preserved. """ - if type(input) is not torch.Tensor: - return - - with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: - output = dispatcher(input, *args, **kwargs) - - spy.assert_called_once() - - # We cannot use `isinstance` here since all datapoints are instances of `torch.Tensor` as well - assert type(output) is torch.Tensor - - -def _check_dispatcher_dispatch_pil(dispatcher, kernel, input, *args, **kwargs): - """Checks if the dispatcher correctly dispatches PIL images to the ``*_image_pil`` kernel and the input type - is preserved in doing so. - """ - if not isinstance(input, PIL.Image.Image): - return - - with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: - output = dispatcher(input, *args, **kwargs) + if isinstance(input, datapoints._datapoint.Datapoint): + # Due to our complex dispatch architecture for datapoints, we cannot spy on the kernel directly, + # but rather have to patch the `Datapoint.__F` attribute to contain the spied on kernel. + spy = mock.MagicMock(wraps=kernel) + with mock.patch.object(F, kernel.__name__, spy): + # Due to Python's name mangling, the `Datapoint.__F` attribute is only accessible from inside the class. + # Since that is not the case here, we need to prefix f"_{cls.__name__}" + # See https://docs.python.org/3/tutorial/classes.html#private-variables for details + with mock.patch.object(datapoints._datapoint.Datapoint, "_Datapoint__F", new=F): + output = dispatcher(input, *args, **kwargs) spy.assert_called_once() - - assert isinstance(output, PIL.Image.Image) - - -def _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs): - """Checks if the dispatcher ultimately correctly dispatches datapoints to corresponding kernel and the input type - is preserved in doing so. For bounding boxes also checks that the format is preserved. - """ - if not isinstance(input, datapoints._datapoint.Datapoint): - return - - # Due to our complex dispatch architecture for datapoints, we cannot spy on the kernel directly, - # but rather have to patch the `Datapoint.__F` attribute to contain the spied on kernel. - spy = mock.MagicMock(wraps=kernel) - with mock.patch.object(F, kernel.__name__, spy): - # Due to Python's name mangling, the `Datapoint.__F` attribute is only accessible from inside the class. - # Since that is not the case here, we need to prefix f"_{cls.__name__}" - # See https://docs.python.org/3/tutorial/classes.html#private-variables for details - with mock.patch.object(datapoints._datapoint.Datapoint, "_Datapoint__F", new=F): + else: + with mock.patch(f"{dispatcher.__module__}.{kernel.__name__}", wraps=kernel) as spy: output = dispatcher(input, *args, **kwargs) - spy.assert_called_once() + spy.assert_called_once() + assert isinstance(output, type(input)) if isinstance(input, datapoints.BoundingBox): @@ -208,9 +181,7 @@ def check_dispatcher( input, *args, check_scripted_smoke=True, - check_dispatch_simple_tensor=True, - check_dispatch_pil=True, - check_dispatch_datapoint=True, + check_dispatch=True, **kwargs, ): with mock.patch("torch._C._log_api_usage_once", wraps=torch._C._log_api_usage_once) as spy: @@ -225,14 +196,8 @@ def check_dispatcher( if check_scripted_smoke: _check_dispatcher_scripted_smoke(dispatcher, input, *args, **kwargs) - if check_dispatch_simple_tensor: - _check_dispatcher_dispatch_simple_tensor(dispatcher, kernel, input, *args, **kwargs) - - if check_dispatch_pil: - _check_dispatcher_dispatch_pil(dispatcher, kernel, input, *args, **kwargs) - - if check_dispatch_datapoint: - _check_dispatcher_dispatch_datapoint(dispatcher, kernel, input, *args, **kwargs) + if check_dispatch: + _check_dispatcher_dispatch(dispatcher, kernel, input, *args, **kwargs) def _check_dispatcher_kernel_signature_match(dispatcher, *, kernel, input_type): From 624b9255720c4c300e7b7185c69e208c4defdb65 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 23:22:15 +0200 Subject: [PATCH 33/39] dont use MAE for correctness checks --- test/test_transforms_v2_refactored.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index e967ca7adf2..fd6ca4a5f88 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -506,8 +506,7 @@ def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size self._check_size(image, actual, size=size, **max_size_kwarg) - mae = (actual.float() - expected.float()).abs().mean() - assert mae < 1 + torch.testing.assert_close(actual, expected, atol=1, rtol=0) @pytest.mark.parametrize("size", OUTPUT_SIZES) # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. @@ -529,8 +528,7 @@ def test_transform_image_correctness(self, size, interpolation, use_max_size): self._check_size(image, actual, size=size, **max_size_kwarg) - mae = (actual.float() - expected.float()).abs().mean() - assert mae < 1 + torch.testing.assert_close(actual, expected, atol=1, rtol=0) @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) @pytest.mark.parametrize( From ad06894dc4f757e4df8fe059379ed9911e851cfb Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 20 Jun 2023 23:29:36 +0200 Subject: [PATCH 34/39] unify image correctness tests --- test/test_transforms_v2_refactored.py | 47 +++++++-------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index fd6ca4a5f88..e558452b5b0 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -1,4 +1,5 @@ import contextlib +import functools import inspect import re from typing import get_type_hints @@ -287,6 +288,13 @@ def check_transform(transform_cls, input, *args, **kwargs): _check_transform_v1_compatibility(transform, input) +def make_parametrizable(fn): + def wrapper(*args, **kwargs): + return functools.partial(fn, *args, **kwargs) + + return wrapper + + # We cannot use `list(transforms.InterpolationMode)` here, since it includes some PIL-only ones as well INTERPOLATION_MODES = [ transforms.InterpolationMode.NEAREST, @@ -481,47 +489,16 @@ def test_transform(self, size, device, input_type): # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) @pytest.mark.parametrize("use_max_size", [True, False]) - def test_kernel_image_tensor_correctness(self, size, interpolation, use_max_size): - if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): - return - - image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") - - actual = F.resize_image_tensor( - image, - size=size, - interpolation=interpolation, - **max_size_kwarg, - # antialias is always True for PIL - antialias=True, - ) - expected = F.to_image_tensor( - F.resize_image_pil( - F.to_image_pil(image), - size=size, - interpolation=interpolation, - **max_size_kwarg, - ) - ) - - self._check_size(image, actual, size=size, **max_size_kwarg) - - torch.testing.assert_close(actual, expected, atol=1, rtol=0) - - @pytest.mark.parametrize("size", OUTPUT_SIZES) - # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. - # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` - @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) - @pytest.mark.parametrize("use_max_size", [True, False]) - def test_transform_image_correctness(self, size, interpolation, use_max_size): + @pytest.mark.parametrize("make_fn", [make_parametrizable(F.resize_image_tensor), transforms.Resize]) + def test_image_correctness(self, size, interpolation, use_max_size, make_fn): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - transform = transforms.Resize(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) + fn = make_fn(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") - actual = transform(image) + actual = fn(image) expected = F.to_image_tensor( F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) ) From 03667b05327122cc214ca5a4f7a1dbae1017f895 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 21 Jun 2023 13:39:28 +0200 Subject: [PATCH 35/39] Update test/test_transforms_v2_refactored.py Co-authored-by: Nicolas Hug --- test/test_transforms_v2_refactored.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index e558452b5b0..f547fba6204 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -244,8 +244,8 @@ def _check_dispatcher_datapoint_signature_match(dispatcher): datapoint_signature = inspect.signature(datapoint_method) datapoint_params = list(datapoint_signature.parameters.values())[1:] - # Because we use `from __future__ import annotations` inside the module where `datapoints._datapoint` is - # defined, the annotations are stored as strings. This makes them concrete again, so they can be compared to the + # Some annotations in the `datapoints._datapoint` module + # are stored as strings. The block below makes them concrete again (non-strings), so they can be compared to the # natively concrete dispatcher annotations. datapoint_annotations = get_type_hints(datapoint_method) for param in datapoint_params: From 4deb952ea906f34047885283f3870c97e8bb0cf9 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 21 Jun 2023 13:48:05 +0200 Subject: [PATCH 36/39] reinstate high bicubic tolerance --- test/test_transforms_v2_refactored.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index e558452b5b0..bf3f736f969 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -387,6 +387,9 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return + uint8_atol = 30 if transforms.InterpolationMode.BICUBIC else 1 + check_cuda_vs_cpu_tolerances = dict(rtol=0, atol=uint8_atol / 255 if dtype.is_floating_point else uint8_atol) + check_kernel( F.resize_image_tensor, self._make_input(datapoints.Image, dtype=dtype, device=device), @@ -394,11 +397,9 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, - # The `InterpolationMode.BICUBIC` implementation on CUDA does not match PILs implementation well. Thus, - # instead of testing with an enormous tolerance, we disable the check all together. - check_cuda_vs_cpu=False - if interpolation is transforms.InterpolationMode.BICUBIC - else dict(rtol=0, atol=1 / 255 if dtype.is_floating_point else 1), + # The `InterpolationMode.BICUBIC` implementation on CUDA does not match CPU implementation well. Thus, + # wee need to test with an enormous tolerance. + check_cuda_vs_cpu=check_cuda_vs_cpu_tolerances, check_scripted_vs_eager=not isinstance(size, int), ) From ff4c0ea63eeff52e1d159d39bd08ca9bf897cb75 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 21 Jun 2023 13:55:20 +0200 Subject: [PATCH 37/39] fix --- test/test_transforms_v2_refactored.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index fc47c89c7bd..7c4d5cf9b99 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -14,7 +14,7 @@ assert_equal, assert_no_warnings, cache, - cpu_and_gpu, + cpu_and_cuda, ignore_jit_no_profile_information_warning, make_bounding_box, make_detection_mask, @@ -382,13 +382,15 @@ def _check_size(self, input, output, *, size, max_size): @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("antialias", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.uint8]) - @pytest.mark.parametrize("device", cpu_and_gpu()) + @pytest.mark.parametrize("device", cpu_and_cuda()) def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, dtype, device): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - uint8_atol = 30 if transforms.InterpolationMode.BICUBIC else 1 - check_cuda_vs_cpu_tolerances = dict(rtol=0, atol=uint8_atol / 255 if dtype.is_floating_point else uint8_atol) + # In contrast to CPU, there is no native `InterpolationMode.BICUBIC` implementation for uint8 images on CUDA. + # Internally, it uses the float path. Thus, we need to test with an enormous tolerance here to account for that. + atol = 30 if transforms.InterpolationMode.BICUBIC and dtype is torch.uint8 else 1 + check_cuda_vs_cpu_tolerances = dict(rtol=0, atol=atol / 255 if dtype.is_floating_point else atol) check_kernel( F.resize_image_tensor, @@ -397,8 +399,6 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, interpolation=interpolation, **max_size_kwarg, antialias=antialias, - # The `InterpolationMode.BICUBIC` implementation on CUDA does not match CPU implementation well. Thus, - # wee need to test with an enormous tolerance. check_cuda_vs_cpu=check_cuda_vs_cpu_tolerances, check_scripted_vs_eager=not isinstance(size, int), ) @@ -407,7 +407,7 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) - @pytest.mark.parametrize("device", cpu_and_gpu()) + @pytest.mark.parametrize("device", cpu_and_cuda()) def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return @@ -470,7 +470,7 @@ def test_dispatcher_signature(self, kernel, input_type): check_dispatcher_signatures_match(F.resize, kernel=kernel, input_type=input_type) @pytest.mark.parametrize("size", OUTPUT_SIZES) - @pytest.mark.parametrize("device", cpu_and_gpu()) + @pytest.mark.parametrize("device", cpu_and_cuda()) @pytest.mark.parametrize( "input_type", [torch.Tensor, PIL.Image.Image, datapoints.Image, datapoints.BoundingBox, datapoints.Mask, datapoints.Video], From d5358b97af7d43c2a65d6f2a19a5b66599bfc1f0 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 21 Jun 2023 14:05:52 +0200 Subject: [PATCH 38/39] remove most old v2 resize tests --- test/test_transforms_v2.py | 24 ------ test/transforms_v2_dispatcher_infos.py | 13 --- test/transforms_v2_kernel_infos.py | 108 +------------------------ 3 files changed, 1 insertion(+), 144 deletions(-) diff --git a/test/test_transforms_v2.py b/test/test_transforms_v2.py index 71df9ad72d8..935d25edd6d 100644 --- a/test/test_transforms_v2.py +++ b/test/test_transforms_v2.py @@ -1711,8 +1711,6 @@ def test_antialias_warning(): tensor_video = torch.randint(0, 256, size=(2, 3, 10, 10), dtype=torch.uint8) match = "The default value of the antialias parameter" - with pytest.warns(UserWarning, match=match): - transforms.Resize((20, 20))(tensor_img) with pytest.warns(UserWarning, match=match): transforms.RandomResizedCrop((20, 20))(tensor_img) with pytest.warns(UserWarning, match=match): @@ -1722,18 +1720,6 @@ def test_antialias_warning(): with pytest.warns(UserWarning, match=match): transforms.RandomResize(10, 20)(tensor_img) - with pytest.warns(UserWarning, match=match): - transforms.functional.resize(tensor_img, (20, 20)) - with pytest.warns(UserWarning, match=match): - transforms.functional.resize_image_tensor(tensor_img, (20, 20)) - - with pytest.warns(UserWarning, match=match): - transforms.functional.resize(tensor_video, (20, 20)) - with pytest.warns(UserWarning, match=match): - transforms.functional.resize_video(tensor_video, (20, 20)) - - with pytest.warns(UserWarning, match=match): - datapoints.Image(tensor_img).resize((20, 20)) with pytest.warns(UserWarning, match=match): datapoints.Image(tensor_img).resized_crop(0, 0, 10, 10, (20, 20)) @@ -1744,27 +1730,17 @@ def test_antialias_warning(): with warnings.catch_warnings(): warnings.simplefilter("error") - transforms.Resize((20, 20))(pil_img) transforms.RandomResizedCrop((20, 20))(pil_img) transforms.ScaleJitter((20, 20))(pil_img) transforms.RandomShortestSize((20, 20))(pil_img) transforms.RandomResize(10, 20)(pil_img) - transforms.functional.resize(pil_img, (20, 20)) - transforms.Resize((20, 20), antialias=True)(tensor_img) transforms.RandomResizedCrop((20, 20), antialias=True)(tensor_img) transforms.ScaleJitter((20, 20), antialias=True)(tensor_img) transforms.RandomShortestSize((20, 20), antialias=True)(tensor_img) transforms.RandomResize(10, 20, antialias=True)(tensor_img) - transforms.functional.resize(tensor_img, (20, 20), antialias=True) - transforms.functional.resize_image_tensor(tensor_img, (20, 20), antialias=True) - transforms.functional.resize(tensor_video, (20, 20), antialias=True) - transforms.functional.resize_video(tensor_video, (20, 20), antialias=True) - - datapoints.Image(tensor_img).resize((20, 20), antialias=True) datapoints.Image(tensor_img).resized_crop(0, 0, 10, 10, (20, 20), antialias=True) - datapoints.Video(tensor_video).resize((20, 20), antialias=True) datapoints.Video(tensor_video).resized_crop(0, 0, 10, 10, (20, 20), antialias=True) diff --git a/test/transforms_v2_dispatcher_infos.py b/test/transforms_v2_dispatcher_infos.py index 1d9dd025254..cb1bc257e50 100644 --- a/test/transforms_v2_dispatcher_infos.py +++ b/test/transforms_v2_dispatcher_infos.py @@ -148,19 +148,6 @@ def fill_sequence_needs_broadcast(args_kwargs): }, pil_kernel_info=PILKernelInfo(F.horizontal_flip_image_pil, kernel_name="horizontal_flip_image_pil"), ), - DispatcherInfo( - F.resize, - kernels={ - datapoints.Image: F.resize_image_tensor, - datapoints.Video: F.resize_video, - datapoints.BoundingBox: F.resize_bounding_box, - datapoints.Mask: F.resize_mask, - }, - pil_kernel_info=PILKernelInfo(F.resize_image_pil), - test_marks=[ - xfail_jit_python_scalar_arg("size"), - ], - ), DispatcherInfo( F.affine, kernels={ diff --git a/test/transforms_v2_kernel_infos.py b/test/transforms_v2_kernel_infos.py index 7b877fb092d..874e2cf657d 100644 --- a/test/transforms_v2_kernel_infos.py +++ b/test/transforms_v2_kernel_infos.py @@ -250,74 +250,11 @@ def _get_resize_sizes(spatial_size): yield height, width -def sample_inputs_resize_image_tensor(): - for image_loader in make_image_loaders(sizes=["random"], color_spaces=["RGB"], dtypes=[torch.float32]): - for size in _get_resize_sizes(image_loader.spatial_size): - yield ArgsKwargs(image_loader, size=size) - - for image_loader, interpolation in itertools.product( - make_image_loaders(sizes=["random"], color_spaces=["RGB"]), - [F.InterpolationMode.NEAREST, F.InterpolationMode.BILINEAR], - ): - yield ArgsKwargs(image_loader, size=[min(image_loader.spatial_size) + 1], interpolation=interpolation) - - yield ArgsKwargs(make_image_loader(size=(11, 17)), size=20, max_size=25) - - -def sample_inputs_resize_image_tensor_bicubic(): - for image_loader, interpolation in itertools.product( - make_image_loaders(sizes=["random"], color_spaces=["RGB"]), [F.InterpolationMode.BICUBIC] - ): - yield ArgsKwargs(image_loader, size=[min(image_loader.spatial_size) + 1], interpolation=interpolation) - - -@pil_reference_wrapper -def reference_resize_image_tensor(*args, **kwargs): - if not kwargs.pop("antialias", False) and kwargs.get("interpolation", F.InterpolationMode.BILINEAR) in { - F.InterpolationMode.BILINEAR, - F.InterpolationMode.BICUBIC, - }: - raise pytest.UsageError("Anti-aliasing is always active in PIL") - return F.resize_image_pil(*args, **kwargs) - - -def reference_inputs_resize_image_tensor(): - for image_loader, interpolation in itertools.product( - make_image_loaders_for_interpolation(), - [ - F.InterpolationMode.NEAREST, - F.InterpolationMode.NEAREST_EXACT, - F.InterpolationMode.BILINEAR, - F.InterpolationMode.BICUBIC, - ], - ): - for size in _get_resize_sizes(image_loader.spatial_size): - yield ArgsKwargs( - image_loader, - size=size, - interpolation=interpolation, - antialias=interpolation - in { - F.InterpolationMode.BILINEAR, - F.InterpolationMode.BICUBIC, - }, - ) - - def sample_inputs_resize_bounding_box(): for bounding_box_loader in make_bounding_box_loaders(): for size in _get_resize_sizes(bounding_box_loader.spatial_size): yield ArgsKwargs(bounding_box_loader, spatial_size=bounding_box_loader.spatial_size, size=size) - - -def sample_inputs_resize_mask(): - for mask_loader in make_mask_loaders(sizes=["random"], num_categories=["random"], num_objects=["random"]): - yield ArgsKwargs(mask_loader, size=[min(mask_loader.shape[-2:]) + 1]) - - -def sample_inputs_resize_video(): - for video_loader in make_video_loaders(sizes=["random"], num_frames=["random"]): - yield ArgsKwargs(video_loader, size=[min(video_loader.shape[-2:]) + 1]) + return def reference_resize_bounding_box(bounding_box, *, spatial_size, size, max_size=None): @@ -352,36 +289,6 @@ def reference_inputs_resize_bounding_box(): KERNEL_INFOS.extend( [ - KernelInfo( - F.resize_image_tensor, - sample_inputs_fn=sample_inputs_resize_image_tensor, - reference_fn=reference_resize_image_tensor, - reference_inputs_fn=reference_inputs_resize_image_tensor, - float32_vs_uint8=True, - closeness_kwargs={ - **pil_reference_pixel_difference(10, mae=True), - **cuda_vs_cpu_pixel_difference(), - **float32_vs_uint8_pixel_difference(1, mae=True), - }, - test_marks=[ - xfail_jit_python_scalar_arg("size"), - ], - ), - KernelInfo( - F.resize_image_tensor, - sample_inputs_fn=sample_inputs_resize_image_tensor_bicubic, - reference_fn=reference_resize_image_tensor, - reference_inputs_fn=reference_inputs_resize_image_tensor, - float32_vs_uint8=True, - closeness_kwargs={ - **pil_reference_pixel_difference(10, mae=True), - **cuda_vs_cpu_pixel_difference(atol=30), - **float32_vs_uint8_pixel_difference(1, mae=True), - }, - test_marks=[ - xfail_jit_python_scalar_arg("size"), - ], - ), KernelInfo( F.resize_bounding_box, sample_inputs_fn=sample_inputs_resize_bounding_box, @@ -394,19 +301,6 @@ def reference_inputs_resize_bounding_box(): xfail_jit_python_scalar_arg("size"), ], ), - KernelInfo( - F.resize_mask, - sample_inputs_fn=sample_inputs_resize_mask, - closeness_kwargs=pil_reference_pixel_difference(10), - test_marks=[ - xfail_jit_python_scalar_arg("size"), - ], - ), - KernelInfo( - F.resize_video, - sample_inputs_fn=sample_inputs_resize_video, - closeness_kwargs=cuda_vs_cpu_pixel_difference(), - ), ] ) From 5957d42e3987c29fd2323b555f6d23263795d0e6 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 21 Jun 2023 15:04:36 +0200 Subject: [PATCH 39/39] port bounding box correctness tests --- test/test_transforms_v2_refactored.py | 171 ++++++++++++++++++++------ test/transforms_v2_kernel_infos.py | 67 ---------- 2 files changed, 133 insertions(+), 105 deletions(-) diff --git a/test/test_transforms_v2_refactored.py b/test/test_transforms_v2_refactored.py index 7c4d5cf9b99..2b9565c74c8 100644 --- a/test/test_transforms_v2_refactored.py +++ b/test/test_transforms_v2_refactored.py @@ -1,10 +1,10 @@ import contextlib -import functools import inspect import re from typing import get_type_hints from unittest import mock +import numpy as np import PIL.Image import pytest @@ -288,9 +288,12 @@ def check_transform(transform_cls, input, *args, **kwargs): _check_transform_v1_compatibility(transform, input) -def make_parametrizable(fn): - def wrapper(*args, **kwargs): - return functools.partial(fn, *args, **kwargs) +def transform_cls_to_functional(transform_cls): + def wrapper(input, *args, **kwargs): + transform = transform_cls(*args, **kwargs) + return transform(input) + + wrapper.__name__ = transform_cls.__name__ return wrapper @@ -310,6 +313,56 @@ def assert_warns_antialias_default_value(): yield +def reference_affine_bounding_box_helper(bounding_box, *, format, spatial_size, affine_matrix): + def transform(bbox, affine_matrix_, format_, spatial_size_): + # Go to float before converting to prevent precision loss in case of CXCYWH -> XYXY and W or H is 1 + in_dtype = bbox.dtype + if not torch.is_floating_point(bbox): + bbox = bbox.float() + bbox_xyxy = F.convert_format_bounding_box( + bbox.as_subclass(torch.Tensor), + old_format=format_, + new_format=datapoints.BoundingBoxFormat.XYXY, + inplace=True, + ) + points = np.array( + [ + [bbox_xyxy[0].item(), bbox_xyxy[1].item(), 1.0], + [bbox_xyxy[2].item(), bbox_xyxy[1].item(), 1.0], + [bbox_xyxy[0].item(), bbox_xyxy[3].item(), 1.0], + [bbox_xyxy[2].item(), bbox_xyxy[3].item(), 1.0], + ] + ) + transformed_points = np.matmul(points, affine_matrix_.T) + out_bbox = torch.tensor( + [ + np.min(transformed_points[:, 0]).item(), + np.min(transformed_points[:, 1]).item(), + np.max(transformed_points[:, 0]).item(), + np.max(transformed_points[:, 1]).item(), + ], + dtype=bbox_xyxy.dtype, + ) + out_bbox = F.convert_format_bounding_box( + out_bbox, old_format=datapoints.BoundingBoxFormat.XYXY, new_format=format_, inplace=True + ) + # It is important to clamp before casting, especially for CXCYWH format, dtype=int64 + out_bbox = F.clamp_bounding_box(out_bbox, format=format_, spatial_size=spatial_size_) + out_bbox = out_bbox.to(dtype=in_dtype) + return out_bbox + + if bounding_box.ndim < 2: + bounding_box = [bounding_box] + + expected_bboxes = [transform(bbox, affine_matrix, format, spatial_size) for bbox in bounding_box] + if len(expected_bboxes) > 1: + expected_bboxes = torch.stack(expected_bboxes) + else: + expected_bboxes = expected_bboxes[0] + + return expected_bboxes + + class TestResize: INPUT_SIZE = (17, 11) OUTPUT_SIZES = [17, [17], (17,), [12, 13], (12, 13)] @@ -348,34 +401,33 @@ def _make_input(self, input_type, *, dtype=None, device="cpu", **kwargs): return input - def _check_size(self, input, output, *, size, max_size): - if isinstance(size, int) or len(size) == 1: - if not isinstance(size, int): - size = size[0] - - old_height, old_width = F.get_spatial_size(input) - ratio = old_width / old_height - if ratio > 1: - new_height = size - new_width = int(ratio * new_height) - else: - new_width = size - new_height = int(new_width / ratio) + def _compute_output_size(self, *, input_size, size, max_size): + if not (isinstance(size, int) or len(size) == 1): + return tuple(size) - if max_size is not None and max(new_height, new_width) > max_size: - # Need to recompute the aspect ratio, since it might have changed due to rounding - ratio = new_width / new_height - if ratio > 1: - new_width = max_size - new_height = int(new_width / ratio) - else: - new_height = max_size - new_width = int(new_height * ratio) + if not isinstance(size, int): + size = size[0] + old_height, old_width = input_size + ratio = old_width / old_height + if ratio > 1: + new_height = size + new_width = int(ratio * new_height) else: - new_height, new_width = size + new_width = size + new_height = int(new_width / ratio) - assert F.get_spatial_size(output) == [new_height, new_width] + if max_size is not None and max(new_height, new_width) > max_size: + # Need to recompute the aspect ratio, since it might have changed due to rounding + ratio = new_width / new_height + if ratio > 1: + new_width = max_size + new_height = int(new_width / ratio) + else: + new_height = max_size + new_width = int(new_height * ratio) + + return new_height, new_width @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("interpolation", INTERPOLATION_MODES) @@ -403,12 +455,12 @@ def test_kernel_image_tensor(self, size, interpolation, use_max_size, antialias, check_scripted_vs_eager=not isinstance(size, int), ) - @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) + @pytest.mark.parametrize("size", OUTPUT_SIZES) @pytest.mark.parametrize("use_max_size", [True, False]) @pytest.mark.parametrize("dtype", [torch.float32, torch.int64]) @pytest.mark.parametrize("device", cpu_and_cuda()) - def test_kernel_bounding_box(self, size, format, use_max_size, dtype, device): + def test_kernel_bounding_box(self, format, size, use_max_size, dtype, device): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return @@ -485,29 +537,72 @@ def test_transform(self, size, device, input_type): antialias=True, ) + def _check_output_size(self, input, output, *, size, max_size): + assert tuple(F.get_spatial_size(output)) == self._compute_output_size( + input_size=F.get_spatial_size(input), size=size, max_size=max_size + ) + @pytest.mark.parametrize("size", OUTPUT_SIZES) # `InterpolationMode.NEAREST` is modeled after the buggy `INTER_NEAREST` interpolation of CV2. # The PIL equivalent of `InterpolationMode.NEAREST` is `InterpolationMode.NEAREST_EXACT` @pytest.mark.parametrize("interpolation", set(INTERPOLATION_MODES) - {transforms.InterpolationMode.NEAREST}) @pytest.mark.parametrize("use_max_size", [True, False]) - @pytest.mark.parametrize("make_fn", [make_parametrizable(F.resize_image_tensor), transforms.Resize]) - def test_image_correctness(self, size, interpolation, use_max_size, make_fn): + @pytest.mark.parametrize("fn", [F.resize, transform_cls_to_functional(transforms.Resize)]) + def test_image_correctness(self, size, interpolation, use_max_size, fn): if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): return - fn = make_fn(size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) - image = self._make_input(torch.Tensor, dtype=torch.uint8, device="cpu") - actual = fn(image) + actual = fn(image, size=size, interpolation=interpolation, **max_size_kwarg, antialias=True) expected = F.to_image_tensor( - F.resize_image_pil(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) + F.resize(F.to_image_pil(image), size=size, interpolation=interpolation, **max_size_kwarg) ) - self._check_size(image, actual, size=size, **max_size_kwarg) - + self._check_output_size(image, actual, size=size, **max_size_kwarg) torch.testing.assert_close(actual, expected, atol=1, rtol=0) + def _reference_resize_bounding_box(self, bounding_box, *, size, max_size=None): + old_height, old_width = bounding_box.spatial_size + new_height, new_width = self._compute_output_size( + input_size=bounding_box.spatial_size, size=size, max_size=max_size + ) + + if (old_height, old_width) == (new_height, new_width): + return bounding_box + + affine_matrix = np.array( + [ + [new_width / old_width, 0, 0], + [0, new_height / old_height, 0], + ], + dtype="float64" if bounding_box.dtype == torch.float64 else "float32", + ) + + expected_bboxes = reference_affine_bounding_box_helper( + bounding_box, + format=bounding_box.format, + spatial_size=(new_height, new_width), + affine_matrix=affine_matrix, + ) + return datapoints.BoundingBox.wrap_like(bounding_box, expected_bboxes, spatial_size=(new_height, new_width)) + + @pytest.mark.parametrize("format", list(datapoints.BoundingBoxFormat)) + @pytest.mark.parametrize("size", OUTPUT_SIZES) + @pytest.mark.parametrize("use_max_size", [True, False]) + @pytest.mark.parametrize("fn", [F.resize, transform_cls_to_functional(transforms.Resize)]) + def test_bounding_box_correctness(self, format, size, use_max_size, fn): + if not (max_size_kwarg := self._make_max_size_kwarg(use_max_size=use_max_size, size=size)): + return + + bounding_box = self._make_input(datapoints.BoundingBox) + + actual = fn(bounding_box, size=size, **max_size_kwarg) + expected = self._reference_resize_bounding_box(bounding_box, size=size, **max_size_kwarg) + + self._check_output_size(bounding_box, actual, size=size, **max_size_kwarg) + torch.testing.assert_close(actual, expected) + @pytest.mark.parametrize("interpolation", set(transforms.InterpolationMode) - set(INTERPOLATION_MODES)) @pytest.mark.parametrize( "input_type", diff --git a/test/transforms_v2_kernel_infos.py b/test/transforms_v2_kernel_infos.py index 874e2cf657d..547e708b726 100644 --- a/test/transforms_v2_kernel_infos.py +++ b/test/transforms_v2_kernel_infos.py @@ -238,73 +238,6 @@ def reference_inputs_flip_bounding_box(): ) -def _get_resize_sizes(spatial_size): - height, width = spatial_size - length = max(spatial_size) - yield length - yield [length] - yield (length,) - new_height = int(height * 0.75) - new_width = int(width * 1.25) - yield [new_height, new_width] - yield height, width - - -def sample_inputs_resize_bounding_box(): - for bounding_box_loader in make_bounding_box_loaders(): - for size in _get_resize_sizes(bounding_box_loader.spatial_size): - yield ArgsKwargs(bounding_box_loader, spatial_size=bounding_box_loader.spatial_size, size=size) - return - - -def reference_resize_bounding_box(bounding_box, *, spatial_size, size, max_size=None): - old_height, old_width = spatial_size - new_height, new_width = F._geometry._compute_resized_output_size(spatial_size, size=size, max_size=max_size) - - if (old_height, old_width) == (new_height, new_width): - return bounding_box, (old_height, old_width) - - affine_matrix = np.array( - [ - [new_width / old_width, 0, 0], - [0, new_height / old_height, 0], - ], - dtype="float64" if bounding_box.dtype == torch.float64 else "float32", - ) - - expected_bboxes = reference_affine_bounding_box_helper( - bounding_box, - format=bounding_box.format, - spatial_size=(new_height, new_width), - affine_matrix=affine_matrix, - ) - return expected_bboxes, (new_height, new_width) - - -def reference_inputs_resize_bounding_box(): - for bounding_box_loader in make_bounding_box_loaders(extra_dims=((), (4,))): - for size in _get_resize_sizes(bounding_box_loader.spatial_size): - yield ArgsKwargs(bounding_box_loader, size=size, spatial_size=bounding_box_loader.spatial_size) - - -KERNEL_INFOS.extend( - [ - KernelInfo( - F.resize_bounding_box, - sample_inputs_fn=sample_inputs_resize_bounding_box, - reference_fn=reference_resize_bounding_box, - reference_inputs_fn=reference_inputs_resize_bounding_box, - closeness_kwargs={ - (("TestKernels", "test_against_reference"), torch.int64, "cpu"): dict(atol=1, rtol=0), - }, - test_marks=[ - xfail_jit_python_scalar_arg("size"), - ], - ), - ] -) - - _AFFINE_KWARGS = combinations_grid( angle=[-87, 15, 90], translate=[(5, 5), (-5, -5)],