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)
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, )
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
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, )
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)
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" ]
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, )
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, )
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, )
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 )
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, )
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"))
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
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)
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
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
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
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)
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], ]), )
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
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
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