diff --git a/colour/__init__.py b/colour/__init__.py index 33f6e82cd6..c5d2414f52 100644 --- a/colour/__init__.py +++ b/colour/__init__.py @@ -127,9 +127,10 @@ chromatically_adapted_primaries, eotf, eotf_inverse, full_to_legal, gamma_function, hdr_CIELab_to_XYZ, hdr_IPT_to_XYZ, legal_to_full, linear_function, log_decoding, log_encoding, matrix_RGB_to_RGB, - normalised_primary_matrix, oetf, oetf_inverse, ootf, ootf_inverse, - primaries_whitepoint, sRGB_to_XYZ, uv_to_Luv, uv_to_UCS, xyY_to_XYZ, - xyY_to_xy, xy_to_Luv_uv, xy_to_UCS_uv, xy_to_XYZ, xy_to_xyY) + matrix_YCbCr, normalised_primary_matrix, oetf, oetf_inverse, offset_YCbCr, + ootf, ootf_inverse, primaries_whitepoint, sRGB_to_XYZ, uv_to_Luv, + uv_to_UCS, xyY_to_XYZ, xyY_to_xy, xy_to_Luv_uv, xy_to_UCS_uv, xy_to_XYZ, + xy_to_xyY) from .corresponding import ( BRENEMAN_EXPERIMENTS, BRENEMAN_EXPERIMENT_PRIMARIES_CHROMATICITIES, CORRESPONDING_CHROMATICITIES_PREDICTION_MODELS, CorrespondingColourDataset, @@ -283,10 +284,10 @@ def __getattr__(self, attribute): 'chromatically_adapted_primaries', 'eotf', 'eotf_inverse', 'full_to_legal', 'gamma_function', 'hdr_CIELab_to_XYZ', 'hdr_IPT_to_XYZ', 'legal_to_full', 'linear_function', 'log_decoding', 'log_encoding', 'matrix_RGB_to_RGB', - 'normalised_primary_matrix', 'oetf', 'oetf_inverse', 'ootf', - 'ootf_inverse', 'primaries_whitepoint', 'sRGB_to_XYZ', 'uv_to_Luv', - 'uv_to_UCS', 'xyY_to_XYZ', 'xyY_to_xy', 'xy_to_Luv_uv', 'xy_to_UCS_uv', - 'xy_to_XYZ', 'xy_to_xyY' + 'matrix_YCbCr', 'normalised_primary_matrix', 'oetf', 'oetf_inverse', + 'offset_YCbCr', 'ootf', 'ootf_inverse', 'primaries_whitepoint', + 'sRGB_to_XYZ', 'uv_to_Luv', 'uv_to_UCS', 'xyY_to_XYZ', 'xyY_to_xy', + 'xy_to_Luv_uv', 'xy_to_UCS_uv', 'xy_to_XYZ', 'xy_to_xyY' ] __all__ += [ 'BRENEMAN_EXPERIMENTS', 'BRENEMAN_EXPERIMENT_PRIMARIES_CHROMATICITIES', diff --git a/colour/models/rgb/__init__.py b/colour/models/rgb/__init__.py index 823ddb7f1e..1c41898a98 100644 --- a/colour/models/rgb/__init__.py +++ b/colour/models/rgb/__init__.py @@ -14,8 +14,8 @@ from .cylindrical import RGB_to_HSV, HSV_to_RGB, RGB_to_HSL, HSL_to_RGB from .cmyk import RGB_to_CMY, CMY_to_RGB, CMY_to_CMYK, CMYK_to_CMY from .prismatic import RGB_to_Prismatic, Prismatic_to_RGB -from .ycbcr import (WEIGHTS_YCBCR, RGB_to_YCbCr, YCbCr_to_RGB, RGB_to_YcCbcCrc, - YcCbcCrc_to_RGB) +from .ycbcr import (WEIGHTS_YCBCR, matrix_YCbCr, offset_YCbCr, RGB_to_YCbCr, + YCbCr_to_RGB, RGB_to_YcCbcCrc, YcCbcCrc_to_RGB) from .ycocg import RGB_to_YCoCg, YCoCg_to_RGB from .ictcp import RGB_to_ICtCp, ICtCp_to_RGB, XYZ_to_ICtCp, ICtCp_to_XYZ @@ -33,8 +33,8 @@ __all__ += ['RGB_to_CMY', 'CMY_to_RGB', 'CMY_to_CMYK', 'CMYK_to_CMY'] __all__ += ['RGB_to_Prismatic', 'Prismatic_to_RGB'] __all__ += [ - 'WEIGHTS_YCBCR', 'RGB_to_YCbCr', 'YCbCr_to_RGB', 'RGB_to_YcCbcCrc', - 'YcCbcCrc_to_RGB' + 'WEIGHTS_YCBCR', 'matrix_YCbCr', 'offset_YCbCr', 'RGB_to_YCbCr', + 'YCbCr_to_RGB', 'RGB_to_YcCbcCrc', 'YcCbcCrc_to_RGB' ] __all__ += ['RGB_to_YCoCg', 'YCoCg_to_RGB'] __all__ += ['RGB_to_ICtCp', 'ICtCp_to_RGB', 'XYZ_to_ICtCp', 'ICtCp_to_XYZ'] diff --git a/colour/models/rgb/tests/test_ycbcr.py b/colour/models/rgb/tests/test_ycbcr.py index 9e31b1e406..f60c4dc786 100644 --- a/colour/models/rgb/tests/test_ycbcr.py +++ b/colour/models/rgb/tests/test_ycbcr.py @@ -7,9 +7,9 @@ import unittest from itertools import permutations -from colour.models.rgb.ycbcr import (RGB_to_YCbCr, YCbCr_to_RGB, - RGB_to_YcCbcCrc, YcCbcCrc_to_RGB, - WEIGHTS_YCBCR) +from colour.models.rgb.ycbcr import (matrix_YCbCr, offset_YCbCr, RGB_to_YCbCr, + YCbCr_to_RGB, RGB_to_YcCbcCrc, + YcCbcCrc_to_RGB, WEIGHTS_YCBCR) from colour.utilities import domain_range_scale, ignore_numpy_errors __author__ = 'Colour Developers' @@ -20,11 +20,100 @@ __status__ = 'Development' __all__ = [ - 'TestRGB_to_YCbCr', 'TestYCbCr_to_RGB', 'TestRGB_to_YcCbcCrc', - 'TestYcCbcCrc_to_RGB' + 'TestMatrixYCbCr', 'TestOffsetYCbCr', 'TestRGB_to_YCbCr', + 'TestYCbCr_to_RGB', 'TestRGB_to_YcCbcCrc', 'TestYcCbcCrc_to_RGB' ] +class TestMatrixYCbCr(unittest.TestCase): + """ + Defines :func:`colour.models.rgb.ycbcr.matrix_YCbCr` definition unit tests + methods. + """ + + def test_matrix_YCbCr(self): + """ + Tests :func:`colour.models.rgb.ycbcr.matrix_YCbCr` definition. + """ + + np.testing.assert_almost_equal( + matrix_YCbCr(), + np.array([ + [1.00000000, 0.00000000, 1.57480000], + [1.00000000, -0.18732427, -0.46812427], + [1.00000000, 1.85560000, 0.00000000], + ]), + decimal=7) + + np.testing.assert_almost_equal( + matrix_YCbCr(K=WEIGHTS_YCBCR['ITU-R BT.601']), + np.array([ + [1.00000000, 0.00000000, 1.40200000], + [1.00000000, -0.34413629, -0.71413629], + [1.00000000, 1.77200000, -0.00000000], + ]), + decimal=7) + + np.testing.assert_almost_equal( + matrix_YCbCr(is_legal=True), + np.array([ + [1.16438356, 0.00000000, 1.79274107], + [1.16438356, -0.21324861, -0.53290933], + [1.16438356, 2.11240179, -0.00000000], + ]), + decimal=7) + + np.testing.assert_almost_equal( + matrix_YCbCr(bits=10), + np.array([ + [1.00000000, 0.00000000, 1.57480000], + [1.00000000, -0.18732427, -0.46812427], + [1.00000000, 1.85560000, 0.00000000], + ]), + decimal=7) + + np.testing.assert_almost_equal( + matrix_YCbCr(bits=10, is_int=True), + np.array([ + [0.00097752, 0.00000000, 0.00153789], + [0.00097752, -0.00018293, -0.00045715], + [0.00097752, 0.00181211, 0.00000000], + ]), + decimal=7) + + +class TestOffsetYCbCr(unittest.TestCase): + """ + Defines :func:`colour.models.rgb.ycbcr.offset_YCbCr` definition unit tests + methods. + """ + + def test_offset_YCbCr(self): + """ + Tests :func:`colour.models.rgb.ycbcr.offset_YCbCr` definition. + """ + + np.testing.assert_almost_equal( + offset_YCbCr(), + np.array([0.00000000, 0.00000000, 0.00000000]), + decimal=7) + + np.testing.assert_almost_equal( + offset_YCbCr(is_legal=True), + np.array([0.06274510, 0.50196078, 0.50196078]), + decimal=7) + + np.testing.assert_almost_equal( + offset_YCbCr(bits=10), + np.array([0.00000000, 0.00000000, 0.00000000]), + decimal=7) + + np.testing.assert_almost_equal( + offset_YCbCr(bits=10, is_int=True), + np.array([0.00000000, 512.00000000, 512.00000000]), + decimal=7) + + class TestRGB_to_YCbCr(unittest.TestCase): """ Defines :func:`colour.models.rgb.ycbcr.RGB_to_YCbCr` definition unit tests diff --git a/colour/models/rgb/ycbcr.py b/colour/models/rgb/ycbcr.py index a820d3f35a..3f6c7d53d8 100644 --- a/colour/models/rgb/ycbcr.py +++ b/colour/models/rgb/ycbcr.py @@ -5,6 +5,9 @@ Defines the *Y'CbCr* colour encoding related attributes and objects: +- :attr:`colour.WEIGHTS_YCBCR` +- :func:`colour.matrix_YCbCr` +- :func:`colour.offset_YCbCr` - :func:`colour.RGB_to_YCbCr` - :func:`colour.YCbCr_to_RGB` - :func:`colour.RGB_to_YcCbcCrc` @@ -60,8 +63,8 @@ __status__ = 'Development' __all__ = [ - 'WEIGHTS_YCBCR', 'ranges_YCbCr', 'RGB_to_YCbCr', 'YCbCr_to_RGB', - 'RGB_to_YcCbcCrc', 'YcCbcCrc_to_RGB' + 'WEIGHTS_YCBCR', 'ranges_YCbCr', 'matrix_YCbCr', 'offset_YCbCr', + 'RGB_to_YCbCr', 'YCbCr_to_RGB', 'RGB_to_YcCbcCrc', 'YcCbcCrc_to_RGB' ] WEIGHTS_YCBCR = CaseInsensitiveMapping({ @@ -135,6 +138,129 @@ def ranges_YCbCr(bits, is_legal, is_int): return ranges +def matrix_YCbCr(K=WEIGHTS_YCBCR['ITU-R BT.709'], + bits=8, + is_legal=False, + is_int=False): + """ + Computes the *R'G'B'* to *Y'CbCr* matrix for given weights, bit depth, + range legality and representation. + + The related offset for the *R'G'B'* to *Y'CbCr* matrix can be computed with + the :func:`colour.offset_YCbCr` definition. + + Parameters + ---------- + K : array_like, optional + Luma weighting coefficients of red and blue. See + :attr:`colour.WEIGHTS_YCBCR` for presets. Default is + *(0.2126, 0.0722)*, the weightings for *ITU-R BT.709*. + bits : int + Bit depth of the *Y'CbCr* colour encoding ranges array. + is_legal : bool + Whether the *Y'CbCr* colour encoding ranges array is legal. + is_int : bool + Whether the *Y'CbCr* colour encoding ranges array represents integer + code values. + + Returns + ------- + ndarray + *Y'CbCr* matrix. + + Examples + -------- + >>> matrix_YCbCr() # doctest: +ELLIPSIS + array([[ 1.0000000...e+00, ..., 1.5748000...e+00], + [ 1.0000000...e+00, -1.8732427...e-01, -4.6812427...e-01], + [ 1.0000000...e+00, 1.8556000...e+00, ...]]) + >>> matrix_YCbCr(K=WEIGHTS_YCBCR['ITU-R BT.601']) # doctest: +ELLIPSIS + array([[ 1.0000000...e+00, ..., 1.4020000...e+00], + [ 1.0000000...e+00, -3.4413628...e-01, -7.1413628...e-01], + [ 1.0000000...e+00, 1.7720000...e+00, ...]]) + >>> matrix_YCbCr(is_legal=True) # doctest: +ELLIPSIS + array([[ 1.1643835...e+00, ..., 1.7927410...e+00], + [ 1.1643835...e+00, -2.1324861...e-01, -5.3290932...e-01], + [ 1.1643835...e+00, 2.1124017...e+00, ...]]) + + Matching the default output of the :func:`colour.RGB_to_YCbCr` is done as + follows: + + >>> from colour.utilities import as_int_array, vector_dot + >>> RGB = np.array([1.0, 1.0, 1.0]) + >>> RGB_to_YCbCr(RGB) # doctest: +ELLIPSIS + array([ 0.9215686..., 0.5019607..., 0.5019607...]) + >>> YCbCr = vector_dot(np.linalg.inv(matrix_YCbCr(is_legal=True)), RGB) + >>> YCbCr += offset_YCbCr(is_legal=True) + >>> YCbCr # doctest: +ELLIPSIS + array([ 0.9215686..., 0.5019607..., 0.5019607...]) + + Matching the int output of the :func:`colour.RGB_to_YCbCr` is done as + follows: + + >>> RGB = np.array([102, 0, 51]) + >>> RGB_to_YCbCr(RGB, in_bits=8, in_int=True, out_bits=8, out_int=True) + ... # doctest: +SKIP + array([ 38, 140, 171]) + >>> YCbCr = vector_dot(np.linalg.inv(matrix_YCbCr(is_legal=True)), RGB) + >>> YCbCr += offset_YCbCr(is_legal=True, is_int=True) + >>> as_int_array(np.around(YCbCr)) + ... # doctest: +SKIP + array([ 38, 140, 171]) + """ + + Kr, Kb = K + Y_min, Y_max, C_min, C_max = ranges_YCbCr(bits, is_legal, is_int) + + Y = np.array([Kr, (1 - Kr - Kb), Kb]) + Cb = 0.5 * (np.array([0, 0, 1]) - Y) / (1 - Kb) + Cr = 0.5 * (np.array([1, 0, 0]) - Y) / (1 - Kr) + Y *= Y_max - Y_min + Cb *= C_max - C_min + Cr *= C_max - C_min + + return np.linalg.inv(np.vstack([Y, Cb, Cr])) + + +def offset_YCbCr(bits=8, is_legal=False, is_int=False): + """ + Computes the *R'G'B'* to *Y'CbCr* offsets for given bit depth, range + legality and representation. + + The related *R'G'B'* to *Y'CbCr* matrix can be computed with the + :func:`colour.matrix_YCbCr` definition. + + Parameters + ---------- + bits : int + Bit depth of the *Y'CbCr* colour encoding ranges array. + is_legal : bool + Whether the *Y'CbCr* colour encoding ranges array is legal. + is_int : bool + Whether the *Y'CbCr* colour encoding ranges array represents integer + code values. + + Returns + ------- + ndarray + *Y'CbCr* matrix. + + Examples + -------- + >>> offset_YCbCr() + array([ 0., 0., 0.]) + >>> offset_YCbCr(is_legal=True) # doctest: +ELLIPSIS + array([ 0.0627451..., 0.5019607..., 0.5019607...]) + """ + + Y_min, _Y_max, C_min, C_max = ranges_YCbCr(bits, is_legal, is_int) + + Y_offset = Y_min + C_offset = (C_min + C_max) / 2 + + return np.array([Y_offset, C_offset, C_offset]) + + def RGB_to_YCbCr(RGB, K=WEIGHTS_YCBCR['ITU-R BT.709'], in_bits=10, diff --git a/docs/colour.models.rst b/docs/colour.models.rst index e4a8e5f0c8..341b17c44f 100644 --- a/docs/colour.models.rst +++ b/docs/colour.models.rst @@ -657,9 +657,11 @@ Y'CbCr Colour Encoding .. autosummary:: :toctree: generated/ + WEIGHTS_YCBCR + matrix_YCbCr + offset_YCbCr RGB_to_YCbCr YCbCr_to_RGB - WEIGHTS_YCBCR RGB_to_YcCbcCrc YcCbcCrc_to_RGB