Exemplo n.º 1
0
    def test_handle_spectral_arguments(self):
        """
        Test :func:`colour.colorimetry.tristimulus_values.\
handle_spectral_arguments` definition.
        """

        cmfs, illuminant = handle_spectral_arguments()
        # pylint: disable=E1102
        self.assertEqual(
            cmfs,
            reshape_msds(MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]),
        )
        self.assertEqual(illuminant, reshape_sd(SDS_ILLUMINANTS["D65"]))

        shape = SpectralShape(400, 700, 20)
        cmfs, illuminant = handle_spectral_arguments(shape_default=shape)
        self.assertEqual(cmfs.shape, shape)
        self.assertEqual(illuminant.shape, shape)

        cmfs, illuminant = handle_spectral_arguments(
            cmfs_default="CIE 2012 2 Degree Standard Observer",
            illuminant_default="E",
            shape_default=shape,
        )
        self.assertEqual(
            cmfs,
            reshape_msds(MSDS_CMFS["CIE 2012 2 Degree Standard Observer"],
                         shape=shape),
        )
        self.assertEqual(illuminant,
                         sd_ones(shape, interpolator=LinearInterpolator) * 100)
Exemplo n.º 2
0
    def test_raise_exception_tristimulus_weighting_factors_ASTME2022(self):
        """
        Test :func:`colour.colorimetry.tristimulus_values.\
tristimulus_weighting_factors_ASTME2022` definition raised exception.
        """

        shape = SpectralShape(360, 830, 10)
        cmfs_1 = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"]
        # pylint: disable=E1102
        cmfs_2 = reshape_msds(cmfs_1, shape)
        A_1 = sd_CIE_standard_illuminant_A(cmfs_1.shape)
        A_2 = sd_CIE_standard_illuminant_A(cmfs_2.shape)

        self.assertRaises(
            ValueError,
            tristimulus_weighting_factors_ASTME2022,
            cmfs_1,
            A_2,
            shape,
        )

        self.assertRaises(
            ValueError,
            tristimulus_weighting_factors_ASTME2022,
            cmfs_2,
            A_1,
            shape,
        )
Exemplo n.º 3
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(
            SDS_COLOURCHECKERS["ColorChecker N Ohta"].values()
        )

        self._tree = Tree_Otsu2018(self._reflectances)
        self._tree.optimise()
        for leaf in self._tree.leaves:
            if len(leaf.parent.children) == 2:
                self._node_a = leaf.parent
                self._node_b, self._node_c = self._node_a.children
                break

        self._data_a = Data_Otsu2018(
            np.transpose(reshape_msds(self._reflectances, self._shape).values),
            self._cmfs,
            self._sd_D65,
        )
        self._data_b = self._node_b.data

        self._partition_axis = self._node_a.partition_axis
Exemplo n.º 4
0
    def test_XYZ_to_sd_Meng2015(self):
        """Test :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition."""

        XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
        np.testing.assert_almost_equal(
            sd_to_XYZ_integration(
                XYZ_to_sd_Meng2015(XYZ, self._cmfs, self._sd_D65),
                self._cmfs,
                self._sd_D65,
            )
            / 100,
            XYZ,
            decimal=7,
        )

        np.testing.assert_almost_equal(
            sd_to_XYZ_integration(
                XYZ_to_sd_Meng2015(XYZ, self._cmfs, self._sd_E),
                self._cmfs,
                self._sd_E,
            )
            / 100,
            XYZ,
            decimal=7,
        )

        np.testing.assert_almost_equal(
            sd_to_XYZ_integration(
                XYZ_to_sd_Meng2015(
                    XYZ,
                    self._cmfs,
                    self._sd_D65,
                    optimisation_kwargs={
                        "options": {
                            "ftol": 1e-10,
                        }
                    },
                ),
                self._cmfs,
                self._sd_D65,
            )
            / 100,
            XYZ,
            decimal=7,
        )

        shape = SpectralShape(400, 700, 5)
        # pylint: disable=E1102
        cmfs = reshape_msds(self._cmfs, shape)
        np.testing.assert_almost_equal(
            sd_to_XYZ_integration(
                XYZ_to_sd_Meng2015(XYZ, cmfs, self._sd_D65), cmfs, self._sd_D65
            )
            / 100,
            XYZ,
            decimal=7,
        )
Exemplo n.º 5
0
    def setUp(self):
        """Initialise the common tests attributes."""

        # pylint: disable=E1102
        self._cmfs = reshape_msds(
            MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],
            SpectralShape(360, 780, 10),
        )
        self._sd_D65 = reshape_sd(SDS_ILLUMINANTS["D65"], self._cmfs.shape)
        self._sd_E = reshape_sd(SDS_ILLUMINANTS["E"], self._cmfs.shape)
Exemplo n.º 6
0
    def __init__(self):
        """Initialise common tests attributes for the mixin."""

        # pylint: disable=E1102
        self._cmfs = reshape_msds(
            MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],
            SpectralShape(360, 780, 10),
        )
        self._sd_D65 = reshape_sd(SDS_ILLUMINANTS["D65"], self._cmfs.shape)
        self._xy_D65 = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"][
            "D65"
        ]
