Basic Usage =========== This page puts an emphasis on basic concepts of **Colour**, those are important to understand. Object Name Categorisation -------------------------- The API tries to bundle the objects by categories by naming them with common prefixes which makes introspection and auto-completion easier. For example, in `IPython `__ or `Jupyter Notebook `__, most of the definitions pertaining to the spectral distribution handling can be found as follows: .. code:: text In [1]: import colour In [2]: colour.sd_ sd_blackbody() sd_gaussian() sd_rayleigh_scattering() sd_zeros sd_CIE_illuminant_D_series() sd_mesopic_luminous_efficiency_function() sd_single_led() sd_CIE_standard_illuminant_A() sd_multi_leds() sd_to_aces_relative_exposure_values() sd_constant() sd_ones() sd_to_XYZ Likewise, for the spectral distribution handling related attributes: .. code:: text In [2]: colour.SD SD_GAUSSIAN_METHODS SD_TO_XYZ_METHODS SDS_ILLUMINANTS SDS_LIGHT_SOURCES SD_MULTI_LEDS_METHODS SDS_COLOURCHECKERS SDS_LEFS SD_SINGLE_LED_METHODS SDS_FILTERS SDS_LENSES Similarly, all the RGB colourspaces can be individually accessed from the ``colour.models`` namespace: .. code:: text In [2]: colour.models.RGB_COLOURSPACE RGB_COLOURSPACE_ACES2065_1 RGB_COLOURSPACE_ACESPROXY RGB_COLOURSPACE_APPLE_RGB RGB_COLOURSPACE_BT470_525 RGB_COLOURSPACE_ACESCC RGB_COLOURSPACE_ADOBE_RGB1998 RGB_COLOURSPACE_BEST_RGB RGB_COLOURSPACE_BT470_625 RGB_COLOURSPACE_ACESCCT RGB_COLOURSPACE_ADOBE_WIDE_GAMUT_RGB RGB_COLOURSPACE_BETA_RGB RGB_COLOURSPACE_BT709 > RGB_COLOURSPACE_ACESCG RGB_COLOURSPACE_ARRI_WIDE_GAMUT_3 RGB_COLOURSPACE_BT2020 RGB_COLOURSPACE_CIE_RGB Abbreviations ------------- The following abbreviations are in use in `Colour `__: - **CAM** : Colour Appearance Model - **CCS** : Chromaticity Coordinates - **CCTF** : Colour Component Transfer Function - **CCT** : Correlated Colour Temperature - **CMY** : Cyan, Magenta, Yellow - **CMYK** : Cyan, Magenta, Yellow, Black - **CVD** : Colour Vision Deficiency - **CV** : Code Value - **EOTF** : Electro-Optical Transfer Function - **IDT** : Input Device Transform - **MSDS** : Multi-Spectral Distributions - **OETF** : Optical-Electrical Transfer Function - **OOTF** : Optical-Optical Transfer Function - **SD** : Spectral Distribution - **TVS** : Tristimulus Values N-Dimensional Array Support --------------------------- Most of **Colour** definitions are fully vectorised and support n-dimensional array by leveraging `Numpy `__. While it is recommended to use `ndarray `__ as input for the API objects, it is possible to use tuples or lists: .. code:: python import colour xyY = (0.4316, 0.3777, 0.1008) colour.xyY_to_XYZ(xyY) .. code-block:: text array([ 0.11518475, 0.1008 , 0.05089373]) .. code:: python xyY = [0.4316, 0.3777, 0.1008] colour.xyY_to_XYZ(xyY) .. code-block:: text array([ 0.11518475, 0.1008 , 0.05089373]) .. code:: python xyY = [ (0.4316, 0.3777, 0.1008), (0.4316, 0.3777, 0.1008), (0.4316, 0.3777, 0.1008), ] colour.xyY_to_XYZ(xyY) .. code-block:: text array([[ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373]]) As shown in the above example, there is widespread support for n-dimensional arrays: .. code:: python import numpy as np xyY = np.array([0.4316, 0.3777, 0.1008]) xyY = np.tile(xyY, (6, 1)) colour.xyY_to_XYZ(xyY) .. code-block:: text array([[ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373]]) .. code:: python colour.xyY_to_XYZ(xyY.reshape([2, 3, 3])) .. code-block:: text array([[[ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373]], [[ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373], [ 0.11518475, 0.1008 , 0.05089373]]]) Which enables image processing: .. code:: python RGB = colour.read_image("_static/Logo_Small_001.png") RGB = RGB[..., 0:3] # Discarding alpha channel. XYZ = colour.sRGB_to_XYZ(RGB) colour.plotting.plot_image(XYZ, text_kwargs={"text": "sRGB to XYZ"}) .. image:: _static/Basics_Logo_Small_001_CIE_XYZ.png .. _spectral-representation-and-continuous-signal: Spectral Representation and Continuous Signal --------------------------------------------- Floating Point Wavelengths ~~~~~~~~~~~~~~~~~~~~~~~~~~ **Colour** `current representation `__ of spectral data is atypical and has been influenced by the failures and shortcomings of the previous implementation that required `less than ideal code `__ to support floating point wavelengths. Wavelengths should not have to be defined as integer values and it is effectively common to get data from instruments whose domain is returned as floating point values. For example, the data from an `Ocean Insight (Optics) STS-VIS `__ spectrometer is typically saved with 3 digits decimal precision: .. code-block:: text Data from Subt2_14-36-15-210.txt Node Date: Sat Nov 17 14:36:15 NZDT 2018 User: kelsolaar Spectrometer: S12286 Trigger mode: 0 Resolution mode: 1024 pixels Integration Time (sec): 5.000000E0 Scans to average: 3 Nonlinearity correction enabled: true Boxcar width: 3 Baseline correction enabled: true XAxis mode: Wavelengths Number of Pixels in Spectrum: 1024 # >>>>>Begin Spectral Data<<<<< 338.028 279.71 338.482 285.43 338.936 291.33 ... 821.513 3112.65 822.008 3133.74 822.503 3107.11 A solution to the problem is to quantize the data at integer values but it is often non-desirable. The spectra representation implementation prior to **Colour 0.3.11** was relying on a `custom mutable mapping `__ which was allowing to retrieve decimal keys within a given precision: .. code:: python data_1 = {0.1999999998: "Nemo", 0.2000000000: "John"} apm_1 = ArbitraryPrecisionMapping(data_1, key_decimals=10) tuple(apm_1.keys()) .. code-block:: text (0.1999999998, 0.2) .. code:: python apm_2 = ArbitraryPrecisionMapping(data_1, key_decimals=7) tuple(apm_2.keys()) .. code-block:: text (0.2,) While functional, the approach was brittle and not elegant which triggered a `significant amount of rework `__. Continuous Signal ~~~~~~~~~~~~~~~~~ All the spectral distributions in **Colour** are instances of the :class:`colour.SpectralDistribution` class (or its sub-classes), a sub-class of the :class:`colour.continuous.Signal` class which is itself an implementation of the :class:`colour.continuous.AbstractContinuousFunction` `ABCMeta `__ class: .. inheritance-diagram:: colour.SpectralDistribution :parts: 1 Likewise, the multi-spectral distributions are instances :class:`colour.MultiSpectralDistributions` class (or its sub-classes), a sub-class of the :class:`colour.continuous.MultiSignals` class which is a container for multiple :class:`colour.continuous.Signal` sub-class instances and also implements the :class:`colour.continuous.AbstractContinuousFunction` ABCMeta class. .. inheritance-diagram:: colour.MultiSpectralDistributions :parts: 1 The :class:`colour.continuous.Signal` class implements the :meth:`Signal.function` method so that evaluating the function for any independent domain :math:`x \in\mathbb{R}` variable returns a corresponding range :math:`y \in\mathbb{R}` variable. It adopts an interpolating function encapsulated inside an extrapolating function. The resulting function independent domain, stored as discrete values in the :attr:`colour.continuous.Signal.domain` attribute corresponds with the function dependent and already known range stored in the :attr:`colour.continuous.Signal.range` attribute. Consequently, it is possible to get the value of a spectral distribution at any given wavelength: .. code:: python data = { 500: 0.0651, 520: 0.0705, 540: 0.0772, 560: 0.0870, 580: 0.1128, 600: 0.1360, } sd = colour.SpectralDistribution(data) sd[555.5] .. code-block:: text 0.083453673782958995 Getting, Setting, Indexing and Slicing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. attention:: Indexing a spectral distribution (or multi-spectral distribution) with a numeric (or a numeric sequence) returns the corresponding value(s). Indexing a spectral distribution (or multi-spectral distribution) with a slice returns the values for the corresponding wavelength *indexes*. While it is tempting to think that the :class:`colour.SpectralDistribution` and :class:`colour.MultiSpectralDistributions` classes behave like Numpy's `ndarray `__, they do not entirely and some peculiarities exist that make them different. An important difference lies in the behaviour with respect to getting and setting the values of the data. Getting the value(s) for a single (or multiple wavelengths) is done by indexing the :class:`colour.SpectralDistribution` (or :class:`colour.MultiSpectralDistributions`) class with the a single numeric or array of numeric wavelengths, e.g. ``sd[555.5]`` or ``sd[555.25, 555.25, 555.75]``. However, if getting the values using a :class:`slice` class instance, e.g. ``sd[0:3]``, the underlying discrete values for the indexes represented by the :class:`slice` class instance are returned instead. As shown in the previous section, getting the value of a wavelength is done as follows: .. code:: python data = { 500: 0.0651, 520: 0.0705, 540: 0.0772, 560: 0.0870, 580: 0.1128, 600: 0.1360, } sd = colour.SpectralDistribution(data) sd[555] .. code-block:: text 0.083135180664062502, Multiple wavelength values can be retrieved as follows: .. code:: python sd[(555.0, 556.25, 557.5, 558.75, 560.0)] .. code-block:: text array([ 0.08313518, 0.08395997, 0.08488108, 0.085897 , 0.087 ]) However, slices will return the values for the corresponding wavelength *indexes*: .. code:: python sd[0:3] .. code-block:: text array([ 0.0651, 0.0705, 0.0772]) .. code:: python sd[:] .. code-block:: text array([ 0.0651, 0.0705, 0.0772, 0.087 , 0.1128, 0.136 ]) .. note:: Indexing a multi-spectral distribution is achieved similarly, it can however be sliced along multiple axes because the data is2-dimensional, e.g. `msds[0:3, 0:2]`. A *copy* of the underlying :class:`colour.SpectralDistribution` and :class:`colour.MultiSpectralDistributions` classes discretized data can be accessed via the ``wavelengths`` and ``values`` properties. However, it cannot be changed directly via the properties or slicing: .. attention:: The data returned by the ``wavelengths`` and ``values`` properties is a *copy* of the underlying :class:`colour.SpectralDistribution` and :class:`colour.MultiSpectralDistributions` classes discretized data: It can only be changed indirectly. .. code:: python data = { 500: 0.0651, 520: 0.0705, 540: 0.0772, 560: 0.0870, 580: 0.1128, 600: 0.1360, } sd = colour.SpectralDistribution(data) # Note: The wavelength 500nm is at index 0. sd.values[0] = 0 sd[500] .. code-block:: text 0.065100000000000019 Instead, the values can be set indirectly: .. code:: python values = sd.values values[0] = 0 sd.values = values sd.values .. code-block:: text array([ 0. , 0.0705, 0.0772, 0.087 , 0.1128, 0.136 ]) Domain-Range Scales ------------------- .. note:: This section contains important information. **Colour** adopts 4 main input domains and output ranges: - *Scalars* usually in domain-range ``[0, 1]`` (or ``[0, 10]`` for *Munsell Value*). - *Percentages* usually in domain-range ``[0, 100]``. - *Degrees* usually in domain-range ``[0, 360]``. - *Integers* usually in domain-range ``[0, 2**n -1]`` where ``n`` is the bit depth. It is error prone but it is also a direct consequence of the inconsistency of the colour science field itself. We have discussed at length about this and we were leaning toward normalisation of the whole API to domain-range ``[0, 1]``, we never committed for reasons highlighted by the following points: - Colour Scientist performing computations related to Munsell Renotation System would be very surprised if the output *Munsell Value* was in range ``[0, 1]`` or ``[0, 100]``. - A Visual Effect Industry artist would be astonished to find out that conversion from *CIE XYZ* to *sRGB* was yielding values in range ``[0, 100]``. However benefits of having a consistent and predictable domain-range scale are numerous thus with `Colour 0.3.12 `__ we have introduced a mechanism to allow users to work within one of the two available domain-range scales. Scale - Reference ~~~~~~~~~~~~~~~~~ **'Reference'** is the default domain-range scale of **Colour**, objects adopt the implemented reference, i.e. paper, publication, etc.., domain-range scale. The **'Reference'** domain-range scale is inconsistent, e.g. colour appearance models, spectral conversions are typically in domain-range ``[0, 100]`` while RGB models will operate in domain-range ``[0, 1]``. Some objects, e.g. :func:``colour.colorimetry.lightness_Fairchild2011`` definition have mismatched domain-range: input domain ``[0, 1]`` and output range ``[0, 100]``. Scale - 1 ~~~~~~~~~ **'1'** is a domain-range scale converting all the relevant objects from **Colour** public API to domain-range ``[0, 1]``: - *Scalars* in domain-range ``[0, 10]``, e.g *Munsell Value* are scaled by *10*. - *Percentages* in domain-range ``[0, 100]`` are scaled by *100*. - *Degrees* in domain-range ``[0, 360]`` are scaled by *360*. - *Integers* in domain-range ``[0, 2**n -1]`` where ``n`` is the bit depth are scaled by *2**n -1*. - *Dimensionless* values are unaffected and are indicated with ``DN``. - *Unaffected* values are unaffected and are indicated with ``UN``. .. warning:: The conversion to **'1'** domain-range scale is a *soft* normalisation and similarly to the **'Reference'** domain-range scale it is normal to encounter values exceeding *1*, e.g. High Dynamic Range Imagery (HDRI) or negative values, e.g. out-of-gamut RGB colourspace values. Some definitions such as :func:`colour.models.eotf_ST2084` which decodes absolute luminance values are not affected by any domain-range scales and are indicated with `UN`. Understanding the Domain-Range Scale of an Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using :func:`colour.adaptation.chromatic_adaptation_CIE1994` definition docstring as an example, the *Notes* section features two tables. The first table is for the domain, and lists the input arguments affected by the two domain-range scales and which normalisation they should adopt depending the domain-range scale in use: +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ_1`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ | ``Y_o`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ The second table is for the range and lists the return value of the definition: +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ_2`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ Working with the Domain-Range Scales ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The current domain-range scale is returned with the :func:`colour.get_domain_range_scale` definition: .. code:: python import colour colour.get_domain_range_scale() .. code-block:: text u'reference' Changing from the **'Reference'** default domain-range scale to **'1'** is done with the :func:`colour.set_domain_range_scale` definition: .. code:: python XYZ_1 = [28.00, 21.26, 5.27] xy_o1 = [0.4476, 0.4074] xy_o2 = [0.3127, 0.3290] Y_o = 20 E_o1 = 1000 E_o2 = 1000 colour.adaptation.chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) .. code-block:: text array([ 24.03379521, 21.15621214, 17.64301199]) .. code:: python colour.set_domain_range_scale("1") XYZ_1 = [0.2800, 0.2126, 0.0527] Y_o = 0.2 colour.adaptation.chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) .. code-block:: text array([ 0.24033795, 0.21156212, 0.17643012]) The output tristimulus values with the **'1'** domain-range scale are equal to those from **'Reference'** default domain-range scale divided by *100*. Passing incorrectly scaled values to the :func:`colour.adaptation.chromatic_adaptation_CIE1994` definition would result in unexpected values and a warning in that case: .. code:: python colour.set_domain_range_scale("Reference") colour.adaptation.chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) .. code-block:: text File "", line 4, in E_o2) File "/colour-science/colour/colour/adaptation/cie1994.py", line 134, in chromatic_adaptation_CIE1994 warning(('"Y_o" luminance factor must be in [18, 100] domain, ' /colour-science/colour/colour/utilities/verbose.py:207: ColourWarning: "Y_o" luminance factor must be in [18, 100] domain, unpredictable results may occur! warn(*args, **kwargs) array([ 0.17171825, 0.13731098, 0.09972054]) Setting the **'1'** domain-range scale has the following effect on the :func:`colour.adaptation.chromatic_adaptation_CIE1994` definition: As it expects values in domain ``[0, 100]``, scaling occurs and the relevant input values, i.e. the values listed in the domain table, ``XYZ_1`` and ``Y_o`` are converted from domain ``[0, 1]`` to domain ``[0, 100]`` by :func:`colour.utilities.to_domain_100` definition and conversely return value ``XYZ_2`` is converted from range ``[0, 100]`` to range ``[0, 1]`` by :func:`colour.utilities.from_range_100` definition. A convenient alternative to the :func:`colour.set_domain_range_scale` definition is the :class:`colour.domain_range_scale` context manager and decorator. It temporarily overrides **Colour** domain-range scale with given scale value: .. code:: python with colour.domain_range_scale("1"): colour.adaptation.chromatic_adaptation_CIE1994(XYZ_1, xy_o1, xy_o2, Y_o, E_o1, E_o2) .. code-block:: text [ 0.24033795 0.21156212 0.17643012] Multiprocessing on Windows with Domain-Range Scales ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Windows does not have a `fork `__ system call, a consequence is that child processes do not necessarily `inherit from changes made to global variables `__. It has crucial `consequences `__ as **Colour** stores the current domain-range scale into a global variable. The solution is to define an initialisation definition that defines the scale upon child processes spawning. The :class:`colour.utilities.multiprocessing_pool` context manager conveniently performs the required initialisation so that the domain-range scale is propagated appropriately to child processes. Safe Power and Division ----------------------- **Colour** default handling of fractional power and zero-division occurring during practical applications is managed via various definitions and context managers. Safe Power ~~~~~~~~~~ NaNs generation occurs when a negative number :math:`a` is raised to the fractional power :math:`p`. This can be avoided using the :func:`colour.algebra.spow` definition that raises to the power as follows: :math:`sign(a) * |a|^p`. To the extent possible, the :func:`colour.algebra.spow` definition has been used throughout the codebase. The default behaviour is controlled with the following definitions: - :func:`colour.algebra.is_spow_enabled` - :func:`colour.algebra.set_spow_enabled` - :func:`colour.algebra.spow_enable` (Context Manager & Decorator) Safe Division ~~~~~~~~~~~~~ NaNs and +/- infs generation occurs when a number :math:`a` is divided 0. This can be avoided using the :func:`colour.algebra.sdiv` definition. It has been used wherever deemed relevant in the codebase. The default behaviour is controlled with the following definitions: - :func:`colour.algebra.get_sdiv_mode` - :func:`colour.algebra.set_sdiv_mode` - :func:`colour.algebra.sdiv_mode` (Context Manager & Decorator) The following modes are available: - ``Numpy``: The current *Numpy* zero-division handling occurs. - ``Ignore``: Zero-division occurs silently. - ``Warning``: Zero-division occurs with a warning. - ``Ignore Zero Conversion``: Zero-division occurs silently and NaNs or +/- infs values are converted to zeros. See :func:`numpy.nan_to_num` definition for more details. - ``Warning Zero Conversion``: Zero-division occurs with a warning and NaNs or +/- infs values are converted to zeros. See :func:`numpy.nan_to_num` definition for more details. - ``Ignore Limit Conversion``: Zero-division occurs silently and NaNs or +/- infs values are converted to zeros or the largest +/- finite floating point values representable by the division result :class:`numpy.dtype`. See :func:`numpy.nan_to_num` definition for more details. - ``Warning Limit Conversion``: Zero-division occurs with a warning and NaNs or +/- infs values are converted to zeros or the largest +/- finite floating point values representable by the division result :class:`numpy.dtype`. .. code:: python colour.algebra.get_sdiv_mode() .. code-block:: text 'Ignore Zero Conversion' .. code:: python colour.algebra.set_sdiv_mode("Numpy") colour.UCS_to_uv([0, 0, 0]) .. code-block:: text /Users/kelsolaar/Documents/Development/colour-science/colour/colour/algebra/common.py:317: RuntimeWarning: invalid value encountered in true_divide c = a / b array([ nan, nan]) .. code:: python colour.algebra.set_sdiv_mode("Ignore Zero Conversion") colour.UCS_to_uv([0, 0, 0]) .. code-block:: text array([ 0., 0.])