Colour is defined as the characteristic of visual perception that can be described by attributes of hue, brightness (or lightness) and colourfulness (or saturation or chroma).
When necessary, to avoid confusion between other meanings of the word, the term "perceived colour" may be used.
Perceived colour depends on the spectral distribution of the colour stimulus, on the size, shape, structure and surround of the stimulus area, on the state of adaptation of the observer's visual system, and on the observer's experience of the prevailing and similar situations of observation. [1]
Light is the electromagnetic radiation that is considered from the point of view of its ability to excite the human visual system. [2]
The portion of the electromatic radiation frequencies perceived in the approximate wavelength range 360-780 nanometres (nm) is called the visible spectrum.
In [1]:
%matplotlib inline
In [2]:
import colour
from colour.plotting import *
colour.filter_warnings(True, False)
colour_plotting_defaults()
# Plotting the visible spectrum.
visible_spectrum_plot()
In [3]:
from pprint import pprint
import colour.colorimetry as colorimetry
pprint(colorimetry.__all__)
Note: colour.colorimetry sub-package public API is directly available from colour namespace.
Colour computations are based on a comprehensive dataset available in pretty much each sub-packages, for example colour.colorimetry.dataset defines the following data:
In [4]:
import colour.colorimetry.dataset as dataset
pprint(dataset.__all__)
Note: colour.colorimetry.dataset sub-package public API is directly available from colour namespace.
Whether it be a sample spectral power distribution, colour matching functions or illuminants, spectral data is manipulated using an object built with the colour.SpectralPowerDistribution class or based on it:
In [5]:
import colour
# Defining a sample spectral power distribution data.
sample_spd_data = {
380: 0.048,
385: 0.051,
390: 0.055,
395: 0.06,
400: 0.065,
405: 0.068,
410: 0.068,
415: 0.067,
420: 0.064,
425: 0.062,
430: 0.059,
435: 0.057,
440: 0.055,
445: 0.054,
450: 0.053,
455: 0.053,
460: 0.052,
465: 0.052,
470: 0.052,
475: 0.053,
480: 0.054,
485: 0.055,
490: 0.057,
495: 0.059,
500: 0.061,
505: 0.062,
510: 0.065,
515: 0.067,
520: 0.070,
525: 0.072,
530: 0.074,
535: 0.075,
540: 0.076,
545: 0.078,
550: 0.079,
555: 0.082,
560: 0.087,
565: 0.092,
570: 0.100,
575: 0.107,
580: 0.115,
585: 0.122,
590: 0.129,
595: 0.134,
600: 0.138,
605: 0.142,
610: 0.146,
615: 0.150,
620: 0.154,
625: 0.158,
630: 0.163,
635: 0.167,
640: 0.173,
645: 0.180,
650: 0.188,
655: 0.196,
660: 0.204,
665: 0.213,
670: 0.222,
675: 0.231,
680: 0.242,
685: 0.251,
690: 0.261,
695: 0.271,
700: 0.282,
705: 0.294,
710: 0.305,
715: 0.318,
720: 0.334,
725: 0.354,
730: 0.372,
735: 0.392,
740: 0.409,
745: 0.420,
750: 0.436,
755: 0.450,
760: 0.462,
765: 0.465,
770: 0.448,
775: 0.432,
780: 0.421}
spd = colour.SpectralPowerDistribution(sample_spd_data, name='Sample')
print(spd)
The sample spectral power distribution can be easily plotted against the visible spectrum:
In [6]:
# Plotting the sample spectral power distribution.
single_spd_plot(spd)
With the sample spectral power distribution defined, we can retrieve its shape:
In [7]:
# Displaying the sample spectral power distribution shape.
print(spd.shape)
The shape returned is an instance of colour.SpectralShape class:
In [8]:
repr(spd.shape)
Out[8]:
colour.SpectralShape is used throughout Colour to define spectral dimensions and is instantiated as follows:
In [9]:
# Using *colour.SpectralShape* with iteration.
shape = colour.SpectralShape(start=0, end=10, interval=1)
for wavelength in shape:
print(wavelength)
# *colour.SpectralShape.range* method is providing the complete range of values.
shape = colour.SpectralShape(0, 10, 0.5)
shape.range()
Out[9]:
Colour defines three convenient objects to create constant spectral power distributions:
In [10]:
# Defining a constant spectral power distribution.
constant_spd = colour.constant_spd(100)
print('"Constant Spectral Power Distribution"')
print(constant_spd.shape)
print(constant_spd[400])
# Defining a zeros filled spectral power distribution.
print('\n"Zeros Filled Spectral Power Distribution"')
zeros_spd = colour.zeros_spd()
print(zeros_spd.shape)
print(zeros_spd[400])
# Defining a ones filled spectral power distribution.
print('\n"Ones Filled Spectral Power Distribution"')
ones_spd = colour.ones_spd()
print(ones_spd.shape)
print(ones_spd[400])
By default the shape used by colour.constant_spd, colour.zeros_spd and colour.ones_spd is the one defined by colour.DEFAULT_SPECTRAL_SHAPE attribute using the CIE 1931 2° Standard Observer shape.
In [11]:
print(repr(colour.DEFAULT_SPECTRAL_SHAPE))
A custom shape can be passed to construct a constant spectral power distribution with tailored dimensions:
In [12]:
colour.ones_spd(colour.SpectralShape(400, 700, 5))[450]
Out[12]:
Often interpolation of the spectral power distribution is needed, this is achieved with the colour.SpectralPowerDistribution.interpolate method. Depending on the wavelengths uniformity, the default interpolation method will differ. Following CIE 167:2005 recommendation: The method developed by Sprague (1880) should be used for interpolating functions having a uniformly spaced independent variable and a Cubic Spline method for non-uniformly spaced independent variable. [4]
We can check the uniformity of the sample spectral power distribution:
In [13]:
# Checking the sample spectral power distribution uniformity.
print(spd.is_uniform())
Since the sample spectral power distribution is uniform the interpolation will be using the colour.SpragueInterpolator interpolator.
Note: Interpolation happens in place and may alter your original data, use the colour.SpectralPowerDistribution.clone method to produce a copy of your spectral power distribution before interpolation.
In [14]:
# Copying the sample spectral power distribution.
spd_copy = spd.copy()
# Interpolating the copied sample spectral power distribution.
spd_copy.interpolate(colour.SpectralShape(400, 770, 1))
spd_copy[401]
Out[14]:
In [15]:
# Comparing the interpolated spectral power distribution with the original one.
multi_spd_plot([spd, spd_copy], bounding_box=[730,780, 0.1, 0.5])
Extrapolation although dangerous can be used to help aligning two spectral power distributions together. CIE 015:2004 Colorimetry, 3rd Edition recommends that unmeasured values may be set equal to the nearest measured value of the appropriate quantity in truncation: [5]
In [16]:
# Extrapolating the copied sample spectral power distribution.
spd_copy.extrapolate(colour.SpectralShape(340, 830))
spd_copy[340], spd_copy[830]
Out[16]:
The underlying interpolator can be swapped for any of the Colour interpolators.
In [17]:
pprint([
export for export in colour.algebra.interpolation.__all__
if 'Interpolator' in export
])
In [18]:
# Changing interpolator while trimming the copied spectral power distribution.
spd_copy.interpolate(
colour.SpectralShape(400, 700, 10), interpolator=colour.LinearInterpolator)
Out[18]:
The extrapolation behaviour can be changed for Linear method instead of the Constant default method or even use arbitrary constant left and right values:
In [19]:
# Extrapolating the copied sample spectral power distribution with *Linear* method.
spd_copy.extrapolate(
colour.SpectralShape(340, 830),
extrapolator_args={'method': 'Linear',
'right': 0})
spd_copy[340], spd_copy[830]
Out[19]:
Aligning a spectral power distribution is a convenient way to first interpolate the current data within its original bounds then if needed extrapolates any missing values to match the requested shape:
In [20]:
# Aligning the cloned sample spectral power distribution.
# We first trim the spectral power distribution as above.
spd_copy.interpolate(colour.SpectralShape(400, 700))
spd_copy.align(colour.SpectralShape(340, 830, 5))
spd_copy[340], spd_copy[830]
Out[20]:
The colour.SpectralPowerDistribution class also supports various arithmetic operations like addition, subtraction, multiplication, division or exponentiation with numeric and array_like variables or other colour.SpectralPowerDistribution class instances:
In [21]:
spd = colour.SpectralPowerDistribution({
410: 0.25,
420: 0.50,
430: 0.75,
440: 1.0,
450: 0.75,
460: 0.50,
480: 0.25
})
print((spd.copy() + 1).values)
print((spd.copy() * 2).values)
print((spd * [0.35, 1.55, 0.75, 2.55, 0.95, 0.65, 0.15]).values)
print((spd * colour.constant_spd(2, spd.shape) * colour.constant_spd(3, spd.shape)).values)
The spectral power distribution can be normalised with an arbitrary factor:
In [22]:
print(spd.normalise().values)
print(spd.normalise(100).values)
In the late 1920's, Wright (1928) and Guild (1931) independently conducted a series of colour matching experiments to quantify the colour ability of an average human observer which laid the foundation for the specification of the CIE XYZ colourspace. The results obtained were summarized by the Wright & Guild 1931 2° RGB CMFs $\bar{r}(\lambda)$,$\bar{g}(\lambda)$,$\bar{b}(\lambda)$ colour matching functions: they represent the amounts of three monochromatic primary colours $\textbf{R}$,$\textbf{G}$,$\textbf{B}$ needed to match the test colour at a single wavelength of light.
See Also: The Colour Matching Functions notebook for in-depth information about the colour matching functions.
In [23]:
# Plotting *Wright & Guild 1931 2 Degree RGB CMFs* colour matching functions.
single_cmfs_plot('Wright & Guild 1931 2 Degree RGB CMFs')
With an RGB model of human vision based on Wright & Guild 1931 2° RGB CMFs $\bar{r}(\lambda)$,$\bar{g}(\lambda)$,$\bar{b}(\lambda)$ colour matching functions and for pragmatic reasons the CIE members developed a new colour space that would relate to the CIE RGB colourspace but for which all tristimulus values would be positive for real colours: CIE XYZ described with $\bar{x}(\lambda)$,$\bar{y}(\lambda)$,$\bar{z}(\lambda)$ colour matching functions.
In [24]:
# Plotting *CIE XYZ 1931 2 Degree Standard Observer* colour matching functions.
single_cmfs_plot('CIE 1931 2 Degree Standard Observer')
In the 1960's it appeared that cones were present in a larger region of eye than the one initially covered by the experiments that lead to the CIE 1931 2° Standard Observer specification.
As a result, colour computations done with the CIE 1931 2° Standard Observer do not always correlate to the visual observation.
In 1964, the CIE defined an additional standard observer: the CIE 1964 10° Standard Observer derived from the work of Stiles and Burch (1959), and Speranskaya (1959). The CIE 1964 10° Standard Observer is believed to be a better representation of the human vision spectral response and recommended when dealing with a field of view of more than 4°.
For example and as per CIE recommendation, the CIE 1964 10° Standard Observer is commonly used with spectrophotometers for colour measurements whereas colorimeters generally use the CIE 1931 2° Standard Observer for quality control and other colour evaluation applications.
The CIE XYZ tristimulus values specify a colour stimulus in terms of the visual system. Their values for colour of a surface with spectral reflectance $\beta(\lambda)$ under an illuminant of relative spectral power $S(\lambda)$ are calculated using the following equations: [6]
$$ \begin{equation} X=k\int_{\lambda}\beta(\lambda)S(\lambda)\bar{x}(\lambda)d\lambda\\ Y=k\int_{\lambda}\beta(\lambda)S(\lambda)\bar{y}(\lambda)d\lambda\\ Z=k\int_{\lambda}\beta(\lambda)S(\lambda)\bar{z}(\lambda)d\lambda \end{equation} $$where $$ \begin{equation} k=\cfrac{100}{\int_{\lambda}S(\lambda)\bar{y}(\lambda)d\lambda} \end{equation} $$
However in virtually all practical computations of CIE XYZ tristimulus values, the integrals are replaced by summations:
$$ \begin{equation} X=k\sum\limits_{\lambda=\lambda_a}^{\lambda_b}\beta(\lambda)S(\lambda)\bar{x}(\lambda)\Delta\lambda\\ Y=k\sum\limits_{\lambda=\lambda_a}^{\lambda_b}\beta(\lambda)S(\lambda)\bar{y}(\lambda)\Delta\lambda\\ Z=k\sum\limits_{\lambda=\lambda_a}^{\lambda_b}\beta(\lambda)S(\lambda)\bar{z}(\lambda)\Delta\lambda\\ \end{equation} $$where $$ \begin{equation} k=\cfrac{100}{\sum\limits_{\lambda=\lambda_a}^{\lambda_b}S(\lambda)\bar{y}(\lambda)\Delta\lambda} \end{equation} $$
Calculating the CIE XYZ tristimulus values of a colour stimulus is done using the colour.spectral_to_XYZ definition which follows ASTM E2022–11 and ASTM E308–15 practises computation method:
In [25]:
spd = colour.SpectralPowerDistribution(sample_spd_data, name='Sample')
cmfs = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']
illuminant = colour.ILLUMINANTS_RELATIVE_SPDS['A']
# Calculating the sample spectral power distribution *CIE XYZ* tristimulus values.
colour.spectral_to_XYZ(spd, cmfs, illuminant)
Out[25]:
Note: Output CIE XYZ colourspace matrix is in domain [0, 100].
CIE XYZ tristimulus values can be plotted into the CIE 1931 Chromaticity Diagram:
In [26]:
import pylab
# Plotting the *CIE 1931 Chromaticity Diagram*.
# The argument *standalone=False* is passed so that the plot doesn't get displayed
# and can be used as a basis for other plots.
chromaticity_diagram_plot_CIE1931(standalone=False)
# Calculating the *xy* chromaticity coordinates.
# The output domain of *colour.spectral_to_XYZ* is [0, 100] and
# the input domain of *colour.XYZ_to_sRGB* is [0, 1].
# We need to take it in account and rescale the input *CIE XYZ* colourspace matrix.
x, y = colour.XYZ_to_xy(colour.spectral_to_XYZ(spd, cmfs, illuminant) / 100)
# Plotting the *xy* chromaticity coordinates.
pylab.plot(x, y, 'o-', color='white')
# Annotating the plot.
pylab.annotate(spd.name,
xy=(x, y),
xytext=(-50, 30),
textcoords='offset points',
arrowprops=dict(arrowstyle='->', connectionstyle='arc3, rad=-0.2'))
# Displaying the plot.
render(standalone=True)
Retrieving the CIE XYZ tristimulus values of any wavelength from colour matching functions is done using the colour.wavelength_to_XYZ definition, if the value requested is not available, the colour matching functions will be interpolated following CIE 167:2005 recommendation:
In [27]:
colour.wavelength_to_XYZ(546.1, colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'])
Out[27]: