#!/usr/bin/env python # coding: utf-8 # # Spectrum # [Colour](http://en.wikipedia.org/wiki/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](http://en.wikipedia.org/wiki/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](http://en.wikipedia.org/wiki/Visible_spectrum). # In[1]: import colour from colour.plotting import * # In[2]: colour_style(); # In[3]: # Plotting the visible spectrum. plot_visible_spectrum(); # The spectrum is defined as the display or specification of the monochromatic components of the radiation considered. [3] # # At the core of [Colour](https://github.com/colour-science/colour/) is the `colour.colorimetry` sub-package, it defines the objects needed for spectral related computations and many others: # In[4]: 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](https://github.com/colour-science/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[5]: import colour.colorimetry.datasets as datasets pprint(datasets.__all__) # > Note: `colour.colorimetry.dataset` sub-package public API is directly available from `colour` namespace. # ## Spectral Distribution # Whether it be a sample spectral distribution, colour matching functions or illuminants, spectral data is manipulated using an object built with the `colour.SpectralDistribution` class or based on it: # In[6]: import colour # Defining a sample spectral distribution data. sample_sd_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} sd = colour.SpectralDistribution(sample_sd_data, name='Sample') print(sd) # The sample spectral distribution can be easily plotted against the visible spectrum: # In[7]: # Plotting the sample spectral distribution. plot_single_sd(sd); # With the sample spectral distribution defined, we can retrieve its shape: # In[8]: # Displaying the sample spectral distribution shape. print(sd.shape) # The shape returned is an instance of `colour.SpectralShape` class: # In[9]: repr(sd.shape) # The `colour.SpectralShape` class is used throughout [Colour](https://github.com/colour-science/colour/) to define spectral dimensions and is instantiated as follows: # In[10]: # 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() # [Colour](https://github.com/colour-science/colour/) defines three convenient objects to create constant spectral distributions: # # * `colour.sd_constant` # * `colour.sd_zeros` # * `colour.sd_ones` # In[11]: # Defining a constant spectral distribution. constant_sd = colour.sd_constant(100) print('"Constant Spectral Distribution"') print(constant_sd.shape) print(constant_sd[400]) # Defining a zeros filled spectral distribution. print('\n"Zeros Filled Spectral Distribution"') zeros_sd = colour.sd_zeros() print(zeros_sd.shape) print(zeros_sd[400]) # Defining a ones filled spectral distribution. print('\n"Ones Filled Spectral Distribution"') ones_sd = colour.sd_ones() print(ones_sd.shape) print(ones_sd[400]) # By default the shape used by `colour.sd_constant`, `colour.sd_zeros` and `colour.sd_ones` is the one defined by `colour.DEFAULT_SPECTRAL_SHAPE` attribute using the *CIE 1931 2° Standard Observer* shape. # In[12]: print(repr(colour.DEFAULT_SPECTRAL_SHAPE)) # A custom shape can be passed to construct a constant spectral distribution with tailored dimensions: # In[13]: colour.sd_ones(colour.SpectralShape(400, 700, 5))[450] # Often interpolation of the spectral distribution is needed, this is achieved with the `colour.SpectralDistribution.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 distribution: # In[14]: # Checking the sample spectral distribution uniformity. print(sd.is_uniform()) # Since the sample spectral 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.SpectralDistribution.copy` method to produce a copy of your spectral distribution before interpolation. # In[15]: # Copying the sample spectral distribution. sd_copy = sd.copy() # Interpolating the copied sample spectral distribution. sd_copy.interpolate(colour.SpectralShape(400, 770, 1)) sd_copy[401] # In[16]: # Comparing the interpolated spectral distribution with the original one. plot_multi_sds([sd, sd_copy], bounding_box=[730,780, 0.1, 0.5]); # Extrapolation although dangerous can be used to help aligning two spectral 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[17]: # Extrapolating the copied sample spectral distribution. sd_copy.extrapolate(colour.SpectralShape(340, 830)) sd_copy[340], sd_copy[830] # The underlying interpolator can be swapped for any of the *Colour* interpolators. # In[18]: pprint([ export for export in colour.algebra.interpolation.__all__ if 'Interpolator' in export ]) # In[19]: # Changing interpolator while trimming the copied spectral distribution. sd_copy.interpolate( colour.SpectralShape(400, 700, 10), interpolator=colour.LinearInterpolator) # 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[20]: # Extrapolating the copied sample spectral distribution with *Linear* method. sd_copy.extrapolate( colour.SpectralShape(340, 830), extrapolator_args={'method': 'Linear', 'right': 0}) sd_copy[340], sd_copy[830] # Aligning a spectral 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[21]: # Aligning the cloned sample spectral distribution. # We first trim the spectral distribution as above. sd_copy.interpolate(colour.SpectralShape(400, 700)) sd_copy.align(colour.SpectralShape(340, 830, 5)) sd_copy[340], sd_copy[830] # The `colour.SpectralDistribution` class also supports various arithmetic operations like *addition*, *subtraction*, *multiplication*, *division* or *exponentiation* with *numeric* and *array_like* variables or other `colour.SpectralDistribution` class instances: # In[22]: sd = colour.SpectralDistribution({ 410: 0.25, 420: 0.50, 430: 0.75, 440: 1.0, 450: 0.75, 460: 0.50, 480: 0.25 }) print((sd.copy() + 1).values) print((sd.copy() * 2).values) print((sd * [0.35, 1.55, 0.75, 2.55, 0.95, 0.65, 0.15]).values) print((sd * colour.sd_constant(2, sd.shape) * colour.sd_constant(3, sd.shape)).values) # The spectral distribution can be normalised with an arbitrary factor: # In[23]: print(sd.normalise().values) print(sd.normalise(100).values) # ## Colour Matching Functions # 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](http://en.wikipedia.org/wiki/CIE_color_space#Definition_of_the_CIE_XYZ_color_space). 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](cmfs.ipynb) notebook for in-depth information about the colour matching functions. # In[24]: # Plotting *Wright & Guild 1931 2 Degree RGB CMFs* colour matching functions. plot_single_cmfs('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[25]: # Plotting *CIE XYZ 1931 2 Degree Standard Observer* colour matching functions. plot_single_cmfs('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. # ## CIE XYZ Tristimulus Values # 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 spectral $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.sd_to_XYZ` definition which follows *ASTM E2022–11* and *ASTM E308–15* practises computation method: # In[26]: sd = colour.SpectralDistribution(sample_sd_data, name='Sample') cmfs = colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'] illuminant = colour.ILLUMINANTS_SDS['A'] # Calculating the sample spectral distribution *CIE XYZ* tristimulus values. colour.sd_to_XYZ(sd, cmfs, illuminant) # > 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[27]: 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. plot_chromaticity_diagram_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.sd_to_XYZ(sd, cmfs, illuminant) / 100) # Plotting the *xy* chromaticity coordinates. pylab.plot(x, y, 'o-', color='white') # Annotating the plot. pylab.annotate(sd.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[28]: colour.wavelength_to_XYZ(546.1, colour.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']) # ## Bibliography # 1. ^ CIE. (n.d.). 17-198 colour (perceived). Retrieved June 26, 2014, from http://eilv.cie.co.at/term/198 # 2. ^ CIE. (n.d.). 17-659 light. Retrieved June 26, 2014, from http://eilv.cie.co.at/term/659 # 3. ^ CIE. (n.d.). 17-1238 spectrum. Retrieved June 27, 2014, from http://eilv.cie.co.at/term/1238 # 4. ^ CIE TC 1-38. (2005). 9. INTERPOLATION. In CIE 167:2005 Recommended Practice for Tabulating Spectral Data for Use in Colour Computations (pp. 14–19). ISBN:978-3-901-90641-1 # 5. ^ CIE TC 1-48. (2004). CIE 015:2004 Colorimetry, 3rd Edition. CIE 015:2004 Colorimetry, 3rd Edition (pp. 1–82). ISBN:978-3-901-90633-6 # 6. ^ Wyszecki, G., & Stiles, W. S. (2000). Integration Replace by Summation. In *Color Science: Concepts and Methods, Quantitative Data and Formulae* (pp. 158–163). Wiley. ISBN:978-0471399186