Exemplo n.º 7
0
    def test_XYZ_outer_surface(self):
        """
        Test :func:`colour.volume.spectrum.XYZ_outer_surface`
        definition.
        """

        shape = SpectralShape(
            SPECTRAL_SHAPE_DEFAULT.start, SPECTRAL_SHAPE_DEFAULT.end, 84
        )
        cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]

        # pylint: disable=E1102
        np.testing.assert_array_almost_equal(
            XYZ_outer_surface(reshape_msds(cmfs, shape)),
            np.array(
                [
                    [0.00000000e00, 0.00000000e00, 0.00000000e00],
                    [9.63613812e-05, 2.90567768e-06, 4.49612264e-04],
                    [2.59105294e-01, 2.10312980e-02, 1.32074689e00],
                    [1.05610219e-01, 6.20382435e-01, 3.54235713e-02],
                    [7.26479803e-01, 3.54608696e-01, 2.10051491e-04],
                    [1.09718745e-02, 3.96354538e-03, 0.00000000e00],
                    [3.07925724e-05, 1.11197622e-05, 0.00000000e00],
                    [2.59201656e-01, 2.10342037e-02, 1.32119651e00],
                    [3.64715514e-01, 6.41413733e-01, 1.35617047e00],
                    [8.32090022e-01, 9.74991131e-01, 3.56336228e-02],
                    [7.37451677e-01, 3.58572241e-01, 2.10051491e-04],
                    [1.10026671e-02, 3.97466514e-03, 0.00000000e00],
                    [1.27153954e-04, 1.40254398e-05, 4.49612264e-04],
                    [3.64811875e-01, 6.41416639e-01, 1.35662008e00],
                    [1.09119532e00, 9.96022429e-01, 1.35638052e00],
                    [8.43061896e-01, 9.78954677e-01, 3.56336228e-02],
                    [7.37482470e-01, 3.58583361e-01, 2.10051491e-04],
                    [1.10990285e-02, 3.97757082e-03, 4.49612264e-04],
                    [2.59232448e-01, 2.10453234e-02, 1.32119651e00],
                    [1.09129168e00, 9.96025335e-01, 1.35683013e00],
                    [1.10216719e00, 9.99985975e-01, 1.35638052e00],
                    [8.43092689e-01, 9.78965796e-01, 3.56336228e-02],
                    [7.37578831e-01, 3.58586267e-01, 6.59663755e-04],
                    [2.70204323e-01, 2.50088688e-02, 1.32119651e00],
                    [3.64842668e-01, 6.41427759e-01, 1.35662008e00],
                    [1.10226355e00, 9.99988880e-01, 1.35683013e00],
                    [1.10219798e00, 9.99997094e-01, 1.35638052e00],
                    [8.43189050e-01, 9.78968702e-01, 3.60832350e-02],
                    [9.96684125e-01, 3.79617565e-01, 1.32140656e00],
                    [3.75814542e-01, 6.45391304e-01, 1.35662008e00],
                    [1.09132247e00, 9.96036455e-01, 1.35683013e00],
                    [1.10229434e00, 1.00000000e00, 1.35683013e00],
                ]
            ),
            decimal=7,
        )
Exemplo n.º 8
0
    def test_reflectances(self):
        """
        Test :attr:`colour.recovery.otsu2018.Tree_Otsu2018.reflectances`
        property.
        """

        np.testing.assert_almost_equal(
            self._tree.reflectances,
            np.transpose(
                reshape_msds(
                    sds_and_msds_to_msds(self._reflectances), self._shape
                ).values
            ),
            decimal=7,
        )
Exemplo n.º 9
0
    def test_domain_range_scale_msds_to_XYZ_ASTME308(self):
        """
        Test :func:`colour.colorimetry.tristimulus_values.\
msds_to_XYZ_ASTME308` definition domain and range scale support.
        """

        cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
        d_r = (("reference", 1), ("1", 0.01), ("100", 1))
        for scale, factor in d_r:
            with domain_range_scale(scale):
                # pylint: disable=E1102
                np.testing.assert_almost_equal(
                    msds_to_XYZ_ASTME308(
                        reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20)),
                        cmfs,
                        SDS_ILLUMINANTS["D65"],
                    ),
                    TVS_D65_ASTME308_MSDS * factor,
                    decimal=7,
                )
Exemplo n.º 10
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 = np.transpose(
            reshape_msds(
                sds_and_msds_to_msds(
                    SDS_COLOURCHECKERS["ColorChecker N Ohta"].values()
                ),
                self._shape,
            ).values
        )

        self._data = Data_Otsu2018(
            self._reflectances, self._cmfs, self._sd_D65
        )
Exemplo n.º 11
0
    def test_msds_to_XYZ_ASTME308(self):
        """
        Test :func:`colour.colorimetry.tristimulus_values.\
msds_to_XYZ_ASTME308` definition.
        """

        cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
        # pylint: disable=E1102
        msds = reshape_msds(MSDS_TWO, SpectralShape(400, 700, 20))
        np.testing.assert_almost_equal(
            msds_to_XYZ_ASTME308(msds, cmfs, SDS_ILLUMINANTS["D65"]),
            TVS_D65_ASTME308_MSDS,
            decimal=7,
        )

        np.testing.assert_almost_equal(
            msds_to_XYZ_ASTME308(msds, cmfs, SDS_ILLUMINANTS["D65"], k=1),
            TVS_D65_ASTME308_K1_MSDS,
            decimal=7,
        )
Exemplo n.º 12
0
    def test_plot_spectral_locus(self):
        """Test :func:`colour.plotting.diagrams.plot_spectral_locus` definition."""

        figure, axes = plot_spectral_locus()

        self.assertIsInstance(figure, Figure)
        self.assertIsInstance(axes, Axes)

        figure, axes = plot_spectral_locus(spectral_locus_colours="RGB")

        self.assertIsInstance(figure, Figure)
        self.assertIsInstance(axes, Axes)

        figure, axes = plot_spectral_locus(method="CIE 1960 UCS",
                                           spectral_locus_colours="RGB")

        self.assertIsInstance(figure, Figure)
        self.assertIsInstance(axes, Axes)

        figure, axes = plot_spectral_locus(method="CIE 1976 UCS",
                                           spectral_locus_colours="RGB")

        self.assertIsInstance(figure, Figure)
        self.assertIsInstance(axes, Axes)

        # pylint: disable=E1102
        figure, axes = plot_spectral_locus(
            reshape_msds(
                MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],
                SpectralShape(400, 700, 10),
            ))

        self.assertIsInstance(figure, Figure)
        self.assertIsInstance(axes, Axes)

        self.assertRaises(ValueError,
                          lambda: plot_spectral_locus(method="Undefined"))
