Repository: kornia/kornia

Issue:
[Feat] Add support to other color representations YUV
## 🚀 Feature
 Add support to other color representations YUV 420, 444


Discussion:
YUV 420/422 is typically separated in 2 or 3 planes, do you have thoughts on how best to represent this as torch tensors. E.g. Options seems to be
1. [B,1,H*1,5,W] typical one buffer format that mimics file layout. Quite ugly
2. [B,3,H,W] represent as 444 but just do the subsample operations so that e.g. every 2x2 pixels have the same value. Probably not what you are after
3. [B,1,H,W] (Y) + [B,2,H/2,W/2] for UV (or UV split as well into 3 different tensors). Somewhat sensible but having multiple tensors breaks with other formats.
Do you have any reference material for what you describe ? This was an external feature request, after quick search I found the following snippet.
https://gist.github.com/Quasimondo/c3590226c924a06b276d606f4f189639
> 3. [B,1,H,W] (Y) + [B,2,H/2,W/2] for UV (or UV split as well into 3 different tensors). Somewhat sensible but having multiple tensors breaks with other formats.

This option looks most reasonable.  Looks like opencv also supports some other YUV formats and has a function to;
"Converts an image from one color space to another **where the source image is stored in two planes**."
https://docs.opencv.org/4.5.3/d8/d01/group__imgproc__color__conversions.html#ga8e873314e72a1a6c0252375538fbf753

Would something like that work?


@edgarriba: Right, that link describes the color conversion itself which you already support (at least in a limited fashion, there are multiple different ways of doing this, i.e. rec 709, 2020 as well as 601). Problems comes form doing subsampling as in the case of YUV 420 where we end up with chroma channels that are lower resolution. 420 is 2x reduction both horizontal vertical while 422 split only horizontal. https://en.wikipedia.org/wiki/Chroma_subsampling is a good start.
@cceyda: Agreed that probably does make most  sense. I guess we can limit to just 2 planes as in that example and perhaps do 420 and 422 with just one chrome offset mode (i.e. defining were the chroma samples reside in relation to the luma samples)
if understand correctly we are targeting the following:
```python
def rgb_to_yub(img: Tensor) -> Tuple[Tensor, Tensor]:
    """Converts a tensor image from RGB to YUV color space.

    Args:
       img: the input RGB image with shape (*,3,H,W).

    Returns:
       A tuple containing the converted YUV image.
       - Tensor containing the Y plane with shape (*,1,H,W)
       - Tensor containing the UV planes with shape (*,2,H,W)
    """
    ....
```
The only concern is that later to compose functions will make usability a bit complicated
```python
pipe = nn.Sequential(
    RgbToYuv(),
    RandomRotate(...),  # this expects consistent tensor image
)
out = pipe(torch.rand(1,3,224,224)
```
I suppose the only thing that is compatible with e.g. the augmentations ops would be option 2 (keep same same tensor format just pretend we do subsampling). I suppose that is somewhat useful as an augmentation but I guess the more common use cases (without knowing the kornia use cases deeply):
1. YUV -> RGB transform when YUV data is subsampled, which can happen from compressed formats or, I guess most commonly, if having lower level access to a camera that happens to do YUV 420/422 output. I assume reading from compresed format usually happens through pytorch or opencv which won't serve you subsampled yuv data.
2. When we need to compress the results by e.g. passing data to a device that prefers subsampled YUV (hardware image/video encoders are probably the most common?) in this case this would typically be done as the last step of the pipeline. E.g. read data, do some computer based image manipulaiton, save the data.
3. RGB -> YUV when you algorithm calls for doing something in YUV colorspace instead, typically you would not use subsampled data but if you used it you would typically need som heavy tailoring of your algorithm anyway
Are there other uses? Otherwise the function you suggest (perhaps named rgb_to_yuv420 and yuv420_to_rgb?)
@oskarflordal the general use case for kornia is to have anything useful to be used during and after the training of neural networks with geometric vision focus. However, it's interesting for us to understand other use cases and how we can make kornia more transversal to different vision applications.

I understand the use cases you mention but overall for our purposes would make this operator not really compatible with the rest of kornia components. In general how we assume things is through TITO (tensor-in-tensor-out) and returning tuples produced conflicts in the past within our API. /cc @ducha-aiki @shijianjian 

