Example #1
0
    def test_n_dimensional_XYZ_to_xy(self):
        """
        Tests :func:`colour.models.cie_xyy.XYZ_to_xy` definition n-dimensions
        support.
        """

        XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
        illuminant = np.array([0.31270, 0.32900])
        xy = np.array([0.54369557, 0.32107944])
        np.testing.assert_almost_equal(
            XYZ_to_xy(XYZ, illuminant), xy, decimal=7)

        XYZ = np.tile(XYZ, (6, 1))
        xy = np.tile(xy, (6, 1))
        np.testing.assert_almost_equal(
            XYZ_to_xy(XYZ, illuminant), xy, decimal=7)

        illuminant = np.tile(illuminant, (6, 1))
        np.testing.assert_almost_equal(
            XYZ_to_xy(XYZ, illuminant), xy, decimal=7)

        XYZ = np.reshape(XYZ, (2, 3, 3))
        illuminant = np.reshape(xy, (2, 3, 2))
        xy = np.reshape(xy, (2, 3, 2))
        np.testing.assert_almost_equal(
            XYZ_to_xy(XYZ, illuminant), xy, decimal=7)
Example #2
0
    def test_n_dimensional_XYZ_to_xy(self):
        """
        Tests :func:`colour.models.cie_xyy.XYZ_to_xy` definition n-dimensions
        support.
        """

        XYZ = np.array([0.69935853, 1.00000000, 0.94824534])
        illuminant = np.array([0.34570, 0.35850])
        xy = np.array([0.26414772, 0.37770001])
        np.testing.assert_almost_equal(XYZ_to_xy(XYZ, illuminant),
                                       xy,
                                       decimal=7)

        XYZ = np.tile(XYZ, (6, 1))
        xy = np.tile(xy, (6, 1))
        np.testing.assert_almost_equal(XYZ_to_xy(XYZ, illuminant),
                                       xy,
                                       decimal=7)

        illuminant = np.tile(illuminant, (6, 1))
        np.testing.assert_almost_equal(XYZ_to_xy(XYZ, illuminant),
                                       xy,
                                       decimal=7)

        XYZ = np.reshape(XYZ, (2, 3, 3))
        illuminant = np.reshape(xy, (2, 3, 2))
        xy = np.reshape(xy, (2, 3, 2))
        np.testing.assert_almost_equal(XYZ_to_xy(XYZ, illuminant),
                                       xy,
                                       decimal=7)
Example #3
0
    def test_XYZ_to_xy(self):
        """Test :func:`colour.models.cie_xyy.XYZ_to_xy` definition."""

        np.testing.assert_almost_equal(
            XYZ_to_xy(np.array([0.20654008, 0.12197225, 0.05136952])),
            np.array([0.54369557, 0.32107944]),
            decimal=7,
        )

        np.testing.assert_almost_equal(
            XYZ_to_xy(np.array([0.14222010, 0.23042768, 0.10495772])),
            np.array([0.29777735, 0.48246446]),
            decimal=7,
        )

        np.testing.assert_almost_equal(
            XYZ_to_xy(np.array([0.07818780, 0.06157201, 0.28099326])),
            np.array([0.18582823, 0.14633764]),
            decimal=7,
        )

        np.testing.assert_almost_equal(
            XYZ_to_xy(np.array([0.00000000, 0.00000000, 0.00000000])),
            np.array([0.31270000, 0.32900000]),
            decimal=7,
        )
Example #4
0
def corresponding_chromaticities_prediction_CMCCAT2000(experiment=1, **kwargs):
    """
    Returns the corresponding chromaticities prediction for CMCCAT2000
    chromatic adaptation model.

    Parameters
    ----------
    experiment : integer, optional
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        Breneman (1987) experiment number.
    \**kwargs : dict, optional
        Keywords arguments.

    Returns
    -------
    tuple
        Corresponding chromaticities prediction.

    Examples
    --------
    >>> from pprint import pprint
    >>> pr = corresponding_chromaticities_prediction_CMCCAT2000(2)
    >>> pr = [(p.uvp_m, p.uvp_p) for p in pr]
    >>> pprint(pr)  # doctest: +SKIP
    [((0.207, 0.486), (0.20832101929657834, 0.47271680534693694)),
     ((0.449, 0.511), (0.44592707020371486, 0.50777351504395707)),
     ((0.263, 0.505), (0.26402624712986333, 0.4955361681706304)),
     ((0.322, 0.545), (0.33168840090358015, 0.54315801981008516)),
     ((0.316, 0.537), (0.32226245779851387, 0.53576245377085929)),
     ((0.265, 0.553), (0.27107058097430181, 0.5501997842556422)),
     ((0.221, 0.538), (0.22618269421847523, 0.52947407170848704)),
     ((0.135, 0.532), (0.14396930475660724, 0.51909841743126817)),
     ((0.145, 0.472), (0.14948357434418671, 0.45567605010224305)),
     ((0.163, 0.331), (0.15631720730028753, 0.31641514460738623)),
     ((0.176, 0.431), (0.17631993066748047, 0.41275893424542082)),
     ((0.244, 0.349), (0.22876382018951744, 0.3499324084859976))]
    """

    experiment_results = list(BRENEMAN_EXPERIMENTS.get(experiment))

    illuminants = experiment_results.pop(0)
    XYZ_w = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_t)) * 100
    XYZ_wr = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_m)) * 100
    xy_wr = XYZ_to_xy(XYZ_wr)
    L_A1 = L_A2 = BRENEMAN_EXPERIMENTS_PRIMARIES_CHROMATICITIES.get(
        experiment).Y

    prediction = []
    for result in experiment_results:
        XYZ_1 = xy_to_XYZ(Luv_uv_to_xy(result.uvp_t)) * 100
        XYZ_2 = chromatic_adaptation_CMCCAT2000(
            XYZ_1, XYZ_w, XYZ_wr, L_A1, L_A2)
        uvp = Luv_to_uv(XYZ_to_Luv(XYZ_2, xy_wr), xy_wr)
        prediction.append(CorrespondingChromaticitiesPrediction(
            result.name,
            result.uvp_t,
            result.uvp_m,
            uvp))

    return tuple(prediction)
