|
29 | 29 | make_bounding_boxes, |
30 | 30 | make_detection_masks, |
31 | 31 | make_image, |
| 32 | + make_image_cvcuda, |
32 | 33 | make_image_pil, |
33 | 34 | make_image_tensor, |
34 | 35 | make_keypoints, |
|
51 | 52 | from torchvision.transforms.v2 import functional as F |
52 | 53 | from torchvision.transforms.v2._utils import check_type, is_pure_tensor |
53 | 54 | from torchvision.transforms.v2.functional._geometry import _get_perspective_coeffs, _parallelogram_to_bounding_boxes |
| 55 | +from torchvision.transforms.v2.functional._type_conversion import _import_cvcuda_modules |
54 | 56 | from torchvision.transforms.v2.functional._utils import _get_kernel, _register_kernel_internal |
55 | 57 |
|
| 58 | +try: |
| 59 | + _import_cvcuda_modules() |
| 60 | + CVCUDA_AVAILABLE = True |
| 61 | +except ImportError: |
| 62 | + CVCUDA_AVAILABLE = False |
| 63 | +CUDA_AVAILABLE = torch.cuda.is_available() |
| 64 | + |
56 | 65 |
|
57 | 66 | # turns all warnings into errors for this module |
58 | 67 | pytestmark = [pytest.mark.filterwarnings("error")] |
@@ -6733,6 +6742,184 @@ def test_functional_error(self): |
6733 | 6742 | F.pil_to_tensor(object()) |
6734 | 6743 |
|
6735 | 6744 |
|
| 6745 | +@pytest.mark.skipif(not CVCUDA_AVAILABLE, reason="test requires CVCUDA") |
| 6746 | +@pytest.mark.skipif(not CUDA_AVAILABLE, reason="test requires CUDA") |
| 6747 | +class TestToCVCUDATensor: |
| 6748 | + """Tests for to_cvcuda_tensor function following patterns from TestToPil""" |
| 6749 | + |
| 6750 | + def test_1_channel_uint8_tensor_to_cvcuda_tensor(self): |
| 6751 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6752 | + img_data = torch.randint(0, 256, (1, 4, 4), dtype=torch.uint8) |
| 6753 | + img_data = img_data.cuda() |
| 6754 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6755 | + # Check that the conversion succeeded and format is correct |
| 6756 | + assert cvcuda_img is not None |
| 6757 | + |
| 6758 | + def test_1_channel_int16_tensor_to_cvcuda_tensor(self): |
| 6759 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6760 | + img_data = torch.randint(0, 256, (1, 4, 4), dtype=torch.int16) |
| 6761 | + img_data = img_data.cuda() |
| 6762 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6763 | + assert cvcuda_img is not None |
| 6764 | + |
| 6765 | + def test_1_channel_int32_tensor_to_cvcuda_tensor(self): |
| 6766 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6767 | + img_data = torch.randint(0, 256, (1, 4, 4), dtype=torch.int32) |
| 6768 | + img_data = img_data.cuda() |
| 6769 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6770 | + assert cvcuda_img is not None |
| 6771 | + |
| 6772 | + def test_1_channel_float32_tensor_to_cvcuda_tensor(self): |
| 6773 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6774 | + img_data = torch.rand(1, 4, 4) |
| 6775 | + img_data = img_data.cuda() |
| 6776 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6777 | + assert cvcuda_img is not None |
| 6778 | + |
| 6779 | + def test_3_channel_uint8_tensor_to_cvcuda_tensor(self): |
| 6780 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6781 | + img_data = torch.randint(0, 256, (3, 4, 4), dtype=torch.uint8) |
| 6782 | + img_data = img_data.cuda() |
| 6783 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6784 | + assert cvcuda_img is not None |
| 6785 | + |
| 6786 | + def test_3_channel_float32_tensor_to_cvcuda_tensor(self): |
| 6787 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6788 | + img_data = torch.rand(3, 4, 4) |
| 6789 | + img_data = img_data.cuda() |
| 6790 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6791 | + assert cvcuda_img is not None |
| 6792 | + |
| 6793 | + def test_unsupported_num_channels(self): |
| 6794 | + # Test 2-channel image (not supported) |
| 6795 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6796 | + img_data = torch.rand(2, 5, 5) |
| 6797 | + img_data = img_data.cuda() |
| 6798 | + with pytest.raises(ValueError, match="Only 1 and 3 channel images are supported"): |
| 6799 | + F.to_cvcuda_tensor(img_data) |
| 6800 | + |
| 6801 | + # Test 4-channel image (not supported) |
| 6802 | + img_data = torch.randint(0, 256, (4, 5, 5), dtype=torch.uint8) |
| 6803 | + img_data = img_data.cuda() |
| 6804 | + with pytest.raises(ValueError, match="Only 1 and 3 channel images are supported"): |
| 6805 | + F.to_cvcuda_tensor(img_data) |
| 6806 | + |
| 6807 | + def test_invalid_input_type(self): |
| 6808 | + with pytest.raises(TypeError, match=r"pic should be `torch.Tensor`"): |
| 6809 | + F.to_cvcuda_tensor("invalid_input") |
| 6810 | + |
| 6811 | + def test_invalid_dimensions(self): |
| 6812 | + # Test 1D array (too few dimensions) |
| 6813 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6814 | + with pytest.raises(ValueError, match=r"pic should be 3 or 4 dimensional"): |
| 6815 | + img_data = torch.randint(0, 256, (4,), dtype=torch.uint8) |
| 6816 | + img_data = img_data.cuda() |
| 6817 | + F.to_cvcuda_tensor(img_data) |
| 6818 | + |
| 6819 | + # Test 2D array (no longer supported) |
| 6820 | + with pytest.raises(ValueError, match=r"pic should be 3 or 4 dimensional"): |
| 6821 | + img_data = torch.randint(0, 256, (4, 4), dtype=torch.uint8) |
| 6822 | + img_data = img_data.cuda() |
| 6823 | + F.to_cvcuda_tensor(img_data) |
| 6824 | + |
| 6825 | + # Test 5D array (too many dimensions) |
| 6826 | + with pytest.raises(ValueError, match=r"pic should be 3 or 4 dimensional"): |
| 6827 | + img_data = torch.randint(0, 256, (1, 1, 3, 4, 4), dtype=torch.uint8) |
| 6828 | + img_data = img_data.cuda() |
| 6829 | + F.to_cvcuda_tensor(img_data) |
| 6830 | + |
| 6831 | + def test_float64_tensor_to_cvcuda_tensor(self): |
| 6832 | + # Test single channel float64 (F64 format is supported) |
| 6833 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6834 | + img_data = torch.rand(1, 4, 4, dtype=torch.float64) |
| 6835 | + img_data = img_data.cuda() |
| 6836 | + cvcuda_img = F.to_cvcuda_tensor(img_data) |
| 6837 | + assert cvcuda_img is not None |
| 6838 | + |
| 6839 | + def test_float64_rgb_not_supported(self): |
| 6840 | + # Test 3-channel float64 is NOT supported (no RGBf64 format in CV-CUDA) |
| 6841 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6842 | + img_data = torch.rand(3, 4, 4, dtype=torch.float64) |
| 6843 | + img_data = img_data.cuda() |
| 6844 | + with pytest.raises(TypeError, match=r"Unsupported dtype"): |
| 6845 | + F.to_cvcuda_tensor(img_data) |
| 6846 | + |
| 6847 | + @pytest.mark.parametrize("num_channels", [1, 3]) |
| 6848 | + @pytest.mark.parametrize("dtype", [torch.uint8, torch.float32, torch.float64]) |
| 6849 | + def test_round_trip(self, num_channels, dtype): |
| 6850 | + # Skip float64 for 3-channel (not supported by CV-CUDA) |
| 6851 | + if num_channels == 3 and dtype == torch.float64: |
| 6852 | + pytest.skip("float64 is not supported for 3-channel RGB images") |
| 6853 | + |
| 6854 | + # Setup: Create a tensor in CHW format (PyTorch standard) |
| 6855 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6856 | + if dtype == torch.uint8: |
| 6857 | + original_tensor = torch.randint(0, 256, (num_channels, 4, 4), dtype=dtype) |
| 6858 | + else: |
| 6859 | + original_tensor = torch.rand(num_channels, 4, 4, dtype=dtype) |
| 6860 | + original_tensor = original_tensor.cuda() |
| 6861 | + |
| 6862 | + # Execute: Convert to CV-CUDA and back to tensor |
| 6863 | + # CHW -> (to_cvcuda_tensor) -> CV-CUDA NHWC -> (cvcuda_to_tensor) -> NCHW |
| 6864 | + cvcuda_tensor = F.to_cvcuda_tensor(original_tensor) |
| 6865 | + result_tensor = F.cvcuda_to_tensor(cvcuda_tensor) |
| 6866 | + |
| 6867 | + # Remove batch dimension that was added during conversion since original was unbatched |
| 6868 | + result_tensor = result_tensor.squeeze(0) |
| 6869 | + |
| 6870 | + # Assert: The round-trip conversion preserves the original tensor exactly |
| 6871 | + torch.testing.assert_close(result_tensor, original_tensor, rtol=0, atol=0) |
| 6872 | + |
| 6873 | + @pytest.mark.parametrize("num_channels", [1, 3]) |
| 6874 | + @pytest.mark.parametrize("dtype", [torch.uint8, torch.float32, torch.float64]) |
| 6875 | + @pytest.mark.parametrize("batch_size", [1, 2, 4]) |
| 6876 | + def test_round_trip_batched(self, num_channels, dtype, batch_size): |
| 6877 | + # Skip float64 for 3-channel (not supported by CV-CUDA) |
| 6878 | + if num_channels == 3 and dtype == torch.float64: |
| 6879 | + pytest.skip("float64 is not supported for 3-channel RGB images") |
| 6880 | + |
| 6881 | + # Setup: Create a batched tensor in NCHW format |
| 6882 | + # Create tensor on CPU first, then move to CUDA to avoid CUDA context issues |
| 6883 | + if dtype == torch.uint8: |
| 6884 | + original_tensor = torch.randint(0, 256, (batch_size, num_channels, 4, 4), dtype=dtype) |
| 6885 | + else: |
| 6886 | + original_tensor = torch.rand(batch_size, num_channels, 4, 4, dtype=dtype) |
| 6887 | + original_tensor = original_tensor.cuda() |
| 6888 | + |
| 6889 | + # Execute: Convert to CV-CUDA and back to tensor |
| 6890 | + # NCHW -> (to_cvcuda_tensor) -> CV-CUDA NHWC -> (cvcuda_to_tensor) -> NCHW |
| 6891 | + cvcuda_tensor = F.to_cvcuda_tensor(original_tensor) |
| 6892 | + result_tensor = F.cvcuda_to_tensor(cvcuda_tensor) |
| 6893 | + |
| 6894 | + # Assert: The round-trip conversion preserves the original batched tensor exactly |
| 6895 | + torch.testing.assert_close(result_tensor, original_tensor, rtol=0, atol=0) |
| 6896 | + # Also verify batch size is preserved |
| 6897 | + assert result_tensor.shape[0] == batch_size |
| 6898 | + |
| 6899 | + |
| 6900 | +@pytest.mark.skipif(not CVCUDA_AVAILABLE, reason="test requires CVCUDA") |
| 6901 | +@pytest.mark.skipif(not CUDA_AVAILABLE, reason="test requires CUDA") |
| 6902 | +class TestCVDUDAToTensor: |
| 6903 | + @pytest.mark.parametrize("color_space", ["RGB", "GRAY"]) |
| 6904 | + @pytest.mark.parametrize( |
| 6905 | + "fn", |
| 6906 | + [F.cvcuda_to_tensor, transform_cls_to_functional(transforms.CVCUDAToTensor)], |
| 6907 | + ) |
| 6908 | + def test_functional_and_transform(self, color_space, fn): |
| 6909 | + input = make_image_cvcuda(color_space=color_space) |
| 6910 | + |
| 6911 | + output = fn(input) |
| 6912 | + |
| 6913 | + assert isinstance(output, torch.Tensor) |
| 6914 | + # Convert input to tensor to compare sizes |
| 6915 | + input_tensor = F.cvcuda_to_tensor(input) |
| 6916 | + assert F.get_size(output) == F.get_size(input_tensor) |
| 6917 | + |
| 6918 | + def test_functional_error(self): |
| 6919 | + with pytest.raises(TypeError, match="cvcuda_img should be `cvcuda.Tensor`"): |
| 6920 | + F.cvcuda_to_tensor(object()) |
| 6921 | + |
| 6922 | + |
6736 | 6923 | class TestLambda: |
6737 | 6924 | @pytest.mark.parametrize("input", [object(), torch.empty(()), np.empty(()), "string", 1, 0.0]) |
6738 | 6925 | @pytest.mark.parametrize("types", [(), (torch.Tensor, np.ndarray)]) |
|
0 commit comments