Besides, I like the your naming proposal. Would you be open to send a PR ? we can assist you in the process.
@edgarriba Yes, I will prepare a PR (code is minimal but plenty of comments/tests to fill in...). I notice the documentation/implementation is inconsistent for the yuv to rgb functions themselves, I will fix that as well (input is supposed to be [-0.5,0.5] for uv instead of [0,1] for the transformation to work properly (I suggest to change the documentation since it is mostly the expected behaviour).
@oskarflordal that's awesome - definitely we can iterate over that.

Resolving patch:
diff --git a/kornia/color/__init__.py b/kornia/color/__init__.py
--- a/kornia/color/__init__.py
+++ b/kornia/color/__init__.py
@@ -23,7 +23,20 @@
 )
 from .xyz import rgb_to_xyz, RgbToXyz, xyz_to_rgb, XyzToRgb
 from .ycbcr import rgb_to_ycbcr, RgbToYcbcr, ycbcr_to_rgb, YcbcrToRgb
-from .yuv import rgb_to_yuv, RgbToYuv, yuv_to_rgb, YuvToRgb
+from .yuv import (
+    rgb_to_yuv,
+    rgb_to_yuv420,
+    rgb_to_yuv422,
+    RgbToYuv,
+    RgbToYuv420,
+    RgbToYuv422,
+    yuv420_to_rgb,
+    Yuv420ToRgb,
+    yuv422_to_rgb,
+    Yuv422ToRgb,
+    yuv_to_rgb,
+    YuvToRgb,
+)
 
 __all__ = [
     "grayscale_to_rgb",
@@ -39,7 +52,11 @@
     "rgb_to_ycbcr",
     "ycbcr_to_rgb",
     "rgb_to_yuv",
+    "rgb_to_yuv420",
+    "rgb_to_yuv422",
     "yuv_to_rgb",
+    "yuv420_to_rgb",
+    "yuv422_to_rgb",
     "rgb_to_xyz",
     "xyz_to_rgb",
     "rgb_to_lab",
@@ -56,7 +73,11 @@
     "RgbToYcbcr",
     "YcbcrToRgb",
     "RgbToYuv",
+    "RgbToYuv420",
+    "RgbToYuv422",
     "YuvToRgb",
+    "Yuv420ToRgb",
+    "Yuv422ToRgb",
     "RgbToXyz",
     "XyzToRgb",
     "RgbToLuv",
diff --git a/kornia/color/yuv.py b/kornia/color/yuv.py
--- a/kornia/color/yuv.py
+++ b/kornia/color/yuv.py
@@ -1,3 +1,5 @@
+from typing import Tuple
+
 import torch
 import torch.nn as nn
 
@@ -38,10 +40,76 @@ def rgb_to_yuv(image: torch.Tensor) -> torch.Tensor:
     return out
 
 
+def rgb_to_yuv420(image: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+    r"""Convert an RGB image to YUV 420 (subsampled).
+
+    The image data is assumed to be in the range of (0, 1). Input need to be padded to be evenly divisible by 2
+    horizontal and vertical. This function will output chroma siting (0.5,0.5)
+
+    Args:
+        image: RGB Image to be converted to YUV with shape :math:`(*, 3, H, W)`.
+
+    Returns:
+       Tensor containing the Y plane with shape :math:`(*, 1, H, W)
+       Tensor containing the UV planes with shape :math:`(*, 2, H/2, W/2)
+
+    Example:
+        >>> input = torch.rand(2, 3, 4, 6)
+        >>> output = rgb_to_yuv420(input)  # (2x1x4x6, 2x2x2x3)
+    """
+    if not isinstance(image, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(image)}")
+
+    if len(image.shape) < 3 or image.shape[-3] != 3:
+        raise ValueError(f"Input size must have a shape of (*, 3, H, W). Got {image.shape}")
+
+    if len(image.shape) < 2 or image.shape[-2] % 2 == 1 or image.shape[-1] % 2 == 1:
+        raise ValueError(f"Input H&W must be evenly disible by 2. Got {image.shape}")
+
+    yuvimage = rgb_to_yuv(image)
+    yuvchunks = torch.chunk(yuvimage, 3, dim=-3)
+
+    return (yuvchunks[0], torch.nn.functional.avg_pool2d(torch.cat(yuvchunks[1:3], dim=-3), (2, 2)))
+
+
+def rgb_to_yuv422(image: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+    r"""Convert an RGB image to YUV 422 (subsampled).
+
+    The image data is assumed to be in the range of (0, 1). Input need to be padded to be evenly divisible by 2
+    vertical. This function will output chroma siting (0.5)
+
+    Args:
+        image: RGB Image to be converted to YUV with shape :math:`(*, 3, H, W)`.
+
+    Returns:
+       Tensor containing the Y plane with shape :math:`(*, 1, H, W)
+       Tensor containing the UV planes with shape :math:`(*, 2, H, W/2)
+
+    Example:
+        >>> input = torch.rand(2, 3, 4, 6)
+        >>> output = rgb_to_yuv420(input)  # (2x1x4x6, 2x1x4x3)
+    """
+    if not isinstance(image, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(image)}")
+
+    if len(image.shape) < 3 or image.shape[-3] != 3:
+        raise ValueError(f"Input size must have a shape of (*, 3, H, W). Got {image.shape}")
+
+    if len(image.shape) < 2 or image.shape[-2] % 2 == 1 or image.shape[-1] % 2 == 1:
+        raise ValueError(f"Input H&W must be evenly disible by 2. Got {image.shape}")
+
+    yuvimage = rgb_to_yuv(image)
+
+    # use chunk to handle 3 and 4 dimensions
+    yuvchunks = torch.chunk(yuvimage, 3, dim=-3)
+
+    return (yuvchunks[0], torch.nn.functional.avg_pool2d(torch.cat(yuvchunks[1:3], dim=-3), (1, 2)))
+
+
 def yuv_to_rgb(image: torch.Tensor) -> torch.Tensor:
     r"""Convert an YUV image to RGB.
 
-    The image data is assumed to be in the range of (0, 1).
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
 
     Args:
         image: YUV Image to be converted to RGB with shape :math:`(*, 3, H, W)`.
@@ -72,6 +140,97 @@ def yuv_to_rgb(image: torch.Tensor) -> torch.Tensor:
     return out
 
 
+def yuv420_to_rgb(imagey: torch.Tensor, imageuv: torch.Tensor) -> torch.Tensor:
+    r"""Convert an YUV420 image to RGB.
+
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
+    Input need to be padded to be evenly divisible by 2 horizontal and vertical.
+    This function assumed chroma siting is (0.5, 0.5)
+
+    Args:
+        imagey: Y (luma) Image plane to be converted to RGB with shape :math:`(*, 1, H, W)`.
+        imageuv: UV (luma) Image planes to be converted to RGB with shape :math:`(*, 2, H/2, W/2)`.
+
+    Returns:
+        RGB version of the image with shape :math:`(*, 3, H, W)`.
+
+    Example:
+        >>> inputy = torch.rand(2, 1, 4, 6)
+        >>> inputuv = torch.rand(2, 2, 2, 3)
+        >>> output = yuv420_to_rgb(inputy, inputuv)  # 2x3x4x6
+    """
+
+    if not isinstance(imagey, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(imagey)}")
+
+    if not isinstance(imageuv, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(imageuv)}")
+
+    if len(imagey.shape) < 3 or imagey.shape[-3] != 1:
+        raise ValueError(f"Input imagey size must have a shape of (*, 1, H, W). Got {imagey.shape}")
+
+    if len(imageuv.shape) < 3 or imageuv.shape[-3] != 2:
+        raise ValueError(f"Input imageuv size must have a shape of (*, 2, H/2, W/2). Got {imageuv.shape}")
+
+    if len(imagey.shape) < 2 or imagey.shape[-2] % 2 == 1 or imagey.shape[-1] % 2 == 1:
+        raise ValueError(f"Input H&W must be evenly disible by 2. Got {imagey.shape}")
+
+    if (len(imageuv.shape) < 2 or len(imagey.shape) < 2 or imagey.shape[-2] / imageuv.shape[-2] != 2
+            or imagey.shape[-1] / imageuv.shape[-1] != 2):
+        raise ValueError(f"Input imageuv H&W must be half the size of the luma plane. "
+                         f"Got {imagey.shape} and {imageuv.shape}")
+
+    # first upsample
+    yuv444image = torch.cat([imagey, imageuv.repeat_interleave(2, dim=-1).repeat_interleave(2, dim=-2)], dim=-3)
+    # then convert the yuv444 tensor
+
+    return yuv_to_rgb(yuv444image)
+
+
+def yuv422_to_rgb(imagey: torch.Tensor, imageuv: torch.Tensor) -> torch.Tensor:
+    r"""Convert an YUV422 image to RGB.
+
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
+    Input need to be padded to be evenly divisible by 2 vertical. This function assumed chroma siting is (0.5)
+
+    Args:
+        imagey: Y (luma) Image plane to be converted to RGB with shape :math:`(*, 1, H, W)`.
+        imageuv: UV (luma) Image planes to be converted to RGB with shape :math:`(*, 2, H, W/2)`.
+
+    Returns:
+        RGB version of the image with shape :math:`(*, 3, H, W)`.
+
+    Example:
+        >>> inputy = torch.rand(2, 1, 4, 6)
+        >>> inputuv = torch.rand(2, 2, 2, 3)
+        >>> output = yuv420_to_rgb(inputy, inputuv)  # 2x3x4x5
+    """
+
+    if not isinstance(imagey, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(imagey)}")
+
+    if not isinstance(imageuv, torch.Tensor):
+        raise TypeError(f"Input type is not a torch.Tensor. Got {type(imageuv)}")
+
+    if len(imagey.shape) < 3 or imagey.shape[-3] != 1:
+        raise ValueError(f"Input imagey size must have a shape of (*, 1, H, W). Got {imagey.shape}")
+
+    if len(imageuv.shape) < 3 or imageuv.shape[-3] != 2:
+        raise ValueError(f"Input imageuv size must have a shape of (*, 2, H, W/2). Got {imageuv.shape}")
+
+    if len(imagey.shape) < 2 or imagey.shape[-2] % 2 == 1 or imagey.shape[-1] % 2 == 1:
+        raise ValueError(f"Input H&W must be evenly disible by 2. Got {imagey.shape}")
+
+    if len(imageuv.shape) < 2 or len(imagey.shape) < 2 or imagey.shape[-1] / imageuv.shape[-1] != 2:
+        raise ValueError(f"Input imageuv W must be half the size of the luma plane. "
+                         f"Got {imagey.shape} and {imageuv.shape}")
+
+    # first upsample
+    yuv444image = torch.cat([imagey, imageuv.repeat_interleave(2, dim=-1)], dim=-3)
+    # then convert the yuv444 tensor
+    return yuv_to_rgb(yuv444image)
+
+
 class RgbToYuv(nn.Module):
     r"""Convert an image from RGB to YUV.
 
@@ -97,10 +256,60 @@ def forward(self, input: torch.Tensor) -> torch.Tensor:
         return rgb_to_yuv(input)
 
 
+class RgbToYuv420(nn.Module):
+    r"""Convert an image from RGB to YUV420.
+
+    The image data is assumed to be in the range of (0, 1). Width and Height evenly divisible by 2.
+
+    Returns:
+        YUV420 version of the image.
+
+    Shape:
+        - image: :math:`(*, 3, H, W)`
+        - output: :math:`(*, 1, H, W)` and :math:`(*, 2, H/2, W/2)`
+
+    Examples:
+        >>> yuvinput = torch.rand(2, 3, 4, 6)
+        >>> yuv = RgbToYuv420()
+        >>> output = yuv(yuvinput)  # # (2x1x4x6, 2x1x2x3)
+
+    Reference::
+        [1] https://es.wikipedia.org/wiki/YUV#RGB_a_Y'UV
+    """
+
+    def forward(self, yuvinput: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:  # skipcq: PYL-R0201
+        return rgb_to_yuv420(yuvinput)
+
+
+class RgbToYuv422(nn.Module):
+    r"""Convert an image from RGB to YUV422.
+
+    The image data is assumed to be in the range of (0, 1). Width evenly disvisible by 2.
+
+    Returns:
+        YUV422 version of the image.
+
+    Shape:
+        - image: :math:`(*, 3, H, W)`
+        - output: :math:`(*, 1, H, W)` and :math:`(*, 2, H, W/2)`
+
+    Examples:
+        >>> yuvinput = torch.rand(2, 3, 4, 6)
+        >>> yuv = RgbToYuv422()
+        >>> output = yuv(yuvinput)  # # (2x1x4x6, 2x2x4x3)
+
+    Reference::
+        [1] https://es.wikipedia.org/wiki/YUV#RGB_a_Y'UV
+    """
+
+    def forward(self, yuvinput: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:  # skipcq: PYL-R0201
+        return rgb_to_yuv422(yuvinput)
+
+
 class YuvToRgb(nn.Module):
     r"""Convert an image from YUV to RGB.
 
-    The image data is assumed to be in the range of (0, 1).
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
 
     Returns:
         RGB version of the image.
@@ -117,3 +326,53 @@ class YuvToRgb(nn.Module):
 
     def forward(self, input: torch.Tensor) -> torch.Tensor:
         return yuv_to_rgb(input)
+
+
+class Yuv420ToRgb(nn.Module):
+    r"""Convert an image from YUV to RGB.
+
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
+    Width and Height evenly divisible by 2.
+
+    Returns:
+        RGB version of the image.
+
+    Shape:
+        - imagey: :math:`(*, 1, H, W)`
+        - imageuv: :math:`(*, 2, H/2, W/2)`
+        - output: :math:`(*, 3, H, W)`
+
+    Examples:
+        >>> inputy = torch.rand(2, 1, 4, 6)
+        >>> inputuv = torch.rand(2, 2, 2, 3)
+        >>> rgb = Yuv420ToRgb()
+        >>> output = rgb(inputy, inputuv)  # 2x3x4x6
+    """
+
+    def forward(self, inputy: torch.Tensor, inputuv: torch.Tensor) -> torch.Tensor:  # skipcq: PYL-R0201
+        return yuv420_to_rgb(inputy, inputuv)
+
+
+class Yuv422ToRgb(nn.Module):
+    r"""Convert an image from YUV to RGB.
+
+    The image data is assumed to be in the range of (0, 1) for luma and (-0.5, 0.5) for chroma.
+    Width evenly divisible by 2.
+
+    Returns:
+        RGB version of the image.
+
+    Shape:
+        - imagey: :math:`(*, 1, H, W)`
+        - imageuv: :math:`(*, 2, H, W/2)`
+        - output: :math:`(*, 3, H, W)`
+
+    Examples:
+        >>> inputy = torch.rand(2, 1, 4, 6)
+        >>> inputuv = torch.rand(2, 2, 4, 3)
+        >>> rgb = Yuv422ToRgb()
+        >>> output = rgb(inputy, inputuv)  # 2x3x4x6
+    """
+
+    def forward(self, inputy: torch.Tensor, inputuv: torch.Tensor) -> torch.Tensor:  # skipcq: PYL-R0201
+        return yuv422_to_rgb(inputy, inputuv)


Test patch:
diff --git a/test/color/test_yuv.py b/test/color/test_yuv.py
--- a/test/color/test_yuv.py
+++ b/test/color/test_yuv.py
@@ -10,7 +10,8 @@ class TestRgbToYuv(BaseTester):
     def test_smoke(self, device, dtype):
         C, H, W = 3, 4, 5
         img = torch.rand(C, H, W, device=device, dtype=dtype)
-        assert isinstance(kornia.color.rgb_to_yuv(img), torch.Tensor)
+        assert isinstance(kornia.color.rgb_to_yuv(img)[0], torch.Tensor)
+        assert isinstance(kornia.color.rgb_to_yuv(img)[1], torch.Tensor)
 
     @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 1), (3, 2, 1)])
     def test_cardinality(self, device, dtype, shape):
@@ -65,6 +66,162 @@ def test_module(self, device, dtype):
         assert_close(ops(img), fcn(img))
 
 
+class TestRgbToYuv420(BaseTester):
+    def test_smoke(self, device, dtype):
+        C, H, W = 3, 4, 6
+        img = torch.rand(C, H, W, device=device, dtype=dtype)
+        assert isinstance(kornia.color.rgb_to_yuv420(img)[0], torch.Tensor)
+        assert isinstance(kornia.color.rgb_to_yuv420(img)[1], torch.Tensor)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 2), (3, 2, 2)])
+    def test_cardinality(self, device, dtype, shape):
+        img = torch.ones(shape, device=device, dtype=dtype)
+        shapey = list(shape)
+        shapey[-3] = 1
+        shapeuv = list(shape)
+        shapeuv[-3] = 2
+        shapeuv[-2] /= 2
+        shapeuv[-1] /= 2
+        assert kornia.color.rgb_to_yuv420(img)[0].shape == tuple(shapey)
+        assert kornia.color.rgb_to_yuv420(img)[1].shape == tuple(shapeuv)
+
+    def test_exception(self, device, dtype):
+        with pytest.raises(TypeError):
+            assert kornia.color.rgb_to_yuv420([0.0])
+
+        with pytest.raises(ValueError):
+            img = torch.ones(1, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv420(img)
+
+        with pytest.raises(ValueError):
+            img = torch.ones(2, 1, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv420(img)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            img = torch.ones(3, 2, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv420(img)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            img = torch.ones(3, 1, 2, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv420(img)
+
+    # TODO: investigate and implement me
+    # def test_unit(self, device, dtype):
+    #    pass
+
+    # This measures accuracy, given the impact of the subsampling we will avoid the issue by
+    # repeating a 2x2 pattern for which mean will match what we get from upscaling again
+    def test_forth_and_back(self, device, dtype):  # skipcq: PYL-R0201
+        data = torch.rand(3, 4, 5, device=device, dtype=dtype).repeat_interleave(2, dim=2).repeat_interleave(2, dim=1)
+
+        yuv = kornia.color.rgb_to_yuv420
+        rgb = kornia.color.yuv420_to_rgb
+        (a, b) = yuv(data)
+        data_out = rgb(a, b)
+        assert_close(data_out, data, rtol=1e-2, atol=1e-2)
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.rand(B, C, H, W, device=device, dtype=torch.float64, requires_grad=True)
+        assert gradcheck(kornia.color.rgb_to_yuv420, (img,), raise_exception=True)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        op = kornia.color.rgb_to_yuv420
+        op_jit = torch.jit.script(op)
+        assert_close(op(img)[0], op_jit(img)[0])
+        assert_close(op(img)[1], op_jit(img)[1])
+
+    @pytest.mark.nn
+    def test_module(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        ops = kornia.color.RgbToYuv420().to(device, dtype)
+        fcn = kornia.color.rgb_to_yuv420
+        assert_close(ops(img)[0], fcn(img)[0])
+        assert_close(ops(img)[1], fcn(img)[1])
+
+
+class TestRgbToYuv422(BaseTester):
+    def test_smoke(self, device, dtype):
+        C, H, W = 3, 4, 6
+        img = torch.rand(C, H, W, device=device, dtype=dtype)
+        assert isinstance(kornia.color.rgb_to_yuv422(img)[0], torch.Tensor)
+        assert isinstance(kornia.color.rgb_to_yuv422(img)[1], torch.Tensor)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 2), (3, 2, 2)])
+    def test_cardinality(self, device, dtype, shape):
+        img = torch.ones(shape, device=device, dtype=dtype)
+        shapey = list(shape)
+        shapey[-3] = 1
+        shapeuv = list(shape)
+        shapeuv[-3] = 2
+        shapeuv[-1] /= 2
+        assert kornia.color.rgb_to_yuv422(img)[0].shape == tuple(shapey)
+        assert kornia.color.rgb_to_yuv422(img)[1].shape == tuple(shapeuv)
+
+    def test_exception(self, device, dtype):
+        with pytest.raises(TypeError):
+            assert kornia.color.rgb_to_yuv422([0.0])
+
+        with pytest.raises(ValueError):
+            img = torch.ones(1, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv422(img)
+
+        with pytest.raises(ValueError):
+            img = torch.ones(2, 1, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv422(img)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            img = torch.ones(3, 2, 1, device=device, dtype=dtype)
+            assert kornia.color.rgb_to_yuv422(img)
+
+    # TODO: investigate and implement me
+    # def test_unit(self, device, dtype):
+    #    pass
+
+    # This measures accuracy, given the impact of the subsampling we will avoid the issue by
+    # repeating a 2x2 pattern for which mena will match what we get from upscaling again
+    def test_forth_and_back(self, device, dtype):  # skipcq: PYL-R0201
+        data = torch.rand(3, 4, 5, device=device, dtype=dtype).repeat_interleave(2, dim=2).repeat_interleave(2, dim=1)
+
+        yuv = kornia.color.rgb_to_yuv422
+        rgb = kornia.color.yuv422_to_rgb
+        (a, b) = yuv(data)
+        data_out = rgb(a, b)
+        assert_close(data_out, data, rtol=1e-2, atol=1e-2)
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.rand(B, C, H, W, device=device, dtype=torch.float64, requires_grad=True)
+        assert gradcheck(kornia.color.rgb_to_yuv422, (img,), raise_exception=True)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        op = kornia.color.rgb_to_yuv422
+        op_jit = torch.jit.script(op)
+        assert_close(op(img)[0], op_jit(img)[0])
+        assert_close(op(img)[1], op_jit(img)[1])
+
+    @pytest.mark.nn
+    def test_module(self, device, dtype):
+        B, C, H, W = 2, 3, 4, 4
+        img = torch.ones(B, C, H, W, device=device, dtype=dtype)
+        ops = kornia.color.RgbToYuv422().to(device, dtype)
+        fcn = kornia.color.rgb_to_yuv422
+        assert_close(ops(img)[0], fcn(img)[0])
+        assert_close(ops(img)[1], fcn(img)[1])
+
+
 class TestYuvToRgb(BaseTester):
     def test_smoke(self, device, dtype):
         C, H, W = 3, 4, 5
@@ -93,7 +250,7 @@ def test_exception(self, device, dtype):
     #    pass
 
     # TODO: improve accuracy
-    def test_forth_and_back(self, device, dtype):
+    def test_forth_and_back(self, device, dtype):  # skipcq: PYL-R0201
         data = torch.rand(3, 4, 5, device=device, dtype=dtype)
         rgb = kornia.color.yuv_to_rgb
         yuv = kornia.color.rgb_to_yuv
@@ -122,3 +279,170 @@ def test_module(self, device, dtype):
         ops = kornia.color.YuvToRgb().to(device, dtype)
         fcn = kornia.color.yuv_to_rgb
         assert_close(ops(img), fcn(img))
+
+
+class TestYuv420ToRgb(BaseTester):
+    def test_smoke(self, device, dtype):
+        H, W = 4, 6
+        imgy = torch.rand(1, H, W, device=device, dtype=dtype)
+        imguv = torch.rand(2, int(H / 2), int(W / 2), device=device, dtype=dtype)
+        assert isinstance(kornia.color.yuv420_to_rgb(imgy, imguv), torch.Tensor)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 2), (3, 2, 2)])
+    def test_cardinality(self, device, dtype, shape):
+        shapey = list(shape)
+        shapey[-3] = 1
+        shapeuv = list(shape)
+        shapeuv[-3] = 2
+        shapeuv[-2] = int(shapeuv[-2] / 2)
+        shapeuv[-1] = int(shapeuv[-1] / 2)
+
+        imgy = torch.ones(shapey, device=device, dtype=dtype)
+        imguv = torch.ones(shapeuv, device=device, dtype=dtype)
+        assert kornia.color.yuv420_to_rgb(imgy, imguv).shape == shape
+
+    def test_exception(self, device, dtype):
+        with pytest.raises(TypeError):
+            assert kornia.color.yuv420_to_rgb([0.0], [0.0])
+
+        with pytest.raises(ValueError):
+            imguv = torch.ones(1, 1, device=device, dtype=dtype)
+            imgy = torch.ones(1, 1, device=device, dtype=dtype)
+            assert kornia.color.yuv420_to_rgb(imgy, imguv)
+
+        with pytest.raises(ValueError):
+            imgy = torch.ones(2, 2, 2, device=device, dtype=dtype)
+            imguv = torch.ones(2, 1, 1, device=device, dtype=dtype)
+            assert kornia.color.yuv420_to_rgb(imgy, imguv)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            imgy = torch.ones(3, 2, 1, device=device, dtype=dtype)
+            imguv = torch.ones(3, 1, 0, device=device, dtype=dtype)
+            assert kornia.color.yuv420_to_rgb(imgy, imguv)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            imgy = torch.ones(3, 1, 2, device=device, dtype=dtype)
+            imguv = torch.ones(3, 0, 1, device=device, dtype=dtype)
+            assert kornia.color.yuv420_to_rgb(imgy, imguv)
+
+    # TODO: investigate and implement me
+    # def test_unit(self, device, dtype):
+    #    pass
+
+    # TODO: improve accuracy
+    def test_forth_and_back(self, device, dtype):  # skipcq: PYL-R0201
+        datay = torch.rand(1, 4, 6, device=device, dtype=dtype)
+        datauv = torch.rand(2, 2, 3, device=device, dtype=dtype)
+        rgb = kornia.color.yuv420_to_rgb
+        yuv = kornia.color.rgb_to_yuv420
+
+        (data_outy, data_outuv) = yuv(rgb(datay, datauv))
+        assert_close(data_outy, datay, rtol=1e-2, atol=1e-2)
+        assert_close(data_outuv, datauv, rtol=1e-2, atol=1e-2)
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.rand(B, 1, H, W, device=device, dtype=torch.float64, requires_grad=True)
+        imguv = torch.rand(B, 2, int(H / 2), int(W / 2), device=device, dtype=torch.float64, requires_grad=True)
+        assert gradcheck(kornia.color.yuv420_to_rgb, (imgy, imguv), raise_exception=True)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.ones(B, 1, H, W, device=device, dtype=dtype)
+        imguv = torch.ones(B, 2, int(H / 2), int(W / 2), device=device, dtype=dtype)
+        op = kornia.color.yuv420_to_rgb
+        op_jit = torch.jit.script(op)
+        assert_close(op(imgy, imguv), op_jit(imgy, imguv))
+
+    @pytest.mark.nn
+    def test_module(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.ones(B, 1, H, W, device=device, dtype=dtype)
+        imguv = torch.ones(B, 2, int(H / 2), int(W / 2), device=device, dtype=dtype)
+        ops = kornia.color.Yuv420ToRgb().to(device, dtype)
+        fcn = kornia.color.yuv420_to_rgb
+        assert_close(ops(imgy, imguv), fcn(imgy, imguv))
+
+
+class TestYuv422ToRgb(BaseTester):
+    def test_smoke(self, device, dtype):
+        H, W = 4, 6
+        imgy = torch.rand(1, H, W, device=device, dtype=dtype)
+        imguv = torch.rand(2, H, int(W / 2), device=device, dtype=dtype)
+        assert isinstance(kornia.color.yuv422_to_rgb(imgy, imguv), torch.Tensor)
+
+    @pytest.mark.parametrize("shape", [(1, 3, 4, 4), (2, 3, 2, 4), (3, 3, 4, 2), (3, 2, 2)])
+    def test_cardinality(self, device, dtype, shape):
+        shapey = list(shape)
+        shapey[-3] = 1
+        shapeuv = list(shape)
+        shapeuv[-3] = 2
+        shapeuv[-1] = int(shapeuv[-1] / 2)
+
+        imgy = torch.ones(shapey, device=device, dtype=dtype)
+        imguv = torch.ones(shapeuv, device=device, dtype=dtype)
+        assert kornia.color.yuv422_to_rgb(imgy, imguv).shape == shape
+
+    def test_exception(self, device, dtype):
+        with pytest.raises(TypeError):
+            assert kornia.color.yuv422_to_rgb([0.0], [0.0])
+
+        with pytest.raises(ValueError):
+            imguv = torch.ones(1, 1, device=device, dtype=dtype)
+            imgy = torch.ones(1, 1, device=device, dtype=dtype)
+            assert kornia.color.yuv422_to_rgb(imgy, imguv)
+
+        with pytest.raises(ValueError):
+            imgy = torch.ones(2, 2, 2, device=device, dtype=dtype)
+            imguv = torch.ones(2, 1, 1, device=device, dtype=dtype)
+            assert kornia.color.yuv422_to_rgb(imgy, imguv)
+
+        # dimensionality test
+        with pytest.raises(ValueError):
+            imgy = torch.ones(3, 2, 1, device=device, dtype=dtype)
+            imguv = torch.ones(3, 1, 0, device=device, dtype=dtype)
+            assert kornia.color.yuv422_to_rgb(imgy, imguv)
+
+    # TODO: investigate and implement me
+    # def test_unit(self, device, dtype):
+    #    pass
+
+    # TODO: improve accuracy
+    def test_forth_and_back(self, device, dtype):  # skipcq: PYL-R0201
+        datay = torch.rand(1, 4, 6, device=device, dtype=dtype)
+        datauv = torch.rand(2, 4, 3, device=device, dtype=dtype)
+        rgb = kornia.color.yuv422_to_rgb
+        yuv = kornia.color.rgb_to_yuv422
+
+        (data_outy, data_outuv) = yuv(rgb(datay, datauv))
+        assert_close(data_outy, datay, rtol=1e-2, atol=1e-2)
+        assert_close(data_outuv, datauv, rtol=1e-2, atol=1e-2)
+
+    @pytest.mark.grad
+    def test_gradcheck(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.rand(B, 1, H, W, device=device, dtype=torch.float64, requires_grad=True)
+        imguv = torch.rand(B, 2, H, int(W / 2), device=device, dtype=torch.float64, requires_grad=True)
+        assert gradcheck(kornia.color.yuv422_to_rgb, (imgy, imguv), raise_exception=True)
+
+    @pytest.mark.jit
+    def test_jit(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.ones(B, 1, H, W, device=device, dtype=dtype)
+        imguv = torch.ones(B, 2, H, int(W / 2), device=device, dtype=dtype)
+        op = kornia.color.yuv422_to_rgb
+        op_jit = torch.jit.script(op)
+        assert_close(op(imgy, imguv), op_jit(imgy, imguv))
+
+    @pytest.mark.nn
+    def test_module(self, device, dtype):
+        B, H, W = 2, 4, 4
+        imgy = torch.ones(B, 1, H, W, device=device, dtype=dtype)
+        imguv = torch.ones(B, 2, H, int(W / 2), device=device, dtype=dtype)
+        ops = kornia.color.Yuv422ToRgb().to(device, dtype)
+        fcn = kornia.color.yuv422_to_rgb
+        assert_close(ops(imgy, imguv), fcn(imgy, imguv))