Example #5
0
    def test_domain_range_scale_XYZ_to_xy(self):
        """
        Tests :func:`colour.models.cie_xyy.XYZ_to_xy` definition domain and
        range scale support.
        """

        XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
        xy = XYZ_to_xy(XYZ)
        XYZ = np.tile(XYZ, (6, 1)).reshape(2, 3, 3)
        xy = np.tile(xy, (6, 1)).reshape(2, 3, 2)

        d_r = (('reference', 1), (1, 1), (100, 1))
        for scale, factor in d_r:
            with domain_range_scale(scale):
                np.testing.assert_almost_equal(
                    XYZ_to_xy(XYZ * factor), xy, decimal=7)
Example #6
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )

        self._reflectances = sds_and_msds_to_msds(
            list(SDS_COLOURCHECKERS["ColorChecker N Ohta"].values())
            + list(SDS_COLOURCHECKERS["BabelColor Average"].values())
        )

        self._tree = Tree_Otsu2018(
            self._reflectances, self._cmfs, self._sd_D65
        )

        self._XYZ_D65 = sd_to_XYZ(self._sd_D65)
        self._xy_D65 = XYZ_to_xy(self._XYZ_D65)

        self._temporary_directory = tempfile.mkdtemp()

        self._path = os.path.join(
            self._temporary_directory, "Test_Otsu2018.npz"
        )
Example #7
0
def corresponding_chromaticities_prediction_Fairchild1990(experiment=1):
    """
    Returns the corresponding chromaticities prediction for *Fairchild (1990)*
    chromatic adaptation model.

    Parameters
    ----------
    experiment : integer, optional
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        *Breneman (1987)* experiment number.

    Returns
    -------
    tuple
        Corresponding chromaticities prediction.

    References
    ----------
    :cite:`Breneman1987b`, :cite:`Fairchild1991a`, :cite:`Fairchild2013s`

    Examples
    --------
    >>> from pprint import pprint
    >>> pr = corresponding_chromaticities_prediction_Fairchild1990(2)
    >>> pr = [(p.uvp_m, p.uvp_p) for p in pr]
    >>> pprint(pr)  # doctest: +SKIP
    [((0.207, 0.486), (0.2089528..., 0.4724034...)),
     ((0.449, 0.511), (0.4375652..., 0.5121030...)),
     ((0.263, 0.505), (0.2621362..., 0.4972538...)),
     ((0.322, 0.545), (0.3235312..., 0.5475665...)),
     ((0.316, 0.537), (0.3151390..., 0.5398333...)),
     ((0.265, 0.553), (0.2634745..., 0.5544335...)),
     ((0.221, 0.538), (0.2211595..., 0.5324470...)),
     ((0.135, 0.532), (0.1396949..., 0.5207234...)),
     ((0.145, 0.472), (0.1512288..., 0.4533041...)),
     ((0.163, 0.331), (0.1715691..., 0.3026264...)),
     ((0.176, 0.431), (0.1825792..., 0.4077892...)),
     ((0.244, 0.349), (0.2418904..., 0.3413401...))]
    """

    with domain_range_scale(1):
        experiment_results = list(BRENEMAN_EXPERIMENTS[experiment])

        illuminants = experiment_results.pop(0)
        XYZ_n = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_t))
        XYZ_r = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_m))
        xy_r = XYZ_to_xy(XYZ_r)
        Y_n = BRENEMAN_EXPERIMENTS_PRIMARIES_CHROMATICITIES[experiment].Y

        prediction = []
        for result in experiment_results:
            XYZ_1 = xy_to_XYZ(Luv_uv_to_xy(result.uvp_t))
            XYZ_2 = chromatic_adaptation_Fairchild1990(XYZ_1, XYZ_n, XYZ_r,
                                                       Y_n)
            uvp = Luv_to_uv(XYZ_to_Luv(XYZ_2, xy_r), xy_r)
            prediction.append(
                CorrespondingChromaticitiesPrediction(
                    result.name, result.uvp_t, result.uvp_m, uvp))

        return tuple(prediction)
Example #8
0
def corresponding_chromaticities_prediction_Fairchild1990(experiment=1,
                                                          **kwargs):
    """
    Returns the corresponding chromaticities prediction for Fairchild (1990)
    chromatic adaptation model.

    Parameters
    ----------
    experiment : integer, optional
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        Breneman (1987) experiment number.
    \**kwargs : dict, optional
        Keywords arguments.

    Returns
    -------
    tuple
        Corresponding chromaticities prediction.

    Examples
    --------
    >>> from pprint import pprint
    >>> pr = corresponding_chromaticities_prediction_Fairchild1990(2)
    >>> pr = [(p.uvp_m, p.uvp_p) for p in pr]
    >>> pprint(pr)  # doctest: +SKIP
    [((0.207, 0.486), (0.2089528677990308, 0.47240345174230519)),
     ((0.449, 0.511), (0.43756528098582792, 0.51210303139041924)),
     ((0.263, 0.505), (0.26213623665658092, 0.49725385033264224)),
     ((0.322, 0.545), (0.3235312762825191, 0.54756652922585702)),
     ((0.316, 0.537), (0.3151390992740366, 0.53983332031574016)),
     ((0.265, 0.553), (0.26347459238415272, 0.55443357809543037)),
     ((0.221, 0.538), (0.22115956537655593, 0.53244703908294599)),
     ((0.135, 0.532), (0.13969494108553854, 0.52072342107668024)),
     ((0.145, 0.472), (0.1512288710743511, 0.45330415352961834)),
     ((0.163, 0.331), (0.17156913711903982, 0.30262647410866889)),
     ((0.176, 0.431), (0.18257922398137369, 0.40778921192793854)),
     ((0.244, 0.349), (0.24189049501108895, 0.34134012046930529))]
    """

    experiment_results = list(BRENEMAN_EXPERIMENTS.get(experiment))

    illuminants = experiment_results.pop(0)
    XYZ_n = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_t)) * 100
    XYZ_r = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_m)) * 100
    xy_r = XYZ_to_xy(XYZ_r)
    Y_n = BRENEMAN_EXPERIMENTS_PRIMARIES_CHROMATICITIES.get(experiment).Y

    prediction = []
    for result in experiment_results:
        XYZ_1 = xy_to_XYZ(Luv_uv_to_xy(result.uvp_t)) * 100
        XYZ_2 = chromatic_adaptation_Fairchild1990(
            XYZ_1, XYZ_n, XYZ_r, Y_n)
        uvp = Luv_to_uv(XYZ_to_Luv(XYZ_2, xy_r), xy_r)
        prediction.append(CorrespondingChromaticitiesPrediction(
            result.name,
            result.uvp_t,
            result.uvp_m,
            uvp))

    return tuple(prediction)
