def test_sd_to_XYZ_integration(self): """ Tests :func:`colour.colorimetry.tristimulus.\ sd_to_XYZ_integration` definition. """ cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] np.testing.assert_almost_equal( sd_to_XYZ_integration(SAMPLE_SD, cmfs, ILLUMINANTS_SDS['A']), np.array([14.46365624, 10.85827910, 2.04662343]), decimal=7) cmfs = CMFS['CIE 1964 10 Degree Standard Observer'] np.testing.assert_almost_equal( sd_to_XYZ_integration(SAMPLE_SD, cmfs, ILLUMINANTS_SDS['C']), np.array([10.77031004, 9.44863775, 6.62745989]), decimal=7) np.testing.assert_almost_equal( sd_to_XYZ_integration(SAMPLE_SD, cmfs, ILLUMINANTS_SDS['FL2']), np.array([11.57834054, 9.98738373, 3.95462625]), decimal=7) np.testing.assert_almost_equal( sd_to_XYZ_integration( SAMPLE_SD, cmfs, ILLUMINANTS_SDS['FL2'], k=683), np.array([122441.23450378, 105616.82732832, 41820.26940409]), decimal=7)
def test_sd_to_XYZ_integration(self): """ Tests :func:`colour.colorimetry.tristimulus.sd_to_XYZ_integration` definition. """ cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer'] np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS['A']), np.array([14.46341147, 10.85819624, 2.04695585]), decimal=7) cmfs = MSDS_CMFS['CIE 1964 10 Degree Standard Observer'] np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS['C']), np.array([10.77002699, 9.44876636, 6.62415290]), decimal=7) np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS['FL2']), np.array([11.57540576, 9.98608874, 3.95242590]), decimal=7) np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS['FL2'], k=683), np.array([122375.09261493, 105572.84645912, 41785.01342332]), decimal=7)
def test_domain_range_scale_XYZ_to_sd_Meng2015(self): """ Test :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition domain and range scale support. """ XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) XYZ_o = sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ_i, self._cmfs, self._sd_D65), self._cmfs, self._sd_D65, ) d_r = (("reference", 1, 1), ("1", 1, 0.01), ("100", 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd_Meng2015( XYZ_i * factor_a, self._cmfs, self._sd_D65 ), self._cmfs, self._sd_D65, ), XYZ_o * factor_b, decimal=7, )
def test_domain_range_scale_XYZ_to_sd(self): """ Tests :func:`colour.recovery.XYZ_to_sd` definition domain and range scale support. """ XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) m = ('Jakob 2019', 'Mallett 2019', 'Meng 2015', 'Otsu 2018', 'Smits 1999') v = [ sd_to_XYZ_integration( XYZ_to_sd(XYZ, method, cmfs=self._cmfs, illuminant=self._sd_D65), self._cmfs, self._sd_D65) for method in m ] d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for method, value in zip(m, v): for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal(sd_to_XYZ_integration( XYZ_to_sd(XYZ * factor_a, method, cmfs=self._cmfs, illuminant=self._sd_D65), self._cmfs, self._sd_D65), value * factor_b, decimal=7)
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 test_XYZ_to_sd_Meng2015(self): """ Tests :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition. """ cmfs = (STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. copy().trim(DEFAULT_SPECTRAL_SHAPE)) shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, 5) cmfs_c = cmfs.copy().align(shape) XYZ = np.array([0.21781186, 0.12541048, 0.04697113]) np.testing.assert_almost_equal( sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ, cmfs_c), cmfs_c) / 100, XYZ, decimal=7) shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, 10) cmfs_c = cmfs.copy().align(shape) np.testing.assert_almost_equal( sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ, cmfs_c), cmfs_c) / 100, XYZ, decimal=7) np.testing.assert_almost_equal(sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, cmfs_c, ILLUMINANTS_SDS['D65']), cmfs_c, ILLUMINANTS_SDS['D65']) / 100, XYZ, decimal=7) np.testing.assert_almost_equal(sd_to_XYZ_integration( XYZ_to_sd_Meng2015( XYZ, cmfs_c, optimisation_parameters={'options': { 'ftol': 1e-10, }}), cmfs_c) / 100, XYZ, decimal=7) shape = SpectralShape(400, 700, 5) cmfs_c = cmfs.copy().align(shape) np.testing.assert_almost_equal( sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ, cmfs_c), cmfs_c) / 100, XYZ, decimal=7)
def test_domain_range_scale_sd_to_XYZ_integration(self): """ Tests :func:`colour.colorimetry.tristimulus.sd_to_XYZ_integration` definition domain and range scale support. """ cmfs = MSDS_CMFS['CIE 1931 2 Degree Standard Observer'] XYZ = sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS['A']) d_r = (('reference', 1), (1, 0.01), (100, 1)) for scale, factor in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal(sd_to_XYZ_integration( SD_SAMPLE, cmfs, SDS_ILLUMINANTS['A']), XYZ * factor, decimal=7)
def constraint_function(a: ArrayLike) -> NDArray: """Define the constraint function.""" sd[:] = a return ( sd_to_XYZ_integration(sd, cmfs=cmfs, illuminant=illuminant) - XYZ )
def test_domain_range_scale_XYZ_to_sd_Meng2015(self): """ Tests :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition domain and range scale support. """ XYZ_i = np.array([0.21781186, 0.12541048, 0.04697113]) XYZ_o = sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ_i)) d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal(sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ_i * factor_a)), XYZ_o * factor_b, decimal=7)
def test_XYZ_to_sd_Meng2015(self): """ Tests :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition. """ cmfs = STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'] shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, 5) cmfs_c = cmfs.copy().align(shape) XYZ = np.array([0.21781186, 0.12541048, 0.04697113]) np.testing.assert_almost_equal( sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ), cmfs=cmfs_c) / 100, XYZ, decimal=7) shape = SpectralShape(cmfs.shape.start, cmfs.shape.end, 10) cmfs_c = cmfs.copy().align(shape) np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, interval=10), cmfs=cmfs_c) / 100, XYZ, decimal=7) np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd_Meng2015( XYZ, interval=10, optimisation_parameters={ 'options': { 'ftol': 1e-10, 'maxiter': 2000 } }), cmfs=cmfs_c) / 100, XYZ, decimal=7) shape = SpectralShape(400, 700, 5) cmfs_c = cmfs.copy().align(shape) np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ, cmfs=cmfs_c), cmfs=cmfs_c) / 100, XYZ, decimal=7)
def test_domain_range_scale_sd_to_XYZ_integration(self): """ Tests :func:`colour.colorimetry.tristimulus.\ sd_to_XYZ_integration` definition domain and range scale support. """ cmfs = CMFS['CIE 1931 2 Degree Standard Observer'] XYZ = sd_to_XYZ_integration(SAMPLE_SD, cmfs, ILLUMINANTS_SDS['A']) d_r = (('reference', 1), (1, 0.01), (100, 1)) for scale, factor in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal( sd_to_XYZ_integration(SAMPLE_SD, cmfs, ILLUMINANTS_SDS['A']), XYZ * factor, decimal=7)
def constraint_function(a): """ Function defining the constraint. """ sd[:] = a return sd_to_XYZ_integration( sd, cmfs=cmfs, illuminant=illuminant) - XYZ
def test_domain_range_scale_XYZ_to_sd_Meng2015(self): """ Tests :func:`colour.recovery.meng2015.XYZ_to_sd_Meng2015` definition domain and range scale support. """ XYZ_i = np.array([0.21781186, 0.12541048, 0.04697113]) XYZ_o = sd_to_XYZ_integration(XYZ_to_sd_Meng2015(XYZ_i)) d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd_Meng2015(XYZ_i * factor_a)), XYZ_o * factor_b, decimal=7)
def test_sd_to_XYZ_integration(self): """ Test :func:`colour.colorimetry.tristimulus_values.\ sd_to_XYZ_integration` definition. """ cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["A"]), np.array([14.46341147, 10.85819624, 2.04695585]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_integration( SD_SAMPLE.values, cmfs, SDS_ILLUMINANTS["A"], shape=SD_SAMPLE.shape, ), np.array([14.46365947, 10.85828084, 2.04663993]), decimal=7, ) cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"] np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["C"]), np.array([10.77002699, 9.44876636, 6.62415290]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"]), np.array([11.57540576, 9.98608874, 3.95242590]), decimal=7, ) np.testing.assert_almost_equal( sd_to_XYZ_integration(SD_SAMPLE, cmfs, SDS_ILLUMINANTS["FL2"], k=683), np.array([122375.09261493, 105572.84645912, 41785.01342332]), decimal=7, )
def test_domain_range_scale_RGB_to_sd_Smits1999(self): """ Tests :func:`colour.recovery.smits1999.RGB_to_sd_Smits1999` definition domain and range scale support. """ XYZ_i = np.array([0.20654008, 0.12197225, 0.05136952]) RGB_i = XYZ_to_RGB_Smits1999(XYZ_i) XYZ_o = sd_to_XYZ_integration(RGB_to_sd_Smits1999(RGB_i)) d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal( sd_to_XYZ_integration( RGB_to_sd_Smits1999(RGB_i * factor_a)), XYZ_o * factor_b, decimal=7)
def test_domain_range_scale_XYZ_to_sd(self): """ Tests :func:`colour.recovery.XYZ_to_sd` definition domain and range scale support. """ XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) m = ('Smits 1999', 'Meng 2015') v = [sd_to_XYZ_integration(XYZ_to_sd(XYZ, method)) for method in m] d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for method, value in zip(m, v): for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal( sd_to_XYZ_integration( XYZ_to_sd(XYZ * factor_a, method=method)), value * factor_b, decimal=7)
def test_domain_range_scale_XYZ_to_sd(self): """ Tests :func:`colour.recovery.XYZ_to_sd` definition domain and range scale support. """ cmfs = (STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']. copy().align(DEFAULT_SPECTRAL_SHAPE_MENG_2015)) XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) m = ('Smits 1999', 'Meng 2015') v = [ sd_to_XYZ_integration(XYZ_to_sd(XYZ, method), cmfs) for method in m ] d_r = (('reference', 1, 1), (1, 1, 0.01), (100, 100, 1)) for method, value in zip(m, v): for scale, factor_a, factor_b in d_r: with domain_range_scale(scale): np.testing.assert_almost_equal(sd_to_XYZ_integration( XYZ_to_sd(XYZ * factor_a, method), cmfs), value * factor_b, decimal=7)
def test_XYZ_to_sd_Meng2015(self): """ Tests :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) cmfs = self._cmfs.copy().align(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 find_coefficients_Jakob2019( XYZ: ArrayLike, cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, coefficients_0: ArrayLike = zeros(3), max_error: Floating = JND_CIE1976 / 100, dimensionalise: Boolean = True, ) -> Tuple[NDArray, Floating]: """ Compute the coefficients for *Jakob and Hanika (2019)* reflectance spectral model. Parameters ---------- XYZ *CIE XYZ* tristimulus values to find the coefficients for. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. illuminant Illuminant spectral distribution, default to *CIE Standard Illuminant D65*. coefficients_0 Starting coefficients for the solver. max_error Maximal acceptable error. Set higher to save computational time. If *None*, the solver will keep going until it is very close to the minimum. The default is ``ACCEPTABLE_DELTA_E``. dimensionalise If *True*, returned coefficients are dimensionful and will not work correctly if fed back as ``coefficients_0``. The default is *True*. Returns ------- :class:`tuple` Tuple of computed coefficients that best fit the given colour and :math:`\\Delta E_{76}` between the target colour and the colour corresponding to the computed coefficients. References ---------- :cite:`Jakob2019` Examples -------- >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) >>> find_coefficients_Jakob2019(XYZ) # doctest: +ELLIPSIS (array([ 1.3723791...e-04, -1.3514399...e-01, 3.0838973...e+01]), \ 0.0141941...) """ coefficients_0 = as_float_array(coefficients_0) cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019 ) def optimize( target_o: NDArray, coefficients_0_o: NDArray ) -> Tuple[NDArray, Floating]: """Minimise the error function using *L-BFGS-B* method.""" try: result = minimize( error_function, coefficients_0_o, (target_o, cmfs, illuminant, max_error), method="L-BFGS-B", jac=True, ) return result.x, result.fun except StopMinimizationEarly as error: return error.coefficients, error.error xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs)) XYZ_good = full(3, 0.5) coefficients_good = zeros(3) divisions = 3 while divisions < 10: XYZ_r = XYZ_good coefficient_r = coefficients_good keep_divisions = False coefficients_0 = coefficient_r for i in range(1, divisions): XYZ_i = (XYZ - XYZ_r) * i / (divisions - 1) + XYZ_r Lab_i = XYZ_to_Lab(XYZ_i) coefficients_0, error = optimize(Lab_i, coefficients_0) if error > max_error: break else: XYZ_good = XYZ_i coefficients_good = coefficients_0 keep_divisions = True else: break if not keep_divisions: divisions += 2 target = XYZ_to_Lab(XYZ, xy_n) coefficients, error = optimize(target, coefficients_0) if dimensionalise: coefficients = dimensionalise_coefficients(coefficients, cmfs.shape) return coefficients, error
def generate( self, colourspace: RGB_Colourspace, cmfs: Optional[MultiSpectralDistributions] = None, illuminant: Optional[SpectralDistribution] = None, size: Integer = 64, print_callable: Callable = print, ): """ Generate the lookup table data for given *RGB* colourspace, colour matching functions, illuminant and given size. Parameters ---------- colourspace The *RGB* colourspace to create a lookup table for. cmfs Standard observer colour matching functions, default to the *CIE 1931 2 Degree Standard Observer*. illuminant Illuminant spectral distribution, default to *CIE Standard Illuminant D65*. size The resolution of the lookup table. Higher values will decrease errors but at the cost of a much longer run time. The published *\\*.coeff* files have a resolution of 64. print_callable Callable used to print progress and diagnostic information. Examples -------- >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS >>> from colour.models import RGB_COLOURSPACE_sRGB >>> from colour.utilities import numpy_print_options >>> cmfs = ( ... MSDS_CMFS['CIE 1931 2 Degree Standard Observer']. ... copy().align(SpectralShape(360, 780, 10)) ... ) >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape) >>> LUT = LUT3D_Jakob2019() >>> print(LUT.interpolator) None >>> LUT.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, 3) ======================================================================\ ========= * \ * * "Jakob et al. (2018)" LUT Optimisation \ * * \ * ======================================================================\ ========= <BLANKLINE> Optimising 27 coefficients... <BLANKLINE> >>> print(LUT.interpolator) ... # doctest: +ELLIPSIS <scipy.interpolate...RegularGridInterpolator object at 0x...> """ cmfs, illuminant = handle_spectral_arguments( cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019 ) shape = cmfs.shape xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs)) # It could be interesting to have different resolutions for lightness # and chromaticity, but the current file format doesn't allow it. lightness_steps = size chroma_steps = size self._lightness_scale = lightness_scale(lightness_steps) self._coefficients = np.empty( [3, chroma_steps, chroma_steps, lightness_steps, 3] ) cube_indexes = np.ndindex(3, chroma_steps, chroma_steps) total_coefficients = chroma_steps**2 * 3 # First, create a list of all the fully bright colours with the order # matching cube_indexes. samples = np.linspace(0, 1, chroma_steps) ij = np.reshape( np.transpose(np.meshgrid([1], samples, samples, indexing="ij")), (-1, 3), ) chromas = np.concatenate( [ as_float_array(ij), np.roll(ij, 1, axis=1), np.roll(ij, 2, axis=1), ] ) message_box( '"Jakob et al. (2018)" LUT Optimisation', print_callable=print_callable, ) print_callable(f"\nOptimising {total_coefficients} coefficients...\n") def optimize(ijkL: ArrayLike, coefficients_0: ArrayLike) -> NDArray: """ Solve for a specific lightness and stores the result in the appropriate cell. """ i, j, k, L = tsplit(ijkL, dtype=DEFAULT_INT_DTYPE) RGB = self._lightness_scale[L] * chroma XYZ = RGB_to_XYZ( RGB, colourspace.whitepoint, xy_n, colourspace.matrix_RGB_to_XYZ, ) coefficients, _error = find_coefficients_Jakob2019( XYZ, cmfs, illuminant, coefficients_0, dimensionalise=False ) self._coefficients[i, L, j, k, :] = dimensionalise_coefficients( coefficients, shape ) return coefficients with tqdm(total=total_coefficients) as progress: for ijk, chroma in zip(cube_indexes, chromas): progress.update() # Starts from somewhere in the middle, similarly to how # feedback works in "colour.recovery.\ # find_coefficients_Jakob2019" definition. L_middle = lightness_steps // 3 coefficients_middle = optimize( np.hstack([ijk, L_middle]), zeros(3) ) # Down the lightness scale. coefficients_0 = coefficients_middle for L in reversed(range(0, L_middle)): coefficients_0 = optimize( np.hstack([ijk, L]), coefficients_0 ) # Up the lightness scale. coefficients_0 = coefficients_middle for L in range(L_middle + 1, lightness_steps): coefficients_0 = optimize( np.hstack([ijk, L]), coefficients_0 ) self._size = size self._create_interpolator()
def error_function( coefficients: ArrayLike, target: ArrayLike, cmfs: MultiSpectralDistributions, illuminant: SpectralDistribution, max_error: Optional[Floating] = None, additional_data: Boolean = False, ) -> Union[ Tuple[Floating, NDArray], Tuple[Floating, NDArray, NDArray, NDArray, NDArray], ]: """ Compute :math:`\\Delta E_{76}` between the target colour and the colour defined by given spectral model, along with its gradient. Parameters ---------- coefficients Dimensionless coefficients for *Jakob and Hanika (2019)* reflectance spectral model. target *CIE L\\*a\\*b\\** colourspace array of the target colour. cmfs Standard observer colour matching functions. illuminant Illuminant spectral distribution. max_error Raise ``StopMinimizationEarly`` if the error is smaller than this. The default is *None* and the function doesn't raise anything. additional_data If *True*, some intermediate calculations are returned, for use in correctness tests: R, XYZ and Lab. Returns ------- :class:`tuple` or :class:`tuple` Tuple of computed :math:`\\Delta E_{76}` error and gradient of error, i.e. the first derivatives of error with respect to the input coefficients or tuple of computed :math:`\\Delta E_{76}` error, gradient of error, computed spectral reflectance, *CIE XYZ* tristimulus values corresponding to ``R`` and *CIE L\\*a\\*b\\** colourspace array corresponding to ``XYZ``. Raises ------ StopMinimizationEarly Raised when the error is below ``max_error``. """ target = as_float_array(target) c_0, c_1, c_2 = as_float_array(coefficients) wv = np.linspace(0, 1, len(cmfs.shape)) U = c_0 * wv**2 + c_1 * wv + c_2 t1 = np.sqrt(1 + U**2) R = 1 / 2 + U / (2 * t1) t2 = 1 / (2 * t1) - U**2 / (2 * t1**3) dR = np.array([wv**2 * t2, wv * t2, t2]) XYZ = sd_to_XYZ_integration(R, cmfs, illuminant, shape=cmfs.shape) / 100 dXYZ = np.transpose( sd_to_XYZ_integration(dR, cmfs, illuminant, shape=cmfs.shape) / 100 ) XYZ_n = sd_to_XYZ_integration(illuminant, cmfs) XYZ_n /= XYZ_n[1] XYZ_XYZ_n = XYZ / XYZ_n XYZ_f = intermediate_lightness_function_CIE1976(XYZ, XYZ_n) dXYZ_f = np.where( XYZ_XYZ_n[..., np.newaxis] > (24 / 116) ** 3, 1 / ( 3 * spow(XYZ_n[..., np.newaxis], 1 / 3) * spow(XYZ[..., np.newaxis], 2 / 3) ) * dXYZ, (841 / 108) * dXYZ / XYZ_n[..., np.newaxis], ) def intermediate_XYZ_to_Lab( XYZ_i: NDArray, offset: Optional[Floating] = 16 ) -> NDArray: """ Return the final intermediate value for the *CIE Lab* to *CIE XYZ* conversion. """ return np.array( [ 116 * XYZ_i[1] - offset, 500 * (XYZ_i[0] - XYZ_i[1]), 200 * (XYZ_i[1] - XYZ_i[2]), ] ) Lab_i = intermediate_XYZ_to_Lab(as_float_array(XYZ_f)) dLab_i = intermediate_XYZ_to_Lab(as_float_array(dXYZ_f), 0) error = np.sqrt(np.sum((Lab_i - target) ** 2)) if max_error is not None and error <= max_error: raise StopMinimizationEarly(coefficients, error) derror = ( np.sum( dLab_i * (Lab_i[..., np.newaxis] - target[..., np.newaxis]), axis=0 ) / error ) if additional_data: return error, derror, R, XYZ, Lab_i else: return error, derror