#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Colorimetry Plotting
====================
Defines the colorimetry plotting objects:
- :func:`single_spd_plot`
- :func:`multi_spd_plot`
- :func:`single_cmfs_plot`
- :func:`multi_cmfs_plot`
- :func:`single_illuminant_relative_spd_plot`
- :func:`multi_illuminants_relative_spd_plot`
- :func:`visible_spectrum_plot`
- :func:`single_lightness_function_plot`
- :func:`multi_lightness_function_plot`
- :func:`blackbody_spectral_radiance_plot`
- :func:`blackbody_colours_plot`
References
----------
.. [1] Spiker, N. (2015). Private Discussion with Mansencal, T. Retrieved from
http://www.repairfaq.org/sam/repspec/
"""
from __future__ import division
import matplotlib.pyplot
import numpy as np
import pylab
from six.moves import reduce
from colour.colorimetry import (DEFAULT_SPECTRAL_SHAPE, ILLUMINANTS,
ILLUMINANTS_RELATIVE_SPDS, LIGHTNESS_METHODS,
SpectralShape, blackbody_spd, spectral_to_XYZ,
wavelength_to_XYZ)
from colour.models import XYZ_to_sRGB
from colour.plotting import (ColourParameter, DEFAULT_PLOTTING_ENCODING_CCTF,
DEFAULT_FIGURE_WIDTH, boundaries, canvas,
colour_parameters_plot, decorate, display,
get_cmfs, get_illuminant, single_colour_plot)
from colour.utilities import normalise_maximum
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2017 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'
__all__ = [
'single_spd_plot', 'multi_spd_plot', 'single_cmfs_plot', 'multi_cmfs_plot',
'single_illuminant_relative_spd_plot',
'multi_illuminants_relative_spd_plot', 'visible_spectrum_plot',
'single_lightness_function_plot', 'multi_lightness_function_plot',
'blackbody_spectral_radiance_plot', 'blackbody_colours_plot'
]
[docs]def single_spd_plot(spd,
cmfs='CIE 1931 2 Degree Standard Observer',
out_of_gamut_clipping=True,
**kwargs):
"""
Plots given spectral power distribution.
Parameters
----------
spd : SpectralPowerDistribution
Spectral power distribution to plot.
out_of_gamut_clipping : bool, optional
Whether to clip out of gamut colours otherwise, the colours will be
offset by the absolute minimal colour leading to a rendering on
gray background, less saturated and smoother. [1]_
cmfs : unicode
Standard observer colour matching functions used for spectrum creation.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
y0_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y0* line.
y1_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y1* line.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> from colour import SpectralPowerDistribution
>>> data = {400: 0.0641, 420: 0.0645, 440: 0.0562}
>>> spd = SpectralPowerDistribution('Custom', data)
>>> single_spd_plot(spd) # doctest: +SKIP
"""
cmfs = get_cmfs(cmfs)
shape = cmfs.shape
spd = spd.clone().interpolate(shape, 'Linear')
wavelengths = spd.wavelengths
values = spd.values
y1 = values
colours = XYZ_to_sRGB(
wavelength_to_XYZ(wavelengths, cmfs),
ILLUMINANTS['CIE 1931 2 Degree Standard Observer']['E'],
apply_encoding_cctf=False)
if not out_of_gamut_clipping:
colours += np.abs(np.min(colours))
colours = DEFAULT_PLOTTING_ENCODING_CCTF(normalise_maximum(colours))
settings = {
'title': '{0} - {1}'.format(spd.title, cmfs.title),
'x_label': 'Wavelength $\\lambda$ (nm)',
'y_label': 'Spectral Power Distribution',
'x_tighten': True,
'y_tighten': True
}
settings.update(kwargs)
return colour_parameters_plot([
ColourParameter(x=x[0], y1=x[1], RGB=x[2])
for x in tuple(zip(wavelengths, y1, colours))
], **settings)
[docs]def multi_spd_plot(spds,
cmfs='CIE 1931 2 Degree Standard Observer',
use_spds_colours=False,
normalise_spds_colours=False,
**kwargs):
"""
Plots given spectral power distributions.
Parameters
----------
spds : list
Spectral power distributions to plot.
cmfs : unicode, optional
Standard observer colour matching functions used for spectrum creation.
use_spds_colours : bool, optional
Whether to use spectral power distributions colours.
normalise_spds_colours : bool
Whether to normalise spectral power distributions colours.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> from colour import SpectralPowerDistribution
>>> data1 = {400: 0.0641, 420: 0.0645, 440: 0.0562}
>>> data2 = {400: 0.134, 420: 0.789, 440: 1.289}
>>> spd1 = SpectralPowerDistribution('Custom1', data1)
>>> spd2 = SpectralPowerDistribution('Custom2', data2)
>>> multi_spd_plot([spd1, spd2]) # doctest: +SKIP
"""
canvas(**kwargs)
cmfs = get_cmfs(cmfs)
if use_spds_colours:
illuminant = ILLUMINANTS_RELATIVE_SPDS['D65']
x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
for spd in spds:
wavelengths, values = tuple(zip(*spd.items))
shape = spd.shape
x_limit_min.append(shape.start)
x_limit_max.append(shape.end)
y_limit_min.append(min(values))
y_limit_max.append(max(values))
if use_spds_colours:
XYZ = spectral_to_XYZ(spd, cmfs, illuminant) / 100
if normalise_spds_colours:
XYZ = normalise_maximum(XYZ, clip=False)
RGB = np.clip(XYZ_to_sRGB(XYZ), 0, 1)
pylab.plot(
wavelengths, values, color=RGB, label=spd.title, linewidth=2)
else:
pylab.plot(wavelengths, values, label=spd.title, linewidth=2)
settings = {
'x_label':
'Wavelength $\\lambda$ (nm)',
'y_label':
'Spectral Power Distribution',
'x_tighten':
True,
'y_tighten':
True,
'legend':
True,
'legend_location':
'upper left',
'limits': (min(x_limit_min), max(x_limit_max), min(y_limit_min),
max(y_limit_max))
}
settings.update(kwargs)
boundaries(**settings)
decorate(**settings)
return display(**settings)
[docs]def single_cmfs_plot(cmfs='CIE 1931 2 Degree Standard Observer', **kwargs):
"""
Plots given colour matching functions.
Parameters
----------
cmfs : unicode, optional
Colour matching functions to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> single_cmfs_plot() # doctest: +SKIP
"""
cmfs = get_cmfs(cmfs)
settings = {'title': '{0} - Colour Matching Functions'.format(cmfs.title)}
settings.update(kwargs)
return multi_cmfs_plot((cmfs.name, ), **settings)
[docs]def multi_cmfs_plot(cmfs=None, **kwargs):
"""
Plots given colour matching functions.
Parameters
----------
cmfs : array_like, optional
Colour matching functions to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> cmfs = [
... 'CIE 1931 2 Degree Standard Observer',
... 'CIE 1964 10 Degree Standard Observer']
>>> multi_cmfs_plot(cmfs) # doctest: +SKIP
"""
canvas(**kwargs)
if cmfs is None:
cmfs = ('CIE 1931 2 Degree Standard Observer',
'CIE 1964 10 Degree Standard Observer')
x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
for axis, rgb in (('x', (1, 0, 0)), ('y', (0, 1, 0)), ('z', (0, 0, 1))):
for i, cmfs_i in enumerate(cmfs):
cmfs_i = get_cmfs(cmfs_i)
rgb = [reduce(lambda y, _: y * 0.5, range(i), x) for x in rgb]
wavelengths, values = tuple(
zip(* [(key, value) for key, value in getattr(cmfs_i, axis)]))
shape = cmfs_i.shape
x_limit_min.append(shape.start)
x_limit_max.append(shape.end)
y_limit_min.append(min(values))
y_limit_max.append(max(values))
pylab.plot(
wavelengths,
values,
color=rgb,
label=u'{0} - {1}'.format(cmfs_i.labels[axis], cmfs_i.title),
linewidth=2)
settings = {
'title':
'{0} - Colour Matching Functions'
.format(', '.join([get_cmfs(c).title for c in cmfs])),
'x_label':
'Wavelength $\\lambda$ (nm)',
'y_label':
'Tristimulus Values',
'x_tighten':
True,
'y_tighten':
True,
'legend':
True,
'legend_location':
'upper right',
'grid':
True,
'y_axis_line':
True,
'limits': (min(x_limit_min), max(x_limit_max), min(y_limit_min),
max(y_limit_max))
}
settings.update(kwargs)
boundaries(**settings)
decorate(**settings)
return display(**settings)
[docs]def single_illuminant_relative_spd_plot(
illuminant='A', cmfs='CIE 1931 2 Degree Standard Observer', **kwargs):
"""
Plots given single illuminant relative spectral power distribution.
Parameters
----------
illuminant : unicode, optional
Factory illuminant to plot.
cmfs : unicode, optional
Standard observer colour matching functions to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
out_of_gamut_clipping : bool, optional
{:func:`single_spd_plot`},
Whether to clip out of gamut colours otherwise, the colours will be
offset by the absolute minimal colour leading to a rendering on
gray background, less saturated and smoother. [1]_
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> single_illuminant_relative_spd_plot() # doctest: +SKIP
"""
cmfs = get_cmfs(cmfs)
title = 'Illuminant {0} - {1}'.format(illuminant, cmfs.title)
illuminant = get_illuminant(illuminant)
settings = {'title': title, 'y_label': 'Relative Power'}
settings.update(kwargs)
return single_spd_plot(illuminant, **settings)
[docs]def multi_illuminants_relative_spd_plot(illuminants=None, **kwargs):
"""
Plots given illuminants relative spectral power distributions.
Parameters
----------
illuminants : array_like, optional
Factory illuminants to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
use_spds_colours : bool, optional
{:func:`multi_spd_plot`}
Whether to use spectral power distributions colours.
normalise_spds_colours : bool
{:func:`multi_spd_plot`}
Whether to normalise spectral power distributions colours.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> multi_illuminants_relative_spd_plot(['A', 'B', 'C']) # doctest: +SKIP
"""
if illuminants is None:
illuminants = ('A', 'B', 'C')
spds = []
for illuminant in illuminants:
spds.append(get_illuminant(illuminant))
settings = {
'title':
'{0} - Illuminants Relative Spectral Power Distribution'
.format(', '.join([spd.title for spd in spds])),
'y_label':
'Relative Power'
}
settings.update(kwargs)
return multi_spd_plot(spds, **settings)
[docs]def visible_spectrum_plot(cmfs='CIE 1931 2 Degree Standard Observer',
out_of_gamut_clipping=True,
**kwargs):
"""
Plots the visible colours spectrum using given standard observer *CIE XYZ*
colour matching functions.
Parameters
----------
cmfs : unicode, optional
Standard observer colour matching functions used for spectrum creation.
out_of_gamut_clipping : bool, optional
Whether to clip out of gamut colours otherwise, the colours will be
offset by the absolute minimal colour leading to a rendering on
gray background, less saturated and smoother. [1]_
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
y0_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y0* line.
y1_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y1* line.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> visible_spectrum_plot() # doctest: +SKIP
"""
cmfs = get_cmfs(cmfs)
cmfs = cmfs.clone().align(DEFAULT_SPECTRAL_SHAPE)
wavelengths = cmfs.shape.range()
colours = XYZ_to_sRGB(
wavelength_to_XYZ(wavelengths, cmfs),
ILLUMINANTS['CIE 1931 2 Degree Standard Observer']['E'],
apply_encoding_cctf=False)
if not out_of_gamut_clipping:
colours += np.abs(np.min(colours))
colours = DEFAULT_PLOTTING_ENCODING_CCTF(normalise_maximum(colours))
settings = {
'title': 'The Visible Spectrum - {0}'.format(cmfs.title),
'x_label': 'Wavelength $\\lambda$ (nm)',
'y_label': False,
'x_tighten': True,
'y_tighten': True,
'y_ticker': False
}
settings.update(kwargs)
return colour_parameters_plot([
ColourParameter(x=x[0], RGB=x[1])
for x in tuple(zip(wavelengths, colours))
], **settings)
[docs]def single_lightness_function_plot(function='CIE 1976', **kwargs):
"""
Plots given *Lightness* function.
Parameters
----------
function : unicode, optional
*Lightness* function to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> single_lightness_function_plot() # doctest: +SKIP
"""
settings = {'title': '{0} - Lightness Function'.format(function)}
settings.update(kwargs)
return multi_lightness_function_plot((function, ), **settings)
[docs]def multi_lightness_function_plot(functions=None, **kwargs):
"""
Plots given *Lightness* functions.
Parameters
----------
functions : array_like, optional
*Lightness* functions to plot.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Raises
------
KeyError
If one of the given *Lightness* function is not found in the factory
*Lightness* functions.
Examples
--------
>>> fs = ('CIE 1976', 'Wyszecki 1963')
>>> multi_lightness_function_plot(fs) # doctest: +SKIP
"""
settings = {'figure_size': (DEFAULT_FIGURE_WIDTH, DEFAULT_FIGURE_WIDTH)}
settings.update(kwargs)
canvas(**settings)
if functions is None:
functions = ('CIE 1976', 'Wyszecki 1963')
samples = np.linspace(0, 100, 1000)
for function in functions:
function, name = LIGHTNESS_METHODS.get(function), function
if function is None:
raise KeyError(('"{0}" "Lightness" function not found in factory '
'"Lightness" functions: "{1}".').format(
name, sorted(LIGHTNESS_METHODS.keys())))
# TODO: Handle condition statement with metadata capabilities.
pylab.plot(
samples, (function(samples / 100) if
name.lower() == 'fairchild 2010' else function(samples)),
label='{0}'.format(name),
linewidth=2)
settings.update({
'title': '{0} - Lightness Functions'.format(', '.join(functions)),
'x_label': 'Relative Luminance Y',
'y_label': 'Lightness',
'x_tighten': True,
'legend': True,
'legend_location': 'upper left',
'grid': True,
'limits': (0, 100, 0, 100),
'aspect': 'equal'
})
settings.update(kwargs)
boundaries(**settings)
decorate(**settings)
return display(**settings)
[docs]def blackbody_spectral_radiance_plot(
temperature=3500,
cmfs='CIE 1931 2 Degree Standard Observer',
blackbody='VY Canis Major',
**kwargs):
"""
Plots given blackbody spectral radiance.
Parameters
----------
temperature : numeric, optional
Blackbody temperature.
cmfs : unicode, optional
Standard observer colour matching functions.
blackbody : unicode, optional
Blackbody name.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> blackbody_spectral_radiance_plot() # doctest: +SKIP
"""
canvas(**kwargs)
cmfs = get_cmfs(cmfs)
matplotlib.pyplot.subplots_adjust(hspace=0.4)
spd = blackbody_spd(temperature, cmfs.shape)
matplotlib.pyplot.figure(1)
matplotlib.pyplot.subplot(211)
settings = {
'title': '{0} - Spectral Radiance'.format(blackbody),
'y_label': 'W / (sr m$^2$) / m',
'standalone': False
}
settings.update(kwargs)
single_spd_plot(spd, cmfs.name, **settings)
XYZ = spectral_to_XYZ(spd, cmfs)
RGB = normalise_maximum(XYZ_to_sRGB(XYZ / 100))
matplotlib.pyplot.subplot(212)
settings = {
'title': '{0} - Colour'.format(blackbody),
'x_label': '{0}K'.format(temperature),
'y_label': '',
'aspect': None,
'standalone': False
}
single_colour_plot(ColourParameter(name='', RGB=RGB), **settings)
settings = {'standalone': True}
settings.update(kwargs)
boundaries(**settings)
decorate(**settings)
return display(**settings)
[docs]def blackbody_colours_plot(shape=SpectralShape(150, 12500, 50),
cmfs='CIE 1931 2 Degree Standard Observer',
**kwargs):
"""
Plots blackbody colours.
Parameters
----------
shape : SpectralShape, optional
Spectral shape to use as plot boundaries.
cmfs : unicode, optional
Standard observer colour matching functions.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
y0_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y0* line.
y1_plot : bool, optional
{:func:`colour_parameters_plot`},
Whether to plot *y1* line.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> blackbody_colours_plot() # doctest: +SKIP
"""
cmfs = get_cmfs(cmfs)
colours = []
temperatures = []
for temperature in shape:
spd = blackbody_spd(temperature, cmfs.shape)
XYZ = spectral_to_XYZ(spd, cmfs)
RGB = normalise_maximum(XYZ_to_sRGB(XYZ / 100))
colours.append(RGB)
temperatures.append(temperature)
settings = {
'title': 'Blackbody Colours',
'x_label': 'Temperature K',
'y_label': '',
'x_tighten': True,
'y_tighten': True,
'y_ticker': False
}
settings.update(kwargs)
return colour_parameters_plot([
ColourParameter(x=x[0], RGB=x[1])
for x in tuple(zip(temperatures, colours))
], **settings)