Example #9
0
def get_secondaries(name='ITU-R BT.2020'):
    """
    secondary color の座標を求める

    Parameters
    ----------
    name : str
        a name of the color space.

    Returns
    -------
    array_like
        secondaries. the order is magenta, yellow, cyan.

    """
    secondary_rgb = np.array([[1.0, 0.0, 1.0],
                              [1.0, 1.0, 0.0],
                              [0.0, 1.0, 1.0]])
    illuminant_XYZ = D65_WHITE
    illuminant_RGB = D65_WHITE
    chromatic_adaptation_transform = 'CAT02'
    rgb_to_xyz_matrix = get_rgb_to_xyz_matrix(name)
    large_xyz = RGB_to_XYZ(secondary_rgb, illuminant_RGB,
                           illuminant_XYZ, rgb_to_xyz_matrix,
                           chromatic_adaptation_transform)

    xy = XYZ_to_xy(large_xyz, illuminant_XYZ)

    return xy, secondary_rgb.reshape((3, 3))
Example #10
0
def vs_colorimetry_data(spd_test,
                        spd_reference,
                        spds_vs,
                        cmfs,
                        chromatic_adaptation=False):

    XYZ_t = spectral_to_XYZ(spd_test, cmfs)
    XYZ_t /= XYZ_t[1]

    XYZ_r = spectral_to_XYZ(spd_reference, cmfs)
    XYZ_r /= XYZ_r[1]
    xy_r = XYZ_to_xy(XYZ_r)

    vs_data = []
    for _key, value in sorted(VS_INDEXES_TO_NAMES.items()):
        spd_vs = spds_vs.get(value)
        XYZ_vs = spectral_to_XYZ(spd_vs, cmfs, spd_test)
        XYZ_vs /= 100

        if chromatic_adaptation:
            XYZ_vs = chromatic_adaptation_VonKries(XYZ_vs,
                                                   XYZ_t,
                                                   XYZ_r,
                                                   transform='CMCCAT2000')

        Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r)
        _L_vs, C_vs, _Hab = Lab_to_LCHab(Lab_vs)

        vs_data.append(VS_ColorimetryData(spd_vs.name, XYZ_vs, Lab_vs, C_vs))
    return vs_data
Example #11
0
        def XYZ_to_ij(XYZ):
            """
            Converts given *CIE XYZ* tristimulus values to *ij* chromaticity
            coordinates.
            """

            return XYZ_to_xy(XYZ)
Example #12
0
def intermediate_values(XYZ_n):
    """
    Returns the intermediate values :math:`\\xi`, :math:`\eta`, :math:`\zeta`.

    Parameters
    ----------
    XYZ_n : array_like, (3,)
        *CIE XYZ* colourspace matrix of reference white in domain [0, 100].

    Returns
    -------
    ndarray, (3,)
        Intermediate values :math:`\\xi`, :math:`\eta`, :math:`\zeta`.

    Examples
    --------
    >>> XYZ_n = np.array([95.05, 100, 108.88])
    >>> intermediate_values(XYZ_n)  # doctest: +ELLIPSIS
    array([ 1.0000421...,  0.9999800...,  0.9997579...])
    """

    # Illuminant chromaticity coordinates.
    x_o, y_o = XYZ_to_xy(XYZ_n)

    # Computing :math:`\xi`, :math:`\eta`, :math:`\zeta` values.
    xi = (0.48105 * x_o + 0.78841 * y_o - 0.08081) / y_o
    eta = (-0.27200 * x_o + 1.11962 * y_o + 0.04570) / y_o
    zeta = (0.91822 * (1 - x_o - y_o)) / y_o

    return np.array([xi, eta, zeta])
Example #13
0
def corresponding_chromaticities_prediction_VonKries(experiment=1,
                                                     transform='CAT02'):
    """
    Returns the corresponding chromaticities prediction for *Von Kries*
    chromatic adaptation model using given transform.

    Parameters
    ----------
    experiment : integer, optional
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        *Breneman (1987)* experiment number.
    transform : unicode, optional
        **{'CAT02', 'XYZ Scaling', 'Von Kries', 'Bradford', 'Sharp',
        'Fairchild', 'CMCCAT97', 'CMCCAT2000', 'CAT02_BRILL_CAT', 'Bianco',
        'Bianco PC'}**,
        Chromatic adaptation transform.

    Returns
    -------
    tuple
        Corresponding chromaticities prediction.

    Examples
    --------
    >>> from pprint import pprint
    >>> pr = corresponding_chromaticities_prediction_VonKries(2, 'Bradford')
    >>> pr = [(p.uvp_m, p.uvp_p) for p in pr]
    >>> pprint(pr)  # doctest: +SKIP
    [((0.207, 0.486), (0.2082014..., 0.4722922...)),
     ((0.449, 0.511), (0.4489102..., 0.5071602...)),
     ((0.263, 0.505), (0.2643545..., 0.4959631...)),
     ((0.322, 0.545), (0.3348730..., 0.5471220...)),
     ((0.316, 0.537), (0.3248758..., 0.5390589...)),
     ((0.265, 0.553), (0.2733105..., 0.5555028...)),
     ((0.221, 0.538), (0.2271480..., 0.5331317...)),
     ((0.135, 0.532), (0.1442730..., 0.5226804...)),
     ((0.145, 0.472), (0.1498745..., 0.4550785...)),
     ((0.163, 0.331), (0.1564975..., 0.3148795...)),
     ((0.176, 0.431), (0.1760593..., 0.4103772...)),
     ((0.244, 0.349), (0.2259805..., 0.3465291...))]
    """

    experiment_results = list(BRENEMAN_EXPERIMENTS[experiment])

    illuminants = experiment_results.pop(0)
    XYZ_w = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_t))
    XYZ_wr = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_m))
    xy_wr = XYZ_to_xy(XYZ_wr)

    prediction = []
    for result in experiment_results:
        XYZ_1 = xy_to_XYZ(Luv_uv_to_xy(result.uvp_t))
        XYZ_2 = chromatic_adaptation_VonKries(XYZ_1, XYZ_w, XYZ_wr, transform)
        uvp = Luv_to_uv(XYZ_to_Luv(XYZ_2, xy_wr), xy_wr)
        prediction.append(
            CorrespondingChromaticitiesPrediction(result.name, result.uvp_t,
                                                  result.uvp_m, uvp))

    return tuple(prediction)
