def test_sd_to_XYZ_ASTME308_mi_10nm(self): """ Test :func:`colour.colorimetry.tristimulus_values.sd_to_XYZ_ASTME308` definition for 10 nm measurement intervals. """ np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 10)), self._cmfs, self._A, ), np.array([14.47779980, 10.86358645, 2.04751388]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 10)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.47773312, 10.86353641, 2.04750445]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, ), np.array([14.54137532, 10.88641727, 2.04931318]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.54167211, 10.88649849, 2.04930374]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 10)), self._cmfs, self._A, k=1, ), np.array([1568.94283333, 1174.59084705, 221.11080639]), 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 test_sd_to_XYZ_ASTME308_mi_1nm(self): """ Test :func:`colour.colorimetry.tristimulus_values.sd_to_XYZ_ASTME308` definition for 1 nm measurement intervals. """ np.testing.assert_almost_equal( sd_to_XYZ_ASTME308(reshape_sd(self._sd, self._cmfs.shape), self._cmfs, self._A), np.array([14.46372680, 10.85832950, 2.04663200]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, self._cmfs.shape), self._cmfs, self._A, use_practice_range=False, ), np.array([14.46366018, 10.85827949, 2.04662258]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, ), np.array([14.54173397, 10.88628632, 2.04965822]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.54203076, 10.88636754, 2.04964877]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 1)), self._cmfs, self._A, k=1, ), np.array([1568.98152997, 1174.57671769, 221.14803420]), decimal=7, )
def test_raise_exception_colour_fidelity_index_CFI2017(self): """ Test :func:`colour.quality.CIE2017.colour_fidelity_index_CFI2017` definition raised exception. """ sd = reshape_sd(SDS_ILLUMINANTS["FL2"], SpectralShape(400, 700, 5)) self.assertWarns(ColourUsageWarning, colour_fidelity_index_CIE2017, sd) sd = reshape_sd(SDS_ILLUMINANTS["FL2"], SpectralShape(380, 780, 10)) self.assertRaises(ValueError, colour_fidelity_index_CIE2017, sd)
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_optimise(self): """Test :class:`colour.recovery.otsu2018.Tree_Otsu2018.optimise` method.""" node_tree = Tree_Otsu2018(self._reflectances, self._cmfs, self._sd_D65) node_tree.optimise(iterations=5) dataset = node_tree.to_dataset() dataset.write(self._path) dataset = Dataset_Otsu2018() dataset.read(self._path) for sd in SDS_COLOURCHECKERS["ColorChecker N Ohta"].values(): XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 Lab = XYZ_to_Lab(XYZ, self._xy_D65) recovered_sd = XYZ_to_sd_Otsu2018( XYZ, self._cmfs, self._sd_D65, dataset, False ) recovered_XYZ = ( sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 ) recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) error = metric_mse( reshape_sd(sd, SPECTRAL_SHAPE_OTSU2018).values, recovered_sd.values, ) self.assertLess(error, 0.075) delta_E = delta_E_CIE1976(Lab, recovered_Lab) self.assertLess(delta_E, 1e-12)
def luminous_flux( sd: SpectralDistribution, lef: Optional[SpectralDistribution] = None, K_m: Floating = CONSTANT_K_M, ) -> Floating: """ Return the *luminous flux* for given spectral distribution using given luminous efficiency function. Parameters ---------- sd test spectral distribution lef :math:`V(\\lambda)` luminous efficiency function, default to the *CIE 1924 Photopic Standard Observer*. K_m :math:`lm\\cdot W^{-1}` maximum photopic luminous efficiency. Returns ------- :class:`numpy.floating` Luminous flux. References ---------- :cite:`Wikipedia2003b` Examples -------- >>> from colour import SDS_LIGHT_SOURCES >>> sd = SDS_LIGHT_SOURCES['Neodimium Incandescent'] >>> luminous_flux(sd) # doctest: +ELLIPSIS 23807.6555273... """ lef = cast( SpectralDistribution, optional(lef, SDS_LEFS_PHOTOPIC["CIE 1924 Photopic Standard Observer"]), ) lef = reshape_sd( lef, sd.shape, extrapolator_kwargs={ "method": "Constant", "left": 0, "right": 0 }, ) flux = K_m * np.trapz(lef.values * sd.values, sd.wavelengths) return as_float_scalar(flux)
def test_raise_exception_sd_to_XYZ_ASTME308(self): """ Test :func:`colour.colorimetry.tristimulus_values.sd_to_XYZ_ASTME308` definition raised exception. """ self.assertRaises( ValueError, sd_to_XYZ_ASTME308, reshape_sd(self._sd, SpectralShape(360, 820, 2)), )
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_plot_colour_quality_bars(self): """ Test :func:`colour.plotting.quality.plot_colour_quality_bars` definition. """ illuminant = SDS_ILLUMINANTS["FL2"] light_source = SDS_LIGHT_SOURCES["Kinoton 75P"] light_source = reshape_sd(light_source, SpectralShape(360, 830, 1)) cqs_i = colour_quality_scale(illuminant, additional_data=True) cqs_l = colour_quality_scale(light_source, additional_data=True) figure, axes = plot_colour_quality_bars([cqs_i, cqs_l]) self.assertIsInstance(figure, Figure) self.assertIsInstance(axes, Axes)
def normalise_illuminant( illuminant: SpectralDistribution, sensitivities: RGB_CameraSensitivities) -> SpectralDistribution: """ Normalise given illuminant with given camera *RGB* spectral sensitivities. The multiplicative inverse scaling factor :math:`k` is computed by multiplying the illuminant by the sensitivies channel with the maximum value. Parameters ---------- illuminant Illuminant spectral distribution. sensitivities Camera *RGB* spectral sensitivities. Returns ------- :class:`colour.SpectralDistribution` Normalised illuminant. 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 = SDS_ILLUMINANTS['D55'] >>> np.sum(illuminant.values) # doctest: +ELLIPSIS 7276.1490000... >>> np.sum(normalise_illuminant(illuminant, sensitivities).values) ... # doctest: +ELLIPSIS 3.4390373... """ shape = sensitivities.shape if illuminant.shape != shape: runtime_warning( f'Aligning "{illuminant.name}" illuminant shape to "{shape}".') illuminant = reshape_sd(illuminant, shape) c_i = np.argmax(np.max(sensitivities.values, axis=0)) k = 1 / np.sum(illuminant.values * sensitivities.values[..., c_i]) return illuminant * k
def white_balance_multipliers(sensitivities: RGB_CameraSensitivities, illuminant: SpectralDistribution) -> NDArray: """ Compute the *RGB* white balance multipliers for given camera *RGB* spectral sensitivities and illuminant. Parameters ---------- sensitivities Camera *RGB* spectral sensitivities. illuminant Illuminant spectral distribution. Returns ------- :class:`numpy.ndarray` *RGB* white balance multipliers. References ---------- :cite:`Dyer2017` 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 = SDS_ILLUMINANTS['D55'] >>> white_balance_multipliers(sensitivities, illuminant) ... # 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) RGB_w = 1 / np.sum( sensitivities.values * illuminant.values[..., np.newaxis], axis=0) RGB_w *= 1 / np.min(RGB_w) return RGB_w
def test_XYZ_to_sd_Otsu2018(self): """Test :func:`colour.recovery.otsu2018.XYZ_to_sd_Otsu2018` definition.""" # Tests the round-trip with values of a colour checker. for _name, sd in SDS_COLOURCHECKERS["ColorChecker N Ohta"].items(): XYZ = sd_to_XYZ(sd, self._cmfs, self._sd_D65) / 100 Lab = XYZ_to_Lab(XYZ, self._xy_D65) recovered_sd = XYZ_to_sd_Otsu2018( XYZ, self._cmfs, self._sd_D65, clip=False ) recovered_XYZ = ( sd_to_XYZ(recovered_sd, self._cmfs, self._sd_D65) / 100 ) recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65) error = metric_mse( reshape_sd(sd, SPECTRAL_SHAPE_OTSU2018).values, recovered_sd.values, ) self.assertLess(error, 0.02) delta_E = delta_E_CIE1976(Lab, recovered_Lab) self.assertLess(delta_E, 1e-12)
def spectral_similarity_index( sd_test: SpectralDistribution, sd_reference: SpectralDistribution ) -> NDArray: """ Return the *Academy Spectral Similarity Index* (SSI) of given test spectral distribution with given reference spectral distribution. Parameters ---------- sd_test Test spectral distribution. sd_reference Reference spectral distribution. Returns ------- :class:`numpy.ndarray` *Academy Spectral Similarity Index* (SSI). References ---------- :cite:`TheAcademyofMotionPictureArtsandSciences2019` Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd_test = SDS_ILLUMINANTS['C'] >>> sd_reference = SDS_ILLUMINANTS['D65'] >>> spectral_similarity_index(sd_test, sd_reference) 94.0 """ global _MATRIX_INTEGRATION if _MATRIX_INTEGRATION is None: _MATRIX_INTEGRATION = zeros( ( len(_SPECTRAL_SHAPE_SSI_LARGE.range()), len(SPECTRAL_SHAPE_SSI.range()), ) ) weights = np.array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5]) for i in range(_MATRIX_INTEGRATION.shape[0]): _MATRIX_INTEGRATION[i, (10 * i) : (10 * i + 11)] = weights settings = { "interpolator": LinearInterpolator, "extrapolator_kwargs": {"left": 0, "right": 0}, } sd_test = reshape_sd(sd_test, SPECTRAL_SHAPE_SSI, "Align", **settings) sd_reference = reshape_sd( sd_reference, SPECTRAL_SHAPE_SSI, "Align", **settings ) test_i = np.dot(_MATRIX_INTEGRATION, sd_test.values) reference_i = np.dot(_MATRIX_INTEGRATION, sd_reference.values) test_i /= np.sum(test_i) reference_i /= np.sum(reference_i) d_i = test_i - reference_i dr_i = d_i / (reference_i + np.mean(reference_i)) wdr_i = dr_i * [ 12 / 45, 22 / 45, 32 / 45, 40 / 45, 44 / 45, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11 / 15, 3 / 15, ] c_wdr_i = convolve1d(np.hstack([0, wdr_i, 0]), [0.22, 0.56, 0.22]) m_v = np.sum(c_wdr_i**2) SSI = np.around(100 - 32 * np.sqrt(m_v)) return SSI
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 test_sd_to_XYZ_ASTME308_mi_5nm(self): """ Test :func:`colour.colorimetry.tristimulus_values.sd_to_XYZ_ASTME308` definition for 5 nm measurement intervals. """ np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, ), np.array([14.46372173, 10.85832502, 2.04664734]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.46366388, 10.85828159, 2.04663915]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, mi_5nm_omission_method=False, ), np.array([14.46373399, 10.85833553, 2.0466465]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, ), np.array([14.54025742, 10.88576251, 2.04950226]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.54051517, 10.88583304, 2.04949406]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, mi_5nm_omission_method=False, ), np.array([14.54022093, 10.88575468, 2.04951057]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 830, 5)), self._cmfs, self._A, use_practice_range=False, mi_5nm_omission_method=False, ), np.array([14.46366737, 10.85828552, 2.04663707]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, use_practice_range=False, mi_5nm_omission_method=False, ), np.array([14.54051772, 10.88583590, 2.04950113]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 5)), self._cmfs, self._A, k=1, ), np.array([1568.82479013, 1174.52212708, 221.13156963]), decimal=7, )
def RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs( wavelength: FloatingOrArrayLike, ) -> NDArray: """ Convert *Wright & Guild 1931 2 Degree RGB CMFs* colour matching functions into the *CIE 1931 2 Degree Standard Observer* colour matching functions. Parameters ---------- wavelength Wavelength :math:`\\lambda` in nm. Returns ------- :class:`numpy.ndarray` *CIE 1931 2 Degree Standard Observer* spectral tristimulus values. Notes ----- - Data for the *CIE 1931 2 Degree Standard Observer* already exists, this definition is intended for educational purpose. References ---------- :cite:`Wyszecki2000bg` Examples -------- >>> from colour.utilities import numpy_print_options >>> with numpy_print_options(suppress=True): ... RGB_2_degree_cmfs_to_XYZ_2_degree_cmfs(700) # doctest: +ELLIPSIS array([ 0.0113577..., 0.004102 , 0. ]) """ cmfs = MSDS_CMFS_RGB["Wright & Guild 1931 2 Degree RGB CMFs"] rgb_bar = cmfs[wavelength] rgb = rgb_bar / np.sum(rgb_bar) M1 = np.array([ [0.49000, 0.31000, 0.20000], [0.17697, 0.81240, 0.01063], [0.00000, 0.01000, 0.99000], ]) M2 = np.array([ [0.66697, 1.13240, 1.20063], [0.66697, 1.13240, 1.20063], [0.66697, 1.13240, 1.20063], ]) xyz = vector_dot(M1, rgb) xyz /= vector_dot(M2, rgb) x, y, z = xyz[..., 0], xyz[..., 1], xyz[..., 2] V = reshape_sd(SDS_LEFS_PHOTOPIC["CIE 1924 Photopic Standard Observer"], cmfs.shape) L = V[wavelength] x_bar = x / y * L y_bar = L z_bar = z / y * L xyz_bar = tstack([x_bar, y_bar, z_bar]) return xyz_bar
def test_sd_to_XYZ_tristimulus_weighting_factors_ASTME308(self): """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_tristimulus_weighting_factors_ASTME308` definition. """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"]), np.array([14.46341867, 10.85820227, 2.04697034]), decimal=7, ) cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["C"]), np.array([10.77005571, 9.44877491, 6.62428210]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"]), np.array([11.57542759, 9.98605604, 3.95273304]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 5), "Trim"), cmfs, SDS_ILLUMINANTS["A"], ), np.array([14.38153638, 10.74503131, 2.01613844]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 10), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], ), np.array([14.38257202, 10.74568178, 2.01588427]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 20), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], ), np.array([14.38329645, 10.74603515, 2.01561113]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_tristimulus_weighting_factors_ASTME308( reshape_sd(SD_SAMPLE, SpectralShape(400, 700, 20), "Interpolate"), cmfs, SDS_ILLUMINANTS["A"], k=1, ), np.array([1636.74881983, 1222.84626486, 229.36669308]), decimal=7, )
def test_tristimulus_weighting_factors_ASTME2022(self): """ Test :func:`colour.colorimetry.tristimulus_values.\ tristimulus_weighting_factors_ASTME2022` definition. Notes ----- - :attr:`TWF_A_CIE_1964_10_10`, :attr:`TWF_A_CIE_1964_10_20` and :attr:`TWF_D65_CIE_1931_2_20` attributes data is matching :cite:`ASTMInternational2015b`. References ---------- :cite:`ASTMInternational2015b` """ cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] A = sd_CIE_standard_illuminant_A(cmfs.shape) twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10)) np.testing.assert_almost_equal(np.round(twf, 3), TWF_A_CIE_1964_10_10, decimal=3) twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 20)) np.testing.assert_almost_equal(np.round(twf, 3), TWF_A_CIE_1964_10_20, decimal=3) cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] D65 = reshape_sd(SDS_ILLUMINANTS["D65"], cmfs.shape, interpolator=LinearInterpolator) twf = tristimulus_weighting_factors_ASTME2022( cmfs, D65, SpectralShape(360, 830, 20)) np.testing.assert_almost_equal(np.round(twf, 3), TWF_D65_CIE_1931_2_20, decimal=3) twf = tristimulus_weighting_factors_ASTME2022(cmfs, D65, SpectralShape( 360, 830, 20), k=1) np.testing.assert_almost_equal(twf, TWF_D65_CIE_1931_2_20_K1, decimal=7) # Testing that the cache returns a copy of the data. cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] twf = tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10)) np.testing.assert_almost_equal(np.round(twf, 3), TWF_A_CIE_1964_10_10, decimal=3) np.testing.assert_almost_equal( np.round( tristimulus_weighting_factors_ASTME2022( cmfs, A, SpectralShape(360, 830, 10)), 3, ), TWF_A_CIE_1964_10_10, decimal=3, )
def sd_to_aces_relative_exposure_values( sd: SpectralDistribution, illuminant: Optional[SpectralDistribution] = None, apply_chromatic_adaptation: Boolean = False, 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 spectral distribution to *ACES2065-1* colourspace relative exposure values. Parameters ---------- sd Spectral distribution. illuminant *Illuminant* spectral distribution, default to *CIE Standard Illuminant D65*. apply_chromatic_adaptation Whether to apply chromatic adaptation using given transform. chromatic_adaptation_transform *Chromatic adaptation* transform. Returns ------- :class:`numpy.ndarray` *ACES2065-1* colourspace relative exposure values array. Notes ----- +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``XYZ`` | [0, 100] | [0, 1] | +------------+-----------------------+---------------+ - The chromatic adaptation method implemented here is a bit unusual as it involves building a new colourspace based on *ACES2065-1* colourspace primaries but using the whitepoint of the illuminant that the spectral distribution was measured under. References ---------- :cite:`Forsythe2018`, :cite:`TheAcademyofMotionPictureArtsandSciences2014q`, :cite:`TheAcademyofMotionPictureArtsandSciences2014r`, :cite:`TheAcademyofMotionPictureArtsandSciencese` Examples -------- >>> from colour import SDS_COLOURCHECKERS >>> sd = SDS_COLOURCHECKERS['ColorChecker N Ohta']['dark skin'] >>> sd_to_aces_relative_exposure_values(sd) # doctest: +ELLIPSIS array([ 0.1171814..., 0.0866360..., 0.0589726...]) >>> sd_to_aces_relative_exposure_values(sd, ... apply_chromatic_adaptation=True) # doctest: +ELLIPSIS array([ 0.1180779..., 0.0869031..., 0.0589125...]) """ illuminant = cast(SpectralDistribution, optional(illuminant, SDS_ILLUMINANTS["D65"])) shape = MSDS_ACES_RICD.shape if sd.shape != MSDS_ACES_RICD.shape: sd = reshape_sd(sd, shape) if illuminant.shape != MSDS_ACES_RICD.shape: illuminant = reshape_sd(illuminant, shape) s_v = sd.values i_v = illuminant.values r_bar, g_bar, b_bar = tsplit(MSDS_ACES_RICD.values) def k(x: NDArray, y: NDArray) -> NDArray: """Compute the :math:`K_r`, :math:`K_g` or :math:`K_b` scale factors.""" return 1 / np.sum(x * y) k_r = k(i_v, r_bar) k_g = k(i_v, g_bar) k_b = k(i_v, b_bar) E_r = k_r * np.sum(i_v * s_v * r_bar) E_g = k_g * np.sum(i_v * s_v * g_bar) E_b = k_b * np.sum(i_v * s_v * b_bar) E_rgb = np.array([E_r, E_g, E_b]) # Accounting for flare. E_rgb += FLARE_PERCENTAGE E_rgb *= S_FLARE_FACTOR if apply_chromatic_adaptation: xy = XYZ_to_xy(sd_to_XYZ(illuminant) / 100) NPM = normalised_primary_matrix(RGB_COLOURSPACE_ACES2065_1.primaries, xy) XYZ = RGB_to_XYZ( E_rgb, xy, RGB_COLOURSPACE_ACES2065_1.whitepoint, NPM, chromatic_adaptation_transform, ) E_rgb = XYZ_to_RGB( XYZ, RGB_COLOURSPACE_ACES2065_1.whitepoint, RGB_COLOURSPACE_ACES2065_1.whitepoint, RGB_COLOURSPACE_ACES2065_1.matrix_XYZ_to_RGB, ) return from_range_1(E_rgb)
def setUp(self): """Initialise the common tests attributes.""" self._cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] self._A = sd_CIE_standard_illuminant_A(self._cmfs.shape) self._sd = reshape_sd(SD_SAMPLE, self._cmfs.shape)
def test_sd_to_XYZ_ASTME308_mi_20nm(self): """ Test :func:`colour.colorimetry.tristimulus_values.sd_to_XYZ_ASTME308` definition for 20 nm measurement intervals. """ np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, ), np.array([14.50187464, 10.87217124, 2.04918305]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.50180785, 10.87212116, 2.04917361]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, mi_20nm_interpolation_method=False, ), np.array([14.50216194, 10.87236873, 2.04977256]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, ), np.array([14.54114025, 10.88634755, 2.04916445]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, use_practice_range=False, ), np.array([14.54143704, 10.88642877, 2.04915501]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, mi_20nm_interpolation_method=False, ), np.array([14.54242562, 10.88694088, 2.04919645]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(360, 820, 20)), self._cmfs, self._A, use_practice_range=False, mi_20nm_interpolation_method=False, ), np.array([14.50209515, 10.87231865, 2.04976312]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, use_practice_range=False, mi_20nm_interpolation_method=False, ), np.array([14.54272240, 10.88702210, 2.04918701]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_ASTME308( reshape_sd(self._sd, SpectralShape(400, 700, 20)), self._cmfs, self._A, k=1, ), np.array([1568.91747040, 1174.58332427, 221.09475945]), decimal=7, )
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 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 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