def colour_quality_scale(spd_test, T, additional_data=False): cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape CCT, _D_uv = T if CCT < 5000: spd_reference = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_CIE_D(CCT) spd_reference = D_illuminant_relative_spd(xy) spd_reference.align(shape) test_vs_colorimetry_data = vs_colorimetry_data(spd_test, spd_reference, VS_SPDS, cmfs, chromatic_adaptation=True) reference_vs_colorimetry_data = vs_colorimetry_data( spd_reference, spd_reference, VS_SPDS, cmfs) XYZ_r = spectral_to_XYZ(spd_reference, cmfs) XYZ_r /= XYZ_r[1] CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r) Q_as = colour_quality_scales(test_vs_colorimetry_data, reference_vs_colorimetry_data, 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) Q_f = scale_conversion(D_E_RMS, CCT_f, 2.928) 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 = 100 - 3.6 * (D_Ep_RMS - p_delta_C) 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 / D65_GAMUT_AREA * 100 Q_d = G_t / G_r * CCT_f * 100 if additional_data: return CQS_Specification( spd_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 test_planckian_table(self): """ Tests :func:`colour.temperature.cct.planckian_table` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal( [(x.Ti, x.ui, x.vi, x.di) for x in planckian_table( np.array([0.1978, 0.3122]), cmfs, 1000, 1010, 10)], PLANCKIAN_TABLE)
def test_planckian_table_minimal_distance_index(self): """ Tests :func:`colour.temperature.cct.planckian_table_minimal_distance_index` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') self.assertEqual( planckian_table_minimal_distance_index( planckian_table((0.1978, 0.3122), cmfs, 1000, 1010, 10)), 9)
def test_planckian_table(self): """ Tests :func:`colour.temperature.cct.planckian_table` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') to_tuple = lambda x: (x.Ti, x.ui, x.vi, x.di) np.testing.assert_almost_equal([ to_tuple(x) for x in planckian_table((0.1978, 0.3122), cmfs, 1000, 1010, 10) ], [to_tuple(x) for x in PLANCKIAN_TABLE])
def test_planckian_table_minimal_distance_index(self): """ Tests :func:`colour.temperature.cct.\ planckian_table_minimal_distance_index` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') self.assertEqual( planckian_table_minimal_distance_index( planckian_table( np.array([0.1978, 0.3122]), cmfs, 1000, 1010, 10)), 9)
def test_CCT_to_uv_ohno2013(self): """ Tests :func:`colour.temperature.cct.CCT_to_uv_ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal(CCT_to_uv_ohno2013( 6507.4342201047066, 0.003223690901512735, cmfs), (0.19780034881616862, 0.31220050291046603), decimal=7) np.testing.assert_almost_equal(CCT_to_uv_ohno2013( 1041.849524611546, -0.067377582728534946, cmfs), (0.43280250331413772, 0.28829975758516474), decimal=7) np.testing.assert_almost_equal(CCT_to_uv_ohno2013( 2448.9489053326438, -0.084324704634692743, cmfs), (0.29256616302348853, 0.27221773141874955), decimal=7)
def test_uv_to_CCT_Ohno2013(self): """ Tests :func:`colour.temperature.cct.uv_to_CCT_Ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.1978, 0.3122]), cmfs), np.array([6507.5470349001507, 0.0032236908012382953]), decimal=7) np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.4328, 0.2883]), cmfs), np.array([1041.8672179878763, -0.067377582642145384]), decimal=7) np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.2927, 0.2722]), cmfs, iterations=4), np.array([2452.1932942782669, -0.084369982045528508]), decimal=7)
def test_uv_to_CCT_ohno2013(self): """ Tests :func:`colour.temperature.cct.uv_to_CCT_ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal(uv_to_CCT_ohno2013( (0.1978, 0.3122), cmfs), (6507.5470349001507, 0.0032236908012382953), decimal=7) np.testing.assert_almost_equal(uv_to_CCT_ohno2013( (0.4328, 0.2883), cmfs), (1041.8672179878763, -0.067377582642145384), decimal=7) np.testing.assert_almost_equal(uv_to_CCT_ohno2013( (0.2927, 0.2722), cmfs, iterations=4), (2452.1932942782669, -0.084369982045528508), decimal=7)
def test_uv_to_CCT_Ohno2013(self): """ Tests :func:`colour.temperature.cct.uv_to_CCT_Ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.1978, 0.3122]), cmfs), np.array([6507.51282029, 0.00322336]), decimal=7) np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.4328, 0.2883]), cmfs), np.array([1041.68315360, -0.06737802]), decimal=7) np.testing.assert_almost_equal( uv_to_CCT_Ohno2013(np.array([0.2927, 0.2722]), cmfs, iterations=4), np.array([2452.15316417, -0.08437064]), decimal=7)
def is_within_visible_spectrum(XYZ, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), tolerance=None): """ Returns if given *CIE XYZ* tristimulus values are within visible spectrum volume / given colour matching functions volume. Parameters ---------- XYZ : array_like *CIE XYZ* tristimulus values. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. tolerance : numeric, optional Tolerance allowed in the inside-triangle check. Returns ------- bool Is within visible spectrum. Notes ----- - Input *CIE XYZ* tristimulus values are in domain [0, 1]. - This definition requires *scipy* to be installed. Examples -------- >>> import numpy as np >>> is_within_visible_spectrum(np.array([0.3205, 0.4131, 0.51])) array(True, dtype=bool) >>> a = np.array([[0.3205, 0.4131, 0.51], ... [-0.0005, 0.0031, 0.001]]) >>> is_within_visible_spectrum(a) array([ True, False], dtype=bool) """ return is_within_mesh_volume(XYZ, cmfs.values, tolerance)
def test_CCT_to_uv_ohno2013(self): """ Tests :func:`colour.temperature.cct.CCT_to_uv_ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal( CCT_to_uv_ohno2013( 6507.4342201047066, 0.003223690901512735, cmfs), (0.19780034881616862, 0.31220050291046603), decimal=7) np.testing.assert_almost_equal( CCT_to_uv_ohno2013( 1041.849524611546, -0.067377582728534946, cmfs), (0.43280250331413772, 0.28829975758516474), decimal=7) np.testing.assert_almost_equal( CCT_to_uv_ohno2013( 2448.9489053326438, -0.084324704634692743, cmfs), (0.29256616302348853, 0.27221773141874955), decimal=7)
def test_CCT_to_uv_Ohno2013(self): """ Tests :func:`colour.temperature.cct.CCT_to_uv_Ohno2013` definition. """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') np.testing.assert_almost_equal( CCT_to_uv_Ohno2013( 6507.43422010, 0.003223690901513, cmfs), np.array([0.19779990, 0.31220046]), decimal=7) np.testing.assert_almost_equal( CCT_to_uv_Ohno2013( 1041.84952461, -0.067377582728535, cmfs), np.array([0.43276248, 0.28830361]), decimal=7) np.testing.assert_almost_equal( CCT_to_uv_Ohno2013( 2448.94890533, -0.084324704634693, cmfs), np.array([0.29256477, 0.2722181]), decimal=7)
def wavelength_to_XYZ(wavelength, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer')): """ Converts given wavelength :math:`\lambda` to *CIE XYZ* colourspace using given colour matching functions. If the wavelength :math:`\lambda` is not available in the colour matching function, its value will be calculated using *CIE* recommendations: The method developed by *Sprague (1880)* should be used for interpolating functions having a uniformly spaced independent variable and a *Cubic Spline* method for non-uniformly spaced independent variable. Parameters ---------- wavelength : numeric Wavelength :math:`\lambda` in nm. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- ndarray, (3,) *CIE XYZ* colourspace matrix. Raises ------ ValueError If wavelength :math:`\lambda` is not in the colour matching functions domain. Notes ----- - Output *CIE XYZ* colourspace matrix is in domain [0, 1]. - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. Examples -------- >>> from colour import CMFS >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> wavelength_to_XYZ(480) # doctest: +ELLIPSIS array([ 0.09564 , 0.13902 , 0.812950...]) """ shape = cmfs.shape if wavelength < shape.start or wavelength > shape.end: raise ValueError( '"{0} nm" wavelength is not in "[{1}, {2}]" domain!'.format( wavelength, shape.start, shape.end)) if wavelength not in cmfs: wavelengths, values, = cmfs.wavelengths, cmfs.values interpolator = (SpragueInterpolator if cmfs.is_uniform() else SplineInterpolator) interpolators = [interpolator(wavelengths, values[:, i]) for i in range(values.shape[-1])] return np.array([interpolator(wavelength) for interpolator in interpolators]) else: return np.array(cmfs.get(wavelength))
def uv_to_CCT_Ohno2013(uv, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), start=CCT_MINIMAL, end=CCT_MAXIMAL, count=CCT_SAMPLES, iterations=CCT_CALCULATION_ITERATIONS): """ Returns the correlated colour temperature :math:`T_{cp}` and :math:`\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity coordinates, colour matching functions and temperature range using Ohno (2013) method. The iterations parameter defines the calculations precision: The higher its value, the more planckian tables will be generated through cascade expansion in order to converge to the exact solution. Parameters ---------- uv : array_like *CIE UCS* colourspace *uv* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. start : numeric, optional Temperature range start in kelvins. end : numeric, optional Temperature range end in kelvins. count : int, optional Temperatures count in the planckian tables. iterations : int, optional Number of planckian tables to generate. Returns ------- ndarray Correlated colour temperature :math:`T_{cp}`, :math:`\Delta_{uv}`. References ---------- .. [3] Ohno, Y. (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47–55. doi:10.1080/15502724.2014.839020 Examples -------- >>> from colour import STANDARD_OBSERVERS_CMFS >>> cmfs = 'CIE 1931 2 Degree Standard Observer' >>> cmfs = STANDARD_OBSERVERS_CMFS.get(cmfs) >>> uv = np.array([0.1978, 0.3122]) >>> uv_to_CCT_Ohno2013(uv, cmfs) # doctest: +ELLIPSIS array([ 6.5075470...e+03, 3.2236908...e-03]) """ # Ensuring we do at least one iteration to initialise variables. if iterations <= 0: iterations = 1 # Planckian table creation through cascade expansion. for _i in range(iterations): table = planckian_table(uv, cmfs, start, end, count) index = planckian_table_minimal_distance_index(table) if index == 0: warning( ('Minimal distance index is on lowest planckian table bound, ' 'unpredictable results may occur!')) index += 1 elif index == len(table) - 1: warning( ('Minimal distance index is on highest planckian table bound, ' 'unpredictable results may occur!')) index -= 1 start = table[index - 1].Ti end = table[index + 1].Ti _ux, vx = uv Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1]) Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di Ti, di = Tuvdi.Ti, Tuvdi.di Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di # Triangular solution. l = np.sqrt((uin - uip)**2 + (vin - vip)**2) x = (dip**2 - din**2 + l**2) / (2 * l) T = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = 1 if vx - vtx >= 0 else -1 D_uv = (dip**2 - x**2)**(1 / 2) * sign # Parabolic solution. if D_uv < 0.002: X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1 b = (-(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 * (di - dip)) * X**-1) c = (-(dip * (Tin - Ti) * Ti * Tin + di * (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X**-1) T = -b / (2 * a) D_uv = sign * (a * T**2 + b * T + c) return np.array([T, D_uv])
def wavelength_to_XYZ(wavelength, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), method=None): """ Converts given wavelength :math:`\lambda` to *CIE XYZ* tristimulus values using given colour matching functions. If the wavelength :math:`\lambda` is not available in the colour matching function, its value will be calculated using *CIE* recommendations: The method developed by Sprague (1880) should be used for interpolating functions having a uniformly spaced independent variable and a *Cubic Spline* method for non-uniformly spaced independent variable. Parameters ---------- wavelength : numeric or array_like Wavelength :math:`\lambda` in nm. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. method : unicode, optional {None, 'Cubic Spline', 'Linear', 'Pchip', 'Sprague'}, Enforce given interpolation method. Returns ------- ndarray *CIE XYZ* tristimulus values. Raises ------ RuntimeError If Sprague (1880) interpolation method is forced with a non-uniformly spaced independent variable. ValueError If the interpolation method is not defined or if wavelength :math:`\lambda` is not contained in the colour matching functions domain. Notes ----- - Output *CIE XYZ* tristimulus values are in domain [0, 1]. - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. - Sprague (1880) interpolator cannot be used for interpolating functions having a non-uniformly spaced independent variable. Warning ------- - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. - *Cubic Spline* interpolator requires at least 3 wavelengths :math:`\lambda_n` for interpolation. - *Linear* interpolator requires at least 2 wavelengths :math:`\lambda_n` for interpolation. - *Pchip* interpolator requires at least 2 wavelengths :math:`\lambda_n` for interpolation. - Sprague (1880) interpolator requires at least 6 wavelengths :math:`\lambda_n` for interpolation. Examples -------- Uniform data is using Sprague (1880) interpolation by default: >>> from colour import CMFS >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> wavelength_to_XYZ(480, cmfs) # doctest: +ELLIPSIS array([ 0.09564 , 0.13902 , 0.812950...]) >>> wavelength_to_XYZ(480.5, cmfs) # doctest: +ELLIPSIS array([ 0.0914287..., 0.1418350..., 0.7915726...]) Enforcing *Cubic Spline* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Cubic Spline') # doctest: +ELLIPSIS array([ 0.0914288..., 0.1418351..., 0.7915729...]) Enforcing *Linear* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Linear') # doctest: +ELLIPSIS array([ 0.0914697..., 0.1418482..., 0.7917337...]) Enforcing *Pchip* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Pchip') # doctest: +ELLIPSIS array([ 0.0914280..., 0.1418341..., 0.7915711...]) """ cmfs_shape = cmfs.shape if (np.min(wavelength) < cmfs_shape.start or np.max(wavelength) > cmfs_shape.end): raise ValueError( '"{0} nm" wavelength is not in "[{1}, {2}]" domain!'.format( wavelength, cmfs_shape.start, cmfs_shape.end)) if wavelength not in cmfs: wavelengths, values, = cmfs.wavelengths, cmfs.values if is_string(method): method = method.lower() is_uniform = cmfs.is_uniform() if method is None: if is_uniform: interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator elif method == 'cubic spline': interpolator = CubicSplineInterpolator elif method == 'linear': interpolator = LinearInterpolator elif method == 'pchip': interpolator = PchipInterpolator elif method == 'sprague': if is_uniform: interpolator = SpragueInterpolator else: raise RuntimeError( ('"Sprague" interpolator can only be used for ' 'interpolating functions having a uniformly spaced ' 'independent variable!')) else: raise ValueError( 'Undefined "{0}" interpolator!'.format(method)) interpolators = [interpolator(wavelengths, values[..., i]) for i in range(values.shape[-1])] XYZ = np.dstack([i(np.ravel(wavelength)) for i in interpolators]) else: XYZ = cmfs.get(wavelength) XYZ = np.reshape(XYZ, np.asarray(wavelength).shape + (3,)) return XYZ
def spectral_to_XYZ(spd, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), illuminant=None): """ Converts given spectral power distribution to *CIE XYZ* colourspace using given colour matching functions and illuminant. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional *Illuminant* spectral power distribution. Returns ------- ndarray, (3,) *CIE XYZ* colourspace matrix. Warning ------- The output domain of that definition is non standard! Notes ----- - Output *CIE XYZ* colourspace matrix is in domain [0, 100]. References ---------- .. [1] **Wyszecki & Stiles**, *Color Science - Concepts and Methods Data and Formulae - Second Edition*, Wiley Classics Library Edition, published 2000, ISBN-10: 0-471-39918-3, page 158. Examples -------- >>> from colour import CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution # noqa >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = {380: 0.0600, 390: 0.0600} >>> spd = SpectralPowerDistribution('Custom', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ(spd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 4.5764852...e-04, 1.2964866...e-05, 2.1615807...e-03]) """ shape = cmfs.shape if spd.shape != cmfs.shape: spd = spd.clone().zeros(shape) if illuminant is None: illuminant = ones_spd(shape) else: if illuminant.shape != cmfs.shape: illuminant = illuminant.clone().zeros(shape) illuminant = illuminant.values spd = spd.values x_bar, y_bar, z_bar = (cmfs.x_bar.values, cmfs.y_bar.values, cmfs.z_bar.values) x_products = spd * x_bar * illuminant y_products = spd * y_bar * illuminant z_products = spd * z_bar * illuminant normalising_factor = 100 / np.sum(y_bar * illuminant) XYZ = np.array([normalising_factor * np.sum(x_products), normalising_factor * np.sum(y_products), normalising_factor * np.sum(z_products)]) return XYZ
def spectral_to_XYZ(spd, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), illuminant=ones_spd( STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer').shape), method='ASTM E308–15', **kwargs): """ Converts given spectral power distribution to *CIE XYZ* tristimulus values using given colour matching functions, illuminant and method. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional Illuminant spectral power distribution. method : unicode, optional **{'ASTM E308–15', 'Integration'}**, Computation method. \**kwargs : dict, optional Keywords arguments. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- The output range of that definition is non standard! Notes ----- - Output *CIE XYZ* tristimulus values are in range [0, 100]. Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution) >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852} >>> spd = SpectralPowerDistribution('Sample', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ( # doctest: +ELLIPSIS ... spd, cmfs, illuminant) array([ 11.5290265..., 9.9502091..., 4.7098882...]) >>> spectral_to_XYZ( # doctest: +ELLIPSIS ... spd, cmfs, illuminant, use_practice_range=False) array([ 11.5291275..., 9.9502369..., 4.7098811...]) >>> spectral_to_XYZ( # doctest: +ELLIPSIS ... spd, cmfs, illuminant, method='Integration') array([ 11.5296285..., 9.9499467..., 4.7066079...]) """ function = SPECTRAL_TO_XYZ_METHODS[method] filter_kwargs(function, **kwargs) return function(spd, cmfs, illuminant, **kwargs)
def wavelength_to_XYZ(wavelength, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), method=None): """ Converts given wavelength :math:`\lambda` to *CIE XYZ* tristimulus values using given colour matching functions. If the wavelength :math:`\lambda` is not available in the colour matching function, its value will be calculated using *CIE* recommendations: The method developed by Sprague (1880) should be used for interpolating functions having a uniformly spaced independent variable and a *Cubic Spline* method for non-uniformly spaced independent variable. Parameters ---------- wavelength : numeric or array_like Wavelength :math:`\lambda` in nm. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. method : unicode, optional {None, 'Cubic Spline', 'Linear', 'Pchip', 'Sprague'}, Enforce given interpolation method. Returns ------- ndarray *CIE XYZ* tristimulus values. Raises ------ RuntimeError If Sprague (1880) interpolation method is forced with a non-uniformly spaced independent variable. ValueError If the interpolation method is not defined or if wavelength :math:`\lambda` is not contained in the colour matching functions domain. Notes ----- - Output *CIE XYZ* tristimulus values are in range [0, 1]. - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. - Sprague (1880) interpolator cannot be used for interpolating functions having a non-uniformly spaced independent variable. Warning ------- - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. - *Cubic Spline* interpolator requires at least 3 wavelengths :math:`\lambda_n` for interpolation. - *Linear* interpolator requires at least 2 wavelengths :math:`\lambda_n` for interpolation. - *Pchip* interpolator requires at least 2 wavelengths :math:`\lambda_n` for interpolation. - Sprague (1880) interpolator requires at least 6 wavelengths :math:`\lambda_n` for interpolation. Examples -------- Uniform data is using Sprague (1880) interpolation by default: >>> from colour import CMFS >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> wavelength_to_XYZ(480, cmfs) # doctest: +ELLIPSIS array([ 0.09564 , 0.13902 , 0.812950...]) >>> wavelength_to_XYZ(480.5, cmfs) # doctest: +ELLIPSIS array([ 0.0914287..., 0.1418350..., 0.7915726...]) Enforcing *Cubic Spline* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Cubic Spline') # doctest: +ELLIPSIS array([ 0.0914288..., 0.1418351..., 0.7915729...]) Enforcing *Linear* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Linear') # doctest: +ELLIPSIS array([ 0.0914697..., 0.1418482..., 0.7917337...]) Enforcing *Pchip* interpolation: >>> wavelength_to_XYZ(480.5, cmfs, 'Pchip') # doctest: +ELLIPSIS array([ 0.0914280..., 0.1418341..., 0.7915711...]) """ cmfs_shape = cmfs.shape if (np.min(wavelength) < cmfs_shape.start or np.max(wavelength) > cmfs_shape.end): raise ValueError( '"{0} nm" wavelength is not in "[{1}, {2}]" domain!'.format( wavelength, cmfs_shape.start, cmfs_shape.end)) if wavelength not in cmfs: wavelengths, values, = cmfs.wavelengths, cmfs.values if is_string(method): method = method.lower() is_uniform = cmfs.is_uniform() if method is None: if is_uniform: interpolator = SpragueInterpolator else: interpolator = CubicSplineInterpolator elif method == 'cubic spline': interpolator = CubicSplineInterpolator elif method == 'linear': interpolator = LinearInterpolator elif method == 'pchip': interpolator = PchipInterpolator elif method == 'sprague': if is_uniform: interpolator = SpragueInterpolator else: raise RuntimeError( ('"Sprague" interpolator can only be used for ' 'interpolating functions having a uniformly spaced ' 'independent variable!')) else: raise ValueError('Undefined "{0}" interpolator!'.format(method)) interpolators = [ interpolator(wavelengths, values[..., i]) for i in range(values.shape[-1]) ] XYZ = np.dstack([i(np.ravel(wavelength)) for i in interpolators]) else: XYZ = cmfs.get(wavelength) XYZ = np.reshape(XYZ, np.asarray(wavelength).shape + (3, )) return XYZ
def spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815( spd, cmfs=STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer'), illuminant=ones_spd( STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer').shape)): """ Converts given spectral power distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant using a table of tristimulus weighting factors accordingly to practise *ASTM E308–15* method [2]_. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional Illuminant spectral power distribution. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- The output range of that definition is non standard! Notes ----- - Output *CIE XYZ* tristimulus values are in range [0, 100]. Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution) >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852} >>> spd = SpectralPowerDistribution('Sample', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815( ... spd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 11.5296311..., 9.9505845..., 4.7098037...]) """ if illuminant.shape != cmfs.shape: warning('Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant, cmfs)) illuminant = illuminant.clone().align(cmfs.shape) if spd.shape.boundaries != cmfs.shape.boundaries: warning('Trimming "{0}" spectral power distribution shape to "{1}" ' 'colour matching functions shape.'.format(illuminant, cmfs)) spd = spd.clone().trim_wavelengths(cmfs.shape) W = tristimulus_weighting_factors_ASTME202211( cmfs, illuminant, SpectralShape(cmfs.shape.start, cmfs.shape.end, spd.shape.interval)) start_w = cmfs.shape.start end_w = cmfs.shape.start + spd.shape.interval * (W.shape[0] - 1) W = adjust_tristimulus_weighting_factors_ASTME30815( W, SpectralShape(start_w, end_w, spd.shape.interval), spd.shape) R = spd.values XYZ = np.sum(W * R[..., np.newaxis], axis=0) return XYZ
def spectral_to_XYZ_ASTME30815( spd, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), illuminant=ones_spd( STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer').shape), use_practice_range=True, mi_5nm_omission_method=True, mi_20nm_interpolation_method=True): """ Converts given spectral power distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant accordingly to practise *ASTM E308–15* method [2]_. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional Illuminant spectral power distribution. use_practice_range : bool, optional Practise *ASTM E308–15* working wavelengths range is [360, 780], if `True` this argument will trim the colour matching functions appropriately. mi_5nm_omission_method : bool, optional 5 nm measurement intervals spectral power distribution conversion to tristimulus values will use a 5 nm version of the colour matching functions instead of a table of tristimulus weighting factors. mi_20nm_interpolation_method : bool, optional 20 nm measurement intervals spectral power distribution conversion to tristimulus values will use a dedicated interpolation method instead of a table of tristimulus weighting factors. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- - The tables of tristimulus weighting factors are cached in :attr:`_TRISTIMULUS_WEIGHTING_FACTORS_CACHE` attribute. Their identifier key is defined by the colour matching functions and illuminant names along the current shape such as: `CIE 1964 10 Degree Standard Observer, A, (360.0, 830.0, 10.0)` Considering the above, one should be mindful that using similar colour matching functions and illuminant names but with different spectral data will lead to unexpected behaviour. - The output range of that definition is non standard! Notes ----- - Output *CIE XYZ* tristimulus values are in range [0, 100]. Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution) >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852} >>> spd = SpectralPowerDistribution('Sample', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ_ASTME30815( ... spd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 11.5290265..., 9.9502091..., 4.7098882...]) """ if spd.shape.interval not in (1, 5, 10, 20): raise ValueError( 'Tristimulus values conversion from spectral data accordingly to ' 'practise *ASTM E308-15* should be performed on spectral data ' 'with measurement interval of 1, 5, 10 or 20nm!') if use_practice_range: cmfs = cmfs.clone().trim_wavelengths(SpectralShape(360, 780, 1)) method = spectral_to_XYZ_tristimulus_weighting_factors_ASTME30815 if spd.shape.interval == 1: method = spectral_to_XYZ_integration elif spd.shape.interval == 5 and mi_5nm_omission_method: if cmfs.shape.interval != 5: cmfs = cmfs.clone().interpolate(SpectralShape(interval=5)) method = spectral_to_XYZ_integration elif spd.shape.interval == 20 and mi_20nm_interpolation_method: spd = spd.clone() if spd.shape.boundaries != cmfs.shape.boundaries: warning( 'Trimming "{0}" spectral power distribution shape to "{1}" ' 'colour matching functions shape.'.format(illuminant, cmfs)) spd.trim_wavelengths(cmfs.shape) # Extrapolation of additional 20nm padding intervals. spd.align(SpectralShape(spd.shape.start - 20, spd.shape.end + 20, 10)) for i in range(2): spd[spd.wavelengths[i]] = (3 * spd.values[i + 2] - 3 * spd.values[i + 4] + spd.values[i + 6]) i_e = len(spd) - 1 - i spd[spd.wavelengths[i_e]] = (spd.values[i_e - 6] - 3 * spd.values[i_e - 4] + 3 * spd.values[i_e - 2]) # Interpolating every odd numbered values. # TODO: Investigate code vectorisation. for i in range(3, len(spd) - 3, 2): spd[spd.wavelengths[i]] = (-0.0625 * spd.values[i - 3] + 0.5625 * spd.values[i - 1] + 0.5625 * spd.values[i + 1] - 0.0625 * spd.values[i + 3]) # Discarding the additional 20nm padding intervals. spd.trim_wavelengths( SpectralShape(spd.shape.start + 20, spd.shape.end - 20, 10)) XYZ = method(spd, cmfs, illuminant) return XYZ
def spectral_to_XYZ(spd, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), illuminant=None): """ Converts given spectral power distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional *Illuminant* spectral power distribution. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- The output domain of that definition is non standard! Notes ----- - Output *CIE XYZ* tristimulus values are in domain [0, 100]. References ---------- .. [1] Wyszecki, G., & Stiles, W. S. (2000). Integration Replace by Summation. In Color Science: Concepts and Methods, Quantitative Data and Formulae (pp. 158–163). Wiley. ISBN:978-0471399186 Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution) >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = {380: 0.0600, 390: 0.0600} >>> spd = SpectralPowerDistribution('Custom', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ(spd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 4.5764852...e-04, 1.2964866...e-05, 2.1615807...e-03]) """ shape = cmfs.shape if spd.shape != cmfs.shape: spd = spd.clone().zeros(shape) if illuminant is None: illuminant = ones_spd(shape) else: if illuminant.shape != cmfs.shape: illuminant = illuminant.clone().zeros(shape) spd = spd.values x_bar, y_bar, z_bar = (cmfs.x_bar.values, cmfs.y_bar.values, cmfs.z_bar.values) illuminant = illuminant.values x_products = spd * x_bar * illuminant y_products = spd * y_bar * illuminant z_products = spd * z_bar * illuminant normalising_factor = 100 / np.sum(y_bar * illuminant) XYZ = np.array([normalising_factor * np.sum(x_products), normalising_factor * np.sum(y_products), normalising_factor * np.sum(z_products)]) return XYZ
def spectral_to_XYZ_integration( spd, cmfs=STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer'), illuminant=ones_spd( STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer').shape)): """ Converts given spectral power distribution to *CIE XYZ* tristimulus values using given colour matching functions and illuminant accordingly to classical integration method. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional Illuminant spectral power distribution. Returns ------- ndarray, (3,) *CIE XYZ* tristimulus values. Warning ------- The output range of that definition is non standard! Notes ----- - Output *CIE XYZ* tristimulus values are in range [0, 100]. References ---------- .. [3] Wyszecki, G., & Stiles, W. S. (2000). Integration Replace by Summation. In Color Science: Concepts and Methods, Quantitative Data and Formulae (pp. 158–163). Wiley. ISBN:978-0471399186 Examples -------- >>> from colour import ( ... CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution) >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = { ... 400: 0.0641, ... 420: 0.0645, ... 440: 0.0562, ... 460: 0.0537, ... 480: 0.0559, ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360, ... 620: 0.1511, ... 640: 0.1688, ... 660: 0.1996, ... 680: 0.2397, ... 700: 0.2852} >>> spd = SpectralPowerDistribution('Sample', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ_integration( # doctest: +ELLIPSIS ... spd, cmfs, illuminant) array([ 11.5296285..., 9.9499467..., 4.7066079...]) """ if illuminant.shape != cmfs.shape: warning('Aligning "{0}" illuminant shape to "{1}" colour matching ' 'functions shape.'.format(illuminant, cmfs)) illuminant = illuminant.clone().align(cmfs.shape) if spd.shape != cmfs.shape: warning('Aligning "{0}" spectral power distribution shape to "{1}" ' 'colour matching functions shape.'.format(spd, cmfs)) spd = spd.clone().align(cmfs.shape) S = illuminant.values x_bar, y_bar, z_bar = tsplit(cmfs.values) R = spd.values dw = cmfs.shape.interval k = 100 / (np.sum(y_bar * S) * dw) X_p = R * x_bar * S * dw Y_p = R * y_bar * S * dw Z_p = R * z_bar * S * dw XYZ = k * np.sum(np.array([X_p, Y_p, Z_p]), axis=-1) return XYZ
def spectral_to_XYZ(spd, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), illuminant=None): """ Converts given spectral power distribution to *CIE XYZ* colourspace using given colour matching functions and illuminant. Parameters ---------- spd : SpectralPowerDistribution Spectral power distribution. cmfs : XYZ_ColourMatchingFunctions Standard observer colour matching functions. illuminant : SpectralPowerDistribution, optional *Illuminant* spectral power distribution. Returns ------- ndarray, (3,) *CIE XYZ* colourspace matrix. Warning ------- The output domain of that definition is non standard! Notes ----- - Output *CIE XYZ* colourspace matrix is in domain [0, 100]. References ---------- .. [1] **Wyszecki & Stiles**, *Color Science - Concepts and Methods Data and Formulae - Second Edition*, Wiley Classics Library Edition, published 2000, ISBN-10: 0-471-39918-3, page 158. Examples -------- >>> from colour import CMFS, ILLUMINANTS_RELATIVE_SPDS, SpectralPowerDistribution # noqa >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> data = {380: 0.0600, 390: 0.0600} >>> spd = SpectralPowerDistribution('Custom', data) >>> illuminant = ILLUMINANTS_RELATIVE_SPDS.get('D50') >>> spectral_to_XYZ(spd, cmfs, illuminant) # doctest: +ELLIPSIS array([ 4.5764852...e-04, 1.2964866...e-05, 2.1615807...e-03]) """ shape = cmfs.shape if spd.shape != cmfs.shape: spd = spd.clone().zeros(shape) if illuminant is None: illuminant = ones_spd(shape) else: if illuminant.shape != cmfs.shape: illuminant = illuminant.clone().zeros(shape) illuminant = illuminant.values spd = spd.values x_bar, y_bar, z_bar = (cmfs.x_bar.values, cmfs.y_bar.values, cmfs.z_bar.values) x_products = spd * x_bar * illuminant y_products = spd * y_bar * illuminant z_products = spd * z_bar * illuminant normalising_factor = 100 / np.sum(y_bar * illuminant) XYZ = np.array([ normalising_factor * np.sum(x_products), normalising_factor * np.sum(y_products), normalising_factor * np.sum(z_products) ]) return XYZ
def uv_to_CCT_Ohno2013(uv, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer'), start=CCT_MINIMAL, end=CCT_MAXIMAL, count=CCT_SAMPLES, iterations=CCT_CALCULATION_ITERATIONS): """ Returns the correlated colour temperature :math:`T_{cp}` and :math:`\Delta_{uv}` from given *CIE UCS* colourspace *uv* chromaticity coordinates, colour matching functions and temperature range using Ohno (2013) method. The iterations parameter defines the calculations precision: The higher its value, the more planckian tables will be generated through cascade expansion in order to converge to the exact solution. Parameters ---------- uv : array_like *CIE UCS* colourspace *uv* chromaticity coordinates. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. start : numeric, optional Temperature range start in kelvins. end : numeric, optional Temperature range end in kelvins. count : int, optional Temperatures count in the planckian tables. iterations : int, optional Number of planckian tables to generate. Returns ------- ndarray Correlated colour temperature :math:`T_{cp}`, :math:`\Delta_{uv}`. References ---------- .. [3] Ohno, Y. (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47–55. doi:10.1080/15502724.2014.839020 Examples -------- >>> from colour import STANDARD_OBSERVERS_CMFS >>> cmfs = 'CIE 1931 2 Degree Standard Observer' >>> cmfs = STANDARD_OBSERVERS_CMFS.get(cmfs) >>> uv = np.array([0.1978, 0.3122]) >>> uv_to_CCT_Ohno2013(uv, cmfs) # doctest: +ELLIPSIS array([ 6.5075470...e+03, 3.2236908...e-03]) """ # Ensuring we do at least one iteration to initialise variables. if iterations <= 0: iterations = 1 # Planckian table creation through cascade expansion. for _i in range(iterations): table = planckian_table(uv, cmfs, start, end, count) index = planckian_table_minimal_distance_index(table) if index == 0: warning( ('Minimal distance index is on lowest planckian table bound, ' 'unpredictable results may occur!')) index += 1 elif index == len(table) - 1: warning( ('Minimal distance index is on highest planckian table bound, ' 'unpredictable results may occur!')) index -= 1 start = table[index - 1].Ti end = table[index + 1].Ti _ux, vx = uv Tuvdip, Tuvdi, Tuvdin = (table[index - 1], table[index], table[index + 1]) Tip, uip, vip, dip = Tuvdip.Ti, Tuvdip.ui, Tuvdip.vi, Tuvdip.di Ti, di = Tuvdi.Ti, Tuvdi.di Tin, uin, vin, din = Tuvdin.Ti, Tuvdin.ui, Tuvdin.vi, Tuvdin.di # Triangular solution. l = np.sqrt((uin - uip) ** 2 + (vin - vip) ** 2) x = (dip ** 2 - din ** 2 + l ** 2) / (2 * l) T = Tip + (Tin - Tip) * (x / l) vtx = vip + (vin - vip) * (x / l) sign = 1 if vx - vtx >= 0 else -1 D_uv = (dip ** 2 - x ** 2) ** (1 / 2) * sign # Parabolic solution. if D_uv < 0.002: X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X ** -1 b = (-(Tip ** 2 * (din - di) + Ti ** 2 * (dip - din) + Tin ** 2 * (di - dip)) * X ** -1) c = (-(dip * (Tin - Ti) * Ti * Tin + di * (Tip - Tin) * Tip * Tin + din * (Ti - Tip) * Tip * Ti) * X ** -1) T = -b / (2 * a) D_uv = sign * (a * T ** 2 + b * T + c) return np.array([T, D_uv])
def colour_rendering_index(test_spd, additional_data=False): """ Returns the *colour rendering index* of given spectral power distribution. Parameters ---------- test_spd : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or (numeric, dict) Colour rendering index, Tsc data. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_rendering_index(spd) # doctest: +ELLIPSIS 64.1507331... """ cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape test_spd = test_spd.clone().align(shape) tcs_spds = {} for index, tcs_spd in sorted(TCS_SPDS.items()): tcs_spds[index] = tcs_spd.clone().align(shape) XYZ = spectral_to_XYZ(test_spd, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, Duv = uv_to_CCT_robertson1968(uv) if CCT < 5000: reference_spd = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_illuminant_D(CCT) reference_spd = D_illuminant_relative_spd(xy) reference_spd.align(shape) test_tcs_colorimetry_data = _tcs_colorimetry_data( test_spd, reference_spd, tcs_spds, cmfs, chromatic_adaptation=True) reference_tcs_colorimetry_data = _tcs_colorimetry_data( reference_spd, reference_spd, tcs_spds, cmfs) colour_rendering_indexes = _colour_rendering_indexes( test_tcs_colorimetry_data, reference_tcs_colorimetry_data) colour_rendering_index = np.average([ v for k, v in colour_rendering_indexes.items() if k in (1, 2, 3, 4, 5, 6, 7, 8) ]) if additional_data: return (colour_rendering_index, colour_rendering_indexes, [test_tcs_colorimetry_data, reference_tcs_colorimetry_data]) else: return colour_rendering_index
def colour_rendering_index(spd_test, additional_data=False): """ Returns the *colour rendering index* :math:`Q_a` of given spectral power distribution. Parameters ---------- spd_test : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or CRI_Specification Colour rendering index. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_rendering_index(spd) # doctest: +ELLIPSIS 64.1507331... """ cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape spd_test = spd_test.clone().align(shape) tcs_spds = {} for index, tcs_spd in TCS_SPDS.items(): tcs_spds[index] = tcs_spd.clone().align(shape) XYZ = spectral_to_XYZ(spd_test, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, _D_uv = uv_to_CCT_Robertson1968(uv) if CCT < 5000: spd_reference = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_CIE_D(CCT) spd_reference = D_illuminant_relative_spd(xy) spd_reference.align(shape) test_tcs_colorimetry_data = tcs_colorimetry_data(spd_test, spd_reference, tcs_spds, cmfs, chromatic_adaptation=True) reference_tcs_colorimetry_data = tcs_colorimetry_data( spd_reference, spd_reference, tcs_spds, cmfs) Q_as = colour_rendering_indexes(test_tcs_colorimetry_data, reference_tcs_colorimetry_data) Q_a = 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 CRI_Specification( spd_test.name, Q_a, Q_as, (test_tcs_colorimetry_data, reference_tcs_colorimetry_data)) else: return Q_a
def colour_quality_scale(spd_test, additional_data=False): """ Returns the *colour quality scale* of given spectral power distribution. Parameters ---------- spd_test : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or CQS_Specification Color quality scale. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_quality_scale(spd) # doctest: +ELLIPSIS 64.6860580... """ cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape spd_test = spd_test.clone().align(shape) vs_spds = {} for index, vs_spd in VS_SPDS.items(): vs_spds[index] = vs_spd.clone().align(shape) XYZ = spectral_to_XYZ(spd_test, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, _D_uv = uv_to_CCT_Ohno2013(uv) if CCT < 5000: spd_reference = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_CIE_D(CCT) spd_reference = D_illuminant_relative_spd(xy) spd_reference.align(shape) test_vs_colorimetry_data = vs_colorimetry_data(spd_test, spd_reference, vs_spds, cmfs, chromatic_adaptation=True) reference_vs_colorimetry_data = vs_colorimetry_data( spd_reference, spd_reference, vs_spds, cmfs) XYZ_r = spectral_to_XYZ(spd_reference, cmfs) XYZ_r /= XYZ_r[1] CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r) Q_as = colour_quality_scales(test_vs_colorimetry_data, reference_vs_colorimetry_data, 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) Q_f = scale_conversion(D_E_RMS, CCT_f, 2.928) 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 = 100 - 3.6 * (D_Ep_RMS - p_delta_C) 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 / D65_GAMUT_AREA * 100 Q_d = G_t / G_r * CCT_f * 100 if additional_data: return CQS_Specification( spd_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 colour_quality_scale(spd_test, additional_data=False): """ Returns the *colour quality scale* of given spectral power distribution. Parameters ---------- spd_test : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or CQS_Specification Color quality scale. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_quality_scale(spd) # doctest: +ELLIPSIS 64.6781117... """ cmfs = STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer') shape = cmfs.shape XYZ = spectral_to_XYZ(spd_test, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, _D_uv = uv_to_CCT_Ohno2013(uv) if CCT < 5000: spd_reference = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_CIE_D(CCT) spd_reference = D_illuminant_relative_spd(xy) spd_reference.align(shape) test_vs_colorimetry_data = vs_colorimetry_data( spd_test, spd_reference, VS_SPDS, cmfs, chromatic_adaptation=True) reference_vs_colorimetry_data = vs_colorimetry_data( spd_reference, spd_reference, VS_SPDS, cmfs) XYZ_r = spectral_to_XYZ(spd_reference, cmfs) XYZ_r /= XYZ_r[1] CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r) Q_as = colour_quality_scales( test_vs_colorimetry_data, reference_vs_colorimetry_data, 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) Q_f = scale_conversion(D_E_RMS, CCT_f, 2.928) 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 = 100 - 3.6 * (D_Ep_RMS - p_delta_C) 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 / D65_GAMUT_AREA * 100 Q_d = G_t / G_r * CCT_f * 100 if additional_data: return CQS_Specification(spd_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 CCT_to_uv_Ohno2013( CCT, D_uv=0, cmfs=STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer')): """ Returns the *CIE UCS* colourspace *uv* chromaticity coordinates from given correlated colour temperature :math:`T_{cp}`, :math:`\Delta_{uv}` and colour matching functions using Ohno (2013) method. Parameters ---------- CCT : numeric Correlated colour temperature :math:`T_{cp}`. D_uv : numeric, optional :math:`\Delta_{uv}`. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- ndarray *CIE UCS* colourspace *uv* chromaticity coordinates. References ---------- .. [4] Ohno, Y. (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47–55. doi:10.1080/15502724.2014.839020 Examples -------- >>> from colour import STANDARD_OBSERVERS_CMFS >>> cmfs = 'CIE 1931 2 Degree Standard Observer' >>> cmfs = STANDARD_OBSERVERS_CMFS.get(cmfs) >>> CCT = 6507.4342201047066 >>> D_uv = 0.003223690901512735 >>> CCT_to_uv_Ohno2013(CCT, D_uv, cmfs) # doctest: +ELLIPSIS array([ 0.1978003..., 0.3122005...]) """ shape = cmfs.shape delta = 0.01 spd = blackbody_spd(CCT, shape) XYZ = spectral_to_XYZ(spd, cmfs) XYZ *= 1 / np.max(XYZ) UVW = XYZ_to_UCS(XYZ) u0, v0 = UCS_to_uv(UVW) if D_uv == 0: return np.array([u0, v0]) else: spd = blackbody_spd(CCT + delta, shape) XYZ = spectral_to_XYZ(spd, cmfs) XYZ *= 1 / np.max(XYZ) UVW = XYZ_to_UCS(XYZ) u1, v1 = UCS_to_uv(UVW) du = u0 - u1 dv = v0 - v1 u = u0 - D_uv * (dv / np.sqrt(du**2 + dv**2)) v = v0 + D_uv * (du / np.sqrt(du**2 + dv**2)) return np.array([u, v])
def colour_rendering_index(spd_test, additional_data=False): """ Returns the *colour rendering index* :math:`Q_a` of given spectral power distribution. Parameters ---------- spd_test : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or CRI_Specification Colour rendering index. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_rendering_index(spd) # doctest: +ELLIPSIS 64.1507331... """ cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape spd_test = spd_test.clone().align(shape) tcs_spds = {} for index, tcs_spd in TCS_SPDS.items(): tcs_spds[index] = tcs_spd.clone().align(shape) XYZ = spectral_to_XYZ(spd_test, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, _D_uv = uv_to_CCT_Robertson1968(uv) if CCT < 5000: spd_reference = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_CIE_D(CCT) spd_reference = D_illuminant_relative_spd(xy) spd_reference.align(shape) test_tcs_colorimetry_data = tcs_colorimetry_data( spd_test, spd_reference, tcs_spds, cmfs, chromatic_adaptation=True) reference_tcs_colorimetry_data = tcs_colorimetry_data( spd_reference, spd_reference, tcs_spds, cmfs) Q_as = colour_rendering_indexes( test_tcs_colorimetry_data, reference_tcs_colorimetry_data) Q_a = 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 CRI_Specification(spd_test.name, Q_a, Q_as, (test_tcs_colorimetry_data, reference_tcs_colorimetry_data)) else: return Q_a
def colour_rendering_index(test_spd, additional_data=False): """ Returns the *colour rendering index* of given spectral power distribution. Parameters ---------- test_spd : SpectralPowerDistribution Test spectral power distribution. additional_data : bool, optional Output additional data. Returns ------- numeric or (numeric, dict) Colour rendering index, Tsc data. Examples -------- >>> from colour import ILLUMINANTS_RELATIVE_SPDS >>> spd = ILLUMINANTS_RELATIVE_SPDS.get('F2') >>> colour_rendering_index(spd) # doctest: +ELLIPSIS 64.1507331... """ cmfs = STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer') shape = cmfs.shape test_spd = test_spd.clone().align(shape) tcs_spds = {} for index, tcs_spd in sorted(TCS_SPDS.items()): tcs_spds[index] = tcs_spd.clone().align(shape) XYZ = spectral_to_XYZ(test_spd, cmfs) uv = UCS_to_uv(XYZ_to_UCS(XYZ)) CCT, Duv = uv_to_CCT_robertson1968(uv) if CCT < 5000: reference_spd = blackbody_spd(CCT, shape) else: xy = CCT_to_xy_illuminant_D(CCT) reference_spd = D_illuminant_relative_spd(xy) reference_spd.align(shape) test_tcs_colorimetry_data = _tcs_colorimetry_data( test_spd, reference_spd, tcs_spds, cmfs, chromatic_adaptation=True) reference_tcs_colorimetry_data = _tcs_colorimetry_data( reference_spd, reference_spd, tcs_spds, cmfs) colour_rendering_indexes = _colour_rendering_indexes( test_tcs_colorimetry_data, reference_tcs_colorimetry_data) colour_rendering_index = np.average( [v for k, v in colour_rendering_indexes.items() if k in (1, 2, 3, 4, 5, 6, 7, 8)]) if additional_data: return (colour_rendering_index, colour_rendering_indexes, [test_tcs_colorimetry_data, reference_tcs_colorimetry_data]) else: return colour_rendering_index
def CCT_to_uv_Ohno2013(CCT, D_uv=0, cmfs=STANDARD_OBSERVERS_CMFS.get( 'CIE 1931 2 Degree Standard Observer')): """ Returns the *CIE UCS* colourspace *uv* chromaticity coordinates from given correlated colour temperature :math:`T_{cp}`, :math:`\Delta_{uv}` and colour matching functions using Ohno (2013) method. Parameters ---------- CCT : numeric Correlated colour temperature :math:`T_{cp}`. D_uv : numeric, optional :math:`\Delta_{uv}`. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- ndarray *CIE UCS* colourspace *uv* chromaticity coordinates. References ---------- .. [4] Ohno, Y. (2014). Practical Use and Calculation of CCT and Duv. LEUKOS, 10(1), 47–55. doi:10.1080/15502724.2014.839020 Examples -------- >>> from colour import STANDARD_OBSERVERS_CMFS >>> cmfs = 'CIE 1931 2 Degree Standard Observer' >>> cmfs = STANDARD_OBSERVERS_CMFS.get(cmfs) >>> CCT = 6507.4342201047066 >>> D_uv = 0.003223690901512735 >>> CCT_to_uv_Ohno2013(CCT, D_uv, cmfs) # doctest: +ELLIPSIS array([ 0.1978003..., 0.3122005...]) """ shape = cmfs.shape delta = 0.01 spd = blackbody_spd(CCT, shape) XYZ = spectral_to_XYZ(spd, cmfs) XYZ *= 1 / np.max(XYZ) UVW = XYZ_to_UCS(XYZ) u0, v0 = UCS_to_uv(UVW) if D_uv == 0: return np.array([u0, v0]) else: spd = blackbody_spd(CCT + delta, shape) XYZ = spectral_to_XYZ(spd, cmfs) XYZ *= 1 / np.max(XYZ) UVW = XYZ_to_UCS(XYZ) u1, v1 = UCS_to_uv(UVW) du = u0 - u1 dv = v0 - v1 u = u0 - D_uv * (dv / np.sqrt(du ** 2 + dv ** 2)) v = v0 + D_uv * (du / np.sqrt(du ** 2 + dv ** 2)) return np.array([u, v])
def wavelength_to_XYZ( wavelength, cmfs=STANDARD_OBSERVERS_CMFS.get('CIE 1931 2 Degree Standard Observer')): """ Converts given wavelength :math:`\lambda` to *CIE XYZ* colourspace using given colour matching functions. If the wavelength :math:`\lambda` is not available in the colour matching function, its value will be calculated using *CIE* recommendations: The method developed by *Sprague (1880)* should be used for interpolating functions having a uniformly spaced independent variable and a *Cubic Spline* method for non-uniformly spaced independent variable. Parameters ---------- wavelength : numeric Wavelength :math:`\lambda` in nm. cmfs : XYZ_ColourMatchingFunctions, optional Standard observer colour matching functions. Returns ------- ndarray, (3,) *CIE XYZ* colourspace matrix. Raises ------ ValueError If wavelength :math:`\lambda` is not in the colour matching functions domain. Notes ----- - Output *CIE XYZ* colourspace matrix is in domain [0, 1]. - If *scipy* is not unavailable the *Cubic Spline* method will fallback to legacy *Linear* interpolation. Examples -------- >>> from colour import CMFS >>> cmfs = CMFS.get('CIE 1931 2 Degree Standard Observer') >>> wavelength_to_XYZ(480) # doctest: +ELLIPSIS array([ 0.09564 , 0.13902 , 0.812950...]) """ shape = cmfs.shape if wavelength < shape.start or wavelength > shape.end: raise ValueError( '"{0} nm" wavelength is not in "[{1}, {2}]" domain!'.format( wavelength, shape.start, shape.end)) if wavelength not in cmfs: wavelengths, values, = cmfs.wavelengths, cmfs.values interpolator = (SpragueInterpolator if cmfs.is_uniform() else SplineInterpolator) interpolators = [ interpolator(wavelengths, values[:, i]) for i in range(values.shape[-1]) ] return np.array( [interpolator(wavelength) for interpolator in interpolators]) else: return np.array(cmfs.get(wavelength))