Source code for colour.plotting.colorimetry

# -*- coding: utf-8 -*-
"""
Colorimetry Plotting
====================

Defines the colorimetry plotting objects:

-   :func:`colour.plotting.single_spd_plot`
-   :func:`colour.plotting.multi_spd_plot`
-   :func:`colour.plotting.single_cmfs_plot`
-   :func:`colour.plotting.multi_cmfs_plot`
-   :func:`colour.plotting.single_illuminant_relative_spd_plot`
-   :func:`colour.plotting.multi_illuminants_relative_spd_plot`
-   :func:`colour.plotting.visible_spectrum_plot`
-   :func:`colour.plotting.single_lightness_function_plot`
-   :func:`colour.plotting.multi_lightness_function_plot`
-   :func:`colour.plotting.blackbody_spectral_radiance_plot`
-   :func:`colour.plotting.blackbody_colours_plot`

References
----------
-   :cite:`Spiker2015a` : Spiker, N. (2015). Private Discussion with
    Mansencal, T. Retrieved from http://www.invisiblelightimages.com/
"""

from __future__ import division

import matplotlib.pyplot
import numpy as np
import pylab
from matplotlib.patches import Polygon
from six.moves import reduce

from colour.algebra import LinearInterpolator
from colour.colorimetry import (DEFAULT_SPECTRAL_SHAPE, ILLUMINANTS,
                                ILLUMINANTS_RELATIVE_SPDS, LIGHTNESS_METHODS,
                                SpectralShape, blackbody_spd, ones_spd,
                                spectral_to_XYZ, wavelength_to_XYZ)
from colour.models import XYZ_to_sRGB
from colour.plotting import (ColourSwatch, DEFAULT_PLOTTING_ENCODING_CCTF,
                             DEFAULT_FIGURE_WIDTH, canvas, get_cmfs,
                             get_illuminant, render, single_colour_swatch_plot)