Example #14
0
def RGB_chromaticity_coordinates_CIE_1931_chromaticity_diagram_plot(
        RGB,
        colourspace,
        **kwargs):
    """
    Plots given *RGB* colourspace array in *CIE 1931 Chromaticity Diagram*.

    Parameters
    ----------
    RGB : array_like
        *RGB* colourspace array.
    colourspace : RGB_Colourspace
        *RGB* colourspace of the *RGB* array.
    \**kwargs : dict, optional
        Keywords arguments.

    Returns
    -------
    bool
        Definition success.

    Examples
    --------
    >>> RGB = np.random.random((10, 10, 3))
    >>> c = 'Rec. 709'
    >>> RGB_chromaticity_coordinates_CIE_1931_chromaticity_diagram_plot(
    ...     RGB, c)  # doctest: +SKIP
    True
    """

    settings = {}
    settings.update(kwargs)
    settings.update({'standalone': False})

    settings['colourspaces'] = (
        [colourspace.name] + settings.get('colourspaces', []))

    RGB_colourspaces_CIE_1931_chromaticity_diagram_plot(**settings)

    alpha_p, colour_p = 0.85, 'black'

    xy = XYZ_to_xy(RGB_to_XYZ(RGB,
                              colourspace.whitepoint,
                              colourspace.whitepoint,
                              colourspace.RGB_to_XYZ_matrix),
                   colourspace.whitepoint)

    pylab.scatter(xy[..., 0],
                  xy[..., 1],
                  alpha=alpha_p / 2,
                  color=colour_p,
                  marker='+')

    settings.update({'standalone': True})

    boundaries(**settings)
    decorate(**settings)

    return display(**settings)
Example #15
0
def chromaticity_diagram_colours_CIE1931(
        samples=4096,
        cmfs='CIE 1931 2 Degree Standard Observer',
        antialiasing=True):
    """
    Plots the *CIE 1931 Chromaticity Diagram* colours.

    Parameters
    ----------
    samples : numeric, optional
        Samples count on one axis.
    cmfs : unicode, optional
        Standard observer colour matching functions used for diagram bounds.
    antialiasing : bool, optional
        Whether to apply anti-aliasing to the image.

    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
    --------
    >>> chromaticity_diagram_colours_CIE1931()  # doctest: +SKIP
    """

    cmfs = get_cmfs(cmfs)

    illuminant = DEFAULT_PLOTTING_ILLUMINANT

    triangulation = Delaunay(XYZ_to_xy(cmfs.values, illuminant),
                             qhull_options='Qu QJ')
    xx, yy = np.meshgrid(np.linspace(0, 1, samples),
                         np.linspace(1, 0, samples))
    xy = tstack((xx, yy))
    mask = (triangulation.find_simplex(xy) < 0).astype(DEFAULT_FLOAT_DTYPE)
    if antialiasing:
        kernel = np.array([
            [0, 1, 0],
            [1, 2, 1],
            [0, 1, 0],
        ]).astype(DEFAULT_FLOAT_DTYPE)
        kernel /= np.sum(kernel)
        mask = convolve(mask, kernel)

    mask = 1 - mask[:, :, np.newaxis]

    XYZ = xy_to_XYZ(xy)

    RGB = normalise_maximum(XYZ_to_sRGB(XYZ, illuminant), axis=-1)

    return np.dstack([RGB, mask])
Example #16
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._shape = SPECTRAL_SHAPE_OTSU2018
        self._cmfs, self._sd_D65 = handle_spectral_arguments(
            shape_default=self._shape
        )
        self._XYZ_D65 = sd_to_XYZ(self._sd_D65)
        self._xy_D65 = XYZ_to_xy(self._XYZ_D65)
Example #17
0
    def test_nan_XYZ_to_xy(self):
        """Test :func:`colour.models.cie_xyy.XYZ_to_xy` definition nan support."""

        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
        cases = set(permutations(cases * 3, r=3))
        for case in cases:
            XYZ = np.array(case)
            illuminant = np.array(case[0:2])
            XYZ_to_xy(XYZ, illuminant)
Example #18
0
def corresponding_chromaticities_prediction_CMCCAT2000(experiment=1):
    """
    Returns the corresponding chromaticities prediction for CMCCAT2000
    chromatic adaptation model.

    Parameters
    ----------
    experiment : integer, optional
        {1, 2, 3, 4, 6, 8, 9, 11, 12}
        Breneman (1987) experiment number.

    Returns
    -------
    tuple
        Corresponding chromaticities prediction.

    Examples
    --------
    >>> from pprint import pprint
    >>> pr = corresponding_chromaticities_prediction_CMCCAT2000(2)
    >>> pr = [(p.uvp_m, p.uvp_p) for p in pr]
    >>> pprint(pr)  # doctest: +SKIP
    [((0.207, 0.486), (0.2083210..., 0.4727168...)),
     ((0.449, 0.511), (0.4459270..., 0.5077735...)),
     ((0.263, 0.505), (0.2640262..., 0.4955361...)),
     ((0.322, 0.545), (0.3316884..., 0.5431580...)),
     ((0.316, 0.537), (0.3222624..., 0.5357624...)),
     ((0.265, 0.553), (0.2710705..., 0.5501997...)),
     ((0.221, 0.538), (0.2261826..., 0.5294740...)),
     ((0.135, 0.532), (0.1439693..., 0.5190984...)),
     ((0.145, 0.472), (0.1494835..., 0.4556760...)),
     ((0.163, 0.331), (0.1563172..., 0.3164151...)),
     ((0.176, 0.431), (0.1763199..., 0.4127589...)),
     ((0.244, 0.349), (0.2287638..., 0.3499324...))]
    """

    experiment_results = list(BRENEMAN_EXPERIMENTS.get(experiment))

    illuminants = experiment_results.pop(0)
    XYZ_w = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_t)) * 100
    XYZ_wr = xy_to_XYZ(Luv_uv_to_xy(illuminants.uvp_m)) * 100
    xy_wr = XYZ_to_xy(XYZ_wr)
    L_A1 = L_A2 = BRENEMAN_EXPERIMENTS_PRIMARIES_CHROMATICITIES.get(
        experiment).Y

    prediction = []
    for result in experiment_results:
        XYZ_1 = xy_to_XYZ(Luv_uv_to_xy(result.uvp_t)) * 100
        XYZ_2 = chromatic_adaptation_CMCCAT2000(XYZ_1, XYZ_w, XYZ_wr, L_A1,
                                                L_A2)
        uvp = Luv_to_uv(XYZ_to_Luv(XYZ_2, xy_wr), xy_wr)
        prediction.append(
            CorrespondingChromaticitiesPrediction(result.name, result.uvp_t,
                                                  result.uvp_m, uvp))

    return tuple(prediction)
