Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions test/test_transforms_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4972,6 +4972,62 @@ def test_random_transform_correctness(self, num_input_channels):
assert_equal(actual, expected, rtol=0, atol=1)


class TestGrayscaleToRgb:
@pytest.mark.parametrize("dtype", [torch.uint8, torch.float32])
@pytest.mark.parametrize("device", cpu_and_cuda())
def test_kernel_image(self, dtype, device):
check_kernel(F.grayscale_to_rgb_image, make_image(dtype=dtype, device=device))

@pytest.mark.parametrize("make_input", [make_image_tensor, make_image_pil, make_image])
def test_functional(self, make_input):
check_functional(F.grayscale_to_rgb, make_input())

@pytest.mark.parametrize(
("kernel", "input_type"),
[
(F.rgb_to_grayscale_image, torch.Tensor),
(F._rgb_to_grayscale_image_pil, PIL.Image.Image),
(F.rgb_to_grayscale_image, tv_tensors.Image),
],
)
def test_functional_signature(self, kernel, input_type):
check_functional_kernel_signature_match(F.grayscale_to_rgb, kernel=kernel, input_type=input_type)

@pytest.mark.parametrize("make_input", [make_image_tensor, make_image_pil, make_image])
def test_transform(self, make_input):
check_transform(transforms.GrayscaleToRgb(), make_input(color_space="GRAY"))

@pytest.mark.parametrize("fn", [F.grayscale_to_rgb, transform_cls_to_functional(transforms.GrayscaleToRgb)])
def test_image_correctness(self, fn):
image = make_image(dtype=torch.uint8, device="cpu", color_space="GRAY")

actual = fn(image)
expected = F.to_image(F.grayscale_to_rgb(F.to_pil_image(image)))

assert_equal(actual, expected, rtol=0, atol=1)

def test_expanded_channels_are_not_views_into_the_same_underlying_tensor(self):
image = make_image(dtype=torch.uint8, device="cpu", color_space="GRAY")

output_image = F.grayscale_to_rgb(image)
assert_equal(output_image[0][0][0], output_image[1][0][0])
output_image[0][0][0] = output_image[0][0][0] + 1
assert output_image[0][0][0] != output_image[1][0][0]

def test_rgb_image_is_unchanged(self):
image = make_image(dtype=torch.uint8, device="cpu", color_space="RGB")
assert_equal(image.shape[-3], 3)
image[0][0][0] = 0
image[1][0][0] = 100
image[2][0][0] = 200
output_image = F.grayscale_to_rgb(image)
assert output_image[0][0][0] == 0
assert output_image[1][0][0] == 100
assert output_image[2][0][0] == 200
print(image)
print(output_image)


class TestRandomZoomOut:
# Tests are light because this largely relies on the already tested `pad` kernels.

Expand Down
1 change: 1 addition & 0 deletions torchvision/transforms/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ._color import (
ColorJitter,
Grayscale,
GrayscaleToRgb,
RandomAdjustSharpness,
RandomAutocontrast,
RandomChannelPermutation,
Expand Down
14 changes: 14 additions & 0 deletions torchvision/transforms/v2/_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ def _transform(self, inpt: Any, params: Dict[str, Any]) -> Any:
return self._call_kernel(F.rgb_to_grayscale, inpt, num_output_channels=params["num_input_channels"])


class GrayscaleToRgb(Transform):
"""Converts grayscale images to RGB images.

If the input is a :class:`torch.Tensor`, it is expected
to have [..., 1 or 3, H, W] shape, where ... means an arbitrary number of leading dimensions
"""

def __init__(self):
super().__init__()

def _transform(self, inpt: Any, params: Dict[str, Any]) -> Any:
return self._call_kernel(F.grayscale_to_rgb, inpt)


class ColorJitter(Transform):
"""Randomly change the brightness, contrast, saturation and hue of an image or video.

Expand Down
2 changes: 2 additions & 0 deletions torchvision/transforms/v2/functional/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
equalize,
equalize_image,
equalize_video,
grayscale_to_rgb,
grayscale_to_rgb_image,
invert,
invert_image,
invert_video,
Expand Down
26 changes: 26 additions & 0 deletions torchvision/transforms/v2/functional/_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ def _rgb_to_grayscale_image_pil(image: PIL.Image.Image, num_output_channels: int
return _FP.to_grayscale(image, num_output_channels=num_output_channels)


def grayscale_to_rgb(inpt: torch.Tensor) -> torch.Tensor:
"""See :class:`~torchvision.transforms.v2.GrayscaleToRgb` for details."""
if torch.jit.is_scripting():
return grayscale_to_rgb_image(inpt)

_log_api_usage_once(grayscale_to_rgb)

kernel = _get_kernel(grayscale_to_rgb, type(inpt))
return kernel(inpt)


@_register_kernel_internal(grayscale_to_rgb, torch.Tensor)
@_register_kernel_internal(grayscale_to_rgb, tv_tensors.Image)
def grayscale_to_rgb_image(image: torch.Tensor) -> torch.Tensor:
if image.shape[-3] >= 3:
# Image already has RGB channels. We don't need to do anything.
return image
# rgb_to_grayscale can be used to add channels so we reuse that function.
return _rgb_to_grayscale_image(image, num_output_channels=3, preserve_dtype=True)


@_register_kernel_internal(grayscale_to_rgb, PIL.Image.Image)
def grayscale_to_rgb_image_pil(image: PIL.Image.Image) -> PIL.Image.Image:
return image.convert(mode="RGB")


def _blend(image1: torch.Tensor, image2: torch.Tensor, ratio: float) -> torch.Tensor:
ratio = float(ratio)
fp = image1.is_floating_point()
Expand Down