from colour.utilities import normalise_maximum, suppress_warnings, tstack

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2018 - 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. cmfs : unicode Standard observer colour matching functions used for spectrum creation. Other Parameters ---------------- \**kwargs : dict, optional {:func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. Returns ------- Figure Current figure or None. References ---------- - :cite:`Spiker2015a` Examples -------- >>> from colour import SpectralPowerDistribution >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360 ... } >>> spd = SpectralPowerDistribution(data, name='Custom') >>> single_spd_plot(spd) # doctest: +SKIP """ axes = canvas(**kwargs).gca() cmfs = get_cmfs(cmfs) spd = spd.copy() spd.interpolator = LinearInterpolator wavelengths = cmfs.wavelengths[np.logical_and( cmfs.wavelengths >= max(min(cmfs.wavelengths), min(spd.wavelengths)), cmfs.wavelengths <= min(max(cmfs.wavelengths), max(spd.wavelengths)), )] values = spd[wavelengths] 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)) x_min, x_max = min(wavelengths), max(wavelengths) y_min, y_max = 0, max(values) polygon = Polygon( np.vstack([ (x_min, 0), tstack((wavelengths, values)), (x_max, 0), ]), facecolor='none', edgecolor='none') axes.add_patch(polygon) axes.bar( x=wavelengths, height=max(values), width=1, color=colours, align='edge', clip_path=polygon) axes.plot(wavelengths, values, color='black', linewidth=1) settings = { 'title': '{0} - {1}'.format(spd.strict_name, cmfs.strict_name), 'x_label': 'Wavelength $\\lambda$ (nm)', 'y_label': 'Spectral Power Distribution', 'limits': (x_min, x_max, y_min, y_max), 'x_tighten': True, 'y_tighten': True } settings.update(kwargs) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. Returns ------- Figure Current figure or None. Examples -------- >>> from colour import SpectralPowerDistribution >>> data_1 = { ... 500: 0.004900, ... 510: 0.009300, ... 520: 0.063270, ... 530: 0.165500, ... 540: 0.290400, ... 550: 0.433450, ... 560: 0.594500 ... } >>> data_2 = { ... 500: 0.323000, ... 510: 0.503000, ... 520: 0.710000, ... 530: 0.862000, ... 540: 0.954000, ... 550: 0.994950, ... 560: 0.995000 ... } >>> spd1 = SpectralPowerDistribution(data_1, name='Custom 1') >>> spd2 = SpectralPowerDistribution(data_2, name='Custom 2') >>> 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 = spd.wavelengths, spd.values 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.strict_name, linewidth=1) else: pylab.plot(wavelengths, values, label=spd.strict_name, linewidth=1) 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) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. Returns ------- Figure Current figure or None. Examples -------- >>> single_cmfs_plot() # doctest: +SKIP """ cmfs = get_cmfs(cmfs) settings = { 'title': '{0} - Colour Matching Functions'.format(cmfs.strict_name) } 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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. 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 i, rgb in enumerate([(1, 0, 0), (0, 1, 0), (0, 0, 1)]): for j, cmfs_i in enumerate(cmfs): cmfs_i = get_cmfs(cmfs_i) rgb = [reduce(lambda y, _: y * 0.5, range(j), x) for x in rgb] values = cmfs_i.values[:, i] 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( cmfs_i.wavelengths, values, color=rgb, label=u'{0} - {1}'.format(cmfs_i.labels[i], cmfs_i.strict_name), linewidth=1) settings = { 'title': '{0} - Colour Matching Functions' .format(', '.join([get_cmfs(c).strict_name 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) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. out_of_gamut_clipping : bool, optional {:func:`colour.plotting.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. Returns ------- Figure Current figure or None. References ---------- - :cite:`Spiker2015a` Examples -------- >>> single_illuminant_relative_spd_plot() # doctest: +SKIP """ cmfs = get_cmfs(cmfs) title = 'Illuminant {0} - {1}'.format(illuminant, cmfs.strict_name) 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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. use_spds_colours : bool, optional {:func:`colour.plotting.multi_spd_plot`} Whether to use spectral power distributions colours. normalise_spds_colours : bool {:func:`colour.plotting.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.strict_name 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. Other Parameters ---------------- \**kwargs : dict, optional {:func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. Returns ------- Figure Current figure or None. References ---------- - :cite:`Spiker2015a` Examples -------- >>> visible_spectrum_plot() # doctest: +SKIP """ settings = {'y_label': None, 'y_ticker': False, 'standalone': False} single_spd_plot( ones_spd(DEFAULT_SPECTRAL_SHAPE), cmfs=cmfs, out_of_gamut_clipping=out_of_gamut_clipping, **settings) cmfs = get_cmfs(cmfs) settings = { 'title': 'The Visible Spectrum - {0}'.format(cmfs.strict_name), 'x_label': 'Wavelength $\\lambda$ (nm)', 'standalone': True } settings.update(kwargs) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. 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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. 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() in ('fairchild 2010', 'fairchild 2011') else function(samples)), label='{0}'.format(name), linewidth=1) 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) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. 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_swatch_plot(ColourSwatch(name='', RGB=RGB), **settings) settings = {'standalone': True} settings.update(kwargs) return render(**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:`colour.plotting.render`}, Please refer to the documentation of the previously listed definition. Returns ------- Figure Current figure or None. Examples -------- >>> blackbody_colours_plot() # doctest: +SKIP """ axes = canvas(**kwargs).gca() cmfs = get_cmfs(cmfs) colours = [] temperatures = [] with suppress_warnings(): 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) x_min, x_max = min(temperatures), max(temperatures) y_min, y_max = 0, 1 axes.bar( x=temperatures, height=1, width=shape.interval, color=colours, align='edge') settings = { 'title': 'Blackbody Colours', 'x_label': 'Temperature K', 'y_label': None, 'limits': (x_min, x_max, y_min, y_max), 'x_tighten': True, 'y_tighten': True, 'y_ticker': False } settings.update(kwargs) return render(**settings)