"""
RGB Colourspace and Transformations
===================================
Define the :class:`colour.RGB_Colourspace` class for the *RGB* colourspaces
datasets from :mod:`colour.models.datasets.aces_rgb`, etc... and the following
*RGB* colourspace transformations or helper definitions:
- :func:`colour.XYZ_to_RGB`
- :func:`colour.RGB_to_XYZ`
- :func:`colour.matrix_RGB_to_RGB`
- :func:`colour.RGB_to_RGB`
References
----------
- :cite:`InternationalElectrotechnicalCommission1999a` : International
Electrotechnical Commission. (1999). IEC 61966-2-1:1999 - Multimedia
systems and equipment - Colour measurement and management - Part 2-1:
Colour management - Default RGB colour space - sRGB (p. 51).
https://webstore.iec.ch/publication/6169
- :cite:`Panasonic2014a` : Panasonic. (2014). VARICAM V-Log/V-Gamut (pp.
1-7).
http://pro-av.panasonic.net/en/varicam/common/pdf/VARICAM_V-Log_V-Gamut.pdf
"""
from __future__ import annotations
import typing
from copy import deepcopy
import numpy as np
from colour.adaptation import matrix_chromatic_adaptation_VonKries
from colour.algebra import vecmul
if typing.TYPE_CHECKING:
from colour.hints import (
Any,
ArrayLike,
Callable,
Domain1,
LiteralChromaticAdaptationTransform,
LiteralRGBColourspace,
NDArrayFloat,
Range1,
)
from colour.hints import cast
from colour.models import xy_to_xyY, xy_to_XYZ, xyY_to_XYZ
from colour.models.rgb import chromatically_adapted_primaries, normalised_primary_matrix
from colour.utilities import (
as_float_array,
attest,
domain_range_scale,
filter_kwargs,
from_range_1,
multiline_repr,
multiline_str,
optional,
to_domain_1,
usage_warning,
validate_method,
)
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"RGB_Colourspace",
"XYZ_to_RGB",
"RGB_to_XYZ",
"matrix_RGB_to_RGB",
"RGB_to_RGB",
]
[docs]
class RGB_Colourspace:
"""
Implement support for *RGB* colourspace datasets from modules including
:mod:`colour.models.datasets.aces_rgb`.
Colour science literature related to *RGB* colourspaces and encodings
defines datasets using different degrees of precision or rounding. While
instances where a whitepoint differs from its canonical agreed value are
rare, normalised primary matrices are commonly rounded at different
decimal places. This can yield large discrepancies in computations.
Such an occurrence is the *V-Gamut* colourspace white paper, which
defines the *V-Gamut* to *ITU-R BT.709* conversion matrix as follows::
[[ 1.806576 -0.695697 -0.110879]
[-0.170090 1.305955 -0.135865]
[-0.025206 -0.154468 1.179674]]
Computing this matrix using *ITU-R BT.709* colourspace derived
normalised primary matrix yields::
[[ 1.8065736 -0.6956981 -0.1108786]
[-0.1700890 1.3059548 -0.1358648]
[-0.0252057 -0.1544678 1.1796737]]
The latter matrix is almost equal to the former, however performing the
same computation using *IEC 61966-2-1:1999* *sRGB* colourspace
normalised primary matrix introduces severe disparities::
[[ 1.8063853 -0.6956147 -0.1109453]
[-0.1699311 1.3058387 -0.1358616]
[-0.0251630 -0.1544899 1.1797117]]
To provide support for both literature-defined datasets and accurate
computations enabling transformations without loss of precision, the
:class:`colour.RGB_Colourspace` class provides two sets of transformation
matrices:
- Instantiation transformation matrices
- Derived transformation matrices
Upon instantiation, the :class:`colour.RGB_Colourspace` class stores the
specified ``matrix_RGB_to_XYZ`` and ``matrix_XYZ_to_RGB`` arguments and
also computes their derived counterparts using the ``primaries`` and
``whitepoint`` arguments.
Whether instantiation or derived matrices are used in subsequent
computations depends on the
:attr:`colour.RGB_Colourspace.use_derived_matrix_RGB_to_XYZ` and
:attr:`colour.RGB_Colourspace.use_derived_matrix_XYZ_to_RGB` attribute
values.
Parameters
----------
name
*RGB* colourspace name.
primaries
*RGB* colourspace primaries.
whitepoint
*RGB* colourspace whitepoint.
whitepoint_name
*RGB* colourspace whitepoint name.
matrix_RGB_to_XYZ
Transformation matrix from colourspace to *CIE XYZ* tristimulus
values.
matrix_XYZ_to_RGB
Transformation matrix from *CIE XYZ* tristimulus values to
colourspace.
cctf_encoding
Encoding colour component transfer function (Encoding CCTF) /
opto-electronic transfer function (OETF) that maps estimated
tristimulus values in a scene to :math:`R'G'B'` video component
signal value.
cctf_decoding
Decoding colour component transfer function (Decoding CCTF) /
electro-optical transfer function (EOTF) that maps an
:math:`R'G'B'` video component signal value to tristimulus values at
the display.
use_derived_matrix_RGB_to_XYZ
Whether to use the instantiation time normalised primary matrix or to
use a computed derived normalised primary matrix.
use_derived_matrix_XYZ_to_RGB
Whether to use the instantiation time inverse normalised primary
matrix or to use a computed derived inverse normalised primary
matrix.
Attributes
----------
- :attr:`~colour.RGB_Colourspace.name`
- :attr:`~colour.RGB_Colourspace.primaries`
- :attr:`~colour.RGB_Colourspace.whitepoint`
- :attr:`~colour.RGB_Colourspace.whitepoint_name`
- :attr:`~colour.RGB_Colourspace.matrix_RGB_to_XYZ`
- :attr:`~colour.RGB_Colourspace.matrix_XYZ_to_RGB`
- :attr:`~colour.RGB_Colourspace.cctf_encoding`
- :attr:`~colour.RGB_Colourspace.cctf_decoding`
- :attr:`~colour.RGB_Colourspace.use_derived_matrix_RGB_to_XYZ`
- :attr:`~colour.RGB_Colourspace.use_derived_matrix_XYZ_to_RGB`
Methods
-------
- :attr:`~colour.RGB_Colourspace.__init__`
- :attr:`~colour.RGB_Colourspace.__str__`
- :attr:`~colour.RGB_Colourspace.__repr__`
- :attr:`~colour.RGB_Colourspace.use_derived_transformation_matrices`
- :attr:`~colour.RGB_Colourspace.chromatically_adapt`
- :attr:`~colour.RGB_Colourspace.copy`
Notes
-----
- The normalised primary matrix defined by
:attr:`colour.RGB_Colourspace.matrix_RGB_to_XYZ` property is treated
as the prime matrix from which the inverse will be calculated as
required by the internal derivation mechanism. This behaviour has
been chosen in accordance with literature where commonly a *RGB*
colourspace is defined by its normalised primary matrix as it is
directly computed from the chosen primaries and whitepoint.
References
----------
:cite:`InternationalElectrotechnicalCommission1999a`,
:cite:`Panasonic2014a`
Examples
--------
>>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700])
>>> whitepoint = np.array([0.32168, 0.33767])
>>> matrix_RGB_to_XYZ = np.identity(3)
>>> matrix_XYZ_to_RGB = np.identity(3)
>>> colourspace = RGB_Colourspace(
... "RGB Colourspace",
... p,
... whitepoint,
... "ACES",
... matrix_RGB_to_XYZ,
... matrix_XYZ_to_RGB,
... )
>>> colourspace.matrix_RGB_to_XYZ
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
>>> colourspace.matrix_XYZ_to_RGB
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
>>> colourspace.use_derived_transformation_matrices(True)
>>> colourspace.matrix_RGB_to_XYZ # doctest: +ELLIPSIS
array([[ 9.5255239...e-01, 0.0000000...e+00, 9.3678631...e-05],
[ 3.4396645...e-01, 7.2816609...e-01, -7.2132546...e-02],
[ 0.0000000...e+00, 0.0000000...e+00, 1.0088251...e+00]])
>>> colourspace.matrix_XYZ_to_RGB # doctest: +ELLIPSIS
array([[ 1.0498110...e+00, 0.0000000...e+00, -9.7484540...e-05],
[ -4.9590302...e-01, 1.3733130...e+00, 9.8240036...e-02],
[ 0.0000000...e+00, 0.0000000...e+00, 9.9125201...e-01]])
>>> colourspace.use_derived_matrix_RGB_to_XYZ = False
>>> colourspace.matrix_RGB_to_XYZ
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
>>> colourspace.use_derived_matrix_XYZ_to_RGB = False
>>> colourspace.matrix_XYZ_to_RGB
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])
"""
[docs]
def __init__(
self,
name: str,
primaries: ArrayLike,
whitepoint: ArrayLike,
whitepoint_name: str | None = None,
matrix_RGB_to_XYZ: ArrayLike | None = None,
matrix_XYZ_to_RGB: ArrayLike | None = None,
cctf_encoding: Callable | None = None,
cctf_decoding: Callable | None = None,
use_derived_matrix_RGB_to_XYZ: bool = False,
use_derived_matrix_XYZ_to_RGB: bool = False,
) -> None:
self._derived_matrix_RGB_to_XYZ: NDArrayFloat = np.array([])
self._derived_matrix_XYZ_to_RGB: NDArrayFloat = np.array([])
self._name: str = f"{self.__class__.__name__} ({id(self)})"
self.name = name
self._primaries: NDArrayFloat = np.array([])
self.primaries = primaries
self._whitepoint: NDArrayFloat = np.array([])
self.whitepoint = whitepoint
self._whitepoint_name: str | None = None
self.whitepoint_name = whitepoint_name
self._matrix_RGB_to_XYZ: NDArrayFloat | None = None
self.matrix_RGB_to_XYZ = matrix_RGB_to_XYZ
self._matrix_XYZ_to_RGB: NDArrayFloat | None = None
self.matrix_XYZ_to_RGB = matrix_XYZ_to_RGB
self._cctf_encoding: Callable | None = None
self.cctf_encoding = cctf_encoding
self._cctf_decoding: Callable | None = None
self.cctf_decoding = cctf_decoding
self._use_derived_matrix_RGB_to_XYZ: bool = False
self.use_derived_matrix_RGB_to_XYZ = use_derived_matrix_RGB_to_XYZ
self._use_derived_matrix_XYZ_to_RGB: bool = False
self.use_derived_matrix_XYZ_to_RGB = use_derived_matrix_XYZ_to_RGB
@property
def name(self) -> str:
"""
Getter and setter for the *RGB* colourspace name.
Parameters
----------
value
Value to set the name with.
Returns
-------
:class:`str`
*RGB* colourspace name.
"""
return self._name
@name.setter
def name(self, value: str) -> None:
"""Setter for the **self.name** property."""
attest(
isinstance(value, str),
f'"name" property: "{value}" type is not "str"!',
)
self._name = value
@property
def primaries(self) -> NDArrayFloat:
"""
Getter and setter for the *RGB* colourspace primaries.
Parameters
----------
value
Value to set the primaries with.
Returns
-------
:class:`numpy.ndarray`
*RGB* colourspace primaries.
"""
return self._primaries
@primaries.setter
def primaries(self, value: ArrayLike) -> None:
"""Setter for the **self.primaries** property."""
attest(
isinstance(value, (tuple, list, np.ndarray, np.matrix)),
f'"primaries" property: "{value!r}" is not a "tuple", '
f'"list", "ndarray" or "matrix" instance!',
)
value = as_float_array(value)
value = np.reshape(value, (3, 2))
self._primaries = value
self._derived_matrix_XYZ_to_RGB = np.array([])
self._derived_matrix_RGB_to_XYZ = np.array([])
@property
def whitepoint(self) -> NDArrayFloat:
"""
Getter and setter for the *RGB* colourspace whitepoint.
Parameters
----------
value
Value to set the whitepoint with.
Returns
-------
:class:`numpy.ndarray`
*RGB* colourspace whitepoint.
"""
return self._whitepoint
@whitepoint.setter
def whitepoint(self, value: ArrayLike) -> None:
"""Setter for the **self.whitepoint** property."""
attest(
isinstance(value, (tuple, list, np.ndarray, np.matrix)),
f'"whitepoint" property: "{value!r}" is not a "tuple", '
f'"list", "ndarray" or "matrix" instance!',
)
value = as_float_array(value)
self._whitepoint = value
self._derived_matrix_XYZ_to_RGB = np.array([])
self._derived_matrix_RGB_to_XYZ = np.array([])
@property
def whitepoint_name(self) -> str | None:
"""
Getter and setter for the *RGB* colourspace whitepoint name.
Define or retrieve the name identifier for the reference illuminant
(whitepoint) used by this *RGB* colourspace. This property allows
tracking of standardized illuminant names such as 'D65', 'D50', or
custom whitepoint identifiers.
Parameters
----------
value
Name identifier to set for the *RGB* colourspace whitepoint. Can
be a standard illuminant name or custom identifier.
Returns
-------
:class:`str` or :py:data:`None`
*RGB* colourspace whitepoint name identifier. Returns
:py:data:`None` if no name has been specified.
"""
return self._whitepoint_name
@whitepoint_name.setter
def whitepoint_name(self, value: str | None) -> None:
"""Setter for the **self.whitepoint_name** property."""
if value is not None:
attest(
isinstance(value, str),
f'"whitepoint_name" property: "{value}" type is not "str"!',
)
self._whitepoint_name = value
@property
def matrix_RGB_to_XYZ(self) -> NDArrayFloat:
"""
Getter and setter for the transformation matrix from RGB colourspace
to *CIE XYZ* tristimulus values.
Parameters
----------
value
Transformation matrix from RGB colourspace to *CIE XYZ*
tristimulus values.
Returns
-------
:class:`numpy.ndarray`
Transformation matrix from RGB colourspace to *CIE XYZ*
tristimulus values.
"""
if self._matrix_RGB_to_XYZ is None or self._use_derived_matrix_RGB_to_XYZ:
if self._derived_matrix_RGB_to_XYZ.size == 0:
self._derive_transformation_matrices()
return self._derived_matrix_RGB_to_XYZ
return self._matrix_RGB_to_XYZ
@matrix_RGB_to_XYZ.setter
def matrix_RGB_to_XYZ(self, value: ArrayLike | None) -> None:
"""Setter for the **self.matrix_RGB_to_XYZ** property."""
if value is not None:
attest(
isinstance(value, (tuple, list, np.ndarray, np.matrix)),
f'"matrix_RGB_to_XYZ" property: "{value!r}" is not a "tuple", '
f'"list", "ndarray" or "matrix" instance!',
)
value = as_float_array(value)
self._matrix_RGB_to_XYZ = value
@property
def matrix_XYZ_to_RGB(self) -> NDArrayFloat:
"""
Getter and setter for the transformation matrix from *CIE XYZ*
tristimulus values to RGB colourspace.
Parameters
----------
value
Transformation matrix from *CIE XYZ* tristimulus values to the
colourspace.
Returns
-------
:class:`numpy.ndarray`
Transformation matrix from *CIE XYZ* tristimulus values to the
colourspace.
"""
if self._matrix_XYZ_to_RGB is None or self._use_derived_matrix_XYZ_to_RGB:
if self._derived_matrix_XYZ_to_RGB.size == 0:
self._derive_transformation_matrices()
return self._derived_matrix_XYZ_to_RGB
return self._matrix_XYZ_to_RGB
@matrix_XYZ_to_RGB.setter
def matrix_XYZ_to_RGB(self, value: ArrayLike | None) -> None:
"""Setter for the **self.matrix_XYZ_to_RGB** property."""
if value is not None:
attest(
isinstance(value, (tuple, list, np.ndarray, np.matrix)),
f'"matrix_XYZ_to_RGB" property: "{value!r}" is not a "tuple", '
f'"list", "ndarray" or "matrix" instance!',
)
value = as_float_array(value)
self._matrix_XYZ_to_RGB = value
@property
def cctf_encoding(self) -> Callable | None:
"""
Getter and setter for the encoding colour component transfer
function (Encoding CCTF) / opto-electronic transfer function (OETF).
Parameters
----------
value
Encoding colour component transfer function (Encoding CCTF) /
opto-electronic transfer function (OETF).
Returns
-------
Callable or :py:data:`None`
Encoding colour component transfer function (Encoding CCTF) /
opto-electronic transfer function (OETF).
"""
return self._cctf_encoding
@cctf_encoding.setter
def cctf_encoding(self, value: Callable | None) -> None:
"""Setter for the **self.cctf_encoding** property."""
if value is not None:
attest(
callable(value),
f'"cctf_encoding" property: "{value}" is not callable!',
)
self._cctf_encoding = value
@property
def cctf_decoding(self) -> Callable | None:
"""
Getter and setter for the decoding colour component transfer
function (Decoding CCTF) / electro-optical transfer function
(EOTF).
Parameters
----------
value
Decoding colour component transfer function (Decoding CCTF) /
electro-optical transfer function (EOTF).
Returns
-------
Callable or :py:data:`None`
Decoding colour component transfer function (Decoding CCTF) /
electro-optical transfer function (EOTF).
"""
return self._cctf_decoding
@cctf_decoding.setter
def cctf_decoding(self, value: Callable | None) -> None:
"""Setter for the **self.cctf_decoding** property."""
if value is not None:
attest(
callable(value),
f'"cctf_decoding" property: "{value}" is not callable!',
)
self._cctf_decoding = value
@property
def use_derived_matrix_RGB_to_XYZ(self) -> bool:
"""
Getter and setter for whether to use the instantiation time normalised
primary matrix or to use a computed derived normalised primary matrix.
Control whether the RGB to XYZ transformation uses the pre-computed
normalised primary matrix from instantiation or derives it dynamically
from the current primaries and whitepoint.
Parameters
----------
value
Whether to use the instantiation time normalised primary matrix or
to use a computed derived normalised primary matrix.
Returns
-------
:class:`bool`
Whether to use the instantiation time normalised primary matrix or
to use a computed derived normalised primary matrix.
"""
return self._use_derived_matrix_RGB_to_XYZ
@use_derived_matrix_RGB_to_XYZ.setter
def use_derived_matrix_RGB_to_XYZ(self, value: bool) -> None:
"""Setter for the **self.use_derived_matrix_RGB_to_XYZ** property."""
attest(
isinstance(value, (bool, np.bool_)),
f'"use_derived_matrix_RGB_to_XYZ" property: "{value}" is not a "bool"!',
)
self._use_derived_matrix_RGB_to_XYZ = value
@property
def use_derived_matrix_XYZ_to_RGB(self) -> bool:
"""
Getter and setter for whether to use the instantiation time inverse
normalised primary matrix or to compute a derived inverse normalised
primary matrix.
Control whether the XYZ to RGB transformation uses the pre-computed
inverse matrix from instantiation or derives it dynamically from the
current primary matrix.
Parameters
----------
value
Whether to use the instantiation time inverse normalised primary
matrix or to compute a derived inverse normalised primary matrix.
Returns
-------
:class:`bool`
Whether to use the instantiation time inverse normalised primary
matrix or to compute a derived inverse normalised primary matrix.
"""
return self._use_derived_matrix_XYZ_to_RGB
@use_derived_matrix_XYZ_to_RGB.setter
def use_derived_matrix_XYZ_to_RGB(self, value: bool) -> None:
"""Setter for the **self.use_derived_matrix_XYZ_to_RGB** property."""
attest(
isinstance(value, (bool, np.bool_)),
f'"use_derived_matrix_XYZ_to_RGB" property: "{value}" is not a "bool"!',
)
self._use_derived_matrix_XYZ_to_RGB = value
[docs]
def __str__(self) -> str:
"""
Generate a formatted string representation of the *RGB* colourspace.
Returns
-------
:class:`str`
Formatted string representation displaying colourspace properties
including primaries, whitepoint, encoding/decoding CCTFs, and
normalised primary matrices.
Examples
--------
>>> p = np.array(
... [0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700]
... )
>>> whitepoint = np.array([0.32168, 0.33767])
>>> matrix_RGB_to_XYZ = np.identity(3)
>>> matrix_XYZ_to_RGB = np.identity(3)
>>> cctf_encoding = lambda x: x
>>> cctf_decoding = lambda x: x
>>> print(
... RGB_Colourspace(
... "RGB Colourspace",
... p,
... whitepoint,
... "ACES",
... matrix_RGB_to_XYZ,
... matrix_XYZ_to_RGB,
... cctf_encoding,
... cctf_decoding,
... )
... )
... # doctest: +ELLIPSIS
RGB Colourspace
---------------
<BLANKLINE>
Primaries : [[ 7.34700000e-01 2.65300000e-01]
[ 0.00000000e+00 1.00000000e+00]
[ 1.00000000e-04 -7.70000000e-02]]
Whitepoint : [ 0.32168 0.33767]
Whitepoint Name : ACES
Encoding CCTF : <function <lambda> at 0x...>
Decoding CCTF : <function <lambda> at 0x...>
NPM : [[ 1. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]
NPM -1 : [[ 1. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]
Derived NPM : \
[[ 9.5255239...e-01 0.0000000...e+00 9.3678631...e-05]
\
[ 3.4396645...e-01 7.2816609...e-01 -7.2132546...e-02]
\
[ 0.0000000...e+00 0.0000000...e+00 1.0088251...e+00]]
Derived NPM -1 : \
[[ 1.0498110...e+00 0.0000000...e+00 -9.7484540...e-05]
\
[ -4.9590302...e-01 1.3733130...e+00 9.8240036...e-02]
\
[ 0.0000000...e+00 0.0000000...e+00 9.9125201...e-01]]
Use Derived NPM : False
Use Derived NPM -1 : False
"""
if self._derived_matrix_XYZ_to_RGB.size == 0:
self._derive_transformation_matrices()
return multiline_str(
self,
[
{"name": "name", "section": True},
{"line_break": True},
{"name": "primaries", "label": "Primaries"},
{"name": "whitepoint", "label": "Whitepoint"},
{"name": "whitepoint_name", "label": "Whitepoint Name"},
{"name": "cctf_encoding", "label": "Encoding CCTF"},
{"name": "cctf_decoding", "label": "Decoding CCTF"},
{"name": "_matrix_RGB_to_XYZ", "label": "NPM"},
{"name": "_matrix_XYZ_to_RGB", "label": "NPM -1"},
{
"name": "_derived_matrix_RGB_to_XYZ",
"label": "Derived NPM",
},
{
"name": "_derived_matrix_XYZ_to_RGB",
"label": "Derived NPM -1",
},
{
"name": "use_derived_matrix_RGB_to_XYZ",
"label": "Use Derived NPM",
},
{
"name": "use_derived_matrix_XYZ_to_RGB",
"label": "Use Derived NPM -1",
},
],
)
[docs]
def __repr__(self) -> str:
"""
Return an evaluable string representation of the *RGB* colourspace.
The representation includes all parameters needed to reconstruct the
colourspace instance.
Returns
-------
:class:`str`
Evaluable string representation.
Examples
--------
>>> from colour.models import linear_function
>>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700])
>>> whitepoint = np.array([0.32168, 0.33767])
>>> matrix_RGB_to_XYZ = np.identity(3)
>>> matrix_XYZ_to_RGB = np.identity(3)
>>> RGB_Colourspace(
... "RGB Colourspace",
... p,
... whitepoint,
... "ACES",
... matrix_RGB_to_XYZ,
... matrix_XYZ_to_RGB,
... linear_function,
... linear_function,
... )
... # doctest: +ELLIPSIS
RGB_Colourspace('RGB Colourspace',
[[ 7.34700000e-01, 2.65300000e-01],
[ 0.00000000e+00, 1.00000000e+00],
[ 1.00000000e-04, -7.70000000e-02]],
[ 0.32168, 0.33767],
'ACES',
[[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]],
[[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]],
linear_function,
linear_function,
False,
False)
"""
return multiline_repr(
self,
[
{"name": "name"},
{"name": "primaries"},
{"name": "whitepoint"},
{"name": "whitepoint_name"},
{"name": "matrix_RGB_to_XYZ"},
{"name": "matrix_XYZ_to_RGB"},
{
"name": "cctf_encoding",
"formatter": lambda x: ( # noqa: ARG005
None
if self.cctf_encoding is None
else (
self.cctf_encoding.__name__
if hasattr(self.cctf_encoding, "__name__")
else str(self.cctf_encoding)
)
),
},
{
"name": "cctf_decoding",
"formatter": lambda x: ( # noqa: ARG005
None
if self.cctf_decoding is None
else (
self.cctf_decoding.__name__
if hasattr(self.cctf_decoding, "__name__")
else str(self.cctf_decoding)
)
),
},
{"name": "use_derived_matrix_RGB_to_XYZ"},
{"name": "use_derived_matrix_XYZ_to_RGB"},
],
)
def _derive_transformation_matrices(self) -> None:
"""
Derive transformation matrices from the RGB colourspace specification.
Compute the normalised primary matrix and its inverse matrix that are
used for transformations between the RGB colourspace and CIE XYZ
tristimulus values.
"""
if self._primaries is not None and self._whitepoint is not None:
npm = normalised_primary_matrix(self._primaries, self._whitepoint)
self._derived_matrix_RGB_to_XYZ = npm
self._derived_matrix_XYZ_to_RGB = np.linalg.inv(npm)
[docs]
def chromatically_adapt(
self,
whitepoint: ArrayLike,
whitepoint_name: str | None = None,
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str
) = "CAT02",
) -> RGB_Colourspace:
"""
Chromatically adapt the *RGB* colourspace *primaries* :math:`xy`
chromaticity coordinates from *RGB* colourspace whitepoint to the
specified reference whitepoint.
Parameters
----------
whitepoint
Reference illuminant / whitepoint :math:`xy` chromaticity
coordinates.
whitepoint_name
Reference illuminant / whitepoint name.
chromatic_adaptation_transform
*Chromatic adaptation* transform.
Returns
-------
:class:`colour.RGB_Colourspace`
Chromatically adapted *RGB* colourspace.
Examples
--------
>>> p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700])
>>> w_t = np.array([0.32168, 0.33767])
>>> w_r = np.array([0.31270, 0.32900])
>>> colourspace = RGB_Colourspace("RGB Colourspace", p, w_t, "D65")
>>> print(colourspace.chromatically_adapt(w_r, "D50", "Bradford"))
... # doctest: +ELLIPSIS
RGB Colourspace - Chromatically Adapted to 'D50'
------------------------------------------------
<BLANKLINE>
Primaries : [[ 0.73485524 0.26422533]
[-0.00617091 1.01131496]
[ 0.01596756 -0.0642355 ]]
Whitepoint : [ 0.3127 0.329 ]
Whitepoint Name : D50
Encoding CCTF : None
Decoding CCTF : None
NPM : None
NPM -1 : None
Derived NPM : [[ 0.93827985 -0.00445145 0.01662752]
[ 0.33736889 0.72952157 -0.06689046]
[ 0.00117395 -0.00371071 1.09159451]]
Derived NPM -1 : [[ 1.06349549 0.00640891 -0.01580679]
[-0.49207413 1.36822341 0.09133709]
[-0.00281646 0.00464417 0.91641857]]
Use Derived NPM : True
Use Derived NPM -1 : True
"""
colourspace = self.copy()
colourspace.primaries = chromatically_adapted_primaries(
colourspace.primaries,
colourspace.whitepoint,
whitepoint,
chromatic_adaptation_transform,
)
colourspace.whitepoint = whitepoint
colourspace.whitepoint_name = whitepoint_name
colourspace._matrix_RGB_to_XYZ = None # noqa: SLF001
colourspace._matrix_XYZ_to_RGB = None # noqa: SLF001
colourspace._derive_transformation_matrices() # noqa: SLF001
colourspace.use_derived_transformation_matrices()
colourspace.name = (
f"{colourspace.name} - Chromatically Adapted to "
f"{cast('str', optional(whitepoint_name, whitepoint))!r}"
)
return colourspace
[docs]
def copy(self) -> RGB_Colourspace:
"""
Create a deep copy of the *RGB* colourspace instance.
Generate an independent copy of this *RGB* colourspace with all
attributes duplicated, including primaries, whitepoint, matrices,
and transfer functions.
Returns
-------
:class:`colour.RGB_Colourspace`
Independent deep copy of the *RGB* colourspace instance.
"""
return deepcopy(self)
[docs]
def XYZ_to_RGB(
XYZ: Domain1,
colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
illuminant: ArrayLike | None = None,
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
apply_cctf_encoding: bool = False,
*args: Any,
**kwargs: Any,
) -> Range1:
"""
Convert from *CIE XYZ* tristimulus values to *RGB* colourspace array.
Parameters
----------
XYZ
*CIE XYZ* tristimulus values.
colourspace
Output *RGB* colourspace.
illuminant
*CIE xy* chromaticity coordinates or *CIE xyY* colourspace array of
the *illuminant* for the input *CIE XYZ* tristimulus values.
chromatic_adaptation_transform
*Chromatic adaptation* transform. If *None*, no chromatic adaptation
is performed.
apply_cctf_encoding
Apply the *RGB* colourspace encoding colour component transfer
function / opto-electronic transfer function.
Other Parameters
----------------
args
Arguments for deprecation management.
kwargs
Keywords arguments for deprecation management.
Returns
-------
:class:`numpy.ndarray`
*RGB* colourspace array.
Notes
-----
+--------------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``XYZ`` | 1 | 1 |
+--------------------+-----------------------+---------------+
| ``illuminant_XYZ`` | 1 | 1 |
+--------------------+-----------------------+---------------+
| ``illuminant_RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
+--------------------+-----------------------+---------------+
| **Range** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB
>>> XYZ = np.array([0.21638819, 0.12570000, 0.03847493])
>>> illuminant = np.array([0.34570, 0.35850])
>>> XYZ_to_RGB(XYZ, RGB_COLOURSPACE_sRGB, illuminant, "Bradford")
... # doctest: +ELLIPSIS
array([ 0.4559528..., 0.0304078..., 0.0408731...])
>>> XYZ_to_RGB(XYZ, "sRGB", illuminant, "Bradford")
... # doctest: +ELLIPSIS
array([ 0.4559528..., 0.0304078..., 0.0408731...])
"""
from colour.models import RGB_COLOURSPACES # noqa: PLC0415
XYZ = to_domain_1(XYZ)
if not isinstance(colourspace, (RGB_Colourspace, str)):
usage_warning(
'The "colour.XYZ_to_RGB" definition signature has changed with '
'"Colour 0.4.3". The used call arguments are deprecated, '
"please refer to the documentation for more information about the "
"new signature."
)
illuminant_XYZ = kwargs.pop("illuminant_XYZ", colourspace)
illuminant_RGB = kwargs.pop("illuminant_RGB", illuminant)
matrix_XYZ_to_RGB = kwargs.pop(
"matrix_XYZ_to_RGB", chromatic_adaptation_transform
)
chromatic_adaptation_transform = kwargs.pop(
"chromatic_adaptation_transform",
(
apply_cctf_encoding
if not isinstance(apply_cctf_encoding, bool)
else "CAT02"
),
)
cctf_encoding = kwargs.pop("cctf_encoding", args[0] if len(args) == 1 else None)
apply_cctf_encoding = True
else:
if isinstance(colourspace, str):
colourspace = validate_method(
colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
colourspace = RGB_COLOURSPACES[colourspace]
illuminant_XYZ = optional(
illuminant,
colourspace.whitepoint, # pyright: ignore
)
illuminant_RGB = colourspace.whitepoint # pyright: ignore
matrix_XYZ_to_RGB = colourspace.matrix_XYZ_to_RGB # pyright: ignore
cctf_encoding = colourspace.cctf_encoding # pyright: ignore
if chromatic_adaptation_transform is not None:
M_CAT = matrix_chromatic_adaptation_VonKries(
xyY_to_XYZ(xy_to_xyY(illuminant_XYZ)),
xyY_to_XYZ(xy_to_xyY(illuminant_RGB)),
transform=chromatic_adaptation_transform,
)
XYZ = vecmul(M_CAT, XYZ)
RGB = vecmul(matrix_XYZ_to_RGB, XYZ)
if apply_cctf_encoding and cctf_encoding is not None:
with domain_range_scale("ignore"):
RGB = cctf_encoding(RGB)
return from_range_1(RGB)
[docs]
def RGB_to_XYZ(
RGB: Domain1,
colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
illuminant: ArrayLike | None = None,
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
apply_cctf_decoding: bool = False,
*args: Any,
**kwargs: Any,
) -> Range1:
"""
Convert specified *RGB* colourspace array to *CIE XYZ* tristimulus values.
Parameters
----------
RGB
*RGB* colourspace array.
colourspace
Input *RGB* colourspace.
illuminant
*CIE xy* chromaticity coordinates or *CIE xyY* colourspace array of
the *illuminant* for the output *CIE XYZ* tristimulus values.
chromatic_adaptation_transform
*Chromatic adaptation* transform, if *None* no chromatic adaptation
is performed.
apply_cctf_decoding
Apply the *RGB* colourspace decoding colour component transfer
function / opto-electronic transfer function.
Other Parameters
----------------
args
Arguments for deprecation management.
kwargs
Keywords arguments for deprecation management.
Returns
-------
:class:`numpy.ndarray`
*CIE XYZ* tristimulus values.
Notes
-----
+--------------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
| ``illuminant_XYZ`` | 1 | 1 |
+--------------------+-----------------------+---------------+
| ``illuminant_RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
+--------------------+-----------------------+---------------+
| **Range** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``XYZ`` | 1 | 1 |
+--------------------+-----------------------+---------------+
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB
>>> RGB = np.array([0.45595571, 0.03039702, 0.04087245])
>>> illuminant = np.array([0.34570, 0.35850])
>>> RGB_to_XYZ(RGB, RGB_COLOURSPACE_sRGB, illuminant, "Bradford")
... # doctest: +ELLIPSIS
array([ 0.2163881..., 0.1257 , 0.0384749...])
>>> RGB_to_XYZ(RGB, "sRGB", illuminant, "Bradford")
... # doctest: +ELLIPSIS
array([ 0.2163881..., 0.1257 , 0.0384749...])
"""
from colour.models import RGB_COLOURSPACES # noqa: PLC0415
RGB = to_domain_1(RGB)
if not isinstance(colourspace, (RGB_Colourspace, str)):
usage_warning(
'The "colour.RGB_to_XYZ" definition signature has changed with '
'"Colour 0.4.3". The used call arguments are deprecated, '
"please refer to the documentation for more information about the "
"new signature."
)
illuminant_RGB = kwargs.pop("illuminant_RGB", colourspace)
illuminant_XYZ = kwargs.pop("illuminant_XYZ", illuminant)
matrix_RGB_to_XYZ = kwargs.pop(
"matrix_RGB_to_XYZ", chromatic_adaptation_transform
)
chromatic_adaptation_transform = kwargs.pop(
"chromatic_adaptation_transform",
(
apply_cctf_decoding
if not isinstance(apply_cctf_decoding, bool)
else "CAT02"
),
)
cctf_decoding = kwargs.pop("cctf_decoding", args[0] if len(args) == 1 else None)
apply_cctf_decoding = True
else:
if isinstance(colourspace, str):
colourspace = validate_method(
colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
colourspace = RGB_COLOURSPACES[colourspace]
illuminant_XYZ = optional(
illuminant,
colourspace.whitepoint, # pyright: ignore
)
illuminant_RGB = colourspace.whitepoint # pyright: ignore
matrix_RGB_to_XYZ = colourspace.matrix_RGB_to_XYZ # pyright: ignore
cctf_decoding = colourspace.cctf_decoding # pyright: ignore
if apply_cctf_decoding and cctf_decoding is not None:
with domain_range_scale("ignore"):
RGB = cctf_decoding(RGB)
XYZ = vecmul(matrix_RGB_to_XYZ, RGB)
if chromatic_adaptation_transform is not None:
M_CAT = matrix_chromatic_adaptation_VonKries(
xyY_to_XYZ(xy_to_xyY(illuminant_RGB)),
xyY_to_XYZ(xy_to_xyY(illuminant_XYZ)),
transform=chromatic_adaptation_transform,
)
XYZ = vecmul(M_CAT, XYZ)
return from_range_1(XYZ)
[docs]
def matrix_RGB_to_RGB(
input_colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
output_colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
) -> NDArrayFloat:
"""
Compute the matrix :math:`M` converting from the specified input *RGB*
colourspace to the specified output *RGB* colourspace using the
specified *chromatic adaptation* method.
Parameters
----------
input_colourspace
*RGB* input colourspace.
output_colourspace
*RGB* output colourspace.
chromatic_adaptation_transform
*Chromatic adaptation* transform. If *None*, no chromatic
adaptation is performed.
Returns
-------
:class:`numpy.ndarray`
Conversion matrix :math:`M`.
Examples
--------
>>> from colour.models import (
... RGB_COLOURSPACE_sRGB,
... RGB_COLOURSPACE_PROPHOTO_RGB,
... )
>>> matrix_RGB_to_RGB(RGB_COLOURSPACE_sRGB, RGB_COLOURSPACE_PROPHOTO_RGB)
... # doctest: +ELLIPSIS
array([[ 0.5288241..., 0.3340609..., 0.1373616...],
[ 0.0975294..., 0.8790074..., 0.0233981...],
[ 0.0163599..., 0.1066124..., 0.8772485...]])
>>> matrix_RGB_to_RGB("sRGB", "ProPhoto RGB")
... # doctest: +ELLIPSIS
array([[ 0.5288241..., 0.3340609..., 0.1373616...],
[ 0.0975294..., 0.8790074..., 0.0233981...],
[ 0.0163599..., 0.1066124..., 0.8772485...]])
"""
from colour.models import RGB_COLOURSPACES # noqa: PLC0415
if isinstance(input_colourspace, str):
input_colourspace = validate_method(
input_colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
input_colourspace = cast("RGB_Colourspace", RGB_COLOURSPACES[input_colourspace])
if isinstance(output_colourspace, str):
output_colourspace = validate_method(
output_colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
output_colourspace = cast(
"RGB_Colourspace", RGB_COLOURSPACES[output_colourspace]
)
M = input_colourspace.matrix_RGB_to_XYZ
if chromatic_adaptation_transform is not None:
M_CAT = matrix_chromatic_adaptation_VonKries(
xy_to_XYZ(input_colourspace.whitepoint),
xy_to_XYZ(output_colourspace.whitepoint),
chromatic_adaptation_transform,
)
M = np.matmul(M_CAT, input_colourspace.matrix_RGB_to_XYZ)
return np.matmul(output_colourspace.matrix_XYZ_to_RGB, M)
[docs]
def RGB_to_RGB(
RGB: Domain1,
input_colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
output_colourspace: RGB_Colourspace | LiteralRGBColourspace | str,
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
apply_cctf_decoding: bool = False,
apply_cctf_encoding: bool = False,
**kwargs: Any,
) -> Range1:
"""
Convert *RGB* colourspace array from the specified input *RGB* colourspace to
specified output *RGB* colourspace using the specified *chromatic adaptation*
method.
Parameters
----------
RGB
*RGB* colourspace array.
input_colourspace
*RGB* input colourspace.
output_colourspace
*RGB* output colourspace.
chromatic_adaptation_transform
*Chromatic adaptation* transform, if *None* no chromatic adaptation
is performed.
apply_cctf_decoding
Apply the input colourspace decoding colour component transfer
function / electro-optical transfer function.
apply_cctf_encoding
Apply the output colourspace encoding colour component transfer
function / opto-electronic transfer function.
Other Parameters
----------------
kwargs
Keywords arguments for the colour component transfer functions.
Returns
-------
:class:`numpy.ndarray`
*RGB* colourspace array.
Notes
-----
+--------------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
+--------------------+-----------------------+---------------+
| **Range** | **Scale - Reference** | **Scale - 1** |
+====================+=======================+===============+
| ``RGB`` | 1 | 1 |
+--------------------+-----------------------+---------------+
Examples
--------
>>> from colour.models import (
... RGB_COLOURSPACE_sRGB,
... RGB_COLOURSPACE_PROPHOTO_RGB,
... )
>>> RGB = np.array([0.45595571, 0.03039702, 0.04087245])
>>> RGB_to_RGB(RGB, RGB_COLOURSPACE_sRGB, RGB_COLOURSPACE_PROPHOTO_RGB)
... # doctest: +ELLIPSIS
array([ 0.2568891..., 0.0721446..., 0.0465553...])
>>> RGB_to_RGB(RGB, "sRGB", "ProPhoto RGB")
... # doctest: +ELLIPSIS
array([ 0.2568891..., 0.0721446..., 0.0465553...])
"""
from colour.models import RGB_COLOURSPACES # noqa: PLC0415
if isinstance(input_colourspace, str):
input_colourspace = validate_method(
input_colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
input_colourspace = cast("RGB_Colourspace", RGB_COLOURSPACES[input_colourspace])
if isinstance(output_colourspace, str):
output_colourspace = validate_method(
output_colourspace,
tuple(RGB_COLOURSPACES),
'"{0}" "RGB" colourspace is invalid, it must be one of {1}!',
)
output_colourspace = cast(
"RGB_Colourspace", RGB_COLOURSPACES[output_colourspace]
)
RGB = to_domain_1(RGB)
if apply_cctf_decoding and input_colourspace.cctf_decoding is not None:
with domain_range_scale("ignore"):
RGB = input_colourspace.cctf_decoding(
RGB, **filter_kwargs(input_colourspace.cctf_decoding, **kwargs)
)
M = matrix_RGB_to_RGB(
input_colourspace, output_colourspace, chromatic_adaptation_transform
)
RGB = vecmul(M, RGB)
if apply_cctf_encoding and output_colourspace.cctf_encoding is not None:
with domain_range_scale("ignore"):
RGB = output_colourspace.cctf_encoding(
RGB,
**filter_kwargs(output_colourspace.cctf_encoding, **kwargs),
)
return from_range_1(RGB)