Example #19
0
def primaries_whitepoint(npm):
    """
    Returns the *primaries* and *whitepoint* :math:`xy` chromaticity
    coordinates using given *normalised primary matrix*.

    Parameters
    ----------
    npm : array_like, (3, 3)
        *Normalised primary matrix*.

    Returns
    -------
    tuple
        *Primaries* and *whitepoint* :math:`xy` chromaticity coordinates.

    References
    ----------
    .. [2]  Trieu, T. (2015). Private Discussion with Mansencal, T.

    Examples
    --------
    >>> npm = np.array([[9.52552396e-01, 0.00000000e+00, 9.36786317e-05],
    ...                 [3.43966450e-01, 7.28166097e-01, -7.21325464e-02],
    ...                 [0.00000000e+00, 0.00000000e+00, 1.00882518e+00]])
    >>> p, w = primaries_whitepoint(npm)
    >>> p  # doctest: +ELLIPSIS
    array([[  7.3470000...e-01,   2.6530000...e-01],
           [  0.0000000...e+00,   1.0000000...e+00],
           [  1.0000000...e-04,  -7.7000000...e-02]])
    >>> w # doctest: +ELLIPSIS
    array([ 0.32168,  0.33767])
    """

    npm = npm.reshape((3, 3))

    primaries = XYZ_to_xy(
        np.transpose(np.dot(npm, np.identity(3))))
    whitepoint = np.squeeze(XYZ_to_xy(
        np.transpose(np.dot(npm, np.ones((3, 1))))))

    # TODO: Investigate if we return an ndarray here with primaries and
    # whitepoint stacked together.
    return primaries, whitepoint
Example #20
0
    def setUp(self):
        """Initialise the common tests attributes."""

        self._xy_s = XYZ_to_xy(
            MSDS_CMFS["CIE 1931 2 Degree Standard Observer"].values
        )

        self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"][
            "D65"
        ]
Example #21
0
def vs_colorimetry_data(
    sd_test: SpectralDistribution,
    sd_reference: SpectralDistribution,
    sds_vs: Dict[str, SpectralDistribution],
    cmfs: MultiSpectralDistributions,
    chromatic_adaptation: Boolean = False,
) -> Tuple[VS_ColorimetryData, ...]:
    """
    Return the *VS test colour samples* colorimetry data.

    Parameters
    ----------
    sd_test
        Test spectral distribution.
    sd_reference
        Reference spectral distribution.
    sds_vs
        *VS test colour samples* spectral distributions.
    cmfs
        Standard observer colour matching functions.
    chromatic_adaptation
        Whether to perform chromatic adaptation.

    Returns
    -------
    :class:`tuple`
        *VS test colour samples* colorimetry data.
    """

    XYZ_t = sd_to_XYZ(sd_test, cmfs)
    XYZ_t /= XYZ_t[1]

    XYZ_r = sd_to_XYZ(sd_reference, cmfs)
    XYZ_r /= XYZ_r[1]
    xy_r = XYZ_to_xy(XYZ_r)

    vs_data = []
    for _key, value in sorted(INDEXES_TO_NAMES_VS.items()):
        sd_vs = sds_vs[value]

        with domain_range_scale("1"):
            XYZ_vs = sd_to_XYZ(sd_vs, cmfs, sd_test)

        if chromatic_adaptation:
            XYZ_vs = chromatic_adaptation_VonKries(XYZ_vs,
                                                   XYZ_t,
                                                   XYZ_r,
                                                   transform="CMCCAT2000")

        Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r)
        _L_vs, C_vs, _Hab = Lab_to_LCHab(Lab_vs)

        vs_data.append(VS_ColorimetryData(sd_vs.name, XYZ_vs, Lab_vs, C_vs))

    return tuple(vs_data)
Example #22
0
def primaries_whitepoint(npm: ArrayLike) -> Tuple[NDArray, NDArray]:
    """
    Compute the *primaries* and *whitepoint* :math:`xy` chromaticity
    coordinates using given *Normalised Primary Matrix* (NPM).

    Parameters
    ----------
    npm
        *Normalised Primary Matrix*.

    Returns
    -------
    :class:`tuple`
        *Primaries* and *whitepoint* :math:`xy` chromaticity coordinates.

    References
    ----------
    :cite:`Trieu2015a`

    Examples
    --------
    >>> npm = np.array([[9.52552396e-01, 0.00000000e+00, 9.36786317e-05],
    ...                 [3.43966450e-01, 7.28166097e-01, -7.21325464e-02],
    ...                 [0.00000000e+00, 0.00000000e+00, 1.00882518e+00]])
    >>> p, w = primaries_whitepoint(npm)
    >>> p  # doctest: +ELLIPSIS
    array([[  7.3470000...e-01,   2.6530000...e-01],
           [  0.0000000...e+00,   1.0000000...e+00],
           [  1.0000000...e-04,  -7.7000000...e-02]])
    >>> w # doctest: +ELLIPSIS
    array([ 0.32168,  0.33767])
    """

    npm = np.reshape(npm, (3, 3))

    primaries = XYZ_to_xy(np.transpose(np.dot(npm, np.identity(3))))
    whitepoint = np.squeeze(XYZ_to_xy(np.transpose(np.dot(npm, ones((3, 1))))))

    # TODO: Investigate if we return an ndarray here with primaries and
    # whitepoint stacked together.
    return primaries, whitepoint
