Skip to content

Commit 2d82dc5

Browse files
Introducing CVCUDA Backend (#9259)
1 parent 9e37c3a commit 2d82dc5

File tree

8 files changed

+228
-9
lines changed

8 files changed

+228
-9
lines changed

test/common_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from torch.testing._comparison import BooleanPair, NonePair, not_close_error_metas, NumberPair, TensorLikePair
2121
from torchvision import io, tv_tensors
2222
from torchvision.transforms._functional_tensor import _max_value as get_max_value
23-
from torchvision.transforms.v2.functional import to_image, to_pil_image
23+
from torchvision.transforms.v2.functional import to_cvcuda_tensor, to_image, to_pil_image
2424
from torchvision.utils import _Image_fromarray
2525

2626

@@ -400,6 +400,10 @@ def make_image_pil(*args, **kwargs):
400400
return to_pil_image(make_image(*args, **kwargs))
401401

402402

403+
def make_image_cvcuda(*args, **kwargs):
404+
return to_cvcuda_tensor(make_image(*args, **kwargs))
405+
406+
403407
def make_keypoints(canvas_size=DEFAULT_SIZE, *, num_points=4, dtype=None, device="cpu"):
404408
y = torch.randint(0, canvas_size[0], size=(num_points, 1), dtype=dtype, device=device)
405409
x = torch.randint(0, canvas_size[1], size=(num_points, 1), dtype=dtype, device=device)

test/test_transforms_v2.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
make_bounding_boxes,
3030
make_detection_masks,
3131
make_image,
32+
make_image_cvcuda,
3233
make_image_pil,
3334
make_image_tensor,
3435
make_keypoints,
@@ -51,8 +52,17 @@
5152
from torchvision.transforms.v2 import functional as F
5253
from torchvision.transforms.v2._utils import check_type, is_pure_tensor
5354
from torchvision.transforms.v2.functional._geometry import _get_perspective_coeffs, _parallelogram_to_bounding_boxes
54-
from torchvision.transforms.v2.functional._utils import _get_kernel, _register_kernel_internal
55+
from torchvision.transforms.v2.functional._utils import (
56+
_get_kernel,
57+
_import_cvcuda,
58+
_is_cvcuda_available,
59+
_register_kernel_internal,
60+
)
61+
5562

63+
CVCUDA_AVAILABLE = _is_cvcuda_available()
64+
if CVCUDA_AVAILABLE:
65+
cvcuda = _import_cvcuda()
5666

5767
# turns all warnings into errors for this module
5868
pytestmark = [pytest.mark.filterwarnings("error")]
@@ -6732,6 +6742,93 @@ def test_functional_error(self):
67326742
F.pil_to_tensor(object())
67336743

67346744

6745+
@pytest.mark.skipif(not CVCUDA_AVAILABLE, reason="test requires CVCUDA")
6746+
@needs_cuda
6747+
class TestToCVCUDATensor:
6748+
@pytest.mark.parametrize("image_type", (torch.Tensor, tv_tensors.Image))
6749+
@pytest.mark.parametrize("dtype", [torch.uint8, torch.uint16, torch.float32, torch.float64])
6750+
@pytest.mark.parametrize("device", cpu_and_cuda())
6751+
@pytest.mark.parametrize("color_space", ["RGB", "GRAY"])
6752+
@pytest.mark.parametrize("batch_dims", [(1,), (2,), (4,)])
6753+
@pytest.mark.parametrize(
6754+
"fn",
6755+
[F.to_cvcuda_tensor, transform_cls_to_functional(transforms.ToCVCUDATensor)],
6756+
)
6757+
def test_functional_and_transform(self, image_type, dtype, device, color_space, batch_dims, fn):
6758+
image = make_image(dtype=dtype, device=device, color_space=color_space, batch_dims=batch_dims)
6759+
if image_type is torch.Tensor:
6760+
image = image.as_subclass(torch.Tensor)
6761+
assert is_pure_tensor(image)
6762+
output = fn(image)
6763+
6764+
assert isinstance(output, cvcuda.Tensor)
6765+
assert F.get_size(output) == F.get_size(image)
6766+
assert output is not None
6767+
6768+
def test_invalid_input_type(self):
6769+
with pytest.raises(TypeError, match=r"inpt should be ``torch.Tensor``"):
6770+
F.to_cvcuda_tensor("invalid_input")
6771+
6772+
def test_invalid_dimensions(self):
6773+
with pytest.raises(ValueError, match=r"pic should be 4 dimensional"):
6774+
img_data = torch.randint(0, 256, (3, 1, 3), dtype=torch.uint8)
6775+
img_data = img_data.cuda()
6776+
F.to_cvcuda_tensor(img_data)
6777+
6778+
with pytest.raises(ValueError, match=r"pic should be 4 dimensional"):
6779+
img_data = torch.randint(0, 256, (4,), dtype=torch.uint8)
6780+
img_data = img_data.cuda()
6781+
F.to_cvcuda_tensor(img_data)
6782+
6783+
with pytest.raises(ValueError, match=r"pic should be 4 dimensional"):
6784+
img_data = torch.randint(0, 256, (4, 4), dtype=torch.uint8)
6785+
img_data = img_data.cuda()
6786+
F.to_cvcuda_tensor(img_data)
6787+
6788+
with pytest.raises(ValueError, match=r"pic should be 4 dimensional"):
6789+
img_data = torch.randint(0, 256, (1, 1, 3, 4, 4), dtype=torch.uint8)
6790+
img_data = img_data.cuda()
6791+
F.to_cvcuda_tensor(img_data)
6792+
6793+
@pytest.mark.parametrize("dtype", [torch.uint8, torch.uint16, torch.float32, torch.float64])
6794+
@pytest.mark.parametrize("device", cpu_and_cuda())
6795+
@pytest.mark.parametrize("color_space", ["RGB", "GRAY"])
6796+
@pytest.mark.parametrize("batch_size", [1, 2, 4])
6797+
def test_round_trip(self, dtype, device, color_space, batch_size):
6798+
original_tensor = make_image_tensor(
6799+
dtype=dtype, device=device, color_space=color_space, batch_dims=(batch_size,)
6800+
)
6801+
cvcuda_tensor = F.to_cvcuda_tensor(original_tensor)
6802+
result_tensor = F.cvcuda_to_tensor(cvcuda_tensor)
6803+
torch.testing.assert_close(result_tensor.to(device), original_tensor, rtol=0, atol=0)
6804+
assert result_tensor.shape[0] == batch_size
6805+
6806+
6807+
@pytest.mark.skipif(not CVCUDA_AVAILABLE, reason="test requires CVCUDA")
6808+
@needs_cuda
6809+
class TestCVDUDAToTensor:
6810+
@pytest.mark.parametrize("dtype", [torch.uint8, torch.uint16, torch.float32, torch.float64])
6811+
@pytest.mark.parametrize("device", cpu_and_cuda())
6812+
@pytest.mark.parametrize("color_space", ["RGB", "GRAY"])
6813+
@pytest.mark.parametrize("batch_dims", [(1,), (2,), (4,)])
6814+
@pytest.mark.parametrize(
6815+
"fn",
6816+
[F.cvcuda_to_tensor, transform_cls_to_functional(transforms.CVCUDAToTensor)],
6817+
)
6818+
def test_functional_and_transform(self, dtype, device, color_space, batch_dims, fn):
6819+
input = make_image_cvcuda(dtype=dtype, device=device, color_space=color_space, batch_dims=batch_dims)
6820+
6821+
output = fn(input)
6822+
6823+
assert isinstance(output, torch.Tensor)
6824+
input_tensor = F.cvcuda_to_tensor(input)
6825+
assert F.get_size(output) == F.get_size(input_tensor)
6826+
6827+
def test_functional_error(self):
6828+
with pytest.raises(TypeError, match="cvcuda_img should be `cvcuda.Tensor`"):
6829+
F.cvcuda_to_tensor(object())
6830+
6831+
67356832
class TestLambda:
67366833
@pytest.mark.parametrize("input", [object(), torch.empty(()), np.empty(()), "string", 1, 0.0])
67376834
@pytest.mark.parametrize("types", [(), (torch.Tensor, np.ndarray)])

torchvision/transforms/v2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
ToDtype,
5656
)
5757
from ._temporal import UniformTemporalSubsample
58-
from ._type_conversion import PILToTensor, ToImage, ToPILImage, ToPureTensor
58+
from ._type_conversion import CVCUDAToTensor, PILToTensor, ToCVCUDATensor, ToImage, ToPILImage, ToPureTensor
5959
from ._utils import check_type, get_bounding_boxes, get_keypoints, has_all, has_any, query_chw, query_size
6060

