Skip to content

Commit

Permalink
Add "colour.matrix_YCbCr" and "colour.offset_YCbCr" definitions.
Browse files Browse the repository at this point in the history
Closes #486.
  • Loading branch information
KelSolaar committed Dec 24, 2020
1 parent 04b960a commit 9e9fd6d
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 19 deletions.
15 changes: 8 additions & 7 deletions colour/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,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,
Expand Down Expand Up @@ -272,10 +273,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',
Expand Down
8 changes: 4 additions & 4 deletions colour/models/rgb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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']
99 changes: 94 additions & 5 deletions colour/models/rgb/tests/test_ycbcr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
129 changes: 127 additions & 2 deletions colour/models/rgb/ycbcr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -135,6 +138,128 @@ 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 *Y'CbCr* matrix for given weights, bit depth, range legality
and representation.
The related offset for the *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: +ELLIPSIS
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))
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 *Y'CbCr* offsets for given bit depth, range legality
and representation.
The related *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,
Expand Down
4 changes: 3 additions & 1 deletion docs/colour.models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -623,9 +623,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

Expand Down

0 comments on commit 9e9fd6d

Please sign in to comment.