Exemplo n.º 13
0
def colour_quality_scale(
    sd_test: SpectralDistribution,
    additional_data: Boolean = False,
    method: Union[Literal["NIST CQS 7.4", "NIST CQS 9.0"],
                  str] = "NIST CQS 9.0",
) -> Union[Floating, ColourRendering_Specification_CQS]:
    """
    Return the *Colour Quality Scale* (CQS) of given spectral distribution
    using given method.

    Parameters
    ----------
    sd_test
        Test spectral distribution.
    additional_data
        Whether to output additional data.
    method
        Computation method.

    Returns
    -------
    :class:`numpy.floating` or \
:class:`colour.quality.ColourRendering_Specification_CQS`
        *Colour Quality Scale* (CQS).

    References
    ----------
    :cite:`Davis2010a`, :cite:`Ohno2008a`, :cite:`Ohno2013`

    Examples
    --------
    >>> from colour import SDS_ILLUMINANTS
    >>> sd = SDS_ILLUMINANTS['FL2']
    >>> colour_quality_scale(sd)  # doctest: +ELLIPSIS
    64.1117031...
    """

    method = validate_method(method, COLOUR_QUALITY_SCALE_METHODS)

    # pylint: disable=E1102
    cmfs = reshape_msds(
        MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],
        SPECTRAL_SHAPE_DEFAULT,
    )

    shape = cmfs.shape
    sd_test = reshape_sd(sd_test, shape)
    vs_sds = {sd.name: reshape_sd(sd, shape) for sd in SDS_VS[method].values()}

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

    uv = UCS_to_uv(XYZ_to_UCS(XYZ))
    CCT, _D_uv = uv_to_CCT_Ohno2013(uv)

    if CCT < 5000:
        sd_reference = sd_blackbody(CCT, shape)
    else:
        xy = CCT_to_xy_CIE_D(CCT)
        sd_reference = sd_CIE_illuminant_D_series(xy)
        sd_reference.align(shape)

    test_vs_colorimetry_data = vs_colorimetry_data(sd_test,
                                                   sd_reference,
                                                   vs_sds,
                                                   cmfs,
                                                   chromatic_adaptation=True)

    reference_vs_colorimetry_data = vs_colorimetry_data(
        sd_reference, sd_reference, vs_sds, cmfs)

    CCT_f: Floating
    if method == "nist cqs 9.0":
        CCT_f = 1
        scaling_f = 3.2
    else:
        XYZ_r = sd_to_XYZ(sd_reference, cmfs)
        XYZ_r /= XYZ_r[1]
        CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r)
        scaling_f = 3.104

    Q_as = colour_quality_scales(
        test_vs_colorimetry_data,
        reference_vs_colorimetry_data,
        scaling_f,
        CCT_f,
    )

    D_E_RMS = delta_E_RMS(Q_as, "D_E_ab")
    D_Ep_RMS = delta_E_RMS(Q_as, "D_Ep_ab")

    Q_a = scale_conversion(D_Ep_RMS, CCT_f, scaling_f)

    if method == "nist cqs 9.0":
        scaling_f = 2.93 * 1.0343
    else:
        scaling_f = 2.928

    Q_f = scale_conversion(D_E_RMS, CCT_f, scaling_f)

    G_t = gamut_area(
        [vs_CQS_data.Lab for vs_CQS_data in test_vs_colorimetry_data])
    G_r = gamut_area(
        [vs_CQS_data.Lab for vs_CQS_data in reference_vs_colorimetry_data])

    Q_g = G_t / GAMUT_AREA_D65 * 100

    if method == "nist cqs 9.0":
        Q_p = Q_d = None
    else:
        p_delta_C = np.average([
            sample_data.D_C_ab if sample_data.D_C_ab > 0 else 0
            for sample_data in Q_as.values()
        ])
        Q_p = as_float_scalar(100 - 3.6 * (D_Ep_RMS - p_delta_C))
        Q_d = as_float_scalar(G_t / G_r * CCT_f * 100)

    if additional_data:
        return ColourRendering_Specification_CQS(
            sd_test.name,
            Q_a,
            Q_f,
            Q_p,
            Q_g,
            Q_d,
            Q_as,
            (test_vs_colorimetry_data, reference_vs_colorimetry_data),
        )
    else:
        return Q_a