6161
from ._deprecated import ToTensor # usort: skip

torchvision/transforms/v2/_type_conversion.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
from typing import Any, Optional, Union
1+
from typing import Any, Optional, TYPE_CHECKING, Union
22

33
import numpy as np
44
import PIL.Image
55
import torch
66

77
from torchvision import tv_tensors
88
from torchvision.transforms.v2 import functional as F, Transform
9-
109
from torchvision.transforms.v2._utils import is_pure_tensor
10+
from torchvision.transforms.v2.functional._utils import _import_cvcuda
11+
12+
if TYPE_CHECKING:
13+
import cvcuda # type: ignore[import-not-found]
1114

1215

1316
class PILToTensor(Transform):
@@ -90,3 +93,31 @@ class ToPureTensor(Transform):
9093

9194
def transform(self, inpt: Any, params: dict[str, Any]) -> torch.Tensor:
9295
return inpt.as_subclass(torch.Tensor)
96+
97+
98+
class ToCVCUDATensor(Transform):
99+
"""Convert a ``torch.Tensor`` with NCHW shape to a ``cvcuda.Tensor``.
100+
If the input tensor is on CPU, it will automatically be transferred to GPU.
101+
Only 1-channel and 3-channel images are supported.
102+
103+
This transform does not support torchscript.
104+
"""
105+
106+
def transform(self, inpt: torch.Tensor, params: dict[str, Any]) -> "cvcuda.Tensor":
107+
return F.to_cvcuda_tensor(inpt)
108+
109+
110+
class CVCUDAToTensor(Transform):
111+
"""Convert a ``cvcuda.Tensor`` to a ``torch.Tensor`` with NCHW shape.
112+
113+
This function does not support torchscript.
114+
"""
115+
116+
try:
117+
cvcuda = _import_cvcuda()
118+
_transformed_types = (cvcuda.Tensor,)
119+
except ImportError:
120+
pass
121+
122+
def transform(self, inpt: Any, params: dict[str, Any]) -> torch.Tensor:
123+
return F.cvcuda_to_tensor(inpt)