Example #23
0
def vs_colorimetry_data(sd_test,
                        sd_reference,
                        sds_vs,
                        cmfs,
                        chromatic_adaptation=False):
    """
    Returns the *VS test colour samples* colorimetry data.

    Parameters
    ----------
    sd_test : SpectralDistribution
        Test spectral distribution.
    sd_reference : SpectralDistribution
        Reference spectral distribution.
    sds_vs : dict
        *VS test colour samples* spectral distributions.
    cmfs : XYZ_ColourMatchingFunctions
        Standard observer colour matching functions.
    chromatic_adaptation : bool, optional
        Perform chromatic adaptation.

    Returns
    -------
    list
        *VS test colour samples* colorimetry data.
    """

    XYZ_t = sd_to_XYZ(sd_test, cmfs)
    XYZ_t /= XYZ_t[1]

    XYZ_r = sd_to_XYZ(sd_reference, cmfs)
    XYZ_r /= XYZ_r[1]
    xy_r = XYZ_to_xy(XYZ_r)

    vs_data = []
    for _key, value in sorted(VS_INDEXES_TO_NAMES.items()):
        sd_vs = sds_vs[value]

        with domain_range_scale('1'):
            XYZ_vs = sd_to_XYZ(sd_vs, cmfs, sd_test)

        if chromatic_adaptation:
            XYZ_vs = chromatic_adaptation_VonKries(XYZ_vs,
                                                   XYZ_t,
                                                   XYZ_r,
                                                   transform='CMCCAT2000')

        Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r)
        _L_vs, C_vs, _Hab = Lab_to_LCHab(Lab_vs)

        vs_data.append(VS_ColorimetryData(sd_vs.name, XYZ_vs, Lab_vs, C_vs))
    return vs_data
Example #24
0
    def test_XYZ_to_xy(self):
        """
        Tests :func:`colour.models.cie_xyy.XYZ_to_xy` definition.
        """

        np.testing.assert_almost_equal(XYZ_to_xy(
            np.array([0.07049534, 0.10080000, 0.09558313])),
                                       np.array([0.26414772, 0.37770001]),
                                       decimal=7)

        np.testing.assert_almost_equal(XYZ_to_xy(
            np.array([0.47097710, 0.34950000, 0.11301649])),
                                       np.array([0.50453169, 0.37440000]),
                                       decimal=7)

        np.testing.assert_almost_equal(XYZ_to_xy(
            np.array([0.25506814, 0.19150000, 0.08849752])),
                                       np.array([0.47670437, 0.35790000]),
                                       decimal=7)

        np.testing.assert_almost_equal(XYZ_to_xy(
            np.array([0.00000000, 0.00000000, 0.00000000])),
                                       np.array([0.34570000, 0.35850000]),
                                       decimal=7)
Example #25
0
def _get_cmfs_xy():
    """
    xy色度図のプロットのための馬蹄形の外枠のxy値を求める。

    Returns
    -------
    array_like
        xy coordinate for chromaticity diagram

    """
    # 基本パラメータ設定
    # ------------------
    cmf = CMFS.get(CMFS_NAME)
    d65_white = D65_WHITE

    # 馬蹄形のxy値を算出
    # --------------------------
    cmf_xy = XYZ_to_xy(cmf.values, d65_white)

    return cmf_xy
Example #26
0
def spds_CIE_1931_chromaticity_diagram_plot(
        spds,
        cmfs='CIE 1931 2 Degree Standard Observer',
        annotate=True,
        **kwargs):
    """
    Plots given spectral power distribution chromaticity coordinates into the
    *CIE 1931 Chromaticity Diagram*.

    Parameters
    ----------
    spds : array_like, optional
        Spectral power distributions to plot.
    cmfs : unicode, optional
        Standard observer colour matching functions used for diagram bounds.
    annotate : bool
        Should resulting chromaticity coordinates annotated with their
        respective spectral power distribution names.

    Other Parameters
    ----------------
    \**kwargs : dict, optional
        {:func:`boundaries`, :func:`canvas`, :func:`decorate`,
        :func:`display`},
        Please refer to the documentation of the previously listed definitions.
    show_diagram_colours : bool, optional
        {:func:`CIE_1931_chromaticity_diagram_plot`},
        Whether to display the chromaticity diagram background colours.

    Returns
    -------
    Figure
        Current figure or None.

    Examples
    --------
    >>> from colour import ILLUMINANTS_RELATIVE_SPDS
    >>> A = ILLUMINANTS_RELATIVE_SPDS['A']
    >>> D65 = ILLUMINANTS_RELATIVE_SPDS['D65']
    >>> spds_CIE_1931_chromaticity_diagram_plot([A, D65])  # doctest: +SKIP
    """

    settings = {}
    settings.update(kwargs)
    settings.update({'standalone': False})

    CIE_1931_chromaticity_diagram_plot(cmfs=cmfs, **settings)

    for spd in spds:
        XYZ = spectral_to_XYZ(spd) / 100
        xy = XYZ_to_xy(XYZ)

        pylab.plot(xy[0], xy[1], 'o', color='white')

        if spd.name is not None and annotate:
            pylab.annotate(spd.name,
                           xy=xy,
                           xytext=(50, 30),
                           color='black',
                           textcoords='offset points',
                           arrowprops=dict(arrowstyle='->',
                                           connectionstyle='arc3, rad=0.2'))

    settings.update({
        'x_tighten': True,
        'y_tighten': True,
        'limits': (-0.1, 0.9, -0.1, 0.9),
        'standalone': True
    })
    settings.update(kwargs)

    boundaries(**settings)
    decorate(**settings)

    return display(**settings)
