Source code for colour.colorimetry.dominant

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Dominant Wavelength and Purity
==============================

Defines objects to compute the *dominant wavelength* and *purity* of a colour
and related quantities:

-   :func:`dominant_wavelength`
-   :func:`complementary_wavelength`
-   :func:`excitation_purity`
-   :func:`colorimetric_purity`

See Also
--------
`Dominant Wavelength and Purity Notebook
<http://nbviewer.jupyter.org/github/colour-science/colour-notebooks/\
blob/master/notebooks/colorimetry/dominant_wavelength.ipynb>`_

References
----------
.. [1]  CIE TC 1-48. (2004). 9.1 Dominant wavelength and purity. In CIE
        015:2004 Colorimetry, 3rd Edition (pp. 32–33). ISBN:978-3-901-90633-6
.. [2]  Erdogan, T. (n.d.). How to Calculate Luminosity, Dominant Wavelength,
        and Excitation Purity, 7. Retrieved from http://www.semrock.com/Data/\
Sites/1/semrockpdfs/whitepaper_howtocalculateluminositywavelengthandpurity.pdf
"""

from __future__ import division, unicode_literals

import numpy as np
import scipy.spatial.distance

from colour.algebra import (
    euclidean_distance,
    extend_line_segment,
    intersect_line_segments)
from colour.colorimetry import CMFS
from colour.models import XYZ_to_xy

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2016 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'

__all__ = ['closest_spectral_locus_wavelength',
           'dominant_wavelength',
           'complementary_wavelength',
           'excitation_purity',
           'colorimetric_purity']


[docs]def closest_spectral_locus_wavelength(xy, xy_n, xy_s, reverse=False): """ Returns the coordinates and closest spectral locus wavelength index to the point where the line defined by the given achromatic stimulus :math:`xy_n` to colour stimulus :math:`xy_n` *xy* chromaticity coordinates intersects the spectral locus. Parameters ---------- xy : array_like Colour stimulus *xy* chromaticity coordinates. xy_n : array_like Achromatic stimulus *xy* chromaticity coordinates. xy_s : array_like Spectral locus *xy* chromaticity coordinates. reverse : bool, optional The intersection will be computed using the colour stimulus :math:`xy` to achromatic stimulus :math:`xy_n` reverse direction. Returns ------- tuple Closest wavelength index, intersection point *xy* chromaticity coordinates. Raises ------ ValueError If no closest spectral locus wavelength index and coordinates found. Examples -------- >>> xy = np.array([0.26415, 0.37770]) >>> xy_n = np.array([0.31270, 0.32900]) >>> xy_s = XYZ_to_xy(CMFS['CIE 1931 2 Degree Standard Observer'].values) >>> closest_spectral_locus_wavelength(xy, xy_n, xy_s) # doctest: +ELLIPSIS (array(144), array([ 0.0036969..., 0.6389577...])) """ xy = np.asarray(xy) xy_n = np.resize(xy_n, xy.shape) xy_s = np.asarray(xy_s) xy_e = (extend_line_segment(xy, xy_n) if reverse else extend_line_segment(xy_n, xy)) # Closing horse-shoe shape to handle line of purples intersections. xy_s = np.vstack((xy_s, xy_s[0, :])) xy_wl = intersect_line_segments( np.concatenate((xy_n, xy_e), -1), np.hstack((xy_s, np.roll(xy_s, 1, axis=0)))).xy xy_wl = xy_wl[~np.isnan(xy_wl).any(axis=-1)] if not len(xy_wl): raise ValueError( 'No closest spectral locus wavelength index and coordinates found ' 'for "{0}" colour stimulus and "{1}" achromatic stimulus "xy" ' 'chromaticity coordinates!'.format(xy, xy_n)) i_wl = np.argmin(scipy.spatial.distance.cdist(xy_wl, xy_s), axis=-1) i_wl = np.reshape(i_wl, xy.shape[0:-1]) xy_wl = np.reshape(xy_wl, xy.shape) return i_wl, xy_wl
[docs]def dominant_wavelength(xy, xy_n, cmfs=CMFS['CIE 1931 2 Degree Standard Observer'], reverse=False): """ Returns the *dominant wavelength* :math:`\lambda_d` for given colour stimulus :math:`xy` and the related :math:`xy_wl` first and :math:`xy_{cw}` second intersection coordinates with the spectral locus. In the eventuality where the :math:`xy_wl` first intersection coordinates are on the line of purples, the *complementary wavelength* will be computed in lieu. The *complementary wavelength* is indicated by a negative sign and the :math:`xy_{cw}` second intersection coordinates which are set by default to the same value than :math:`xy_wl` first intersection coordinates will be set to the *complementary dominant wavelength* intersection coordinates with the spectral locus. Parameters ---------- xy : array_like Colour stimulus *xy* chromaticity coordinates. xy_n : array_like Achromatic stimulus *xy* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. reverse : bool, optional Reverse the computation direction to retrieve the *complementary wavelength*. Returns ------- tuple *Dominant wavelength*, first intersection point *xy* chromaticity coordinates, second intersection point *xy* chromaticity coordinates. See Also -------- complementary_wavelength Examples -------- *Dominant wavelength* computation: >>> from pprint import pprint >>> xy = np.array([0.26415, 0.37770]) >>> xy_n = np.array([0.31270, 0.32900]) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> pprint(dominant_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS (array(504...), array([ 0.0036969..., 0.6389577...]), array([ 0.0036969..., 0.6389577...])) *Complementary dominant wavelength* is returned if the first intersection is located on the line of purples: >>> xy = np.array([0.35000, 0.25000]) >>> pprint(dominant_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS (array(-520...), array([ 0.4133314..., 0.1158663...]), array([ 0.0743553..., 0.8338050...])) """ xy = np.asarray(xy) xy_n = np.resize(xy_n, xy.shape) xy_s = XYZ_to_xy(cmfs.values) i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, xy_s, reverse) xy_cwl = xy_wl wl = cmfs.wavelengths[i_wl] xy_e = (extend_line_segment(xy, xy_n) if reverse else extend_line_segment(xy_n, xy)) intersect = intersect_line_segments( np.concatenate((xy_n, xy_e), -1), np.hstack((xy_s[0], xy_s[-1]))).intersect intersect = np.reshape(intersect, wl.shape) i_wl_r, xy_cwl_r = closest_spectral_locus_wavelength( xy, xy_n, xy_s, not reverse) wl_r = -cmfs.wavelengths[i_wl_r] wl = np.where(intersect, wl_r, wl) xy_cwl = np.where(intersect[..., np.newaxis], xy_cwl_r, xy_cwl) return wl, np.squeeze(xy_wl), np.squeeze(xy_cwl)
[docs]def complementary_wavelength(xy, xy_n, cmfs=CMFS['CIE 1931 2 Degree Standard Observer']): """ Returns the *complementary wavelength* :math:`\lambda_c` for given colour stimulus :math:`xy` and the related :math:`xy_wl` first and :math:`xy_{cw}` second intersection coordinates with the spectral locus. In the eventuality where the :math:`xy_wl` first intersection coordinates are on the line of purples, the *dominant wavelength* will be computed in lieu. The *dominant wavelength* is indicated by a negative sign and the :math:`xy_{cw}` second intersection coordinates which are set by default to the same value than :math:`xy_wl` first intersection coordinates will be set to the *dominant wavelength* intersection coordinates with the spectral locus. Parameters ---------- xy : array_like Colour stimulus *xy* chromaticity coordinates. xy_n : array_like Achromatic stimulus *xy* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- tuple *Complementary wavelength*, first intersection point *xy* chromaticity coordinates, second intersection point *xy* chromaticity coordinates. See Also -------- dominant_wavelength Examples -------- *Complementary wavelength* computation: >>> from pprint import pprint >>> xy = np.array([0.35000, 0.25000]) >>> xy_n = np.array([0.31270, 0.32900]) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> pprint(complementary_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS (array(520...), array([ 0.0743553..., 0.8338050...]), array([ 0.0743553..., 0.8338050...])) *Dominant wavelength* is returned if the first intersection is located on the line of purples: >>> xy = np.array([0.26415, 0.37770]) >>> pprint(complementary_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS (array(-504...), array([ 0.4897494..., 0.1514035...]), array([ 0.0036969..., 0.6389577...])) """ return dominant_wavelength(xy, xy_n, cmfs, True)
[docs]def excitation_purity(xy, xy_n, cmfs=CMFS['CIE 1931 2 Degree Standard Observer']): """ Returns the *excitation purity* :math:`P_e` for given colour stimulus :math:`xy`. Parameters ---------- xy : array_like Colour stimulus *xy* chromaticity coordinates. xy_n : array_like Achromatic stimulus *xy* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- numeric or array_like *Excitation purity* :math:`P_e`. Examples -------- >>> xy = np.array([0.28350, 0.68700]) >>> xy_n = np.array([0.31270, 0.32900]) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> excitation_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS 0.9386035... """ wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n, cmfs) P_e = euclidean_distance(xy_n, xy) / euclidean_distance(xy_n, xy_wl) return P_e
[docs]def colorimetric_purity(xy, xy_n, cmfs=CMFS['CIE 1931 2 Degree Standard Observer']): """ Returns the *colorimetric purity* :math:`P_c` for given colour stimulus :math:`xy`. Parameters ---------- xy : array_like Colour stimulus *xy* chromaticity coordinates. xy_n : array_like Achromatic stimulus *xy* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- numeric or array_like *Colorimetric purity* :math:`P_c`. Examples -------- >>> xy = np.array([0.28350, 0.68700]) >>> xy_n = np.array([0.31270, 0.32900]) >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] >>> colorimetric_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS 0.9705976... """ xy = np.asarray(xy) wl, xy_wl, xy_cwl = dominant_wavelength(xy, xy_n, cmfs) P_e = excitation_purity(xy, xy_n, cmfs) P_c = P_e * xy_wl[..., 1] / xy[..., 1] return P_c