torchvision/transforms/v2/functional/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,6 @@
162162
to_dtype_video,
163163
)
164164
from ._temporal import uniform_temporal_subsample, uniform_temporal_subsample_video
165-
from ._type_conversion import pil_to_tensor, to_image, to_pil_image
165+
from ._type_conversion import cvcuda_to_tensor, pil_to_tensor, to_cvcuda_tensor, to_image, to_pil_image
166166

167167
from ._deprecated import get_image_size, to_tensor # usort: skip

torchvision/transforms/v2/functional/_meta.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional, Union
1+
from typing import Optional, TYPE_CHECKING, Union
22

33
import PIL.Image
44
import torch
@@ -9,7 +9,14 @@
99

1010
from torchvision.utils import _log_api_usage_once
1111

12-
from ._utils import _get_kernel, _register_kernel_internal, is_pure_tensor
12+
from ._utils import _get_kernel, _import_cvcuda, _is_cvcuda_available, _register_kernel_internal, is_pure_tensor
13+
14+
CVCUDA_AVAILABLE = _is_cvcuda_available()
15+
16+
if TYPE_CHECKING:
17+
import cvcuda # type: ignore[import-not-found]
18+
if CVCUDA_AVAILABLE:
19+
cvcuda = _import_cvcuda() # noqa: F811
1320

1421

1522
def get_dimensions(inpt: torch.Tensor) -> list[int]:
@@ -107,6 +114,20 @@ def _get_size_image_pil(image: PIL.Image.Image) -> list[int]:
107114
return [height, width]
108115

109116

117+
def get_size_image_cvcuda(image: "cvcuda.Tensor") -> list[int]:
118+
"""Get size of `cvcuda.Tensor` with NHWC layout."""
119+
hw = list(image.shape[-3:-1])
120+
ndims = len(hw)
121+
if ndims == 2:
122+
return hw
123+
else:
124+
raise TypeError(f"Input tensor should have at least two dimensions, but got {ndims}")
125+
126+
127+
if CVCUDA_AVAILABLE:
128+
_get_size_image_cvcuda = _register_kernel_internal(get_size, cvcuda.Tensor)(get_size_image_cvcuda)
129+
130+
110131
@_register_kernel_internal(get_size, tv_tensors.Video, tv_tensor_wrapper=False)
111132
def get_size_video(video: torch.Tensor) -> list[int]:
112133
return get_size_image(video)

