diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index f2a75b33..7318049a 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -224,3 +224,6 @@ - Evgeni Burovski Adaptation of ImageJ 3D skeletonization algorithm. + +- Alex Izvorski + Color spaces for YUV and related spaces diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index d68332f4..80d8a03a 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -22,6 +22,14 @@ from .colorconv import (convert_colorspace, hed2rgb, lab2lch, lch2lab, + rgb2yuv, + yuv2rgb, + rgb2yiq, + yiq2rgb, + rgb2ypbpr, + ypbpr2rgb, + rgb2ycbcr, + ycbcr2rgb, separate_stains, combine_stains, rgb_from_hed, @@ -75,6 +83,14 @@ __all__ = ['convert_colorspace', 'hed2rgb', 'lab2lch', 'lch2lab', + 'rgb2yuv', + 'yuv2rgb', + 'rgb2yiq', + 'yiq2rgb', + 'rgb2ypbpr', + 'ypbpr2rgb', + 'rgb2ycbcr', + 'ycbcr2rgb', 'separate_stains', 'combine_stains', 'rgb_from_hed', diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 93961c49..08cd1775 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -40,6 +40,7 @@ Supported color spaces :author: Ralf Gommers (hsv2rgb) :author: Travis Oliphant (XYZ and RGB CIE functions) :author: Matt Terry (lab2lch) +:author: Alex Izvorski (yuv2rgb, rgb2yuv and related) :license: modified BSD @@ -124,9 +125,11 @@ def convert_colorspace(arr, fromspace, tospace): >>> img_hsv = convert_colorspace(img, 'RGB', 'HSV') """ fromdict = {'RGB': lambda im: im, 'HSV': hsv2rgb, 'RGB CIE': rgbcie2rgb, - 'XYZ': xyz2rgb} + 'XYZ': xyz2rgb, 'YUV': yuv2rgb, 'YIQ': yiq2rgb, + 'YPbPr': ypbpr2rgb, 'YCbCr': ycbcr2rgb } todict = {'RGB': lambda im: im, 'HSV': rgb2hsv, 'RGB CIE': rgb2rgbcie, - 'XYZ': rgb2xyz} + 'XYZ': rgb2xyz, 'YUV': rgb2yuv, 'YIQ': rgb2yiq, + 'YPbPr': rgb2ypbpr, 'YCbCr': rgb2ycbcr } fromspace = fromspace.upper() tospace = tospace.upper() @@ -322,6 +325,30 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) +yuv_from_rgb = np.array([[ 0.299 , 0.587 , 0.114 ], + [-0.14714119, -0.28886916, 0.43601035 ], + [ 0.61497538, -0.51496512, -0.10001026 ]]) + +rgb_from_yuv = linalg.inv(yuv_from_rgb) + +yiq_from_rgb = np.array([[0.299 , 0.587 , 0.114 ], + [0.59590059, -0.27455667, -0.32134392], + [0.21153661, -0.52273617, 0.31119955]]) + +rgb_from_yiq = linalg.inv(yiq_from_rgb) + +ypbpr_from_rgb = np.array([[ 0.299 , 0.587 , 0.114 ], + [-0.168736,-0.331264, 0.5 ], + [ 0.5 ,-0.418688,-0.081312]]) + +rgb_from_ypbpr = linalg.inv(ypbpr_from_rgb) + +ycbcr_from_rgb = np.array([[ 65.481, 128.553, 24.966], + [ -37.797, -74.203, 112.0 ], + [ 112.0 , -93.786, -18.214]]) + +rgb_from_ycbcr = linalg.inv(ycbcr_from_rgb) + # CIE LAB constants for Observer=2A, Illuminant=D65 # NOTE: this is actually the XYZ values for the illuminant above. lab_ref_white = np.array([0.95047, 1., 1.08883]) @@ -1450,3 +1477,210 @@ def _prepare_lab_array(arr): if shape[-1] < 3: raise ValueError('Input array has less than 3 color channels') return dtype.img_as_float(arr, force_copy=True) + + +def rgb2yuv(rgb): + """RGB to YUV color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in YUV format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + Y is between 0 and 1. Use YCbCr instead of YUV for the color space which + is commonly used by video codecs (where Y ranges from 16 to 235) + """ + return _convert(yuv_from_rgb, rgb) + + +def rgb2yiq(rgb): + """RGB to YIQ color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in YIQ format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + """ + return _convert(yiq_from_rgb, rgb) + + +def rgb2ypbpr(rgb): + """RGB to YIQ color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in YIQ format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + """ + return _convert(ypbpr_from_rgb, rgb) + + +def rgb2ycbcr(rgb): + """RGB to YCbCr color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in YCbCr format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + Y is between 16 and 235. This is the color space which is commonly used + by video codecs, it is sometimes incorrectly called "YUV" + """ + arr = _convert(ycbcr_from_rgb, rgb) + arr[..., 0] += 16 + arr[..., 1] += 128 + arr[..., 2] += 128 + return arr + + +def yuv2rgb(yuv): + """RGB to YIQ color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in YIQ format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + """ + return _convert(rgb_from_yuv, yuv) + + +def yiq2rgb(yiq): + """YIQ to RGB color space conversion. + + Parameters + ---------- + yiq : array_like + The image in YIQ format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `yiq` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + """ + return _convert(rgb_from_yiq, yiq) + + +def ypbpr2rgb(ypbpr): + """YPbPr to RGB color space conversion. + + Parameters + ---------- + ypbpr : array_like + The image in YPbPr format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `ypbpr` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + """ + return _convert(rgb_from_ypbpr, ypbpr) + + +def ycbcr2rgb(ycbcr): + """YCbCr to RGB color space conversion. + + Parameters + ---------- + ycbcr : array_like + The image in YCbCr format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Returns + ------- + out : ndarray + The image in RGB format, in a 3- or 4-D array of shape + ``(M, N, [P,] 3)``. + + Raises + ------ + ValueError + If `ycbcr` is not a 3- or 4-D array of shape ``(M, N, [P,] 3)``. + + Notes + ----- + Y is between 16 and 235. This is the color space which is commonly used + by video codecs, it is sometimes incorrectly called "YUV" + """ + arr = ycbcr.copy() + arr[..., 0] -= 16 + arr[..., 1] -= 128 + arr[..., 2] -= 128 + return _convert(rgb_from_ycbcr, arr) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 96057e3b..106eabee 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -37,6 +37,10 @@ from skimage.color import (rgb2hsv, hsv2rgb, xyz2luv, luv2xyz, luv2rgb, rgb2luv, lab2lch, lch2lab, + rgb2yuv, yuv2rgb, + rgb2yiq, yiq2rgb, + rgb2ypbpr, ypbpr2rgb, + rgb2ycbcr, ycbcr2rgb, guess_spatial_dimensions ) @@ -438,6 +442,33 @@ class TestColorconv(TestCase): rgb = img_as_float(self.img_rgb[:1, :1, :]) return rgb2lab(rgb)[0, 0, :] + def test_yuv(self): + rgb = np.array([[[1.0, 1.0, 1.0]]]) + assert_array_almost_equal(rgb2yuv(rgb), np.array([[[1, 0, 0]]])) + assert_array_almost_equal(rgb2yiq(rgb), np.array([[[1, 0, 0]]])) + assert_array_almost_equal(rgb2ypbpr(rgb), np.array([[[1, 0, 0]]])) + assert_array_almost_equal(rgb2ycbcr(rgb), np.array([[[235, 128, 128]]])) + rgb = np.array([[[0.0, 1.0, 0.0]]]) + assert_array_almost_equal(rgb2yuv(rgb), np.array([[[0.587, -0.28886916, -0.51496512]]])) + assert_array_almost_equal(rgb2yiq(rgb), np.array([[[0.587, -0.27455667, -0.52273617]]])) + assert_array_almost_equal(rgb2ypbpr(rgb), np.array([[[0.587, -0.331264, -0.418688]]])) + assert_array_almost_equal(rgb2ycbcr(rgb), np.array([[[144.553, 53.797, 34.214]]])) + + def test_yuv_roundtrip(self): + img_rgb = img_as_float(self.img_rgb)[::16, ::16] + assert_array_almost_equal(yuv2rgb(rgb2yuv(img_rgb)), img_rgb) + assert_array_almost_equal(yiq2rgb(rgb2yiq(img_rgb)), img_rgb) + assert_array_almost_equal(ypbpr2rgb(rgb2ypbpr(img_rgb)), img_rgb) + assert_array_almost_equal(ycbcr2rgb(rgb2ycbcr(img_rgb)), img_rgb) + + def test_rgb2yiq_conversion(self): + rgb = img_as_float(self.img_rgb)[::16, ::16] + yiq = rgb2yiq(rgb).reshape(-1, 3) + gt = np.array([colorsys.rgb_to_yiq(pt[0], pt[1], pt[2]) + for pt in rgb.reshape(-1, 3)] + ) + assert_almost_equal(yiq, gt, decimal=2) + def test_gray2rgb(): x = np.array([0, 0.5, 1])