Colour spreads over various domains of the Colour Science from colour models to optical phenomenons, this tutorial will not give you a complete overview of the API but will still be good introduction.
Note: A directory full of examples is available at this path in your Colour installation: colour/examples. You can also explore it directly on Github: https://github.com/colour-science/colour/tree/master/colour/examples
In [1]:
%matplotlib inline
In [2]:
from colour.plotting import *
visible_spectrum_plot()
Out[2]:
Colour is organised around various sub-packages:
Most of the public API is available from the root colour namespace:
In [3]:
import colour
print(colour.__all__[:5] + ['...'])
The various sub-packages also expose their public API:
In [4]:
from pprint import pprint
import colour.plotting
for sub_package in ('adaptation', 'algebra', 'appearance', 'characterisation',
'colorimetry', 'constants', 'corresponding', 'difference',
'io', 'models', 'notation', 'phenomenons', 'plotting',
'quality', 'recovery', 'temperature', 'utilities',
'volume'):
print(sub_package.title())
pprint(getattr(colour, sub_package).__all__)
print('\n')
The code is well documented and almost every docstrings have usage examples:
In [5]:
print(colour.CCT_to_uv_Ohno2013.__doc__)
At the core of Colour is the colour.colorimetry sub-package, it defines the objects needed for spectral related computations and many others:
In [6]:
from pprint import pprint
import colour.colorimetry as colorimetry
pprint(colorimetry.__all__)
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 [7]:
import colour.colorimetry.dataset as dataset
pprint(dataset.__all__)
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 [8]:
# Defining a sample spectral power distribution data.
sample_spd_data = {
380: 0.048,
385: 0.051,
390: 0.055,
395: 0.060,
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', sample_spd_data)
print(spd)
The sample spectral power distribution can be easily plotted against the visible spectrum:
In [9]:
# Plotting the sample spectral power distribution.
single_spd_plot(spd)
Out[9]:
With the sample spectral power distribution defined, we can retrieve its shape:
In [10]:
# Displaying the sample spectral power distribution shape.
print(spd.shape)
The shape returned is an instance of colour.SpectralShape class:
In [11]:
repr(spd.shape)
Out[11]:
colour.SpectralShape is used throughout Colour to define spectral dimensions and is instantiated as follows:
In [12]:
# Using *colour.SpectralShape* with iteration.
shape = colour.SpectralShape(start=0, end=10, steps=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[12]:
Colour defines three convenient objects to create constant spectral power distributions:
In [13]:
# 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 [14]:
print(repr(colour.DEFAULT_SPECTRAL_SHAPE))
A custom shape can be passed to construct a constant spectral power distribution with tailored dimensions:
In [15]:
colour.ones_spd(colour.SpectralShape(400, 700, 5))[450]
Out[15]:
The colour.SpectralPowerDistribution class supports the following arithmetical operations:
In [16]:
spd1 = colour.ones_spd()
print('"Ones Filled Spectral Power Distribution"')
print(spd1[400])
print('\n"x2 Constant Multiplied"')
print((spd1 * 2)[400])
print('\n"+ Spectral Power Distribution"')
print((spd1 + colour.ones_spd())[400])
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. [1]</a>
We can check the uniformity of the sample spectral power distribution:
In [17]:
# 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 [18]:
# Cloning the sample spectral power distribution.
clone_spd = spd.clone()
# Interpolating the cloned sample spectral power distribution.
clone_spd.interpolate(colour.SpectralShape(400, 770, 1))
clone_spd[401]
Out[18]:
In [19]:
# Comparing the interpolated spectral power distribution with the original one.
multi_spd_plot([spd, clone_spd], bounding_box=[730,780, 0.1, 0.5])
Out[19]:
Extrapolation although dangerous can be used to help aligning two spectral power distributions together. CIE publication CIE 15:2004 "Colorimetry" recommends that unmeasured values may be set equal to the nearest measured value of the appropriate quantity in truncation: [2]</a>
In [20]:
# Extrapolating the cloned sample spectral power distribution.
clone_spd.extrapolate(colour.SpectralShape(340, 830))
clone_spd[340], clone_spd[830]
Out[20]:
The extrapolation can be forced to use Linear method instead of the Constant default method or even use arbitrary constant left and right values:
In [21]:
# Extrapolating the cloned sample spectral power distribution with *Linear* method.
# We first trim the spectral power distribution using the interpolation method,
# the steps being the same, no interpolation will actually occur.
clone_spd.interpolate(colour.SpectralShape(400, 700))
clone_spd.extrapolate(colour.SpectralShape(340, 830), method='Linear', right=0)
clone_spd[340], clone_spd[830]
Out[21]:
Aligning a spectral power distribution is a convenient way to first interpolatesthe current data within its original bounds then if needed extrapolates any missing values to match the requested shape:
In [22]:
# Aligning the cloned sample spectral power distribution.
# We first trim the spectral power distribution as above.
clone_spd.interpolate(colour.SpectralShape(400, 700))
clone_spd.align(colour.SpectralShape(340, 830, 5))
clone_spd[340], clone_spd[830]
Out[22]:
The colour.SpectralPowerDistribution class also supports various arithmetic operations like addition, subtraction, multiplication or division with numeric and array_like variables or other colour.SpectralPowerDistribution class instances:
In [23]:
spd = colour.SpectralPowerDistribution('Sample', {410: 0.50, 420: 0.75, 430: 1., 440: 0.75, 450: 0.50})
print((spd.clone() + 1).values)
print((spd.clone() * 2).values)
print((spd * [0.35, 1.55, 0.75, 2.55, 0.95]).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 [24]:
print(spd.normalise().values)
print(spd.normalise(100).values)
From a given spectral power distribution, CIE XYZ tristimulus values can be calculated:
In [25]:
spd = colour.SpectralPowerDistribution('Sample', sample_spd_data)
cmfs = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']
illuminant = colour.ILLUMINANTS_RELATIVE_SPDS['D65']
# Calculating the sample spectral power distribution *CIE XYZ* tristimulus values.
XYZ = colour.spectral_to_XYZ(spd, cmfs, illuminant)
print(XYZ)
Note: Output CIE XYZ colourspace matrix is in domain [0, 100].
CIE XYZ is the central colourspace for Colour Science from which many computations are available, cascading to even more computations:
In [26]:
# Displaying objects interacting directly with the *CIE XYZ* colourspace.
pprint([name for name in colour.__all__ if name.startswith('XYZ_to')])
We can for instance converts the CIE XYZ tristimulus values into sRGB colourspace RGB values in order to display them on screen:
In [27]:
# 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.
RGB = colour.XYZ_to_sRGB(XYZ / 100)
print(RGB)
In [28]:
# Plotting the *sRGB* colourspace colour of the *Sample* spectral power distribution.
single_colour_plot(colour_parameter('Sample', RGB), text_size=32)
Out[28]:
In the same way, we can compute values from a colour rendition chart sample.
Note: This is useful for render time checks in the VFX industry, where you can use a synthetic colour chart into your render and ensure the colour management is acting as expected.
The colour.characterisation sub-package contains the dataset for various colour rendition charts:
In [29]:
# Colour rendition charts chromaticity coordinates.
print(sorted(colour.characterisation.COLOURCHECKERS.keys()))
# Colour rendition charts spectral power distributions.
print(sorted(colour.characterisation.COLOURCHECKERS_SPDS.keys()))
Note: The above cc2005, babel_average and cc_ohta keys are convenient aliases for respectively ColorChecker 2005, BabelColor Average and ColorChecker N Ohta keys.
In [30]:
# Plotting the *sRGB* colourspace colour of *neutral 5 (.70 D)* patch.
patch_name = 'neutral 5 (.70 D)'
patch_spd = colour.COLOURCHECKERS_SPDS['ColorChecker N Ohta'][patch_name]
XYZ = colour.spectral_to_XYZ(patch_spd, cmfs, illuminant)
RGB = colour.XYZ_to_sRGB(XYZ / 100)
single_colour_plot(colour_parameter(patch_name.title(), RGB), text_size=32)
Out[30]:
Colour defines a convenient plotting object to draw synthetic colour rendition charts figures:
In [31]:
colour_checker_plot(colour_checker='ColorChecker 2005', text_display=False)
Out[31]:
Given a spectral power distribution, chromaticity coordinates xy can be computed using the colour.XYZ_to_xy definition:
In [32]:
# Computing *xy* chromaticity coordinates for the *neutral 5 (.70 D)* patch.
xy = colour.XYZ_to_xy(XYZ)
print(xy)
Chromaticity coordinates xy can be plotted into the CIE 1931 Chromaticity Diagram:
In [33]:
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.
CIE_1931_chromaticity_diagram_plot(standalone=False)
# Plotting the *xy* chromaticity coordinates.
x, y = xy
pylab.plot(x, y, 'o-', color='white')
# Annotating the plot.
pylab.annotate(patch_spd.name.title(),
xy=xy,
xytext=(-50, 30),
textcoords='offset points',
arrowprops=dict(arrowstyle='->', connectionstyle='arc3, rad=-0.2'))
# Displaying the plot.
display(standalone=True)
Out[33]:
We hope that this small introduction has been useful and gave you the envy to see more, if you want to explore the API a good place to start is the IPython Notebooks page.