Basic Concepts
==============
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:: python
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:: python
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:: python
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 varous 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.])