Example #27
0
def CIE_1931_chromaticity_diagram_colours_plot(
        surface=1,
        samples=4096,
        cmfs='CIE 1931 2 Degree Standard Observer',
        **kwargs):
    """
    Plots the *CIE 1931 Chromaticity Diagram* colours.

    Parameters
    ----------
    surface : numeric, optional
        Generated markers surface.
    samples : numeric, optional
        Samples count on one axis.
    cmfs : unicode, optional
        Standard observer colour matching functions used for diagram bounds.

    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
    --------
    >>> CIE_1931_chromaticity_diagram_colours_plot()  # doctest: +SKIP
    """

    settings = {'figure_size': (64, 64)}
    settings.update(kwargs)

    canvas(**settings)

    cmfs = get_cmfs(cmfs)

    illuminant = DEFAULT_PLOTTING_ILLUMINANT

    triangulation = Delaunay(XYZ_to_xy(cmfs.values, illuminant),
                             qhull_options='QJ')
    xx, yy = np.meshgrid(np.linspace(0, 1, samples),
                         np.linspace(0, 1, samples))
    xy = tstack((xx, yy))
    xy = xy[triangulation.find_simplex(xy) > 0]

    XYZ = xy_to_XYZ(xy)

    RGB = normalise_maximum(XYZ_to_sRGB(XYZ, illuminant), axis=-1)

    x_dot, y_dot = tsplit(xy)

    pylab.scatter(x_dot, y_dot, color=RGB, s=surface)

    settings.update({
        'x_ticker': False,
        'y_ticker': False,
        'bounding_box': (0, 1, 0, 1)
    })
    settings.update(kwargs)

    ax = matplotlib.pyplot.gca()
    matplotlib.pyplot.setp(ax, frame_on=False)

    boundaries(**settings)
    decorate(**settings)

    return display(**settings)
Example #28
0
def CIE_1931_chromaticity_diagram_plot(
        cmfs='CIE 1931 2 Degree Standard Observer',
        show_diagram_colours=True,
        **kwargs):
    """
    Plots the *CIE 1931 Chromaticity Diagram*.

    Parameters
    ----------
    cmfs : unicode, optional
        Standard observer colour matching functions used for diagram bounds.
    show_diagram_colours : bool, optional
        Whether to display the chromaticity diagram background 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
    --------
    >>> CIE_1931_chromaticity_diagram_plot()  # doctest: +SKIP
    """

    settings = {'figure_size': (DEFAULT_FIGURE_WIDTH, DEFAULT_FIGURE_WIDTH)}
    settings.update(kwargs)

    canvas(**settings)

    cmfs = get_cmfs(cmfs)

    illuminant = DEFAULT_PLOTTING_ILLUMINANT

    if show_diagram_colours:
        image = matplotlib.image.imread(
            os.path.join(
                PLOTTING_RESOURCES_DIRECTORY,
                'CIE_1931_Chromaticity_Diagram_{0}.png'.format(
                    cmfs.name.replace(' ', '_'))))
        pylab.imshow(image, interpolation=None, extent=(0, 1, 0, 1))

    labels = (390, 460, 470, 480, 490, 500, 510, 520, 540, 560, 580, 600, 620,
              700)

    wavelengths = cmfs.wavelengths
    equal_energy = np.array([1 / 3] * 2)

    xy = XYZ_to_xy(cmfs.values, illuminant)

    wavelengths_chromaticity_coordinates = dict(tuple(zip(wavelengths, xy)))

    pylab.plot(xy[..., 0], xy[..., 1], color='black', linewidth=2)
    pylab.plot((xy[-1][0], xy[0][0]), (xy[-1][1], xy[0][1]),
               color='black',
               linewidth=2)

    for label in labels:
        x, y = wavelengths_chromaticity_coordinates[label]
        pylab.plot(x, y, 'o', color='black', linewidth=2)

        index = bisect.bisect(wavelengths, label)
        left = wavelengths[index - 1] if index >= 0 else wavelengths[index]
        right = (wavelengths[index]
                 if index < len(wavelengths) else wavelengths[-1])

        dx = (wavelengths_chromaticity_coordinates[right][0] -
              wavelengths_chromaticity_coordinates[left][0])
        dy = (wavelengths_chromaticity_coordinates[right][1] -
              wavelengths_chromaticity_coordinates[left][1])

        xy = np.array([x, y])
        direction = np.array([-dy, dx])

        normal = (np.array([
            -dy, dx
        ]) if np.dot(normalise_vector(xy - equal_energy),
                     normalise_vector(direction)) > 0 else np.array([dy, -dx]))
        normal = normalise_vector(normal)
        normal /= 25

        pylab.plot((x, x + normal[0] * 0.75), (y, y + normal[1] * 0.75),
                   color='black',
                   linewidth=1.5)
        pylab.text(x + normal[0],
                   y + normal[1],
                   label,
                   color='black',
                   clip_on=True,
                   ha='left' if normal[0] >= 0 else 'right',
                   va='center',
                   fontdict={'size': 'small'})

    ticks = np.arange(-10, 10, 0.1)

    pylab.xticks(ticks)
    pylab.yticks(ticks)

    settings.update({
        'title':
        'CIE 1931 Chromaticity Diagram - {0}'.format(cmfs.title),
        'x_label':
        'CIE x',
        'y_label':
        'CIE y',
        'grid':
        True,
        'bounding_box': (0, 1, 0, 1)
    })
    settings.update(kwargs)

    boundaries(**settings)
    decorate(**settings)

    return display(**settings)