torchvision/transforms/v2/functional/_type_conversion.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
from typing import Union
1+
from typing import TYPE_CHECKING, Union
22

33
import numpy as np
44
import PIL.Image
55
import torch
66
from torchvision import tv_tensors
77
from torchvision.transforms import functional as _F
8+
from torchvision.utils import _log_api_usage_once
9+
10+
from ._utils import _import_cvcuda
11+
12+
if TYPE_CHECKING:
13+
import cvcuda # type: ignore[import-not-found]
814

915

1016
@torch.jit.unused
@@ -25,3 +31,34 @@ def to_image(inpt: Union[torch.Tensor, PIL.Image.Image, np.ndarray]) -> tv_tenso
2531

2632
to_pil_image = _F.to_pil_image
2733
pil_to_tensor = _F.pil_to_tensor
34+
35+
36+
@torch.jit.unused
37+
def to_cvcuda_tensor(inpt: torch.Tensor) -> "cvcuda.Tensor":
38+
"""See :class:``~torchvision.transforms.v2.ToCVCUDATensor`` for details."""
39+
cvcuda = _import_cvcuda()
40+
if not torch.jit.is_scripting() and not torch.jit.is_tracing():
41+
_log_api_usage_once(to_cvcuda_tensor)
42+
if not isinstance(inpt, (torch.Tensor, tv_tensors.Image)):
43+
raise TypeError(f"inpt should be ``torch.Tensor`` or ``tv_tensors.Image``. Got {type(inpt)}.")
44+
if inpt.ndim != 4:
45+
raise ValueError(f"pic should be 4 dimensional. Got {inpt.ndim} dimensions.")
46+
# Convert to NHWC as CVCUDA transforms do not support NCHW
47+
inpt = inpt.permute(0, 2, 3, 1)
48+
return cvcuda.as_tensor(inpt.cuda().contiguous(), cvcuda.TensorLayout.NHWC)
49+
50+
51+
@torch.jit.unused
52+
def cvcuda_to_tensor(cvcuda_img: "cvcuda.Tensor") -> torch.Tensor:
53+
"""See :class:``~torchvision.transforms.v2.CVCUDAToTensor`` for details."""
54+
cvcuda = _import_cvcuda()
55+
if not torch.jit.is_scripting() and not torch.jit.is_tracing():
56+
_log_api_usage_once(cvcuda_to_tensor)
57+
if not isinstance(cvcuda_img, cvcuda.Tensor):
58+
raise TypeError(f"cvcuda_img should be ``cvcuda.Tensor``. Got {type(cvcuda_img)}.")
59+
cuda_tensor = torch.as_tensor(cvcuda_img.cuda(), device="cuda")
60+
if cvcuda_img.ndim != 4:
61+
raise ValueError(f"Image should be 4 dimensional. Got {cuda_tensor.ndim} dimensions.")
62+
# Convert to NCHW shape from CVCUDA default NHWC
63+
img = cuda_tensor.permute(0, 3, 1, 2)
64+
return img

torchvision/transforms/v2/functional/_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,32 @@ def decorator(kernel):
140140
return kernel
141141

142142
return decorator
143+
144+
145+
def _import_cvcuda():
146+
"""Import CV-CUDA modules with informative error message if not installed.
147+
148+
Returns:
149+
cvcuda module.
150+
151+
Raises:
152+
RuntimeError: If CV-CUDA is not installed.
153+
"""
154+
try:
155+
import cvcuda # type: ignore[import-not-found]
156+
157+
return cvcuda
158+
except ImportError as e:
159+
raise ImportError(
160+
"CV-CUDA is required but not installed. "
161+
"Please install it following the instructions at "
162+
"https://github.com/CVCUDA/CV-CUDA."
163+
) from e
164+
165+
166+
def _is_cvcuda_available():
167+
try:
168+
_ = _import_cvcuda()
169+
return True
170+
except ImportError:
171+
return False

0 commit comments

Comments
 (0)