Exemplo n.º 14
0
def plot_visible_spectrum_section(
    cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[
        MultiSpectralDistributions,
        str]], ] = "CIE 1931 2 Degree Standard Observer",
    illuminant: Union[SpectralDistribution, str] = "D65",
    model: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS", "CAM16LCD",
                         "CAM16SCD", "CAM16UCS", "CIE XYZ", "CIE xyY",
                         "CIE Lab", "CIE Luv", "CIE UCS", "CIE UVW", "DIN99",
                         "Hunter Lab", "Hunter Rdab", "ICaCb", "ICtCp", "IPT",
                         "IgPgTg", "Jzazbz", "OSA UCS", "Oklab", "hdr-CIELAB",
                         "hdr-IPT", ], str, ] = "CIE xyY",
    axis: Union[Literal["+z", "+x", "+y"], str] = "+z",
    origin: Floating = 0.5,
    normalise: Boolean = True,
    show_section_colours: Boolean = True,
    show_section_contour: Boolean = True,
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot the visible spectrum volume, i.e. *Rösch-MacAdam* colour solid,
    section colours along given axis and origin.

    Parameters
    ----------
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.  ``cmfs`` can be of any type or
        form supported by the :func:`colour.plotting.filter_cmfs` definition.
    illuminant
        Illuminant spectral distribution, default to *CIE Illuminant D65*.
        ``illuminant`` can be of any type or form supported by the
        :func:`colour.plotting.filter_illuminants` definition.
    model
        Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for
        the list of supported colourspace models.
    axis
        Axis the hull section will be normal to.
    origin
        Coordinate along ``axis`` at which to plot the hull section.
    normalise
        Whether to normalise ``axis`` to the extent of the hull along it.
    show_section_colours
        Whether to show the hull section colours.
    show_section_contour
        Whether to show the hull section contour.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`,
        :func:`colour.plotting.section.plot_hull_section_colours`
        :func:`colour.plotting.section.plot_hull_section_contour`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour.utilities import is_trimesh_installed
    >>> if is_trimesh_installed:
    ...     plot_visible_spectrum_section(
    ...         section_colours='RGB', section_opacity=0.15)
    ...     # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...AxesSubplot...>)

    .. image:: ../_static/Plotting_Plot_Visible_Spectrum_Section.png
        :align: center
        :alt: plot_visible_spectrum_section
    """

    import trimesh

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    # pylint: disable=E1102
    cmfs = reshape_msds(first_item(filter_cmfs(cmfs).values()),
                        SpectralShape(360, 780, 1))
    illuminant = cast(
        SpectralDistribution,
        first_item(filter_illuminants(illuminant).values()),
    )

    vertices = solid_RoschMacAdam(
        cmfs,
        illuminant,
        point_order="Pulse Wave Width",
        filter_jagged_points=True,
    )
    mesh = trimesh.Trimesh(vertices)
    hull = trimesh.convex.convex_hull(mesh)

    if show_section_colours:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["standalone"] = False

        plot_hull_section_colours(hull, model, axis, origin, normalise,
                                  **settings)

    if show_section_contour:
        settings = {"axes": axes}
        settings.update(kwargs)
        settings["standalone"] = False

        plot_hull_section_contour(hull, model, axis, origin, normalise,
                                  **settings)

    title = (f"Visible Spectrum Section - "
             f"{f'{origin * 100}%' if normalise else origin} - "
             f"{model} - "
             f"{cmfs.strict_name}")

    plane = MAPPING_AXIS_TO_PLANE[axis]

    labels = np.array(COLOURSPACE_MODELS_AXIS_LABELS[model])[as_int_array(
        colourspace_model_axis_reorder([0, 1, 2], model))]
    x_label, y_label = labels[plane[0]], labels[plane[1]]

    settings.update({
        "axes": axes,
        "standalone": True,
        "title": title,
        "x_label": x_label,
        "y_label": y_label,
    })
    settings.update(kwargs)

    return render(**settings)
Exemplo n.º 15
0
def training_data_sds_to_RGB(
    training_data: MultiSpectralDistributions,
    sensitivities: RGB_CameraSensitivities,
    illuminant: SpectralDistribution,
) -> Tuple[NDArray, NDArray]:
    """
    Convert given training data to *RGB* tristimulus values using given
    illuminant and given camera *RGB* spectral sensitivities.

    Parameters
    ----------
    training_data
        Training data multi-spectral distributions.
    sensitivities
         Camera *RGB* spectral sensitivities.
    illuminant
        Illuminant spectral distribution.

    Returns
    -------
    :class:`tuple`
        Tuple of training data *RGB* tristimulus values and white balance
        multipliers.

    Examples
    --------
    >>> path = os.path.join(
    ...     RESOURCES_DIRECTORY_RAWTOACES,
    ...     'CANON_EOS_5DMark_II_RGB_Sensitivities.csv')
    >>> sensitivities = sds_and_msds_to_msds(
    ...     read_sds_from_csv_file(path).values())
    >>> illuminant = normalise_illuminant(
    ...     SDS_ILLUMINANTS['D55'], sensitivities)
    >>> training_data = read_training_data_rawtoaces_v1()
    >>> RGB, RGB_w = training_data_sds_to_RGB(
    ...     training_data, sensitivities, illuminant)
    >>> RGB[:5]  # doctest: +ELLIPSIS
    array([[ 0.0207582...,  0.0196857...,  0.0213935...],
           [ 0.0895775...,  0.0891922...,  0.0891091...],
           [ 0.7810230...,  0.7801938...,  0.7764302...],
           [ 0.1995   ...,  0.1995   ...,  0.1995   ...],
           [ 0.5898478...,  0.5904015...,  0.5851076...]])
    >>> RGB_w  # doctest: +ELLIPSIS
    array([ 2.3414154...,  1.        ,  1.5163375...])
    """

    shape = sensitivities.shape
    if illuminant.shape != shape:
        runtime_warning(
            f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
        illuminant = reshape_sd(illuminant, shape)

    if training_data.shape != shape:
        runtime_warning(
            f'Aligning "{training_data.name}" training data shape to "{shape}".'
        )
        # pylint: disable=E1102
        training_data = reshape_msds(training_data, shape)

    RGB_w = white_balance_multipliers(sensitivities, illuminant)

    RGB = np.dot(
        np.transpose(illuminant.values[..., np.newaxis] *
                     training_data.values),
        sensitivities.values,
    )

    RGB *= RGB_w

    return RGB, RGB_w
Exemplo n.º 16
0
def training_data_sds_to_XYZ(
    training_data: MultiSpectralDistributions,
    cmfs: MultiSpectralDistributions,
    illuminant: SpectralDistribution,
    chromatic_adaptation_transform: Union[Literal["Bianco 2010",
                                                  "Bianco PC 2010", "Bradford",
                                                  "CAT02 Brill 2008", "CAT02",
                                                  "CAT16", "CMCCAT2000",
                                                  "CMCCAT97", "Fairchild",
                                                  "Sharp", "Von Kries",
                                                  "XYZ Scaling", ],
                                          str, ] = "CAT02",
) -> NDArray:
    """
    Convert given training data to *CIE XYZ* tristimulus values using given
    illuminant and given standard observer colour matching functions.

    Parameters
    ----------
    training_data
        Training data multi-spectral distributions.
    cmfs
        Standard observer colour matching functions.
    illuminant
        Illuminant spectral distribution.
    chromatic_adaptation_transform
        *Chromatic adaptation* transform, if *None* no chromatic adaptation is
        performed.

    Returns
    -------
    :class:`numpy.ndarray`
        Training data *CIE XYZ* tristimulus values.

    Examples
    --------
    >>> from colour import MSDS_CMFS
    >>> path = os.path.join(
    ...     RESOURCES_DIRECTORY_RAWTOACES,
    ...     'CANON_EOS_5DMark_II_RGB_Sensitivities.csv')
    >>> cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer']
    >>> sensitivities = sds_and_msds_to_msds(
    ...     read_sds_from_csv_file(path).values())
    >>> illuminant = normalise_illuminant(
    ...     SDS_ILLUMINANTS['D55'], sensitivities)
    >>> training_data = read_training_data_rawtoaces_v1()
    >>> training_data_sds_to_XYZ(training_data, cmfs, illuminant)[:5]
    ... # doctest: +ELLIPSIS
    array([[ 0.0174353...,  0.0179504...,  0.0196109...],
           [ 0.0855607...,  0.0895735...,  0.0901703...],
           [ 0.7455880...,  0.7817549...,  0.7834356...],
           [ 0.1900528...,  0.1995   ...,  0.2012606...],
           [ 0.5626319...,  0.5914544...,  0.5894500...]])
    """

    shape = cmfs.shape
    if illuminant.shape != shape:
        runtime_warning(
            f'Aligning "{illuminant.name}" illuminant shape to "{shape}".')
        illuminant = reshape_sd(illuminant, shape)

    if training_data.shape != shape:
        runtime_warning(
            f'Aligning "{training_data.name}" training data shape to "{shape}".'
        )
        # pylint: disable=E1102
        training_data = reshape_msds(training_data, shape)

    XYZ = np.dot(
        np.transpose(illuminant.values[..., np.newaxis] *
                     training_data.values),
        cmfs.values,
    )

    XYZ *= 1 / np.sum(cmfs.values[..., 1] * illuminant.values)

    XYZ_w = np.dot(np.transpose(cmfs.values), illuminant.values)
    XYZ_w *= 1 / XYZ_w[1]

    if chromatic_adaptation_transform is not None:
        M_CAT = matrix_chromatic_adaptation_VonKries(
            XYZ_w,
            xy_to_XYZ(RGB_COLOURSPACE_ACES2065_1.whitepoint),
            chromatic_adaptation_transform,
        )

        XYZ = vector_dot(M_CAT, XYZ)

    return XYZ
Exemplo n.º 17
0
def matrix_idt(
    sensitivities: RGB_CameraSensitivities,
    illuminant: SpectralDistribution,
    training_data: Optional[MultiSpectralDistributions] = None,
    cmfs: Optional[MultiSpectralDistributions] = None,
    optimisation_factory: Callable = optimisation_factory_rawtoaces_v1,
    optimisation_kwargs: Optional[Dict] = None,
    chromatic_adaptation_transform: Union[Literal["Bianco 2010",
                                                  "Bianco PC 2010", "Bradford",
                                                  "CAT02 Brill 2008", "CAT02",
                                                  "CAT16", "CMCCAT2000",
                                                  "CMCCAT97", "Fairchild",
                                                  "Sharp", "Von Kries",
                                                  "XYZ Scaling", ],
                                          str, ] = "CAT02",
    additional_data: Boolean = False,
) -> Union[Tuple[NDArray, NDArray, NDArray, NDArray], Tuple[NDArray, NDArray]]:
    """
    Compute an *Input Device Transform* (IDT) matrix for given camera *RGB*
    spectral sensitivities, illuminant, training data, standard observer colour
    matching functions and optimization settings according to *RAW to ACES* v1
    and *P-2013-001* procedures.

    Parameters
    ----------
    sensitivities
         Camera *RGB* spectral sensitivities.
    illuminant
        Illuminant spectral distribution.
    training_data
        Training data multi-spectral distributions, defaults to using the
        *RAW to ACES* v1 190 patches.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    optimisation_factory
        Callable producing the objective function and the *CIE XYZ* to
        optimisation colour model function.
    optimisation_kwargs
        Parameters for :func:`scipy.optimize.minimize` definition.
    chromatic_adaptation_transform
        *Chromatic adaptation* transform, if *None* no chromatic adaptation is
        performed.
    additional_data
        If *True*, the *XYZ* and *RGB* tristimulus values are also returned.

    Returns
    -------
    :class:`tuple`
        Tuple of *Input Device Transform* (IDT) matrix and white balance
        multipliers or tuple of *Input Device Transform* (IDT) matrix, white
        balance multipliers, *XYZ* and *RGB* tristimulus values.

    References
    ----------
    :cite:`Dyer2017`, :cite:`TheAcademyofMotionPictureArtsandSciences2015c`

    Examples
    --------
    Computing the *Input Device Transform* (IDT) matrix for a
    *CANON EOS 5DMark II* and *CIE Illuminant D Series* *D55* using
    the method given in *RAW to ACES* v1:

    >>> path = os.path.join(
    ...     RESOURCES_DIRECTORY_RAWTOACES,
    ...     'CANON_EOS_5DMark_II_RGB_Sensitivities.csv')
    >>> sensitivities = sds_and_msds_to_msds(
    ...     read_sds_from_csv_file(path).values())
    >>> illuminant = SDS_ILLUMINANTS['D55']
    >>> M, RGB_w = matrix_idt(sensitivities, illuminant)
    >>> np.around(M, 3)
    array([[ 0.85 , -0.016,  0.151],
           [ 0.051,  1.126, -0.185],
           [ 0.02 , -0.194,  1.162]])
    >>> RGB_w  # doctest: +ELLIPSIS
    array([ 2.3414154...,  1.        ,  1.5163375...])

    The *RAW to ACES* v1 matrix for the same camera and optimized by
    `Ceres Solver <http://ceres-solver.org/>`__ is as follows::

        0.864994 -0.026302 0.161308
        0.056527 1.122997 -0.179524
        0.023683 -0.202547 1.178864

    >>> M, RGB_w = matrix_idt(
    ...     sensitivities, illuminant,
    ...     optimisation_factory=optimisation_factory_Jzazbz)
    >>> np.around(M, 3)
    array([[ 0.848, -0.016,  0.158],
           [ 0.053,  1.114, -0.175],
           [ 0.023, -0.225,  1.196]])
    >>> RGB_w  # doctest: +ELLIPSIS
    array([ 2.3414154...,  1.        ,  1.5163375...])
    """

    training_data = optional(training_data, read_training_data_rawtoaces_v1())

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_RAWTOACES)

    shape = cmfs.shape
    if sensitivities.shape != shape:
        runtime_warning(
            f'Aligning "{sensitivities.name}" sensitivities shape to "{shape}".'
        )
        # pylint: disable=E1102
        sensitivities = reshape_msds(sensitivities,
                                     shape)  # type: ignore[assignment]

    if training_data.shape != shape:
        runtime_warning(
            f'Aligning "{training_data.name}" training data shape to "{shape}".'
        )
        # pylint: disable=E1102
        training_data = reshape_msds(training_data, shape)

    illuminant = normalise_illuminant(illuminant, sensitivities)

    RGB, RGB_w = training_data_sds_to_RGB(training_data, sensitivities,
                                          illuminant)

    XYZ = training_data_sds_to_XYZ(training_data, cmfs, illuminant,
                                   chromatic_adaptation_transform)

    (
        objective_function,
        XYZ_to_optimization_colour_model,
    ) = optimisation_factory()
    optimisation_settings = {
        "method": "BFGS",
        "jac": "2-point",
    }
    if optimisation_kwargs is not None:
        optimisation_settings.update(optimisation_kwargs)

    M = minimize(
        objective_function,
        np.ravel(np.identity(3)),
        (RGB, XYZ_to_optimization_colour_model(XYZ)),
        **optimisation_settings,
    ).x.reshape([3, 3])

    if additional_data:
        return M, RGB_w, XYZ, RGB
    else:
        return M, RGB_w
Exemplo n.º 18
0
def matrix_anomalous_trichromacy_Machado2009(
    cmfs: LMS_ConeFundamentals,
    primaries: RGB_DisplayPrimaries,
    d_LMS: ArrayLike,
) -> NDArray:
    """
    Compute the *Machado et al. (2009)* *CVD* matrix for given *LMS* cone
    fundamentals colour matching functions and display primaries tri-spectral
    distributions with given :math:`\\Delta_{LMS}` shift amount in nanometers
    to simulate anomalous trichromacy.

    Parameters
    ----------
    cmfs
        *LMS* cone fundamentals colour matching functions.
    primaries
        *RGB* display primaries tri-spectral distributions.
    d_LMS
        :math:`\\Delta_{LMS}` shift amount in nanometers.

    Notes
    -----
    -   Input *LMS* cone fundamentals colour matching functions interval is
        expected to be 1 nanometer, incompatible input will be interpolated
        at 1 nanometer interval.
    -   Input :math:`\\Delta_{LMS}` shift amount is in domain [0, 20].

    Returns
    -------
    :class:`numpy.ndarray`
        Anomalous trichromacy matrix.

    References
    ----------
    :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`,
    :cite:`Machado2009`

    Examples
    --------
    >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES
    >>> from colour.colorimetry import MSDS_CMFS_LMS
    >>> cmfs = MSDS_CMFS_LMS['Stockman & Sharpe 2 Degree Cone Fundamentals']
    >>> d_LMS = np.array([15, 0, 0])
    >>> primaries = MSDS_DISPLAY_PRIMARIES['Apple Studio Display']
    >>> matrix_anomalous_trichromacy_Machado2009(cmfs, primaries, d_LMS)
    ... # doctest: +ELLIPSIS
    array([[-0.2777465...,  2.6515008..., -1.3737543...],
           [ 0.2718936...,  0.2004786...,  0.5276276...],
           [ 0.0064404...,  0.2592157...,  0.7343437...]])
    """

    if cmfs.shape.interval != 1:
        # pylint: disable=E1102
        cmfs = reshape_msds(
            cmfs,  # type: ignore[assignment]
            SpectralShape(cmfs.shape.start, cmfs.shape.end, 1),
            "Interpolate",
        )

    M_n = matrix_RGB_to_WSYBRG(cmfs, primaries)
    cmfs_a = msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, d_LMS)
    M_a = matrix_RGB_to_WSYBRG(cmfs_a, primaries)

    return matrix_dot(np.linalg.inv(M_n), M_a)
Exemplo n.º 19
0
    def test_matrix_idt(self):
        """
        Test :func:`colour.characterisation.aces_it.matrix_idt`
        definition.
        """

        # The *RAW to ACES* v1 matrix for the same camera and optimized by
        # `Ceres Solver <http://ceres-solver.org/>`__ is as follows:
        #
        # 0.864994 -0.026302 0.161308
        # 0.056527 1.122997 -0.179524
        # 0.023683 -0.202547 1.178864
        np.testing.assert_allclose(
            matrix_idt(MSDS_CANON_EOS_5DMARK_II, SDS_ILLUMINANTS["D55"])[0],
            np.array([
                [0.84993207, -0.01605594, 0.15143504],
                [0.05090392, 1.12559930, -0.18498249],
                [0.02006825, -0.19445149, 1.16206549],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )

        # The *RAW to ACES* v1 matrix for the same camera and optimized by
        # `Ceres Solver <http://ceres-solver.org/>`__ is as follows:
        #
        # 0.888492 -0.077505 0.189014
        # 0.021805 1.066614 -0.088418
        # -0.019718 -0.206664 1.226381
        np.testing.assert_allclose(
            matrix_idt(MSDS_CANON_EOS_5DMARK_II,
                       SD_AMPAS_ISO7589_STUDIO_TUNGSTEN)[0],
            np.array([
                [0.85895300, -0.04381920, 0.15978620],
                [0.01024800, 1.08825364, -0.11392229],
                [-0.02327674, -0.18044292, 1.15903609],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )

        M, RGB_w = matrix_idt(
            MSDS_CANON_EOS_5DMARK_II,
            SDS_ILLUMINANTS["D55"],
            optimisation_factory=optimisation_factory_Jzazbz,
        )
        np.testing.assert_allclose(
            M,
            np.array([
                [0.84841492, -0.01569765, 0.15799332],
                [0.05333075, 1.11428542, -0.17523500],
                [0.02262287, -0.22527728, 1.19646895],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )
        np.testing.assert_allclose(
            RGB_w,
            np.array([2.34141541, 1.00000000, 1.51633759]),
            rtol=0.0001,
            atol=0.0001,
        )

        M, RGB_w = matrix_idt(
            MSDS_CANON_EOS_5DMARK_II,
            SDS_ILLUMINANTS["D55"],
            optimisation_kwargs={"method": "Nelder-Mead"},
        )
        np.testing.assert_allclose(
            M,
            np.array([
                [0.71327381, 0.19213397, 0.11115511],
                [-0.05788252, 1.31165598, -0.21730625],
                [-0.05913103, -0.02787107, 1.10737947],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )
        np.testing.assert_allclose(
            RGB_w,
            np.array([2.34141541, 1.00000000, 1.51633759]),
            rtol=0.0001,
            atol=0.0001,
        )

        training_data = sds_and_msds_to_msds(
            SDS_COLOURCHECKERS["BabelColor Average"].values())

        # pylint: disable=E1102
        np.testing.assert_allclose(
            matrix_idt(
                reshape_msds(
                    MSDS_CAMERA_SENSITIVITIES["Nikon 5100 (NPL)"],
                    SpectralShape(400, 700, 10),
                ),
                SD_AMPAS_ISO7589_STUDIO_TUNGSTEN,
                training_data=training_data,
            )[0],
            np.array([
                [0.74041064, 0.10951105, 0.11963256],
                [-0.00467360, 1.09238438, -0.11398966],
                [0.06728533, -0.29530438, 1.18589793],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )

        np.testing.assert_allclose(
            matrix_idt(
                MSDS_CANON_EOS_5DMARK_II,
                SDS_ILLUMINANTS["D55"],
                chromatic_adaptation_transform="Bradford",
            )[0],
            np.array([
                [0.85020607, -0.01371074, 0.14907913],
                [0.05074081, 1.12898863, -0.18800656],
                [0.02095822, -0.20110079, 1.16769711],
            ]),
            rtol=0.0001,
            atol=0.0001,
        )

        _M, RGB_w, XYZ, RGB = matrix_idt(
            MSDS_CANON_EOS_5DMARK_II,
            SDS_ILLUMINANTS["D55"],
            additional_data=True,
        )

        np.testing.assert_almost_equal(
            RGB_w, np.array([2.34141541, 1.00000000, 1.51633759]))

        np.testing.assert_almost_equal(
            XYZ[:5, ...],
            np.array([
                [0.01743160, 0.01794927, 0.01960625],
                [0.08556139, 0.08957352, 0.09017387],
                [0.74560311, 0.78175547, 0.78350814],
                [0.19005289, 0.19950000, 0.20126062],
                [0.56264334, 0.59145486, 0.58950505],
            ]),
        )

        np.testing.assert_almost_equal(
            RGB[:5, ...],
            np.array([
                [0.02075823, 0.01968577, 0.02139352],
                [0.08957758, 0.08919227, 0.08910910],
                [0.78102307, 0.78019384, 0.77643020],
                [0.19950000, 0.19950000, 0.19950000],
                [0.58984787, 0.59040152, 0.58510766],
            ]),
        )
Exemplo n.º 20
0
def matrix_RGB_to_WSYBRG(cmfs: LMS_ConeFundamentals,
                         primaries: RGB_DisplayPrimaries) -> NDArray:
    """
    Compute the matrix transforming from *RGB* colourspace to opponent-colour
    space using *Machado et al. (2009)* method.

    Parameters
    ----------
    cmfs
        *LMS* cone fundamentals colour matching functions.
    primaries
        *RGB* display primaries tri-spectral distributions.

    Returns
    -------
    :class:`numpy.ndarray`
        Matrix transforming from *RGB* colourspace to opponent-colour space.

    Examples
    --------
    >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES
    >>> from colour.colorimetry import MSDS_CMFS_LMS
    >>> cmfs = MSDS_CMFS_LMS['Stockman & Sharpe 2 Degree Cone Fundamentals']
    >>> d_LMS = np.array([15, 0, 0])
    >>> primaries = MSDS_DISPLAY_PRIMARIES['Apple Studio Display']
    >>> matrix_RGB_to_WSYBRG(  # doctest: +ELLIPSIS
    ...     cmfs, primaries)
    array([[  0.2126535...,   0.6704626...,   0.1168838...],
           [  4.7095295...,  12.4862869..., -16.1958165...],
           [-11.1518474...,  15.2534789...,  -3.1016315...]])
    """

    wavelengths = cmfs.wavelengths
    WSYBRG = vector_dot(MATRIX_LMS_TO_WSYBRG, cmfs.values)
    WS, YB, RG = tsplit(WSYBRG)

    # pylint: disable=E1102
    primaries = reshape_msds(
        primaries,  # type: ignore[assignment]
        cmfs.shape,
        extrapolator_kwargs={
            "method": "Constant",
            "left": 0,
            "right": 0
        },
    )

    R, G, B = tsplit(primaries.values)

    WS_R = np.trapz(R * WS, wavelengths)
    WS_G = np.trapz(G * WS, wavelengths)
    WS_B = np.trapz(B * WS, wavelengths)

    YB_R = np.trapz(R * YB, wavelengths)
    YB_G = np.trapz(G * YB, wavelengths)
    YB_B = np.trapz(B * YB, wavelengths)

    RG_R = np.trapz(R * RG, wavelengths)
    RG_G = np.trapz(G * RG, wavelengths)
    RG_B = np.trapz(B * RG, wavelengths)

    M_G = np.array([
        [WS_R, WS_G, WS_B],
        [YB_R, YB_G, YB_B],
        [RG_R, RG_G, RG_B],
    ])

    PWS = 1 / (WS_R + WS_G + WS_B)
    PYB = 1 / (YB_R + YB_G + YB_B)
    PRG = 1 / (RG_R + RG_G + RG_B)

    M_G *= np.array([PWS, PYB, PRG])[:, np.newaxis]

    return M_G
Exemplo n.º 21
0
def colour_rendering_index(
    sd_test: SpectralDistribution, additional_data: Boolean = False
) -> Union[Floating, ColourRendering_Specification_CRI]:
    """
    Return the *Colour Rendering Index* (CRI) :math:`Q_a` of given spectral
    distribution.

    Parameters
    ----------
    sd_test
        Test spectral distribution.
    additional_data
        Whether to output additional data.

    Returns
    -------
    :class:`numpy.floating` or \
:class:`colour.quality.ColourRendering_Specification_CRI`
        *Colour Rendering Index* (CRI).

    References
    ----------
    :cite:`Ohno2008a`

    Examples
    --------
    >>> from colour import SDS_ILLUMINANTS
    >>> sd = SDS_ILLUMINANTS['FL2']
    >>> colour_rendering_index(sd)  # doctest: +ELLIPSIS
    64.2337241...
    """

    # pylint: disable=E1102
    cmfs = reshape_msds(
        MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],
        SPECTRAL_SHAPE_DEFAULT,
    )

    shape = cmfs.shape
    sd_test = reshape_sd(sd_test, shape)
    tcs_sds = {sd.name: reshape_sd(sd, shape) for sd in SDS_TCS.values()}

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

    uv = UCS_to_uv(XYZ_to_UCS(XYZ))
    CCT, _D_uv = uv_to_CCT_Robertson1968(uv)

    if CCT < 5000:
        sd_reference = sd_blackbody(CCT, shape)
    else:
        xy = CCT_to_xy_CIE_D(CCT)
        sd_reference = sd_CIE_illuminant_D_series(xy)
        sd_reference.align(shape)

    test_tcs_colorimetry_data = tcs_colorimetry_data(
        sd_test, sd_reference, tcs_sds, cmfs, chromatic_adaptation=True
    )

    reference_tcs_colorimetry_data = tcs_colorimetry_data(
        sd_reference, sd_reference, tcs_sds, cmfs
    )

    Q_as = colour_rendering_indexes(
        test_tcs_colorimetry_data, reference_tcs_colorimetry_data
    )

    Q_a = as_float_scalar(
        np.average(
            [v.Q_a for k, v in Q_as.items() if k in (1, 2, 3, 4, 5, 6, 7, 8)]
        )
    )

    if additional_data:
        return ColourRendering_Specification_CRI(
            sd_test.name,
            Q_a,
            Q_as,
            (test_tcs_colorimetry_data, reference_tcs_colorimetry_data),
        )
    else:
        return Q_a
Exemplo n.º 22
0
def colour_fidelity_index_CIE2017(
    sd_test: SpectralDistribution,
    additional_data: Boolean = False
) -> Union[Floating, ColourRendering_Specification_CIE2017]:
    """
    Return the *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` of given
    spectral distribution.

    Parameters
    ----------
    sd_test
        Test spectral distribution.
    additional_data
        Whether to output additional data.

    Returns
    -------
    :class:`numpy.floating` or \
:class:`colour.quality.ColourRendering_Specification_CIE2017`
        *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`.

    References
    ----------
    :cite:`CIETC1-902017`

    Examples
    --------
    >>> from colour.colorimetry import SDS_ILLUMINANTS
    >>> sd = SDS_ILLUMINANTS['FL2']
    >>> colour_fidelity_index_CIE2017(sd)  # doctest: +ELLIPSIS
    70.1208254...
    """

    if sd_test.shape.start > 380 or sd_test.shape.end < 780:
        usage_warning("Test spectral distribution shape does not span the"
                      "recommended 380-780nm range, missing values will be"
                      "filled with zeros!")

        # NOTE: "CIE 2017 Colour Fidelity Index" standard recommends filling
        # missing values with zeros.
        sd_test = cast(SpectralDistribution, sd_test.copy())
        sd_test.extrapolator = Extrapolator
        sd_test.extrapolator_kwargs = {
            "method": "constant",
            "left": 0,
            "right": 0,
        }

    if sd_test.shape.interval > 5:
        raise ValueError("Test spectral distribution interval is greater than"
                         "5nm which is the maximum recommended value "
                         'for computing the "CIE 2017 Colour Fidelity Index"!')

    shape = SpectralShape(
        SPECTRAL_SHAPE_CIE2017.start,
        SPECTRAL_SHAPE_CIE2017.end,
        sd_test.shape.interval,
    )

    CCT, D_uv = tsplit(CCT_reference_illuminant(sd_test))
    sd_reference = sd_reference_illuminant(CCT, shape)

    # NOTE: All computations except CCT calculation use the
    # "CIE 1964 10 Degree Standard Observer".
    # pylint: disable=E1102
    cmfs_10 = reshape_msds(MSDS_CMFS["CIE 1964 10 Degree Standard Observer"],
                           shape)

    # pylint: disable=E1102
    sds_tcs = reshape_msds(load_TCS_CIE2017(shape), shape)

    test_tcs_colorimetry_data = tcs_colorimetry_data(sd_test, sds_tcs, cmfs_10)
    reference_tcs_colorimetry_data = tcs_colorimetry_data(
        sd_reference, sds_tcs, cmfs_10)

    delta_E_s = np.empty(len(sds_tcs.labels))
    for i, _delta_E in enumerate(delta_E_s):
        delta_E_s[i] = euclidean_distance(
            test_tcs_colorimetry_data[i].Jpapbp,
            reference_tcs_colorimetry_data[i].Jpapbp,
        )

    R_s = as_float_array(delta_E_to_R_f(delta_E_s))
    R_f = as_float_scalar(delta_E_to_R_f(np.average(delta_E_s)))

    if additional_data:
        return ColourRendering_Specification_CIE2017(
            sd_test.name,
            sd_reference,
            R_f,
            R_s,
            CCT,
            D_uv,
            (test_tcs_colorimetry_data, reference_tcs_colorimetry_data),
            delta_E_s,
        )
    else:
        return R_f