Example #29
0
def dominant_wavelength(xy,
                        xy_n,
                        cmfs=CMFS['CIE 1931 2 Degree Standard Observer'],
                        inverse=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 *CIE xy* chromaticity coordinates.
    xy_n : array_like
        Achromatic stimulus *CIE xy* chromaticity coordinates.
    cmfs : XYZ_ColourMatchingFunctions, optional
        Standard observer colour matching functions.
    inverse : bool, optional
        Inverse the computation direction to retrieve the
        *complementary wavelength*.

    Returns
    -------
    tuple
        *Dominant wavelength*, first intersection point *CIE xy* chromaticity
        coordinates, second intersection point *CIE xy* chromaticity
        coordinates.

    References
    ----------
    :cite:`CIETC1-482004o`, :cite:`Erdogana`

    Examples
    --------
    *Dominant wavelength* computation:

    >>> from pprint import pprint
    >>> xy = np.array([0.54369557, 0.32107944])
    >>> xy_n = np.array([0.31270000, 0.32900000])
    >>> cmfs = CMFS['CIE 1931 2 Degree Standard Observer']
    >>> pprint(dominant_wavelength(xy, xy_n, cmfs))  # doctest: +ELLIPSIS
    (array(616...),
     array([ 0.6835474...,  0.3162840...]),
     array([ 0.6835474...,  0.3162840...]))

    *Complementary dominant wavelength* is returned if the first intersection
    is located on the line of purples:

    >>> xy = np.array([0.37605506, 0.24452225])
    >>> pprint(dominant_wavelength(xy, xy_n, cmfs))  # doctest: +ELLIPSIS
    (array(-509.0),
     array([ 0.4572314...,  0.1362814...]),
     array([ 0.0104096...,  0.7320745...]))
    """

    xy = as_float_array(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, inverse)
    xy_cwl = xy_wl
    wl = cmfs.wavelengths[i_wl]

    xy_e = (extend_line_segment(xy, xy_n) if inverse 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 inverse)
    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)
Example #30
0
def plot_spectral_locus(cmfs='CIE 1931 2 Degree Standard Observer',
                        spectral_locus_colours=None,
                        spectral_locus_labels=None,
                        method='CIE 1931',
                        **kwargs):
    """
    Plots the *Spectral Locus* according to given method.

    Parameters
    ----------
    cmfs : unicode, optional
        Standard observer colour matching functions defining the
        *Spectral Locus*.
    spectral_locus_colours : array_like or unicode, optional
        *Spectral Locus* colours, if ``spectral_locus_colours`` is set to
        *RGB*, the colours will be computed according to the corresponding
        chromaticity coordinates.
    spectral_locus_labels : array_like, optional
        Array of wavelength labels used to customise which labels will be drawn
        around the spectral locus. Passing an empty array will result in no
        wavelength labels being drawn.
    method : unicode, optional
        **{'CIE 1931', 'CIE 1960 UCS', 'CIE 1976 UCS'}**,
        *Chromaticity Diagram* method.

    Other Parameters
    ----------------
    \\**kwargs : dict, optional
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        Please refer to the documentation of the previously listed definitions.

    Returns
    -------
    tuple
        Current figure and axes.

    Examples
    --------
    >>> plot_spectral_locus(spectral_locus_colours='RGB')  # doctest: +SKIP

    .. image:: ../_static/Plotting_Plot_Spectral_Locus.png
        :align: center
        :alt: plot_spectral_locus
    """

    if spectral_locus_colours is None:
        spectral_locus_colours = COLOUR_STYLE_CONSTANTS.colour.dark

    settings = {'uniform': True}
    settings.update(kwargs)

    figure, axes = artist(**settings)

    method = method.upper()

    cmfs = first_item(filter_cmfs(cmfs).values())

    illuminant = COLOUR_STYLE_CONSTANTS.colour.colourspace.whitepoint

    wavelengths = cmfs.wavelengths
    equal_energy = np.array([1 / 3] * 2)

    if method == 'CIE 1931':
        ij = XYZ_to_xy(cmfs.values, illuminant)
        labels = ((390, 460, 470, 480, 490, 500, 510, 520, 540, 560, 580, 600,
                   620, 700)
                  if spectral_locus_labels is None else spectral_locus_labels)
    elif method == 'CIE 1960 UCS':
        ij = UCS_to_uv(XYZ_to_UCS(cmfs.values))
        labels = ((420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540,
                   550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680)
                  if spectral_locus_labels is None else spectral_locus_labels)
    elif method == 'CIE 1976 UCS':
        ij = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant)
        labels = ((420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540,
                   550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680)
                  if spectral_locus_labels is None else spectral_locus_labels)
    else:
        raise ValueError(
            'Invalid method: "{0}", must be one of '
            '{\'CIE 1931\', \'CIE 1960 UCS\', \'CIE 1976 UCS\'}'.format(
                method))

    pl_ij = tstack([
        np.linspace(ij[0][0], ij[-1][0], 20),
        np.linspace(ij[0][1], ij[-1][1], 20)
    ]).reshape(-1, 1, 2)
    sl_ij = np.copy(ij).reshape(-1, 1, 2)

    if spectral_locus_colours.upper() == 'RGB':
        spectral_locus_colours = normalise_maximum(
            XYZ_to_plotting_colourspace(cmfs.values), axis=-1)

        if method == 'CIE 1931':
            XYZ = xy_to_XYZ(pl_ij)
        elif method == 'CIE 1960 UCS':
            XYZ = xy_to_XYZ(UCS_uv_to_xy(pl_ij))
        elif method == 'CIE 1976 UCS':
            XYZ = xy_to_XYZ(Luv_uv_to_xy(pl_ij))
        purple_line_colours = normalise_maximum(
            XYZ_to_plotting_colourspace(XYZ.reshape(-1, 3)), axis=-1)
    else:
        purple_line_colours = spectral_locus_colours

    for slp_ij, slp_colours in ((pl_ij, purple_line_colours),
                                (sl_ij, spectral_locus_colours)):
        line_collection = LineCollection(
            np.concatenate([slp_ij[:-1], slp_ij[1:]], axis=1),
            colors=slp_colours)
        axes.add_collection(line_collection)

    wl_ij = dict(tuple(zip(wavelengths, ij)))
    for label in labels:
        i, j = wl_ij[label]

        index = bisect.bisect(wavelengths, label)
        left = wavelengths[index - 1] if index >= 0 else wavelengths[index]
        right = (wavelengths[index]
                 if index < len(wavelengths) else wavelengths[-1])

        dx = wl_ij[right][0] - wl_ij[left][0]
        dy = wl_ij[right][1] - wl_ij[left][1]

        ij = np.array([i, j])
        direction = np.array([-dy, dx])

        normal = (np.array([-dy, dx]) if np.dot(
            normalise_vector(ij - equal_energy), normalise_vector(direction)) >
                  0 else np.array([dy, -dx]))
        normal = normalise_vector(normal) / 30

        label_colour = (spectral_locus_colours
                        if is_string(spectral_locus_colours) else
                        spectral_locus_colours[index])
        axes.plot(
            (i, i + normal[0] * 0.75), (j, j + normal[1] * 0.75),
            color=label_colour)

        axes.plot(i, j, 'o', color=label_colour)

        axes.text(
            i + normal[0],
            j + normal[1],
            label,
            clip_on=True,
            ha='left' if normal[0] >= 0 else 'right',
            va='center',
            fontdict={'size': 'small'})

    settings = {'axes': axes}
    settings.update(kwargs)

    return render(**kwargs)