diff --git a/BIBLIOGRAPHY.bib b/BIBLIOGRAPHY.bib index dc3403b0fe..a5c78835e4 100644 --- a/BIBLIOGRAPHY.bib +++ b/BIBLIOGRAPHY.bib @@ -199,6 +199,16 @@ @inproceedings{Barten2003 keywords = {contrast sensitivity,csf,human eye,orientation angle,standard observer,surround luminance} } +@techreport{BBCResearchDepartment1988, + title = {The Assessment of the Colorimetric Properties of Light Sources for Use in Television Scene Lighting}, + author = {Taylor, E. W.}, + year = 1988, + institution = {BBC Research Department}, + number = {BBC R\&D Report 1988/2}, + urldate = {2026-06-04}, + howpublished = {http://downloads.bbc.co.uk/rd/pubs/reports/1988-02.pdf} +} + @article{Bianco2010a, title = {Two New von {{Kries}} Based Chromatic Adaptation Transforms Found by Numerical Optimization}, author = {Bianco, S. and Schettini, R.}, @@ -997,6 +1007,16 @@ @misc{EuropeanBroadcastingUnion1975 month = aug } +@techreport{EuropeanBroadcastingUnion2017, + title = {Method for the Assessment of the Colorimetric Properties of Luminaires: The {{Television Lighting Consistency Index}} ({{TLCI-2012}}) and the {{Television Luminaire Matching Factor}} ({{TLMF-2013}})}, + author = {Prokopczuk, Joanna and Roe, Trevor}, + year = 2017, + institution = {European Broadcasting Union}, + number = {Tech 3355}, + urldate = {2026-06-04}, + howpublished = {https://tech.ebu.ch/publications/tech3355} +} + @misc{EuropeanColorInitiative2002a, title = {{{ECI RGB}} V2}, author = {{European Color Initiative}}, diff --git a/colour/__init__.py b/colour/__init__.py index 779bb58798..727a515b78 100644 --- a/colour/__init__.py +++ b/colour/__init__.py @@ -455,10 +455,14 @@ COLOUR_FIDELITY_INDEX_METHODS, COLOUR_QUALITY_SCALE_METHODS, COLOUR_RENDERING_INDEX_METHODS, + ColourQuality_Specification_TLCI2012, + ColourQuality_Specification_TLMF2013, colour_fidelity_index, colour_quality_scale, colour_rendering_index, spectral_similarity_index, + television_lighting_consistency_index, + television_luminaire_matching_factor, ) from .recovery import XYZ_TO_MSDS_METHODS, XYZ_TO_SD_METHODS, XYZ_to_msds, XYZ_to_sd from .temperature import ( @@ -885,10 +889,14 @@ "COLOUR_FIDELITY_INDEX_METHODS", "COLOUR_QUALITY_SCALE_METHODS", "COLOUR_RENDERING_INDEX_METHODS", + "ColourQuality_Specification_TLCI2012", + "ColourQuality_Specification_TLMF2013", "colour_fidelity_index", "colour_quality_scale", "colour_rendering_index", "spectral_similarity_index", + "television_lighting_consistency_index", + "television_luminaire_matching_factor", ] __all__ += [ "XYZ_TO_MSDS_METHODS", diff --git a/colour/examples/quality/examples_tlci.py b/colour/examples/quality/examples_tlci.py new file mode 100644 index 0000000000..276a447ad9 --- /dev/null +++ b/colour/examples/quality/examples_tlci.py @@ -0,0 +1,18 @@ +""" +Demonstrate *Television Lighting Consistency Index* (TLCI-2012) computations. +""" + +import colour +from colour.utilities import message_box + +message_box("Television Lighting Consistency Index (TLCI-2012) Computations") + +message_box('Computing TLCI-2012 for "FL2".') +print(colour.television_lighting_consistency_index(colour.SDS_ILLUMINANTS["FL2"])) + +message_box("Fetching TLCI-2012 additional data.") +spec = colour.television_lighting_consistency_index( + colour.SDS_ILLUMINANTS["FL2"], + additional_data=True, +) +print(spec) diff --git a/colour/examples/quality/examples_tlmf.py b/colour/examples/quality/examples_tlmf.py new file mode 100644 index 0000000000..9a71a19f13 --- /dev/null +++ b/colour/examples/quality/examples_tlmf.py @@ -0,0 +1,23 @@ +""" +Demonstrate *Television Luminaire Matching Factor* (TLMF-2013) computations. +""" + +import colour +from colour.utilities import message_box + +message_box("Television Luminaire Matching Factor (TLMF-2013) Computations") + +message_box('Computing TLMF-2013 for "FL2" vs "D65".') +print( + colour.television_luminaire_matching_factor( + colour.SDS_ILLUMINANTS["FL2"], colour.SDS_ILLUMINANTS["D65"] + ) +) + +message_box("Fetching TLMF-2013 additional data.") +spec = colour.television_luminaire_matching_factor( + colour.SDS_ILLUMINANTS["FL2"], + colour.SDS_ILLUMINANTS["D65"], + additional_data=True, +) +print(spec) diff --git a/colour/quality/__init__.py b/colour/quality/__init__.py index e00d1a6ee4..4dc2dc7da2 100644 --- a/colour/quality/__init__.py +++ b/colour/quality/__init__.py @@ -25,6 +25,12 @@ ) from .datasets import * # noqa: F403 from .ssi import spectral_similarity_index +from .tlci import ( + ColourQuality_Specification_TLCI2012, + ColourQuality_Specification_TLMF2013, + television_lighting_consistency_index, + television_luminaire_matching_factor, +) from .tm3018 import ( ColourQuality_Specification_ANSIIESTM3018, colour_fidelity_index_ANSIIESTM3018, @@ -52,6 +58,14 @@ "ColourQuality_Specification_ANSIIESTM3018", "colour_fidelity_index_ANSIIESTM3018", ] +__all__ += [ + "ColourQuality_Specification_TLCI2012", + "television_lighting_consistency_index", +] +__all__ += [ + "ColourQuality_Specification_TLMF2013", + "television_luminaire_matching_factor", +] COLOUR_FIDELITY_INDEX_METHODS = CanonicalMapping( { diff --git a/colour/quality/datasets/__init__.py b/colour/quality/datasets/__init__.py index 8b90eeb47d..aacc4e331c 100644 --- a/colour/quality/datasets/__init__.py +++ b/colour/quality/datasets/__init__.py @@ -1,9 +1,39 @@ from .tcs import SDS_TCS +from .tlci2012 import ( + DATA_CAMERA_SENSITIVITIES_TLCI2012, + DATA_DAYLIGHT_BASIS_TLCI2012, + DATA_DAYLIGHT_LOCUS_TLCI2012, + DATA_PLANCKIAN_LOCUS_TLCI2012, + DATA_TCS_TLCI2012, + MATRIX_TLCI2012_CAMERA, + MATRIX_TLCI2012_DISPLAY, + MATRIX_TLCI2012_SATURATION, + MSDS_CAMERA_SENSITIVITIES_TLCI2012, + MSDS_DAYLIGHT_BASIS_TLCI2012, + NAMES_TCS_TLCI2012, + SDS_TCS_TLCI2012, + SPECTRAL_SHAPE_TLCI2012, +) from .vs import SDS_VS __all__ = [ "SDS_TCS", ] +__all__ += [ + "DATA_CAMERA_SENSITIVITIES_TLCI2012", + "DATA_DAYLIGHT_BASIS_TLCI2012", + "DATA_DAYLIGHT_LOCUS_TLCI2012", + "DATA_PLANCKIAN_LOCUS_TLCI2012", + "DATA_TCS_TLCI2012", + "MATRIX_TLCI2012_CAMERA", + "MATRIX_TLCI2012_DISPLAY", + "MATRIX_TLCI2012_SATURATION", + "MSDS_CAMERA_SENSITIVITIES_TLCI2012", + "MSDS_DAYLIGHT_BASIS_TLCI2012", + "NAMES_TCS_TLCI2012", + "SDS_TCS_TLCI2012", + "SPECTRAL_SHAPE_TLCI2012", +] __all__ += [ "SDS_VS", ] diff --git a/colour/quality/datasets/tlci2012.py b/colour/quality/datasets/tlci2012.py new file mode 100644 index 0000000000..25b9253385 --- /dev/null +++ b/colour/quality/datasets/tlci2012.py @@ -0,0 +1,2624 @@ +""" +TLCI-2012 Datasets +================== + +Define the datasets for the *Television Lighting Consistency Index 2012* +(TLCI-2012) and *Television Luminaire Matching Factor 2013* (TLMF-2013) +computations. + +- :attr:`colour.quality.datasets.tlci2012.SPECTRAL_SHAPE_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.MATRIX_TLCI2012_CAMERA` +- :attr:`colour.quality.datasets.tlci2012.MATRIX_TLCI2012_SATURATION` +- :attr:`colour.quality.datasets.tlci2012.MATRIX_TLCI2012_DISPLAY` +- :attr:`colour.quality.datasets.tlci2012.DATA_CAMERA_SENSITIVITIES_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.MSDS_CAMERA_SENSITIVITIES_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.DATA_PLANCKIAN_LOCUS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.DATA_DAYLIGHT_LOCUS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.DATA_DAYLIGHT_BASIS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.MSDS_DAYLIGHT_BASIS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.DATA_TCS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.NAMES_TCS_TLCI2012` +- :attr:`colour.quality.datasets.tlci2012.SDS_TCS_TLCI2012` + +References +---------- +- :cite:`EuropeanBroadcastingUnion2017` : Prokopczuk, J., & Roe, T. (2017). + Method for the Assessment of the Colorimetric Properties of Luminaires: + The Television Lighting Consistency Index (TLCI-2012) and the Television + Luminaire Matching Factor (TLMF-2013). EBU Technical Document Tech 3355. + https://tech.ebu.ch/publications/tech3355 +""" + +from __future__ import annotations + +import typing +from functools import partial + +import numpy as np + +from colour.characterisation import RGB_CameraSensitivities +from colour.colorimetry import ( + MultiSpectralDistributions, + SpectralDistribution, + SpectralShape, +) +from colour.utilities import CanonicalMapping, LazyCanonicalMapping + +if typing.TYPE_CHECKING: + from colour.hints import NDArrayFloat + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "SPECTRAL_SHAPE_TLCI2012", + "MATRIX_TLCI2012_CAMERA", + "MATRIX_TLCI2012_SATURATION", + "MATRIX_TLCI2012_DISPLAY", + "DATA_CAMERA_SENSITIVITIES_TLCI2012", + "MSDS_CAMERA_SENSITIVITIES_TLCI2012", + "DATA_DAYLIGHT_BASIS_TLCI2012", + "MSDS_DAYLIGHT_BASIS_TLCI2012", + "DATA_PLANCKIAN_LOCUS_TLCI2012", + "DATA_DAYLIGHT_LOCUS_TLCI2012", + "DATA_TCS_TLCI2012", + "NAMES_TCS_TLCI2012", + "SDS_TCS_TLCI2012", +] + +SPECTRAL_SHAPE_TLCI2012: SpectralShape = SpectralShape(380, 760, 5) +""" +Spectral shape for *TLCI-2012* computations: 380-760 nm, 5 nm interval. + +*EBU Tech 3355* section 1.2, following equation [18], defines this spectral +range and interval for all *TLCI-2012* and *TLMF-2013* mathematics. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +MATRIX_TLCI2012_CAMERA: NDArrayFloat = np.array( + [ + [1.182, -0.209, 0.027], + [0.107, 0.890, 0.003], + [0.040, -0.134, 1.094], + ] +) +""" +3x3 linear camera matrix :math:`M` for *TLCI-2012*. + +This matrix transforms the camera *RGB* signal (from the EBU standard camera +curves) towards a more colour-accurate representation, optimised for use with +the non-linear OETF and display pipeline. + +*EBU Tech 3355* section 1.3.2 defines the matrix operation in equations +[20]-[22]; the matrix terms are tabulated in Appendix 5. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +MATRIX_TLCI2012_SATURATION: NDArrayFloat = np.array( + [ + [14 / 15, 1 / 30, 1 / 30], + [1 / 30, 14 / 15, 1 / 30], + [1 / 30, 1 / 30, 14 / 15], + ] +) +""" +3x3 saturation matrix for *TLCI-2012* at 90% saturation level. + +*EBU Tech 3355* section 1.3.2, equations [23]-[25], defines the saturation +matrix. Using the specification's percentage notation, :math:`S = 90` and +:math:`a = (1 - S / 100) / 3 = 1 / 30`. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +MATRIX_TLCI2012_DISPLAY: NDArrayFloat = np.array( + [ + [0.412391, 0.357584, 0.180481], + [0.212639, 0.715169, 0.072192], + [0.019331, 0.119195, 0.950532], + ] +) +""" +*BT.709* linear *RGB* to *CIE XYZ* display synthesis matrix for *TLCI-2012*. + +*EBU Tech 3355* section 1.4.2 gives the *BT.709* primaries and D65 white in +Table 1 and defines this matrix in equation [29]. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +# fmt: off +DATA_CAMERA_SENSITIVITIES_TLCI2012: NDArrayFloat = np.array( + [ + # [r-bar, g-bar, b-bar] - 380 to 760 nm, 5 nm steps (77 rows) + # Source: EBU Tech 3355, Appendix 5 (derived from EBU Tech 3353) + [0.000000, 0.000000, 0.000000], # 380 + [0.000000, 0.000000, 0.000215], # 385 + [0.000000, 0.000000, 0.000649], # 390 + [0.000000, 0.000000, 0.001397], # 395 + [0.000000, 0.000000, 0.003902], # 400 + [0.000000, 0.000000, 0.007905], # 405 + [0.000450, 0.000110, 0.014223], # 410 + [0.001000, 0.000211, 0.021958], # 415 + [0.001230, 0.000321, 0.029294], # 420 + [0.001300, 0.000482, 0.036253], # 425 + [0.001300, 0.000562, 0.042739], # 430 + [0.001190, 0.000633, 0.048916], # 435 + [0.001120, 0.000753, 0.055513], # 440 + [0.001040, 0.000934, 0.061881], # 445 + [0.000930, 0.001175, 0.068628], # 450 + [0.000860, 0.001386, 0.073179], # 455 + [0.000820, 0.001777, 0.075694], # 460 + [0.000820, 0.002239, 0.077461], # 465 + [0.000890, 0.002902, 0.078010], # 470 + [0.000890, 0.003555, 0.074377], # 475 + [0.000970, 0.004488, 0.066582], # 480 + [0.000890, 0.006175, 0.053601], # 485 + [0.000890, 0.010664, 0.036619], # 490 + [0.000890, 0.021297, 0.022597], # 495 + [0.000930, 0.036343, 0.012366], # 500 + [0.001040, 0.046681, 0.008274], # 505 + [0.001120, 0.054634, 0.006138], # 510 + [0.001340, 0.059594, 0.004461], # 515 + [0.001490, 0.062114, 0.003254], # 520 + [0.001710, 0.063761, 0.002455], # 525 + [0.001790, 0.065066, 0.001996], # 530 + [0.001670, 0.066341, 0.001707], # 535 + [0.001410, 0.067426, 0.001407], # 540 + [0.001150, 0.068018, 0.001228], # 545 + [0.001120, 0.068109, 0.000998], # 550 + [0.001190, 0.066994, 0.000928], # 555 + [0.001710, 0.063530, 0.000779], # 560 + [0.005920, 0.054222, 0.000599], # 565 + [0.015630, 0.041319, 0.000489], # 570 + [0.032310, 0.025210, 0.000369], # 575 + [0.054310, 0.013264, 0.000329], # 580 + [0.067330, 0.007350, 0.000260], # 585 + [0.072830, 0.004418, 0.000190], # 590 + [0.073060, 0.002611, 0.000110], # 595 + [0.071650, 0.001707, 0.000070], # 600 + [0.068640, 0.001084, 0.000000], # 605 + [0.063800, 0.000482, 0.000000], # 610 + [0.059260, 0.000030, 0.000000], # 615 + [0.054600, 0.000011, 0.000000], # 620 + [0.049390, 0.000008, 0.000000], # 625 + [0.044370, 0.000006, 0.000000], # 630 + [0.039570, 0.000003, 0.000000], # 635 + [0.034540, 0.000000, 0.000000], # 640 + [0.029440, 0.000000, 0.000000], # 645 + [0.025030, 0.000000, 0.000000], # 650 + [0.021030, 0.000000, 0.000000], # 655 + [0.017900, 0.000000, 0.000000], # 660 + [0.014960, 0.000000, 0.000000], # 665 + [0.012170, 0.000000, 0.000000], # 670 + [0.009710, 0.000000, 0.000000], # 675 + [0.007740, 0.000000, 0.000000], # 680 + [0.005880, 0.000000, 0.000000], # 685 + [0.004350, 0.000000, 0.000000], # 690 + [0.002900, 0.000000, 0.000000], # 695 + [0.002000, 0.000000, 0.000000], # 700 + [0.001340, 0.000000, 0.000000], # 705 + [0.000950, 0.000000, 0.000000], # 710 + [0.000710, 0.000000, 0.000000], # 715 + [0.000530, 0.000000, 0.000000], # 720 + [0.000440, 0.000000, 0.000000], # 725 + [0.000310, 0.000000, 0.000000], # 730 + [0.000190, 0.000000, 0.000000], # 735 + [0.000060, 0.000000, 0.000000], # 740 + [0.000000, 0.000000, 0.000000], # 745 + [0.000000, 0.000000, 0.000000], # 750 + [0.000000, 0.000000, 0.000000], # 755 + [0.000000, 0.000000, 0.000000], # 760 + ] +) +""" +*EBU* standard camera spectral sensitivities data array (77 x 3) for +*TLCI-2012*. + +Columns are :math:`\\bar{r}`, :math:`\\bar{g}`, :math:`\\bar{b}` from +380 nm to 760 nm at 5 nm intervals. Each column sums to 1.000000. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +# fmt: off +DATA_PLANCKIAN_LOCUS_TLCI2012: NDArrayFloat = np.array( + [ + [1000.0, 0.652355, 0.344814], + [1020.0, 0.649530, 0.347301], + [1040.0, 0.646716, 0.349749], + [1060.0, 0.643912, 0.352157], + [1080.0, 0.641119, 0.354524], + [1100.0, 0.638337, 0.356848], + [1120.0, 0.635567, 0.359128], + [1140.0, 0.632808, 0.361364], + [1160.0, 0.630061, 0.363554], + [1180.0, 0.627326, 0.365697], + [1200.0, 0.624603, 0.367794], + [1220.0, 0.621892, 0.369843], + [1240.0, 0.619192, 0.371844], + [1260.0, 0.616505, 0.373797], + [1280.0, 0.613830, 0.375701], + [1300.0, 0.611167, 0.377555], + [1320.0, 0.608516, 0.379361], + [1340.0, 0.605878, 0.381117], + [1360.0, 0.603251, 0.382823], + [1380.0, 0.600637, 0.384480], + [1400.0, 0.598035, 0.386088], + [1420.0, 0.595445, 0.387646], + [1440.0, 0.592868, 0.389156], + [1460.0, 0.590303, 0.390617], + [1480.0, 0.587751, 0.392029], + [1500.0, 0.585212, 0.393393], + [1520.0, 0.582684, 0.394709], + [1540.0, 0.580170, 0.395977], + [1560.0, 0.577669, 0.397199], + [1580.0, 0.575180, 0.398374], + [1600.0, 0.572705, 0.399503], + [1620.0, 0.570242, 0.400586], + [1640.0, 0.567793, 0.401625], + [1660.0, 0.565357, 0.402619], + [1680.0, 0.562935, 0.403570], + [1700.0, 0.560526, 0.404477], + [1720.0, 0.558131, 0.405342], + [1740.0, 0.555749, 0.406166], + [1760.0, 0.553382, 0.406948], + [1780.0, 0.551028, 0.407689], + [1800.0, 0.548689, 0.408391], + [1820.0, 0.546364, 0.409054], + [1840.0, 0.544054, 0.409679], + [1860.0, 0.541758, 0.410266], + [1880.0, 0.539476, 0.410817], + [1900.0, 0.537210, 0.411331], + [1920.0, 0.534958, 0.411810], + [1940.0, 0.532721, 0.412254], + [1960.0, 0.530499, 0.412665], + [1980.0, 0.528292, 0.413043], + [2000.0, 0.526100, 0.413388], + [2020.0, 0.523924, 0.413702], + [2040.0, 0.521763, 0.413985], + [2060.0, 0.519618, 0.414238], + [2080.0, 0.517488, 0.414461], + [2100.0, 0.515373, 0.414656], + [2120.0, 0.513275, 0.414823], + [2140.0, 0.511192, 0.414963], + [2160.0, 0.509124, 0.415076], + [2180.0, 0.507073, 0.415164], + [2200.0, 0.505037, 0.415227], + [2220.0, 0.503017, 0.415265], + [2240.0, 0.501013, 0.415279], + [2260.0, 0.499025, 0.415271], + [2280.0, 0.497053, 0.415240], + [2300.0, 0.495096, 0.415187], + [2320.0, 0.493156, 0.415113], + [2340.0, 0.491231, 0.415019], + [2360.0, 0.489323, 0.414905], + [2380.0, 0.487430, 0.414772], + [2400.0, 0.485553, 0.414620], + [2420.0, 0.483692, 0.414450], + [2440.0, 0.481847, 0.414262], + [2460.0, 0.480017, 0.414058], + [2480.0, 0.478204, 0.413837], + [2500.0, 0.476406, 0.413600], + [2520.0, 0.474624, 0.413348], + [2540.0, 0.472857, 0.413081], + [2560.0, 0.471106, 0.412800], + [2580.0, 0.469370, 0.412505], + [2600.0, 0.467650, 0.412197], + [2620.0, 0.465945, 0.411876], + [2640.0, 0.464256, 0.411543], + [2660.0, 0.462582, 0.411197], + [2680.0, 0.460923, 0.410841], + [2700.0, 0.459279, 0.410473], + [2720.0, 0.457650, 0.410094], + [2740.0, 0.456036, 0.409705], + [2760.0, 0.454437, 0.409307], + [2780.0, 0.452853, 0.408899], + [2800.0, 0.451283, 0.408482], + [2820.0, 0.449728, 0.408056], + [2840.0, 0.448187, 0.407622], + [2860.0, 0.446661, 0.407179], + [2880.0, 0.445149, 0.406730], + [2900.0, 0.443652, 0.406273], + [2920.0, 0.442168, 0.405808], + [2940.0, 0.440698, 0.405338], + [2960.0, 0.439243, 0.404860], + [2980.0, 0.437801, 0.404377], + [3000.0, 0.436373, 0.403888], + [3010.0, 0.435664, 0.403641], + [3020.0, 0.434958, 0.403394], + [3030.0, 0.434256, 0.403144], + [3040.0, 0.433557, 0.402894], + [3050.0, 0.432861, 0.402642], + [3060.0, 0.432169, 0.402389], + [3070.0, 0.431480, 0.402135], + [3080.0, 0.430795, 0.401879], + [3090.0, 0.430112, 0.401623], + [3100.0, 0.429433, 0.401365], + [3110.0, 0.428758, 0.401107], + [3120.0, 0.428085, 0.400847], + [3130.0, 0.427416, 0.400587], + [3140.0, 0.426750, 0.400325], + [3150.0, 0.426087, 0.400062], + [3160.0, 0.425427, 0.399799], + [3170.0, 0.424770, 0.399535], + [3180.0, 0.424117, 0.399269], + [3190.0, 0.423467, 0.399003], + [3200.0, 0.422820, 0.398736], + [3210.0, 0.422176, 0.398469], + [3220.0, 0.421535, 0.398200], + [3230.0, 0.420897, 0.397931], + [3240.0, 0.420263, 0.397662], + [3250.0, 0.419631, 0.397391], + [3300.0, 0.416518, 0.396029], + [3350.0, 0.413480, 0.394653], + [3400.0, 0.410515, 0.393267], + [3450.0, 0.407620, 0.391871], + [3500.0, 0.404795, 0.390470], + [3550.0, 0.402037, 0.389064], + [3600.0, 0.399345, 0.387655], + [3650.0, 0.396717, 0.386246], + [3700.0, 0.394152, 0.384837], + [3750.0, 0.391648, 0.383431], + [3800.0, 0.389203, 0.382028], + [3850.0, 0.386816, 0.380630], + [3900.0, 0.384485, 0.379238], + [3950.0, 0.382208, 0.377853], + [4000.0, 0.379985, 0.376475], + [4100.0, 0.375693, 0.373747], + [4200.0, 0.371597, 0.371058], + [4300.0, 0.367687, 0.368413], + [4400.0, 0.363953, 0.365816], + [4500.0, 0.360385, 0.363269], + [4600.0, 0.356975, 0.360774], + [4700.0, 0.353713, 0.358334], + [4800.0, 0.350593, 0.355947], + [4900.0, 0.347606, 0.353616], + [4999.0, 0.344774, 0.351363], + [5000.0, 0.344746, 0.351341], + ] +) +# fmt: on +""" +*EBU Tech 3355* Appendix 2 Planckian radiator chromaticities. + +Columns are CCT, :math:`x`, and :math:`y`. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +# fmt: off +DATA_DAYLIGHT_LOCUS_TLCI2012: NDArrayFloat = np.array( + [ + [5000.0, 0.345747, 0.358680], + [5001.0, 0.345718, 0.358657], + [5100.0, 0.342875, 0.356370], + [5200.0, 0.340120, 0.354109], + [5300.0, 0.337478, 0.351897], + [5400.0, 0.334942, 0.349735], + [5500.0, 0.332508, 0.347624], + [5600.0, 0.330171, 0.345563], + [5700.0, 0.327926, 0.343552], + [5800.0, 0.325769, 0.341592], + [5900.0, 0.323694, 0.339680], + [6000.0, 0.321699, 0.337818], + [6020.0, 0.321309, 0.337451], + [6040.0, 0.320923, 0.337086], + [6060.0, 0.320539, 0.336723], + [6080.0, 0.320158, 0.336362], + [6100.0, 0.319780, 0.336003], + [6120.0, 0.319405, 0.335646], + [6140.0, 0.319032, 0.335290], + [6160.0, 0.318663, 0.334937], + [6180.0, 0.318296, 0.334585], + [6200.0, 0.317932, 0.334235], + [6220.0, 0.317571, 0.333887], + [6240.0, 0.317212, 0.333541], + [6260.0, 0.316857, 0.333197], + [6300.0, 0.316153, 0.332514], + [6320.0, 0.315805, 0.332175], + [6340.0, 0.315460, 0.331838], + [6360.0, 0.315117, 0.331503], + [6380.0, 0.314777, 0.331169], + [6400.0, 0.314439, 0.330837], + [6410.0, 0.314271, 0.330672], + [6420.0, 0.314104, 0.330508], + [6430.0, 0.313937, 0.330343], + [6440.0, 0.313771, 0.330179], + [6450.0, 0.313606, 0.330016], + [6460.0, 0.313441, 0.329853], + [6470.0, 0.313277, 0.329690], + [6480.0, 0.313113, 0.329528], + [6490.0, 0.312950, 0.329367], + [6500.0, 0.312787, 0.329205], + [6510.0, 0.312626, 0.329045], + [6520.0, 0.312464, 0.328884], + [6530.0, 0.312304, 0.328724], + [6540.0, 0.312144, 0.328565], + [6550.0, 0.311984, 0.328406], + [6560.0, 0.311825, 0.328247], + [6570.0, 0.311667, 0.328089], + [6580.0, 0.311509, 0.327931], + [6590.0, 0.311352, 0.327773], + [6600.0, 0.311195, 0.327616], + [6620.0, 0.310883, 0.327304], + [6640.0, 0.310574, 0.326993], + [6660.0, 0.310267, 0.326683], + [6680.0, 0.309962, 0.326376], + [6700.0, 0.309659, 0.326070], + [6720.0, 0.309359, 0.325765], + [6740.0, 0.309060, 0.325462], + [6760.0, 0.308764, 0.325161], + [6780.0, 0.308470, 0.324862], + [6800.0, 0.308178, 0.324564], + [6820.0, 0.307888, 0.324267], + [6840.0, 0.307600, 0.323973], + [6860.0, 0.307314, 0.323679], + [6880.0, 0.307030, 0.323388], + [6900.0, 0.306748, 0.323098], + [6920.0, 0.306467, 0.322809], + [6940.0, 0.306189, 0.322522], + [6960.0, 0.305913, 0.322237], + [6980.0, 0.305639, 0.321953], + [7000.0, 0.305366, 0.321670], + [7100.0, 0.304026, 0.320275], + [7200.0, 0.302732, 0.318916], + [7300.0, 0.301481, 0.317594], + [7400.0, 0.300271, 0.316306], + [7500.0, 0.299101, 0.315052], + [7600.0, 0.297968, 0.313830], + [7700.0, 0.296872, 0.312640], + [7800.0, 0.295809, 0.311480], + [7900.0, 0.294780, 0.310350], + [8000.0, 0.293782, 0.309248], + [8250.0, 0.291417, 0.306613], + [8500.0, 0.289221, 0.304137], + [8750.0, 0.287179, 0.301807], + [9000.0, 0.285276, 0.299614], + [9250.0, 0.283498, 0.297545], + [9500.0, 0.281834, 0.295593], + [9750.0, 0.280275, 0.293747], + [10000.0, 0.278811, 0.292002], + [11000.0, 0.273760, 0.285879], + [12000.0, 0.269721, 0.280874], + [13000.0, 0.266429, 0.276722], + [14000.0, 0.263701, 0.273232], + [15000.0, 0.261409, 0.270265], + [16000.0, 0.259459, 0.267716], + [17000.0, 0.257782, 0.265506], + [18000.0, 0.256327, 0.263575], + [19000.0, 0.255053, 0.261874], + [20000.0, 0.253930, 0.260365], + [21000.0, 0.252933, 0.259020], + [22000.0, 0.252042, 0.257814], + [23000.0, 0.251243, 0.256726], + [24000.0, 0.250521, 0.255741], + [25000.0, 0.249866, 0.254845], + ] +) +# fmt: on +""" +*EBU Tech 3355* Appendix 2 daylight radiator chromaticities. + +Columns are CCT, :math:`x`, and :math:`y`. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" +# fmt: off +DATA_DAYLIGHT_BASIS_TLCI2012: NDArrayFloat = np.array( + [ + # [S0, S1, S2] - 380 to 760 nm, 5 nm steps (77 rows) + # Source: EBU Tech 3355, Appendix 3. + [63.4, 38.5, 3.0], # 380 + [62.45000, 35.98125, 2.05], # 385 + [65.8, 35.0, 1.2], # 390 + [79.825, 38.8, -0.1], # 395 + [94.8, 43.4, -1.1], # 400 + [101.54375, 45.525, -0.93125], # 405 + [104.8, 46.3, -0.5], # 410 + [106.54375, 45.70625, -0.53125], # 415 + [105.9, 43.9, -0.7], # 420 + [100.35, 40.375, -0.875], # 425 + [96.8, 37.1, -1.2], # 430 + [104.05, 36.525, -1.9125], # 435 + [113.9, 36.7, -2.6], # 440 + [120.825, 36.48125, -2.84375], # 445 + [125.6, 35.9, -2.9], # 450 + [126.54375, 34.49375, -2.88125], # 455 + [125.5, 32.6, -2.8], # 460 + [123.39375, 30.26875, -2.69375], # 465 + [121.3, 27.9, -2.6], # 470 + [121.525, 26.06875, -2.6375], # 475 + [121.3, 24.3, -2.6], # 480 + [117.425, 22.21875, -2.21875], # 485 + [113.5, 20.1, -1.8], # 490 + [112.95625, 18.075, -1.6125], # 495 + [113.1, 16.2, -1.5], # 500 + [112.19375, 14.74375, -1.3875], # 505 + [110.8, 13.2, -1.3], # 510 + [108.3625, 10.86875, -1.25], # 515 + [106.5, 8.6, -1.2], # 520 + [107.6, 7.18125, -1.125], # 525 + [108.8, 6.1, -1.0], # 530 + [107.25, 5.1375, -0.75], # 535 + [105.3, 4.2, -0.5], # 540 + [104.90625, 3.05, -0.3875], # 545 + [104.4, 1.9, -0.3], # 550 + [102.39375, 0.90625, -0.15], # 555 + [100.0, 0.0, 0.0], # 560 + [97.78125, -0.8, 0.1], # 565 + [96.0, -1.6, 0.2], # 570 + [95.675, -2.65, 0.2625], # 575 + [95.1, -3.5, 0.5], # 580 + [91.95625, -3.475, 1.25], # 585 + [89.1, -3.5, 2.1], # 590 + [89.4375, -4.5625, 2.69375], # 595 + [90.5, -5.8, 3.2], # 600 + [90.60625, -6.55625, 3.68125], # 605 + [90.3, -7.2, 4.1], # 610 + [89.6125, -7.93125, 4.43125], # 615 + [88.4, -8.6, 4.7], # 620 + [86.0125, -9.05, 4.8375], # 625 + [84.0, -9.5, 5.1], # 630 + [84.475, -10.26875, 5.8875], # 635 + [85.1, -10.9, 6.7], # 640 + [83.525, -10.80625, 7.01875], # 645 + [81.9, -10.7, 7.3], # 650 + [81.90625, -11.2125, 7.9125], # 655 + [82.6, -12.0, 8.6], # 660 + [84.01875, -13.10625, 9.25625], # 665 + [84.9, -14.0, 9.8], # 670 + [83.83125, -14.025, 10.19375], # 675 + [81.3, -13.6, 10.2], # 680 + [76.225, -12.69375, 9.19375], # 685 + [71.9, -12.0, 8.3], # 690 + [72.38125, -12.575, 8.9], # 695 + [74.3, -13.3, 9.6], # 700 + [76.31875, -13.325, 9.225], # 705 + [76.4, -12.9, 8.5], # 710 + [69.45625, -11.6625, 7.64375], # 715 + [63.3, -10.6, 7.0], # 720 + [66.35, -10.91875, 7.18125], # 725 + [71.7, -11.6, 7.6], # 730 + [75.6125, -12.0875, 7.91875], # 735 + [77.0, -12.2, 8.0], # 740 + [72.525, -11.3875, 7.46875], # 745 + [65.2, -10.2, 6.7], # 750 + [54.40625, -8.6625, 5.73125], # 755 + [47.7, -7.8, 5.2], # 760 + ] +) +# fmt: on +""" +*EBU Tech 3355* Appendix 3 daylight radiation vector data array (77 x 3). + +Columns are :math:`S_0`, :math:`S_1`, and :math:`S_2` from 380 nm to +760 nm at 5 nm intervals. + +Notes +----- +- *EBU Tech 3355* attributes these daylight radiation vectors to + *CIE 15:2004*, but the tabulated 5 nm values differ from the + *CIE Colorimetry (4th Edition)* Table 6 daylight components: 37 of the + 77 :math:`S_0` samples differ, all at the odd 5 nm wavelengths that are + not part of the original 10 nm *Judd et al. (1964)* anchors (e.g. + :math:`S_0` at 385 nm is 62.45 here versus 64.60 in *CIE Colorimetry*), + while the 10 nm *Judd* anchors match. This dataset therefore reproduces + the *EBU Tech 3355* Appendix 3 table as published, rather than the + library :attr:`colour.colorimetry.SDS_ILLUMINANTS` daylight components. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +MSDS_DAYLIGHT_BASIS_TLCI2012: MultiSpectralDistributions = MultiSpectralDistributions( + DATA_DAYLIGHT_BASIS_TLCI2012, + np.arange(380, 761, 5), + labels=["S0", "S1", "S2"], + name="EBU Tech 3355 Daylight Radiation Vectors", +) +""" +*EBU Tech 3355* Appendix 3 daylight radiation vector multi-spectral +distributions. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + +DATA_TCS_TLCI2012: dict[str, dict[int, float]] = { + "dark skin": { + 380: 0.054, + 385: 0.057, + 390: 0.063, + 395: 0.066, + 400: 0.075, + 405: 0.078, + 410: 0.078, + 415: 0.076, + 420: 0.074, + 425: 0.070, + 430: 0.066, + 435: 0.064, + 440: 0.062, + 445: 0.060, + 450: 0.059, + 455: 0.060, + 460: 0.058, + 465: 0.060, + 470: 0.060, + 475: 0.062, + 480: 0.058, + 485: 0.063, + 490: 0.063, + 495: 0.067, + 500: 0.068, + 505: 0.070, + 510: 0.072, + 515: 0.077, + 520: 0.079, + 525: 0.081, + 530: 0.081, + 535: 0.083, + 540: 0.083, + 545: 0.084, + 550: 0.084, + 555: 0.088, + 560: 0.093, + 565: 0.098, + 570: 0.104, + 575: 0.111, + 580: 0.121, + 585: 0.127, + 590: 0.133, + 595: 0.140, + 600: 0.144, + 605: 0.149, + 610: 0.151, + 615: 0.154, + 620: 0.160, + 625: 0.164, + 630: 0.170, + 635: 0.175, + 640: 0.179, + 645: 0.184, + 650: 0.193, + 655: 0.203, + 660: 0.213, + 665: 0.220, + 670: 0.236, + 675: 0.241, + 680: 0.248, + 685: 0.257, + 690: 0.269, + 695: 0.280, + 700: 0.289, + 705: 0.300, + 710: 0.314, + 715: 0.337, + 720: 0.346, + 725: 0.361, + 730: 0.382, + 735: 0.404, + 740: 0.425, + 745: 0.439, + 750: 0.464, + 755: 0.476, + 760: 0.490, + }, + "light skin": { + 380: 0.092, + 385: 0.109, + 390: 0.134, + 395: 0.161, + 400: 0.186, + 405: 0.200, + 410: 0.205, + 415: 0.206, + 420: 0.207, + 425: 0.209, + 430: 0.211, + 435: 0.213, + 440: 0.216, + 445: 0.221, + 450: 0.227, + 455: 0.237, + 460: 0.246, + 465: 0.259, + 470: 0.273, + 475: 0.285, + 480: 0.294, + 485: 0.304, + 490: 0.305, + 495: 0.309, + 500: 0.314, + 505: 0.323, + 510: 0.334, + 515: 0.340, + 520: 0.332, + 525: 0.316, + 530: 0.300, + 535: 0.292, + 540: 0.290, + 545: 0.295, + 550: 0.300, + 555: 0.302, + 560: 0.297, + 565: 0.295, + 570: 0.304, + 575: 0.328, + 580: 0.365, + 585: 0.409, + 590: 0.450, + 595: 0.488, + 600: 0.520, + 605: 0.540, + 610: 0.556, + 615: 0.566, + 620: 0.574, + 625: 0.582, + 630: 0.593, + 635: 0.602, + 640: 0.607, + 645: 0.625, + 650: 0.631, + 655: 0.639, + 660: 0.655, + 665: 0.661, + 670: 0.687, + 675: 0.693, + 680: 0.711, + 685: 0.722, + 690: 0.737, + 695: 0.757, + 700: 0.768, + 705: 0.786, + 710: 0.798, + 715: 0.815, + 720: 0.822, + 725: 0.823, + 730: 0.835, + 735: 0.845, + 740: 0.855, + 745: 0.848, + 750: 0.862, + 755: 0.861, + 760: 0.868, + }, + "blue sky": { + 380: 0.105, + 385: 0.127, + 390: 0.164, + 395: 0.213, + 400: 0.271, + 405: 0.314, + 410: 0.333, + 415: 0.344, + 420: 0.345, + 425: 0.344, + 430: 0.346, + 435: 0.346, + 440: 0.347, + 445: 0.343, + 450: 0.337, + 455: 0.333, + 460: 0.327, + 465: 0.324, + 470: 0.319, + 475: 0.306, + 480: 0.290, + 485: 0.288, + 490: 0.280, + 495: 0.274, + 500: 0.265, + 505: 0.258, + 510: 0.250, + 515: 0.240, + 520: 0.229, + 525: 0.220, + 530: 0.212, + 535: 0.207, + 540: 0.203, + 545: 0.198, + 550: 0.193, + 555: 0.191, + 560: 0.187, + 565: 0.181, + 570: 0.174, + 575: 0.170, + 580: 0.167, + 585: 0.162, + 590: 0.158, + 595: 0.161, + 600: 0.156, + 605: 0.152, + 610: 0.150, + 615: 0.145, + 620: 0.142, + 625: 0.137, + 630: 0.133, + 635: 0.132, + 640: 0.126, + 645: 0.127, + 650: 0.121, + 655: 0.118, + 660: 0.115, + 665: 0.115, + 670: 0.112, + 675: 0.110, + 680: 0.110, + 685: 0.109, + 690: 0.108, + 695: 0.108, + 700: 0.106, + 705: 0.105, + 710: 0.105, + 715: 0.106, + 720: 0.106, + 725: 0.105, + 730: 0.107, + 735: 0.105, + 740: 0.106, + 745: 0.105, + 750: 0.108, + 755: 0.107, + 760: 0.110, + }, + "foliage": { + 380: 0.050, + 385: 0.052, + 390: 0.052, + 395: 0.050, + 400: 0.052, + 405: 0.052, + 410: 0.052, + 415: 0.053, + 420: 0.051, + 425: 0.053, + 430: 0.053, + 435: 0.053, + 440: 0.055, + 445: 0.058, + 450: 0.059, + 455: 0.061, + 460: 0.060, + 465: 0.063, + 470: 0.063, + 475: 0.067, + 480: 0.065, + 485: 0.067, + 490: 0.069, + 495: 0.072, + 500: 0.077, + 505: 0.088, + 510: 0.105, + 515: 0.132, + 520: 0.159, + 525: 0.182, + 530: 0.195, + 535: 0.199, + 540: 0.191, + 545: 0.180, + 550: 0.167, + 555: 0.156, + 560: 0.144, + 565: 0.133, + 570: 0.131, + 575: 0.130, + 580: 0.129, + 585: 0.123, + 590: 0.118, + 595: 0.114, + 600: 0.110, + 605: 0.102, + 610: 0.101, + 615: 0.103, + 620: 0.104, + 625: 0.105, + 630: 0.105, + 635: 0.106, + 640: 0.102, + 645: 0.102, + 650: 0.101, + 655: 0.101, + 660: 0.101, + 665: 0.101, + 670: 0.107, + 675: 0.115, + 680: 0.132, + 685: 0.152, + 690: 0.185, + 695: 0.233, + 700: 0.283, + 705: 0.339, + 710: 0.383, + 715: 0.419, + 720: 0.444, + 725: 0.445, + 730: 0.465, + 735: 0.473, + 740: 0.477, + 745: 0.480, + 750: 0.489, + 755: 0.492, + 760: 0.498, + }, + "blue flower": { + 380: 0.101, + 385: 0.127, + 390: 0.170, + 395: 0.233, + 400: 0.310, + 405: 0.373, + 410: 0.409, + 415: 0.424, + 420: 0.432, + 425: 0.437, + 430: 0.437, + 435: 0.438, + 440: 0.437, + 445: 0.432, + 450: 0.428, + 455: 0.423, + 460: 0.417, + 465: 0.412, + 470: 0.405, + 475: 0.395, + 480: 0.380, + 485: 0.373, + 490: 0.364, + 495: 0.355, + 500: 0.342, + 505: 0.333, + 510: 0.316, + 515: 0.296, + 520: 0.267, + 525: 0.245, + 530: 0.227, + 535: 0.212, + 540: 0.206, + 545: 0.203, + 550: 0.203, + 555: 0.204, + 560: 0.196, + 565: 0.190, + 570: 0.190, + 575: 0.194, + 580: 0.201, + 585: 0.210, + 590: 0.216, + 595: 0.225, + 600: 0.228, + 605: 0.232, + 610: 0.238, + 615: 0.240, + 620: 0.236, + 625: 0.236, + 630: 0.240, + 635: 0.248, + 640: 0.261, + 645: 0.289, + 650: 0.322, + 655: 0.362, + 660: 0.407, + 665: 0.446, + 670: 0.488, + 675: 0.512, + 680: 0.546, + 685: 0.546, + 690: 0.555, + 695: 0.563, + 700: 0.564, + 705: 0.575, + 710: 0.578, + 715: 0.586, + 720: 0.590, + 725: 0.589, + 730: 0.601, + 735: 0.604, + 740: 0.606, + 745: 0.605, + 750: 0.614, + 755: 0.616, + 760: 0.617, + }, + "bluish green": { + 380: 0.108, + 385: 0.132, + 390: 0.168, + 395: 0.213, + 400: 0.260, + 405: 0.292, + 410: 0.308, + 415: 0.317, + 420: 0.320, + 425: 0.328, + 430: 0.336, + 435: 0.342, + 440: 0.352, + 445: 0.360, + 450: 0.371, + 455: 0.386, + 460: 0.405, + 465: 0.433, + 470: 0.465, + 475: 0.497, + 480: 0.528, + 485: 0.557, + 490: 0.576, + 495: 0.591, + 500: 0.586, + 505: 0.591, + 510: 0.586, + 515: 0.582, + 520: 0.567, + 525: 0.559, + 530: 0.545, + 535: 0.533, + 540: 0.512, + 545: 0.492, + 550: 0.472, + 555: 0.445, + 560: 0.429, + 565: 0.402, + 570: 0.380, + 575: 0.355, + 580: 0.332, + 585: 0.309, + 590: 0.284, + 595: 0.262, + 600: 0.247, + 605: 0.233, + 610: 0.224, + 615: 0.217, + 620: 0.212, + 625: 0.209, + 630: 0.207, + 635: 0.205, + 640: 0.200, + 645: 0.198, + 650: 0.199, + 655: 0.197, + 660: 0.199, + 665: 0.203, + 670: 0.210, + 675: 0.216, + 680: 0.218, + 685: 0.226, + 690: 0.232, + 695: 0.236, + 700: 0.238, + 705: 0.242, + 710: 0.242, + 715: 0.239, + 720: 0.232, + 725: 0.227, + 730: 0.229, + 735: 0.230, + 740: 0.237, + 745: 0.248, + 750: 0.256, + 755: 0.269, + 760: 0.274, + }, + "orange": { + 380: 0.052, + 385: 0.054, + 390: 0.052, + 395: 0.050, + 400: 0.052, + 405: 0.052, + 410: 0.052, + 415: 0.051, + 420: 0.050, + 425: 0.050, + 430: 0.052, + 435: 0.050, + 440: 0.051, + 445: 0.051, + 450: 0.052, + 455: 0.051, + 460: 0.051, + 465: 0.053, + 470: 0.053, + 475: 0.054, + 480: 0.055, + 485: 0.056, + 490: 0.055, + 495: 0.058, + 500: 0.061, + 505: 0.063, + 510: 0.068, + 515: 0.077, + 520: 0.086, + 525: 0.098, + 530: 0.120, + 535: 0.145, + 540: 0.175, + 545: 0.206, + 550: 0.236, + 555: 0.270, + 560: 0.302, + 565: 0.341, + 570: 0.375, + 575: 0.410, + 580: 0.440, + 585: 0.467, + 590: 0.488, + 595: 0.509, + 600: 0.518, + 605: 0.532, + 610: 0.540, + 615: 0.551, + 620: 0.557, + 625: 0.562, + 630: 0.568, + 635: 0.575, + 640: 0.581, + 645: 0.584, + 650: 0.585, + 655: 0.590, + 660: 0.601, + 665: 0.596, + 670: 0.600, + 675: 0.596, + 680: 0.604, + 685: 0.603, + 690: 0.606, + 695: 0.607, + 700: 0.608, + 705: 0.615, + 710: 0.617, + 715: 0.621, + 720: 0.622, + 725: 0.619, + 730: 0.625, + 735: 0.628, + 740: 0.630, + 745: 0.627, + 750: 0.635, + 755: 0.639, + 760: 0.640, + }, + "purplish blue": { + 380: 0.094, + 385: 0.113, + 390: 0.141, + 395: 0.186, + 400: 0.235, + 405: 0.275, + 410: 0.297, + 415: 0.316, + 420: 0.317, + 425: 0.333, + 430: 0.346, + 435: 0.355, + 440: 0.368, + 445: 0.378, + 450: 0.381, + 455: 0.377, + 460: 0.368, + 465: 0.356, + 470: 0.340, + 475: 0.322, + 480: 0.296, + 485: 0.269, + 490: 0.241, + 495: 0.220, + 500: 0.197, + 505: 0.182, + 510: 0.166, + 515: 0.151, + 520: 0.138, + 525: 0.127, + 530: 0.120, + 535: 0.115, + 540: 0.108, + 545: 0.104, + 550: 0.101, + 555: 0.095, + 560: 0.090, + 565: 0.084, + 570: 0.082, + 575: 0.081, + 580: 0.081, + 585: 0.081, + 590: 0.081, + 595: 0.083, + 600: 0.083, + 605: 0.080, + 610: 0.079, + 615: 0.080, + 620: 0.081, + 625: 0.081, + 630: 0.084, + 635: 0.089, + 640: 0.092, + 645: 0.096, + 650: 0.103, + 655: 0.107, + 660: 0.112, + 665: 0.111, + 670: 0.112, + 675: 0.109, + 680: 0.104, + 685: 0.102, + 690: 0.099, + 695: 0.099, + 700: 0.100, + 705: 0.100, + 710: 0.103, + 715: 0.106, + 720: 0.109, + 725: 0.113, + 730: 0.122, + 735: 0.127, + 740: 0.138, + 745: 0.153, + 750: 0.173, + 755: 0.193, + 760: 0.215, + }, + "moderate red": { + 380: 0.088, + 385: 0.102, + 390: 0.121, + 395: 0.136, + 400: 0.151, + 405: 0.153, + 410: 0.151, + 415: 0.144, + 420: 0.142, + 425: 0.141, + 430: 0.139, + 435: 0.135, + 440: 0.136, + 445: 0.135, + 450: 0.133, + 455: 0.132, + 460: 0.129, + 465: 0.130, + 470: 0.129, + 475: 0.127, + 480: 0.121, + 485: 0.118, + 490: 0.109, + 495: 0.105, + 500: 0.105, + 505: 0.104, + 510: 0.101, + 515: 0.100, + 520: 0.094, + 525: 0.091, + 530: 0.089, + 535: 0.092, + 540: 0.095, + 545: 0.097, + 550: 0.104, + 555: 0.109, + 560: 0.111, + 565: 0.113, + 570: 0.116, + 575: 0.134, + 580: 0.167, + 585: 0.223, + 590: 0.291, + 595: 0.362, + 600: 0.426, + 605: 0.474, + 610: 0.511, + 615: 0.537, + 620: 0.551, + 625: 0.562, + 630: 0.565, + 635: 0.570, + 640: 0.575, + 645: 0.574, + 650: 0.579, + 655: 0.577, + 660: 0.579, + 665: 0.577, + 670: 0.580, + 675: 0.581, + 680: 0.579, + 685: 0.581, + 690: 0.581, + 695: 0.583, + 700: 0.581, + 705: 0.581, + 710: 0.580, + 715: 0.586, + 720: 0.585, + 725: 0.584, + 730: 0.589, + 735: 0.587, + 740: 0.590, + 745: 0.582, + 750: 0.589, + 755: 0.592, + 760: 0.590, + }, + "purple": { + 380: 0.083, + 385: 0.100, + 390: 0.125, + 395: 0.154, + 400: 0.183, + 405: 0.198, + 410: 0.206, + 415: 0.207, + 420: 0.207, + 425: 0.201, + 430: 0.194, + 435: 0.184, + 440: 0.175, + 445: 0.163, + 450: 0.154, + 455: 0.142, + 460: 0.129, + 465: 0.120, + 470: 0.109, + 475: 0.102, + 480: 0.095, + 485: 0.090, + 490: 0.081, + 495: 0.077, + 500: 0.070, + 505: 0.067, + 510: 0.065, + 515: 0.063, + 520: 0.059, + 525: 0.058, + 530: 0.056, + 535: 0.053, + 540: 0.052, + 545: 0.052, + 550: 0.051, + 555: 0.053, + 560: 0.055, + 565: 0.056, + 570: 0.054, + 575: 0.052, + 580: 0.053, + 585: 0.049, + 590: 0.051, + 595: 0.055, + 600: 0.058, + 605: 0.063, + 610: 0.073, + 615: 0.087, + 620: 0.103, + 625: 0.120, + 630: 0.137, + 635: 0.149, + 640: 0.161, + 645: 0.175, + 650: 0.188, + 655: 0.197, + 660: 0.208, + 665: 0.218, + 670: 0.229, + 675: 0.241, + 680: 0.249, + 685: 0.262, + 690: 0.272, + 695: 0.284, + 700: 0.292, + 705: 0.304, + 710: 0.312, + 715: 0.325, + 720: 0.329, + 725: 0.333, + 730: 0.343, + 735: 0.346, + 740: 0.350, + 745: 0.350, + 750: 0.359, + 755: 0.360, + 760: 0.362, + }, + "yellow green": { + 380: 0.045, + 385: 0.048, + 390: 0.050, + 395: 0.050, + 400: 0.054, + 405: 0.053, + 410: 0.053, + 415: 0.055, + 420: 0.053, + 425: 0.057, + 430: 0.059, + 435: 0.059, + 440: 0.062, + 445: 0.065, + 450: 0.070, + 455: 0.075, + 460: 0.081, + 465: 0.092, + 470: 0.102, + 475: 0.116, + 480: 0.136, + 485: 0.158, + 490: 0.185, + 495: 0.225, + 500: 0.274, + 505: 0.328, + 510: 0.390, + 515: 0.446, + 520: 0.485, + 525: 0.511, + 530: 0.529, + 535: 0.538, + 540: 0.539, + 545: 0.535, + 550: 0.526, + 555: 0.521, + 560: 0.511, + 565: 0.500, + 570: 0.484, + 575: 0.467, + 580: 0.450, + 585: 0.435, + 590: 0.412, + 595: 0.395, + 600: 0.377, + 605: 0.363, + 610: 0.352, + 615: 0.346, + 620: 0.339, + 625: 0.337, + 630: 0.337, + 635: 0.331, + 640: 0.326, + 645: 0.322, + 650: 0.323, + 655: 0.320, + 660: 0.325, + 665: 0.327, + 670: 0.334, + 675: 0.340, + 680: 0.347, + 685: 0.355, + 690: 0.362, + 695: 0.369, + 700: 0.373, + 705: 0.376, + 710: 0.375, + 715: 0.379, + 720: 0.372, + 725: 0.365, + 730: 0.367, + 735: 0.375, + 740: 0.379, + 745: 0.388, + 750: 0.403, + 755: 0.415, + 760: 0.430, + }, + "orange yellow": { + 380: 0.049, + 385: 0.052, + 390: 0.054, + 395: 0.055, + 400: 0.054, + 405: 0.057, + 410: 0.057, + 415: 0.059, + 420: 0.057, + 425: 0.057, + 430: 0.059, + 435: 0.057, + 440: 0.058, + 445: 0.060, + 450: 0.061, + 455: 0.061, + 460: 0.062, + 465: 0.067, + 470: 0.072, + 475: 0.081, + 480: 0.088, + 485: 0.098, + 490: 0.106, + 495: 0.112, + 500: 0.120, + 505: 0.130, + 510: 0.143, + 515: 0.163, + 520: 0.188, + 525: 0.218, + 530: 0.256, + 535: 0.304, + 540: 0.351, + 545: 0.399, + 550: 0.442, + 555: 0.476, + 560: 0.505, + 565: 0.532, + 570: 0.544, + 575: 0.561, + 580: 0.579, + 585: 0.539, + 590: 0.597, + 595: 0.604, + 600: 0.617, + 605: 0.617, + 610: 0.618, + 615: 0.624, + 620: 0.625, + 625: 0.630, + 630: 0.647, + 635: 0.635, + 640: 0.638, + 645: 0.642, + 650: 0.649, + 655: 0.650, + 660: 0.649, + 665: 0.650, + 670: 0.677, + 675: 0.657, + 680: 0.653, + 685: 0.659, + 690: 0.658, + 695: 0.662, + 700: 0.661, + 705: 0.666, + 710: 0.668, + 715: 0.672, + 720: 0.671, + 725: 0.667, + 730: 0.677, + 735: 0.678, + 740: 0.682, + 745: 0.678, + 750: 0.686, + 755: 0.693, + 760: 0.690, + }, + "blue": { + 380: 0.068, + 385: 0.084, + 390: 0.104, + 395: 0.127, + 400: 0.156, + 405: 0.178, + 410: 0.194, + 415: 0.209, + 420: 0.221, + 425: 0.234, + 430: 0.250, + 435: 0.264, + 440: 0.287, + 445: 0.308, + 450: 0.318, + 455: 0.323, + 460: 0.317, + 465: 0.303, + 470: 0.276, + 475: 0.255, + 480: 0.225, + 485: 0.193, + 490: 0.160, + 495: 0.139, + 500: 0.117, + 505: 0.104, + 510: 0.087, + 515: 0.077, + 520: 0.066, + 525: 0.060, + 530: 0.056, + 535: 0.053, + 540: 0.050, + 545: 0.047, + 550: 0.045, + 555: 0.042, + 560: 0.043, + 565: 0.040, + 570: 0.040, + 575: 0.038, + 580: 0.038, + 585: 0.037, + 590: 0.036, + 595: 0.037, + 600: 0.038, + 605: 0.036, + 610: 0.037, + 615: 0.037, + 620: 0.037, + 625: 0.039, + 630: 0.039, + 635: 0.042, + 640: 0.040, + 645: 0.042, + 650: 0.044, + 655: 0.045, + 660: 0.047, + 665: 0.048, + 670: 0.050, + 675: 0.048, + 680: 0.046, + 685: 0.050, + 690: 0.048, + 695: 0.051, + 700: 0.049, + 705: 0.052, + 710: 0.054, + 715: 0.057, + 720: 0.060, + 725: 0.065, + 730: 0.069, + 735: 0.076, + 740: 0.087, + 745: 0.102, + 750: 0.123, + 755: 0.147, + 760: 0.174, + }, + "green": { + 380: 0.045, + 385: 0.048, + 390: 0.054, + 395: 0.054, + 400: 0.057, + 405: 0.059, + 410: 0.060, + 415: 0.060, + 420: 0.060, + 425: 0.062, + 430: 0.054, + 435: 0.064, + 440: 0.069, + 445: 0.070, + 450: 0.075, + 455: 0.079, + 460: 0.083, + 465: 0.090, + 470: 0.099, + 475: 0.109, + 480: 0.120, + 485: 0.132, + 490: 0.144, + 495: 0.158, + 500: 0.175, + 505: 0.196, + 510: 0.231, + 515: 0.272, + 520: 0.307, + 525: 0.338, + 530: 0.352, + 535: 0.357, + 540: 0.353, + 545: 0.341, + 550: 0.323, + 555: 0.305, + 560: 0.286, + 565: 0.265, + 570: 0.244, + 575: 0.224, + 580: 0.203, + 585: 0.180, + 590: 0.161, + 595: 0.144, + 600: 0.124, + 605: 0.108, + 610: 0.098, + 615: 0.089, + 620: 0.084, + 625: 0.080, + 630: 0.076, + 635: 0.075, + 640: 0.071, + 645: 0.071, + 650: 0.070, + 655: 0.067, + 660: 0.067, + 665: 0.067, + 670: 0.068, + 675: 0.070, + 680: 0.070, + 685: 0.074, + 690: 0.076, + 695: 0.079, + 700: 0.080, + 705: 0.082, + 710: 0.086, + 715: 0.085, + 720: 0.083, + 725: 0.081, + 730: 0.081, + 735: 0.081, + 740: 0.083, + 745: 0.086, + 750: 0.091, + 755: 0.094, + 760: 0.098, + }, + "red": { + 380: 0.043, + 385: 0.045, + 390: 0.046, + 395: 0.045, + 400: 0.047, + 405: 0.046, + 410: 0.048, + 415: 0.046, + 420: 0.046, + 425: 0.046, + 430: 0.048, + 435: 0.044, + 440: 0.046, + 445: 0.047, + 450: 0.047, + 455: 0.047, + 460: 0.046, + 465: 0.046, + 470: 0.044, + 475: 0.044, + 480: 0.040, + 485: 0.042, + 490: 0.039, + 495: 0.040, + 500: 0.040, + 505: 0.039, + 510: 0.040, + 515: 0.040, + 520: 0.038, + 525: 0.038, + 530: 0.039, + 535: 0.038, + 540: 0.040, + 545: 0.040, + 550: 0.041, + 555: 0.042, + 560: 0.044, + 565: 0.046, + 570: 0.047, + 575: 0.054, + 580: 0.064, + 585: 0.081, + 590: 0.112, + 595: 0.156, + 600: 0.216, + 605: 0.283, + 610: 0.358, + 615: 0.434, + 620: 0.499, + 625: 0.549, + 630: 0.585, + 635: 0.607, + 640: 0.624, + 645: 0.633, + 650: 0.650, + 655: 0.652, + 660: 0.652, + 665: 0.656, + 670: 0.661, + 675: 0.666, + 680: 0.664, + 685: 0.671, + 690: 0.671, + 695: 0.677, + 700: 0.673, + 705: 0.678, + 710: 0.680, + 715: 0.689, + 720: 0.688, + 725: 0.685, + 730: 0.691, + 735: 0.694, + 740: 0.696, + 745: 0.692, + 750: 0.698, + 755: 0.704, + 760: 0.700, + }, + "yellow": { + 380: 0.047, + 385: 0.047, + 390: 0.048, + 395: 0.047, + 400: 0.050, + 405: 0.052, + 410: 0.052, + 415: 0.051, + 420: 0.051, + 425: 0.053, + 430: 0.053, + 435: 0.053, + 440: 0.057, + 445: 0.056, + 450: 0.058, + 455: 0.060, + 460: 0.062, + 465: 0.067, + 470: 0.076, + 475: 0.090, + 480: 0.109, + 485: 0.142, + 490: 0.183, + 495: 0.228, + 500: 0.274, + 505: 0.319, + 510: 0.360, + 515: 0.405, + 520: 0.443, + 525: 0.475, + 530: 0.510, + 535: 0.544, + 540: 0.571, + 545: 0.594, + 550: 0.612, + 555: 0.630, + 560: 0.646, + 565: 0.656, + 570: 0.668, + 575: 0.677, + 580: 0.691, + 585: 0.696, + 590: 0.701, + 595: 0.702, + 600: 0.729, + 605: 0.701, + 610: 0.704, + 615: 0.707, + 620: 0.708, + 625: 0.713, + 630: 0.721, + 635: 0.716, + 640: 0.717, + 645: 0.718, + 650: 0.726, + 655: 0.729, + 660: 0.730, + 665: 0.728, + 670: 0.747, + 675: 0.739, + 680: 0.737, + 685: 0.743, + 690: 0.740, + 695: 0.756, + 700: 0.742, + 705: 0.749, + 710: 0.751, + 715: 0.753, + 720: 0.754, + 725: 0.750, + 730: 0.760, + 735: 0.762, + 740: 0.769, + 745: 0.762, + 750: 0.774, + 755: 0.776, + 760: 0.779, + }, + "magenta": { + 380: 0.106, + 385: 0.129, + 390: 0.168, + 395: 0.229, + 400: 0.297, + 405: 0.346, + 410: 0.367, + 415: 0.372, + 420: 0.377, + 425: 0.373, + 430: 0.362, + 435: 0.351, + 440: 0.340, + 445: 0.323, + 450: 0.306, + 455: 0.293, + 460: 0.276, + 465: 0.259, + 470: 0.250, + 475: 0.234, + 480: 0.220, + 485: 0.206, + 490: 0.190, + 495: 0.179, + 500: 0.169, + 505: 0.163, + 510: 0.152, + 515: 0.140, + 520: 0.126, + 525: 0.113, + 530: 0.104, + 535: 0.098, + 540: 0.098, + 545: 0.102, + 550: 0.104, + 555: 0.103, + 560: 0.104, + 565: 0.103, + 570: 0.106, + 575: 0.118, + 580: 0.140, + 585: 0.170, + 590: 0.212, + 595: 0.257, + 600: 0.313, + 605: 0.354, + 610: 0.403, + 615: 0.457, + 620: 0.501, + 625: 0.546, + 630: 0.587, + 635: 0.612, + 640: 0.637, + 645: 0.655, + 650: 0.677, + 655: 0.684, + 660: 0.693, + 665: 0.695, + 670: 0.714, + 675: 0.710, + 680: 0.720, + 685: 0.715, + 690: 0.714, + 695: 0.739, + 700: 0.719, + 705: 0.726, + 710: 0.728, + 715: 0.733, + 720: 0.737, + 725: 0.732, + 730: 0.743, + 735: 0.742, + 740: 0.748, + 745: 0.741, + 750: 0.753, + 755: 0.754, + 760: 0.761, + }, + "cyan": { + 380: 0.085, + 385: 0.102, + 390: 0.130, + 395: 0.163, + 400: 0.201, + 405: 0.228, + 410: 0.247, + 415: 0.254, + 420: 0.262, + 425: 0.278, + 430: 0.282, + 435: 0.300, + 440: 0.319, + 445: 0.332, + 450: 0.348, + 455: 0.363, + 460: 0.382, + 465: 0.401, + 470: 0.419, + 475: 0.431, + 480: 0.438, + 485: 0.441, + 490: 0.438, + 495: 0.429, + 500: 0.415, + 505: 0.404, + 510: 0.381, + 515: 0.358, + 520: 0.339, + 525: 0.316, + 530: 0.288, + 535: 0.262, + 540: 0.236, + 545: 0.210, + 550: 0.186, + 555: 0.162, + 560: 0.142, + 565: 0.129, + 570: 0.116, + 575: 0.105, + 580: 0.099, + 585: 0.092, + 590: 0.088, + 595: 0.086, + 600: 0.081, + 605: 0.077, + 610: 0.078, + 615: 0.076, + 620: 0.076, + 625: 0.076, + 630: 0.076, + 635: 0.076, + 640: 0.076, + 645: 0.077, + 650: 0.078, + 655: 0.077, + 660: 0.081, + 665: 0.080, + 670: 0.081, + 675: 0.079, + 680: 0.079, + 685: 0.079, + 690: 0.077, + 695: 0.076, + 700: 0.075, + 705: 0.074, + 710: 0.074, + 715: 0.076, + 720: 0.077, + 725: 0.081, + 730: 0.084, + 735: 0.090, + 740: 0.098, + 745: 0.111, + 750: 0.130, + 755: 0.151, + 760: 0.179, + }, + "white 9.5 (.05 D)": { + 380: 0.126, + 385: 0.169, + 390: 0.212, + 395: 0.264, + 400: 0.318, + 405: 0.491, + 410: 0.664, + 415: 0.757, + 420: 0.851, + 425: 0.868, + 430: 0.887, + 435: 0.888, + 440: 0.890, + 445: 0.893, + 450: 0.895, + 455: 0.896, + 460: 0.898, + 465: 0.900, + 470: 0.902, + 475: 0.900, + 480: 0.897, + 485: 0.904, + 490: 0.901, + 495: 0.900, + 500: 0.900, + 505: 0.898, + 510: 0.897, + 515: 0.900, + 520: 0.902, + 525: 0.902, + 530: 0.901, + 535: 0.900, + 540: 0.899, + 545: 0.896, + 550: 0.893, + 555: 0.895, + 560: 0.898, + 565: 0.900, + 570: 0.902, + 575: 0.904, + 580: 0.905, + 585: 0.906, + 590: 0.907, + 595: 0.905, + 600: 0.903, + 605: 0.904, + 610: 0.905, + 615: 0.907, + 620: 0.898, + 625: 0.897, + 630: 0.896, + 635: 0.898, + 640: 0.900, + 645: 0.900, + 650: 0.899, + 655: 0.901, + 660: 0.904, + 665: 0.904, + 670: 0.905, + 675: 0.902, + 680: 0.899, + 685: 0.899, + 690: 0.900, + 695: 0.899, + 700: 0.898, + 705: 0.898, + 710: 0.899, + 715: 0.898, + 720: 0.898, + 725: 0.899, + 730: 0.901, + 735: 0.898, + 740: 0.896, + 745: 0.895, + 750: 0.898, + 755: 0.899, + 760: 0.898, + }, + "neutral 8 (.23 D)": { + 380: 0.084, + 385: 0.113, + 390: 0.141, + 395: 0.176, + 400: 0.211, + 405: 0.327, + 410: 0.442, + 415: 0.504, + 420: 0.567, + 425: 0.578, + 430: 0.590, + 435: 0.591, + 440: 0.592, + 445: 0.594, + 450: 0.595, + 455: 0.596, + 460: 0.597, + 465: 0.599, + 470: 0.600, + 475: 0.599, + 480: 0.597, + 485: 0.602, + 490: 0.599, + 495: 0.599, + 500: 0.599, + 505: 0.597, + 510: 0.597, + 515: 0.599, + 520: 0.600, + 525: 0.600, + 530: 0.599, + 535: 0.599, + 540: 0.598, + 545: 0.596, + 550: 0.594, + 555: 0.595, + 560: 0.597, + 565: 0.599, + 570: 0.600, + 575: 0.602, + 580: 0.602, + 585: 0.603, + 590: 0.604, + 595: 0.602, + 600: 0.601, + 605: 0.602, + 610: 0.602, + 615: 0.604, + 620: 0.597, + 625: 0.597, + 630: 0.596, + 635: 0.597, + 640: 0.599, + 645: 0.599, + 650: 0.598, + 655: 0.599, + 660: 0.602, + 665: 0.602, + 670: 0.602, + 675: 0.600, + 680: 0.598, + 685: 0.598, + 690: 0.599, + 695: 0.598, + 700: 0.597, + 705: 0.597, + 710: 0.598, + 715: 0.597, + 720: 0.597, + 725: 0.598, + 730: 0.599, + 735: 0.597, + 740: 0.596, + 745: 0.595, + 750: 0.597, + 755: 0.598, + 760: 0.597, + }, + "neutral 6.5 (.44 D)": { + 380: 0.051, + 385: 0.068, + 390: 0.085, + 395: 0.106, + 400: 0.128, + 405: 0.198, + 410: 0.267, + 415: 0.305, + 420: 0.342, + 425: 0.349, + 430: 0.357, + 435: 0.357, + 440: 0.358, + 445: 0.359, + 450: 0.360, + 455: 0.360, + 460: 0.361, + 465: 0.362, + 470: 0.363, + 475: 0.362, + 480: 0.361, + 485: 0.364, + 490: 0.362, + 495: 0.362, + 500: 0.362, + 505: 0.361, + 510: 0.361, + 515: 0.362, + 520: 0.363, + 525: 0.363, + 530: 0.362, + 535: 0.362, + 540: 0.361, + 545: 0.360, + 550: 0.359, + 555: 0.360, + 560: 0.361, + 565: 0.362, + 570: 0.363, + 575: 0.364, + 580: 0.364, + 585: 0.364, + 590: 0.365, + 595: 0.364, + 600: 0.363, + 605: 0.364, + 610: 0.364, + 615: 0.365, + 620: 0.361, + 625: 0.361, + 630: 0.360, + 635: 0.361, + 640: 0.362, + 645: 0.362, + 650: 0.361, + 655: 0.362, + 660: 0.364, + 665: 0.364, + 670: 0.364, + 675: 0.363, + 680: 0.361, + 685: 0.361, + 690: 0.362, + 695: 0.361, + 700: 0.361, + 705: 0.361, + 710: 0.361, + 715: 0.361, + 720: 0.361, + 725: 0.361, + 730: 0.362, + 735: 0.361, + 740: 0.360, + 745: 0.360, + 750: 0.361, + 755: 0.361, + 760: 0.361, + }, + "neutral 5 (.70 D)": { + 380: 0.0277, + 385: 0.0372, + 390: 0.0465, + 395: 0.0580, + 400: 0.0698, + 405: 0.1079, + 410: 0.1457, + 415: 0.1663, + 420: 0.1869, + 425: 0.1907, + 430: 0.1947, + 435: 0.1949, + 440: 0.1954, + 445: 0.1960, + 450: 0.1965, + 455: 0.1967, + 460: 0.1971, + 465: 0.1976, + 470: 0.1980, + 475: 0.1976, + 480: 0.1969, + 485: 0.1985, + 490: 0.1978, + 495: 0.1976, + 500: 0.1976, + 505: 0.1971, + 510: 0.1969, + 515: 0.1976, + 520: 0.1980, + 525: 0.1980, + 530: 0.1978, + 535: 0.1976, + 540: 0.1974, + 545: 0.1967, + 550: 0.1960, + 555: 0.1965, + 560: 0.1971, + 565: 0.1976, + 570: 0.1980, + 575: 0.1985, + 580: 0.1987, + 585: 0.1989, + 590: 0.1991, + 595: 0.1987, + 600: 0.1982, + 605: 0.1985, + 610: 0.1987, + 615: 0.1991, + 620: 0.1971, + 625: 0.1969, + 630: 0.1967, + 635: 0.1971, + 640: 0.1976, + 645: 0.1976, + 650: 0.1974, + 655: 0.1978, + 660: 0.1985, + 665: 0.1985, + 670: 0.1987, + 675: 0.1980, + 680: 0.1974, + 685: 0.1974, + 690: 0.1976, + 695: 0.1974, + 700: 0.1971, + 705: 0.1971, + 710: 0.1974, + 715: 0.1971, + 720: 0.1971, + 725: 0.1974, + 730: 0.1978, + 735: 0.1971, + 740: 0.1967, + 745: 0.1965, + 750: 0.1971, + 755: 0.1974, + 760: 0.1971, + }, + "neutral 3.5 (1.05 D)": { + 380: 0.0126, + 385: 0.0169, + 390: 0.0212, + 395: 0.0264, + 400: 0.0318, + 405: 0.0491, + 410: 0.0664, + 415: 0.0757, + 420: 0.0851, + 425: 0.0868, + 430: 0.0886, + 435: 0.0887, + 440: 0.0889, + 445: 0.0893, + 450: 0.0895, + 455: 0.0896, + 460: 0.0898, + 465: 0.0900, + 470: 0.0902, + 475: 0.0900, + 480: 0.0897, + 485: 0.0904, + 490: 0.0901, + 495: 0.0900, + 500: 0.0900, + 505: 0.0898, + 510: 0.0897, + 515: 0.0900, + 520: 0.0902, + 525: 0.0902, + 530: 0.0901, + 535: 0.0900, + 540: 0.0899, + 545: 0.0896, + 550: 0.0893, + 555: 0.0895, + 560: 0.0898, + 565: 0.0900, + 570: 0.0902, + 575: 0.0904, + 580: 0.0905, + 585: 0.0906, + 590: 0.0907, + 595: 0.0905, + 600: 0.0903, + 605: 0.0904, + 610: 0.0905, + 615: 0.0907, + 620: 0.0898, + 625: 0.0897, + 630: 0.0896, + 635: 0.0898, + 640: 0.0900, + 645: 0.0900, + 650: 0.0899, + 655: 0.0901, + 660: 0.0904, + 665: 0.0904, + 670: 0.0905, + 675: 0.0902, + 680: 0.0899, + 685: 0.0899, + 690: 0.0900, + 695: 0.0899, + 700: 0.0898, + 705: 0.0898, + 710: 0.0899, + 715: 0.0898, + 720: 0.0898, + 725: 0.0899, + 730: 0.0901, + 735: 0.0898, + 740: 0.0896, + 745: 0.0895, + 750: 0.0898, + 755: 0.0899, + 760: 0.0898, + }, + "black 2 (1.5 D)": { + 380: 0.00439, + 385: 0.00589, + 390: 0.00737, + 395: 0.00919, + 400: 0.01105, + 405: 0.01708, + 410: 0.02308, + 415: 0.02635, + 420: 0.02961, + 425: 0.03020, + 430: 0.03084, + 435: 0.03087, + 440: 0.03094, + 445: 0.03105, + 450: 0.03112, + 455: 0.03115, + 460: 0.03122, + 465: 0.03129, + 470: 0.03136, + 475: 0.03129, + 480: 0.03119, + 485: 0.03143, + 490: 0.03133, + 495: 0.03129, + 500: 0.03129, + 505: 0.03122, + 510: 0.03119, + 515: 0.03129, + 520: 0.03136, + 525: 0.03136, + 530: 0.03133, + 535: 0.03129, + 540: 0.03126, + 545: 0.03115, + 550: 0.03105, + 555: 0.03112, + 560: 0.03122, + 565: 0.03129, + 570: 0.03136, + 575: 0.03143, + 580: 0.03147, + 585: 0.03150, + 590: 0.03154, + 595: 0.03147, + 600: 0.03140, + 605: 0.03143, + 610: 0.03147, + 615: 0.03154, + 620: 0.03122, + 625: 0.03119, + 630: 0.03115, + 635: 0.03122, + 640: 0.03129, + 645: 0.03129, + 650: 0.03126, + 655: 0.03133, + 660: 0.03143, + 665: 0.03143, + 670: 0.03147, + 675: 0.03136, + 680: 0.03126, + 685: 0.03126, + 690: 0.03129, + 695: 0.03126, + 700: 0.03122, + 705: 0.03122, + 710: 0.03126, + 715: 0.03122, + 720: 0.03122, + 725: 0.03126, + 730: 0.03133, + 735: 0.03122, + 740: 0.03115, + 745: 0.03112, + 750: 0.03122, + 755: 0.03126, + 760: 0.03122, + }, +} +""" +*EBU Tech 3355* Appendix 4 test colour sample reflectance data. + +Values are those mandated by *TLCI-2012* and *TLMF-2013*. *EBU Tech +3355* states that coloured patches 1-18 are derived from *BBC R&D Report +1988/2*, with small deviations in the published table over the overlapping +spectral range. Greyscale patches 19-24 use unpublished *BBC R&D* data. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017`, :cite:`BBCResearchDepartment1988` +""" + +NAMES_TCS_TLCI2012: tuple[str, ...] = ( + "dark skin", + "light skin", + "blue sky", + "foliage", + "blue flower", + "bluish green", + "orange", + "purplish blue", + "moderate red", + "purple", + "yellow green", + "orange yellow", + "blue", + "green", + "red", + "yellow", + "magenta", + "cyan", + "white 9.5 (.05 D)", + "neutral 8 (.23 D)", + "neutral 6.5 (.44 D)", + "neutral 5 (.70 D)", + "neutral 3.5 (1.05 D)", + "black 2 (1.5 D)", +) +""" +*EBU Tech 3355* patch names selected by *TLCI-2012*. Patches +1-18 are used in *TLCI-2012*; patches 1-24 are used in *TLMF-2013*. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" + + +SDS_TCS_TLCI2012: LazyCanonicalMapping = LazyCanonicalMapping( + (key, partial(SpectralDistribution, value, name=key)) + for key, value in DATA_TCS_TLCI2012.items() +) +""" +*EBU Tech 3355* Appendix 4 test colour sample spectral distributions. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017`, :cite:`BBCResearchDepartment1988` +""" + +MSDS_CAMERA_SENSITIVITIES_TLCI2012: CanonicalMapping = CanonicalMapping( + { + "EBU Standard Camera": RGB_CameraSensitivities( + DATA_CAMERA_SENSITIVITIES_TLCI2012, + np.arange(380, 761, 5), + name="EBU Standard Camera", + ) + } +) +""" +Mapping of *TLCI-2012* *EBU* standard camera spectral sensitivities. + +References +---------- +:cite:`EuropeanBroadcastingUnion2017` +""" diff --git a/colour/quality/tests/test_tlci.py b/colour/quality/tests/test_tlci.py new file mode 100644 index 0000000000..c69cd5b6c2 --- /dev/null +++ b/colour/quality/tests/test_tlci.py @@ -0,0 +1,536 @@ +"""Define the unit tests for the :mod:`colour.quality.tlci` module.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from colour.algebra import linstep_function +from colour.characterisation import RGB_CameraSensitivities +from colour.colorimetry import ( + SDS_ILLUMINANTS, + SDS_LIGHT_SOURCES, + SpectralDistribution, + SpectralShape, + reshape_sd, +) +from colour.constants import TOLERANCE_ABSOLUTE_TESTS +from colour.quality.datasets import ( + DATA_DAYLIGHT_BASIS_TLCI2012, + DATA_DAYLIGHT_LOCUS_TLCI2012, + DATA_PLANCKIAN_LOCUS_TLCI2012, + DATA_TCS_TLCI2012, + MSDS_CAMERA_SENSITIVITIES_TLCI2012, + SDS_TCS_TLCI2012, +) +from colour.quality.tlci import ( + ColourQuality_Specification_TLCI2012, + ColourQuality_Specification_TLMF2013, + _nearest_locus_sample_TLCI2012, + _Q_from_delta_E, + sd_daylight_TLCI2012, + sd_planckian_TLCI2012, + sd_reference_illuminant_TLCI2012, + television_lighting_consistency_index, + television_luminaire_matching_factor, + uv_to_CCT_TLCI2012, +) + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "SPECTRAL_SHAPE_TLCI_TLMF_TESTS", + "gaussian", + "sd_normalised", + "sd_planckian", + "sd_daylight", + "sd_mixed_reference", + "sd_phosphor_led_warm", + "sd_phosphor_led_cool", + "sd_rgb_led_balanced", + "TestDatasetsTLCI2012", + "TestSdPlanckianTLCI2012", + "TestSdDaylightTLCI2012", + "TestUvToCCTTLCI2012", + "TestSdReferenceIlluminantTLCI2012", + "TestTelevisionLightingConsistencyIndex", + "TestTelevisionLuminaireMatchingFactor", +] + +SPECTRAL_SHAPE_TLCI_TLMF_TESTS: SpectralShape = SpectralShape(380, 760, 5) + + +def gaussian(wavelengths: np.ndarray, centre: float, width: float) -> np.ndarray: + """Return a unit Gaussian spectral lobe.""" + + return np.exp(-0.5 * ((wavelengths - centre) / width) ** 2) + + +def sd_normalised(values: np.ndarray, name: str) -> SpectralDistribution: + """Return a peak-normalised spectral distribution.""" + + values = np.clip(values, 0, None) + peak = np.max(values) + + if peak != 0: + values = values / peak + + return SpectralDistribution(values, SPECTRAL_SHAPE_TLCI_TLMF_TESTS, name=name) + + +def sd_planckian(CCT: float) -> SpectralDistribution: + """Return a generated Planckian spectrum.""" + + sd = sd_planckian_TLCI2012(CCT, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + + return sd_normalised(sd.values, f"planckian-{CCT:.0f}k") + + +def sd_daylight(CCT: float) -> SpectralDistribution: + """Return a generated daylight spectrum.""" + + sd = sd_daylight_TLCI2012(CCT, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + + return sd_normalised(sd.values, f"daylight-{CCT:.0f}k") + + +def sd_mixed_reference(CCT: float) -> SpectralDistribution: + """Return a generated mixed-reference spectrum.""" + + planckian_3400 = sd_planckian_TLCI2012(3400, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + daylight_5000 = sd_daylight_TLCI2012(5000, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + planckian_3400_values = planckian_3400.values / planckian_3400[560] + daylight_5000_values = daylight_5000.values / daylight_5000[560] + weight = (CCT - 3400) / (5000 - 3400) + + return sd_normalised( + linstep_function(weight, planckian_3400_values, daylight_5000_values), + f"mixed-reference-{CCT:.0f}k", + ) + + +def sd_phosphor_led_warm() -> SpectralDistribution: + """Return a generated warm phosphor LED spectrum.""" + + wavelengths = SPECTRAL_SHAPE_TLCI_TLMF_TESTS.wavelengths + + return sd_normalised( + 0.85 * gaussian(wavelengths, 455, 12) + + 1.00 * gaussian(wavelengths, 600, 80) + + 0.18 * gaussian(wavelengths, 630, 25), + "phosphor-led-warm", + ) + + +def sd_phosphor_led_cool() -> SpectralDistribution: + """Return a generated cool phosphor LED spectrum.""" + + wavelengths = SPECTRAL_SHAPE_TLCI_TLMF_TESTS.wavelengths + + return sd_normalised( + 1.00 * gaussian(wavelengths, 450, 10) + + 0.82 * gaussian(wavelengths, 545, 75) + + 0.22 * gaussian(wavelengths, 610, 35), + "phosphor-led-cool", + ) + + +def sd_rgb_led_balanced() -> SpectralDistribution: + """Return a generated RGB LED spectrum.""" + + wavelengths = SPECTRAL_SHAPE_TLCI_TLMF_TESTS.wavelengths + + return sd_normalised( + 0.72 * gaussian(wavelengths, 455, 14) + + 1.00 * gaussian(wavelengths, 535, 18) + + 0.86 * gaussian(wavelengths, 625, 18), + "rgb-led-balanced", + ) + + +class TestDatasetsTLCI2012: + """ + Define :mod:`colour.quality.datasets.tlci2012` datasets unit tests + methods. + """ + + def test_datasets(self) -> None: + """Test the *TLCI-2012* datasets.""" + + assert DATA_PLANCKIAN_LOCUS_TLCI2012.shape == (152, 3) + assert DATA_DAYLIGHT_LOCUS_TLCI2012.shape == (104, 3) + assert DATA_DAYLIGHT_BASIS_TLCI2012.shape == (77, 3) + assert len(DATA_TCS_TLCI2012) == 24 + assert len(SDS_TCS_TLCI2012) == 24 + + # Representative values and published sums from *EBU Tech 3355* + # Appendix 2, covering the Planckian and daylight radiator tables. + for table, index, values in ( + (DATA_PLANCKIAN_LOCUS_TLCI2012, 0, [1000, 0.652355, 0.344814]), + (DATA_PLANCKIAN_LOCUS_TLCI2012, 150, [4999, 0.344774, 0.351363]), + (DATA_PLANCKIAN_LOCUS_TLCI2012, 151, [5000, 0.344746, 0.351341]), + (DATA_DAYLIGHT_LOCUS_TLCI2012, 0, [5000, 0.345747, 0.358680]), + (DATA_DAYLIGHT_LOCUS_TLCI2012, 1, [5001, 0.345718, 0.358657]), + (DATA_DAYLIGHT_LOCUS_TLCI2012, 103, [25000, 0.249866, 0.254845]), + ): + np.testing.assert_allclose(table[index], values) + + np.testing.assert_allclose( + np.sum(DATA_PLANCKIAN_LOCUS_TLCI2012[:, 1:], axis=0), + [74.357659, 60.180968], + ) + np.testing.assert_allclose( + np.sum(DATA_DAYLIGHT_LOCUS_TLCI2012[:, 1:], axis=0), + [31.602241, 33.132636], + ) + + # Representative values from *EBU Tech 3355* Appendix 3, covering the + # daylight radiation vector table and its published sums. + for index, values in ( + (0, [63.4, 38.5, 3.0]), + (1, [62.45, 35.98125, 2.05]), + (5, [101.54375, 45.525, -0.93125]), + (76, [47.7, -7.8, 5.2]), + ): + np.testing.assert_allclose(DATA_DAYLIGHT_BASIS_TLCI2012[index], values) + + np.testing.assert_allclose( + np.sum(DATA_DAYLIGHT_BASIS_TLCI2012, axis=0), + [7140.45, 545.05, 201.55], + ) + + # Representative values from *EBU Tech 3355* Appendix 4, covering the + # coloured and greyscale test colour sample tables. + for sample, wavelength, value in ( + ("dark skin", 380, 0.054), + ("dark skin", 760, 0.490), + ("white 9.5 (.05 D)", 380, 0.126), + ("white 9.5 (.05 D)", 760, 0.898), + ): + np.testing.assert_allclose(SDS_TCS_TLCI2012[sample][wavelength], value) + + # Values mandated by *EBU Tech 3355* Appendix 4 that differ from the + # cited *BBC R&D Report 1988/2* table over the overlapping spectral + # range. + for sample, wavelength, value, bbc_value in ( + ("foliage", 725, 0.445, 0.455), + ("bluish green", 555, 0.445, 0.449), + ("green", 430, 0.054, 0.064), + ): + np.testing.assert_allclose(SDS_TCS_TLCI2012[sample][wavelength], value) + assert not np.isclose(SDS_TCS_TLCI2012[sample][wavelength], bbc_value) + + camera = MSDS_CAMERA_SENSITIVITIES_TLCI2012["EBU Standard Camera"] + assert isinstance(camera, RGB_CameraSensitivities) + assert camera.wavelengths[0] == 380 + + +class TestSdPlanckianTLCI2012: + """ + Define :func:`colour.quality.tlci.sd_planckian_TLCI2012` definition unit + tests methods. + """ + + def test_sd_planckian_TLCI2012(self) -> None: + """Test :func:`colour.quality.tlci.sd_planckian_TLCI2012` definition.""" + + sd = sd_planckian_TLCI2012(3400, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + + assert sd[560] == pytest.approx(1) + + for wavelength in (380, 560, 760): + # EBU Tech 3355 section 1.1.2.1, equation [9], normalised to unity + # at 560 nm instead of the published value of 100. + value = (560 / wavelength) ** 5 * ( + np.expm1(1.435e7 / (560 * 3400)) + / np.expm1(1.435e7 / (wavelength * 3400)) + ) + assert sd[wavelength] == pytest.approx(value) + + +class TestSdDaylightTLCI2012: + """ + Define :func:`colour.quality.tlci.sd_daylight_TLCI2012` definition unit + tests methods. + """ + + def test_sd_daylight_TLCI2012(self) -> None: + """Test :func:`colour.quality.tlci.sd_daylight_TLCI2012` definition.""" + + sd = sd_daylight_TLCI2012(6500, SPECTRAL_SHAPE_TLCI_TLMF_TESTS) + + # EBU Tech 3355 section 1.1.2.2 anchors the daylight components at + # 560 nm, so the reconstruction is 100 there for any CCT. + assert sd[560] == pytest.approx(100) + np.testing.assert_allclose(sd[400], 82.423248, atol=TOLERANCE_ABSOLUTE_TESTS) + + +class TestUvToCCTTLCI2012: + """ + Define :func:`colour.quality.tlci.uv_to_CCT_TLCI2012` definition unit + tests methods. + """ + + def test_uv_to_CCT_TLCI2012(self) -> None: + """Test :func:`colour.quality.tlci.uv_to_CCT_TLCI2012` definition.""" + + # EBU Tech 3355 section 1.1.1 selects the Planckian locus below the + # daylight range and the daylight locus above it. + CCT, _uv_locus, is_daylight = uv_to_CCT_TLCI2012(np.array([0.26, 0.35])) + np.testing.assert_allclose(CCT, 2755.437250, atol=TOLERANCE_ABSOLUTE_TESTS) + assert not is_daylight + + CCT, _uv_locus, is_daylight = uv_to_CCT_TLCI2012(np.array([0.19, 0.31])) + np.testing.assert_allclose(CCT, 7255.262985, atol=TOLERANCE_ABSOLUTE_TESTS) + assert is_daylight + + def test_uv_to_CCT_TLCI2012_nearest_locus_sample(self) -> None: + """ + Test :func:`colour.quality.tlci.uv_to_CCT_TLCI2012` definition nearest + locus sample fallback. + """ + + # A test colour outside the tabulated normal-intersection range falls + # back to the nearest Appendix 2 locus sample, here the 25000 K + # daylight endpoint. + uv = np.array([0.18, 0.20]) + CCT, uv_locus, is_daylight = uv_to_CCT_TLCI2012(uv) + np.testing.assert_allclose(CCT, 25000, atol=TOLERANCE_ABSOLUTE_TESTS) + assert is_daylight + + CCT_nearest, uv_locus_nearest, is_daylight_nearest = ( + _nearest_locus_sample_TLCI2012(uv) + ) + np.testing.assert_array_equal(CCT, CCT_nearest) + assert is_daylight == is_daylight_nearest + np.testing.assert_array_equal(uv_locus, uv_locus_nearest) + + +class TestSdReferenceIlluminantTLCI2012: + """ + Define :func:`colour.quality.tlci.sd_reference_illuminant_TLCI2012` + definition unit tests methods. + """ + + def test_sd_reference_illuminant_TLCI2012(self) -> None: + """ + Test :func:`colour.quality.tlci.sd_reference_illuminant_TLCI2012` + definition. + """ + + # EBU Tech 3355 section 1.1.2 uses a Planckian reference below 3400 K + # and a daylight reference above 5000 K. + _sd_reference, CCT, D_uv = sd_reference_illuminant_TLCI2012( + SDS_ILLUMINANTS["A"] + ) + np.testing.assert_allclose(CCT, 2848.132209, atol=TOLERANCE_ABSOLUTE_TESTS) + assert D_uv == pytest.approx(0, abs=1.5e-2) + + _sd_reference, CCT, D_uv = sd_reference_illuminant_TLCI2012( + SDS_ILLUMINANTS["D65"] + ) + np.testing.assert_allclose(CCT, 6505.096585, atol=TOLERANCE_ABSOLUTE_TESTS) + assert D_uv == pytest.approx(0, abs=1.5e-2) + + def test_sd_reference_illuminant_TLCI2012_D_uv_sign(self) -> None: + """ + Test :func:`colour.quality.tlci.sd_reference_illuminant_TLCI2012` + definition ``D_uv`` sign convention. + """ + + sd_D65 = reshape_sd( + SDS_ILLUMINANTS["D65"], SPECTRAL_SHAPE_TLCI_TLMF_TESTS, "Align", copy=False + ) + wavelengths = SPECTRAL_SHAPE_TLCI_TLMF_TESTS.wavelengths + peak = np.max(sd_D65.values) + sd_green = sd_normalised( + sd_D65.values + 0.2 * peak * gaussian(wavelengths, 545, 20), + "green-offset", + ) + sd_magenta = sd_normalised( + sd_D65.values + + 0.1 * peak * gaussian(wavelengths, 450, 15) + + 0.1 * peak * gaussian(wavelengths, 650, 20), + "magenta-offset", + ) + + # EBU Tech 3355 section 1.1.1 reverses the sign for green-side offsets + # (uT < uL); section 1.1.2.3 equations [16]-[17] label positive d as + # magenta and negative d as green. + assert sd_reference_illuminant_TLCI2012(sd_green)[2] < -0.5 + assert sd_reference_illuminant_TLCI2012(sd_magenta)[2] > 0.5 + + +class TestTelevisionLightingConsistencyIndex: + """ + Define :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition unit tests methods. + """ + + def test_television_lighting_consistency_index(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition. + """ + + np.testing.assert_allclose( + television_lighting_consistency_index(SDS_ILLUMINANTS["FL2"]), + 29.492541753138433, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + # A non-default camera name selects the same *EBU Standard Camera* + # sensitivities and therefore yields the same score. + assert television_lighting_consistency_index( + SDS_ILLUMINANTS["FL2"], camera="EBU Standard Camera" + ) == television_lighting_consistency_index(SDS_ILLUMINANTS["FL2"]) + + def test_television_lighting_consistency_index_additional_data(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition with additional data. + """ + + specification = television_lighting_consistency_index( + SDS_ILLUMINANTS["FL2"], additional_data=True + ) + assert isinstance(specification, ColourQuality_Specification_TLCI2012) + assert 0.0 <= specification.Q_a <= 100.0 + assert specification.delta_E_a >= 0.0 + assert specification.delta_E_s.shape == (18,) + + # EBU reference and near-blackbody illuminants score close to 100 with + # a near-zero reference-locus distance. + for name in ("A", "D65"): + specification = television_lighting_consistency_index( + SDS_ILLUMINANTS[name], additional_data=True + ) + assert specification.Q_a == pytest.approx(100, abs=5e-4) + assert specification.D_uv == pytest.approx(0, abs=1.5e-2) + + def test_television_lighting_consistency_index_mixed_reference(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition mixed-reference region. + """ + + specification = television_lighting_consistency_index( + sd_planckian(4000), additional_data=True + ) + assert specification.Q_a == pytest.approx(100, abs=0.5) + assert specification.D_uv == pytest.approx(0, abs=2e-2) + + specification = television_lighting_consistency_index( + sd_mixed_reference(4500), additional_data=True + ) + assert 4000 < specification.CCT < 5000 + assert specification.Q_a == pytest.approx(100, abs=0.5) + + def test_television_lighting_consistency_index_validation_vectors(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition against generated and + in-tree validation spectra. + """ + + # Generated, redistributable validation spectra covering the main + # specified algorithm paths. + for sd, reference in ( + (sd_planckian(3000), 100), + (sd_planckian(4000), 100), + (sd_daylight(5600), 100), + (sd_mixed_reference(4500), 100), + (sd_phosphor_led_warm(), 88), + (sd_phosphor_led_cool(), 77), + (sd_rgb_led_balanced(), 58), + ): + assert television_lighting_consistency_index(sd) == pytest.approx( + reference, abs=0.5 + ) + + # Public in-tree CIE/NIST LED spectral distributions as regression + # coverage. + for name, source, reference in ( + ("LED-B3", SDS_ILLUMINANTS, 73), + ("LED-RGB1", SDS_ILLUMINANTS, 30), + ("Phosphor LED YAG", SDS_LIGHT_SOURCES, 60), + ("4-LED-1 (461/526/576/624)", SDS_LIGHT_SOURCES, 79), + ): + assert television_lighting_consistency_index(source[name]) == pytest.approx( + reference, abs=0.5 + ) + + def test_raise_exception_television_lighting_consistency_index(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_lighting_consistency_index` definition raised exception when all + samples are excluded. + """ + + with pytest.raises(ValueError, match="All TLCI/TLMF samples were excluded"): + _Q_from_delta_E(np.array([])) + + +class TestTelevisionLuminaireMatchingFactor: + """ + Define :func:`colour.quality.tlci.\ +television_luminaire_matching_factor` definition unit tests methods. + """ + + def test_television_luminaire_matching_factor(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_luminaire_matching_factor` definition. + """ + + # Identical test and reference sources are a perfect match. + assert television_luminaire_matching_factor( + SDS_ILLUMINANTS["D65"], SDS_ILLUMINANTS["D65"] + ) == pytest.approx(100.0, abs=1e-10) + + np.testing.assert_allclose( + television_luminaire_matching_factor( + SDS_ILLUMINANTS["FL2"], SDS_ILLUMINANTS["D65"] + ), + 5.393109771266282, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + def test_television_luminaire_matching_factor_additional_data(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_luminaire_matching_factor` definition with additional data. + """ + + specification = television_luminaire_matching_factor( + SDS_ILLUMINANTS["FL2"], SDS_ILLUMINANTS["D65"], additional_data=True + ) + assert isinstance(specification, ColourQuality_Specification_TLMF2013) + assert 0.0 <= specification.Q_a <= 100.0 + assert specification.delta_E_a >= 0.0 + assert specification.delta_E_s.shape == (24,) + + def test_television_luminaire_matching_factor_validation_vectors(self) -> None: + """ + Test :func:`colour.quality.tlci.\ +television_luminaire_matching_factor` definition against generated + validation spectra. + """ + + for sd_test, sd_reference, reference in ( + (sd_planckian(3000), sd_planckian(3000), 100), + (sd_daylight(5600), sd_daylight(5600), 100), + (sd_phosphor_led_warm(), sd_planckian(3000), 3), + (sd_phosphor_led_cool(), sd_daylight(5600), 61), + (sd_rgb_led_balanced(), sd_daylight(5600), 4), + (sd_mixed_reference(4000), sd_planckian(3400), 18), + (sd_mixed_reference(4990), sd_daylight(5000), 100), + ): + assert television_luminaire_matching_factor( + sd_test, sd_reference + ) == pytest.approx(reference, abs=0.5) diff --git a/colour/quality/tlci.py b/colour/quality/tlci.py new file mode 100644 index 0000000000..ee8b5e58db --- /dev/null +++ b/colour/quality/tlci.py @@ -0,0 +1,920 @@ +""" +Television Lighting Consistency Index (TLCI-2012) and Television Luminaire +Matching Factor (TLMF-2013) +========================================================================== + +Define the *EBU Tech 3355* TLCI-2012 and TLMF-2013 computation objects. + +- :class:`colour.quality.ColourQuality_Specification_TLCI2012` +- :class:`colour.quality.ColourQuality_Specification_TLMF2013` +- :func:`colour.television_lighting_consistency_index` +- :func:`colour.television_luminaire_matching_factor` +""" + +from __future__ import annotations + +import typing +from dataclasses import dataclass + +import numpy as np + +from colour.algebra import linstep_function, spow +from colour.colorimetry import ( + MSDS_CMFS, + MultiSpectralDistributions, + SpectralDistribution, + SpectralShape, + msds_to_XYZ, + reshape_msds, + reshape_sd, + sd_to_XYZ, +) +from colour.difference import delta_E +from colour.models import UCS_to_uv, XYZ_to_Lab, XYZ_to_UCS, xy_to_UCS_uv +from colour.models.rgb.transfer_functions import oetf_BT709 +from colour.quality.datasets import ( + DATA_DAYLIGHT_LOCUS_TLCI2012, + DATA_PLANCKIAN_LOCUS_TLCI2012, + DATA_TCS_TLCI2012, + MATRIX_TLCI2012_CAMERA, + MATRIX_TLCI2012_DISPLAY, + MATRIX_TLCI2012_SATURATION, + MSDS_CAMERA_SENSITIVITIES_TLCI2012, + MSDS_DAYLIGHT_BASIS_TLCI2012, + NAMES_TCS_TLCI2012, + SPECTRAL_SHAPE_TLCI2012, +) +from colour.temperature import CCT_to_xy_CIE_D +from colour.utilities import ( + as_float_scalar, + domain_range_scale, + optional, +) + +if typing.TYPE_CHECKING: + from colour.hints import Literal, NDArrayBoolean, NDArrayFloat + +__author__ = "Colour Developers" +__copyright__ = "Copyright 2013 Colour Developers" +__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" +__maintainer__ = "Colour Developers" +__email__ = "colour-developers@colour-science.org" +__status__ = "Production" + +__all__ = [ + "ColourQuality_Specification_TLCI2012", + "ColourQuality_Specification_TLMF2013", + "sd_planckian_TLCI2012", + "sd_daylight_TLCI2012", + "uv_to_CCT_TLCI2012", + "sd_reference_illuminant_TLCI2012", + "television_lighting_consistency_index", + "television_luminaire_matching_factor", +] + + +@dataclass() +class ColourQuality_Specification_TLCI2012: + """ + Define the *Television Lighting Consistency Index* (TLCI-2012) + colour quality specification. + + Parameters + ---------- + name + Name of the test spectral distribution. + Q_a + *TLCI-2012* score. + delta_E_a + Aggregate colour difference. + delta_E_s + Per-sample colour differences used for the final score. + CCT + Correlated colour temperature of the test source. + D_uv + Distance from the reference locus in *EBU Tech 3355* ``d`` units. + Following *EBU Tech 3355* section 1.1.1, negative values indicate that + the test source is towards green (:math:`u_T < u_L` in CIE 1960 UCS) + and positive values indicate that it is towards magenta. Absolute + values above 1.0 indicate reduced CCT reliability. + + .. note:: + + This sign convention follows the published *EBU Tech 3355* + specification (section 1.1.1 and equations [16]-[17]). The *EBU + TLCI* application reports the opposite sign: its manual, section + 2.1.3.1.2, shows :math:`d > +1/2` towards green and + :math:`d < -1/2` towards magenta. Values compared against that + tool will therefore appear negated. + """ + + name: str + Q_a: float + delta_E_a: float + delta_E_s: NDArrayFloat + CCT: float + D_uv: float + + +@dataclass() +class ColourQuality_Specification_TLMF2013: + """ + Define the *Television Luminaire Matching Factor* (TLMF-2013) + colour quality specification. + + Parameters + ---------- + name + Name of the test signal. + Q_a + *TLMF-2013* score. + delta_E_a + Aggregate colour difference. + delta_E_s + Per-sample colour differences used for the final score. + """ + + name: str + Q_a: float + delta_E_a: float + delta_E_s: NDArrayFloat + + +# EBU Tech 3355 section 1.4.2, Table 1. +_D65_XY = np.array([0.3127, 0.3290]) +# EBU Tech 3355 section 1.5.1, equations [58]-[59]. +_TLCI_K = 3.16 +_TLCI_P = 2.4 +# EBU Tech 3355 section 1.4.1, equation [28]. +_DISPLAY_GAMMA = 2.4 +# EBU Tech 3355 section 2, equation [61], maps the notional camera signal +# R_C' to television coding as 16 + 219 R_C'. The text states that signals +# need not be clipped at nominal peak level 235; the corresponding full-scale +# signal at code value 255 is therefore: +_STUDIO_SWING_WHITE = (255 - 16) / (235 - 16) + + +def _reference_loci_TLCI2012() -> tuple[tuple[NDArrayFloat, NDArrayFloat, bool], ...]: + """ + Return the *EBU Tech 3355* reference-locus data used for CCT selection. + + *EBU Tech 3355* section 1.1.1 defines the CCT search against Appendix 2 + Planckian and daylight locus values instead of the existing CCT/D_uv + methods such as Ohno (2013) and Robertson (1968). + """ + + planckian_temperatures = DATA_PLANCKIAN_LOCUS_TLCI2012[:, 0] + planckian_uv = xy_to_UCS_uv(DATA_PLANCKIAN_LOCUS_TLCI2012[:, 1:]) + + daylight_temperatures = DATA_DAYLIGHT_LOCUS_TLCI2012[:, 0] + daylight_uv = xy_to_UCS_uv(DATA_DAYLIGHT_LOCUS_TLCI2012[:, 1:]) + + return ( + (planckian_temperatures, planckian_uv, False), + (daylight_temperatures, daylight_uv, True), + ) + + +# EBU Tech 3355 section 1.1.1 Appendix 2 Planckian and daylight reference loci. +_REFERENCE_LOCI_TLCI2012 = _reference_loci_TLCI2012() + +# EBU Tech 3355 Appendix 4 test-colour samples assembled as a single +# multi-spectral distribution so the camera integration can be vectorised. +# The first 18 samples are the coloured ColorChecker patches used by TLCI; +# the remaining 6 grey scale patches are used only by TLMF. Built from the +# ``DATA_TCS_TLCI2012`` mapping rather than the lazily-evaluated +# ``SDS_TCS_TLCI2012`` so it can be assembled at module import time. +_MSDS_TCS_TLCI2012 = MultiSpectralDistributions( + np.transpose( + [list(DATA_TCS_TLCI2012[name].values()) for name in NAMES_TCS_TLCI2012] + ), + SPECTRAL_SHAPE_TLCI2012.wavelengths, + labels=NAMES_TCS_TLCI2012, +) + + +def _sd_uv_TLCI2012(sd: SpectralDistribution) -> NDArrayFloat: + """Compute CIE 1960 UCS *uv* chromaticity coordinates for ``sd``.""" + + cmfs = reshape_msds( + MSDS_CMFS["CIE 1931 2 Degree Standard Observer"], + SPECTRAL_SHAPE_TLCI2012, + copy=False, + ) + with domain_range_scale("1"): + XYZ = sd_to_XYZ(sd, cmfs, method="Integration") + + return UCS_to_uv(XYZ_to_UCS(XYZ)) + + +def _closest_locus_intersection_TLCI2012( + uv: NDArrayFloat, + temperatures: NDArrayFloat, + uv_loci: NDArrayFloat, + is_daylight: bool, +) -> tuple[float, float, NDArrayFloat, bool] | None: + """ + Return the closest normal intersection on one *EBU Tech 3355* locus. + """ + + uv_loci_start = uv_loci[:-1] + uv_loci_end = uv_loci[1:] + uv_loci_delta = uv_loci_end - uv_loci_start + segment_lengths = np.linalg.norm(uv_loci_delta, axis=1) + + # EBU Tech 3355 section 1.1.1, equations [4]-[7]: slope of the locus, + # distance from the test colour to the locus point, angle to the + # horizontal, and internal angle to the CCT line. + slopes = np.arctan2(uv_loci_delta[:, 1], uv_loci_delta[:, 0]) + uv_test_delta = uv - uv_loci_start + radii = np.linalg.norm(uv_test_delta, axis=1) + angles = np.arctan2(uv_test_delta[:, 1], uv_test_delta[:, 0]) + internal_angles = (angles - slopes + np.pi) % (2 * np.pi) - np.pi + + distances_along_locus = radii * np.cos(internal_angles) + factors = distances_along_locus / segment_lengths + distances = (radii * np.sin(internal_angles)) ** 2 + + # EBU Tech 3355 section 1.1.1 defines a match as adjacent locus points + # whose internal angles are both less than 90 degrees in magnitude. This + # is equivalent to accepting normal projections on the locus segment. + valid = (np.abs(internal_angles) <= np.pi / 2) & (factors >= 0) & (factors <= 1) + if not np.any(valid): + return None + + indices = np.nonzero(valid)[0] + uv_intersections = ( + uv_loci_start[indices] + factors[indices, None] * uv_loci_delta[indices] + ) + distances = distances[indices] + index = np.argmin(distances) + segment_index = indices[index] + CCT = temperatures[segment_index] + factors[segment_index] * ( + temperatures[segment_index + 1] - temperatures[segment_index] + ) + + return ( + as_float_scalar(distances[index]), + as_float_scalar(CCT), + uv_intersections[index], + is_daylight, + ) + + +def _nearest_locus_sample_TLCI2012( + uv: NDArrayFloat, +) -> tuple[float, NDArrayFloat, bool]: + """ + Return the nearest *EBU Tech 3355* locus sample for ``uv``. + """ + + # EBU Tech 3355 section 1.1.1 defines the CCT as the locus colour that + # most closely matches the test colour; use the nearest Appendix 2 sample + # when no normal intersection is found inside the tabulated locus range. + candidates = [ + ( + np.sum((uv_locus - uv) ** 2, axis=1), + temperatures, + uv_locus, + is_daylight, + ) + for temperatures, uv_locus, is_daylight in _REFERENCE_LOCI_TLCI2012 + ] + distances, temperatures, uv_loci, is_daylight = min( + candidates, key=lambda candidate: np.min(candidate[0]) + ) + index = np.argmin(distances) + + return ( + as_float_scalar(temperatures[index]), + uv_loci[index], + is_daylight, + ) + + +def uv_to_CCT_TLCI2012(uv: NDArrayFloat) -> tuple[float, NDArrayFloat, bool]: + """ + Compute the *EBU Tech 3355* correlated colour temperature and + reference-locus point for given CIE 1960 UCS *uv* chromaticity + coordinates. + + Parameters + ---------- + uv + CIE 1960 UCS *uv* chromaticity coordinates of the test source. + + Returns + ------- + :class:`tuple` + Correlated colour temperature :math:`K`, CIE 1960 UCS *uv* + chromaticity coordinates of the closest reference-locus point, and + whether that point lies on the daylight locus (as opposed to the + Planckian locus). + + Notes + ----- + - *EBU Tech 3355* section 1.1.1 defines the CCT search against the + Appendix 2 Planckian and daylight locus values, not the existing + CCT/D_uv methods such as *Ohno (2013)* and *Robertson (1968)*. The + Planckian and daylight loci are kept separate because section 1.1.2.3 + states that they do not join in the 3400 K to 5000 K mixed-reference + region. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> CCT, uv_locus, is_daylight = uv_to_CCT_TLCI2012(np.array([0.19, 0.31])) + >>> CCT # doctest: +ELLIPSIS + np.float64(7255.2629...) + >>> is_daylight + True + """ + + candidates: list[tuple[float, float, NDArrayFloat, bool]] = [] + for temperatures, uv_loci, is_daylight in _REFERENCE_LOCI_TLCI2012: + # EBU Tech 3355 section 1.1.1, equations [4]-[8], finds the normal + # intersection with adjacent locus points. Treat the Planckian and + # daylight loci separately because section 1.1.2.3 states that they + # do not join in the 3400 K to 5000 K mixed-reference region. + candidate = _closest_locus_intersection_TLCI2012( + uv, temperatures, uv_loci, is_daylight + ) + if candidate is not None: + candidates.append(candidate) + + if len(candidates) == 0: + return _nearest_locus_sample_TLCI2012(uv) + + _, CCT, uv_locus, is_daylight = min(candidates, key=lambda candidate: candidate[0]) + + return ( + CCT, + uv_locus, + is_daylight, + ) + + +def _sd_normalise_560_TLCI2012(sd: SpectralDistribution) -> SpectralDistribution: + """ + Normalise ``sd`` to unity at 560 nm. + + *EBU Tech 3355* sections 1.1.2.1 and 1.1.2.2 specify a value of 100 at + 560 nm. The implementation uses unity because the global scale is absorbed + by the camera white-balance step and does not affect the computed + *TLCI-2012* or *TLMF-2013* scores. + """ + + sd = reshape_sd(sd, SPECTRAL_SHAPE_TLCI2012, "Align", copy=False) + + return SpectralDistribution( + sd.values / sd[560], + SPECTRAL_SHAPE_TLCI2012, + name=sd.name, + ) + + +def sd_planckian_TLCI2012( + CCT: float, shape: SpectralShape = SPECTRAL_SHAPE_TLCI2012 +) -> SpectralDistribution: + """ + Return the *EBU Tech 3355* Planckian reference spectral distribution for + given correlated colour temperature. + + Parameters + ---------- + CCT + Correlated colour temperature :math:`K`. + shape + Spectral shape of the returned spectral distribution. + + Returns + ------- + :class:`colour.SpectralDistribution` + *EBU Tech 3355* Planckian reference spectral distribution, normalised + to unity at 560 nm. + + Notes + ----- + - *EBU Tech 3355* section 1.1.2.1, equation [9], uses a simplified + Planckian expression with wavelength in nanometres, a + :math:`1.435 \\times 10^7` nm K radiation constant, and normalisation + at 560 nm. This implementation returns unity at 560 nm instead of the + published value of 100; the global scale is absorbed by the camera + white-balance step and does not affect the computed scores. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> sd = sd_planckian_TLCI2012(3200) + >>> sd[560] # doctest: +ELLIPSIS + np.float64(1.0...) + >>> sd[600] # doctest: +ELLIPSIS + np.float64(1.2081916...) + """ + + # EBU Tech 3355 section 1.1.2.1, equation [9]. + wavelengths = shape.wavelengths + c_2 = 1.435e7 + values = (560 / wavelengths) ** 5 * ( + np.expm1(c_2 / (560 * CCT)) / np.expm1(c_2 / (wavelengths * CCT)) + ) + + return SpectralDistribution( + values, + shape, + name=f"TLCI-2012 Planckian {CCT:.0f}K", + ) + + +def sd_daylight_TLCI2012( + CCT: float, shape: SpectralShape = SPECTRAL_SHAPE_TLCI2012 +) -> SpectralDistribution: + """ + Return the *EBU Tech 3355* daylight reference spectral distribution for + given correlated colour temperature. + + Parameters + ---------- + CCT + Correlated colour temperature :math:`K`. + shape + Spectral shape of the returned spectral distribution. + + Returns + ------- + :class:`colour.SpectralDistribution` + *EBU Tech 3355* daylight reference spectral distribution. + + Notes + ----- + - *EBU Tech 3355* section 1.1.2.2, equations [10]-[14], uses the + Appendix 3 daylight radiation vectors and *EBU Tech 3355* coefficients + for :math:`M`, :math:`M_1`, and :math:`M_2`. This differs from + Colour's general :func:`colour.sd_CIE_illuminant_D_series` helper, + which uses the library CIE D-series basis data and its standard + coefficient path. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> sd = sd_daylight_TLCI2012(5600) + >>> sd[560] # doctest: +ELLIPSIS + np.float64(99.9999...) + """ + + # EBU Tech 3355 section 1.1.2.2, equations [10]-[14], uses the Appendix 3 + # daylight radiation vectors and coefficients. + x, y = CCT_to_xy_CIE_D(CCT) + M = 0.02387 + 0.25539 * x - 0.73217 * y + M1 = (-1.34674 - 1.77861 * x + 5.90757 * y) / M + M2 = (0.03638 - 31.44464 * x + 30.06400 * y) / M + + daylight_basis = reshape_msds( + MSDS_DAYLIGHT_BASIS_TLCI2012, shape, "Align", copy=False + ).values + + return SpectralDistribution( + daylight_basis[..., 0] + + M1 * daylight_basis[..., 1] + + M2 * daylight_basis[..., 2], + shape, + name=f"TLCI-2012 Daylight {CCT:.0f}K", + ) + + +def sd_reference_illuminant_TLCI2012( + sd_test: SpectralDistribution, +) -> tuple[SpectralDistribution, float, float]: + """ + Compute the *TLCI-2012* reference illuminant for given test spectral + distribution. + + Parameters + ---------- + sd_test + Test spectral distribution. + + Returns + ------- + :class:`tuple` + *TLCI-2012* reference illuminant spectral distribution, correlated + colour temperature :math:`K` of the test source, and the ``D_uv`` + distance from the reference locus in *EBU Tech 3355* ``d`` units. + + Notes + ----- + - *EBU Tech 3355* section 1.1.2 uses a Planckian reference below 3400 K, + a daylight reference above 5000 K, and the section 1.1.2.3 mixed + reference between them. The mixed reference is a linear interpolation + between fixed Planckian 3400 K and daylight 5000 K spectra, not + spectra at the test source CCT. + - The ``D_uv`` sign follows the published *EBU Tech 3355* convention; + see :class:`colour.quality.ColourQuality_Specification_TLCI2012`. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> from colour import SDS_ILLUMINANTS + >>> sd_reference, CCT, D_uv = sd_reference_illuminant_TLCI2012( + ... SDS_ILLUMINANTS["D65"] + ... ) + >>> CCT # doctest: +ELLIPSIS + np.float64(6505.0965...) + """ + + shape = SPECTRAL_SHAPE_TLCI2012 + sd_test = reshape_sd(sd_test, shape, "Align", copy=False) + + # EBU Tech 3355 section 1.1.1, equations [1]-[3]. + uv = _sd_uv_TLCI2012(sd_test) + CCT, uv_locus, is_daylight = uv_to_CCT_TLCI2012(uv) + + # EBU Tech 3355 section 1.1.2 uses Planckian below 3400 K, daylight + # above 5000 K, and the section 1.1.2.3 mixed reference between them. + if CCT < 3400: + sd_reference = sd_planckian_TLCI2012(CCT, shape) + elif is_daylight: + sd_reference = sd_daylight_TLCI2012(CCT, shape) + else: + # Sections 1.1.2.1 and 1.1.2.2 normalise P3400 and D5000 at + # 560 nm before the section 1.1.2.3 interpolation, equation [15]. + sd_planckian_3400 = sd_planckian_TLCI2012(3400, shape) + sd_daylight_5000 = _sd_normalise_560_TLCI2012(sd_daylight_TLCI2012(5000, shape)) + weight = (CCT - 3400) / (5000 - 3400) + sd_reference = SpectralDistribution( + linstep_function(weight, sd_planckian_3400.values, sd_daylight_5000.values), + shape, + name=f"TLCI-2012 Reference {CCT:.0f}K", + ) + + # EBU Tech 3355 section 1.1.1, equation [8], normalised to 0.0054 per + # the prose following that equation. + # Tech 3355 section 1.1.1 reverses the sign for green-side offsets, + # where uT < uL. Section 1.1.2.3 equations [16]-[17] label d > 0 as + # magenta and d <= 0 as green. + D_uv = as_float_scalar(np.linalg.norm(uv - uv_locus) / 0.0054) + + if uv[0] < uv_locus[0]: + D_uv *= -1 + + if 4000 <= CCT < 5000: + # Section 1.1.2.3 equations [16]-[17] adjust d because the + # Planckian and daylight loci are separated in the mixed region. + if D_uv > 0: + D_uv += 0.9 * (5000 - CCT) / (5000 - 4000) + else: + D_uv -= 0.9 * (CCT - 4000) / (5000 - 4000) + + return sd_reference, CCT, D_uv + + +def _camera_RGB_illuminated( + sd_illuminant: SpectralDistribution, + msds_tcs: MultiSpectralDistributions, + msds_camera: MultiSpectralDistributions, + shape: SpectralShape, +) -> NDArrayFloat: + """ + Compute raw camera RGB signals for ``msds_tcs`` under ``sd_illuminant``. + + ``msds_tcs`` and ``msds_camera`` are expected to be already aligned to + ``shape``. + """ + + with domain_range_scale("1"): + # EBU Tech 3355 sections 1.2 and 1.3.1, equations [18]-[19], with the + # camera spectral sensitivities standing in for the colour matching + # functions. + return msds_to_XYZ( + msds_tcs, + msds_camera, + reshape_sd(sd_illuminant, shape, "Align", copy=False), + method="Integration", + ) + + +def _camera_RGB_flat_reflector( + sd_illuminant: SpectralDistribution, + msds_camera: MultiSpectralDistributions, + shape: SpectralShape, + reflectance: float = 0.9, +) -> NDArrayFloat: + """ + Compute camera RGB signals for a flat neutral reflector. + + ``msds_camera`` is expected to be already aligned to ``shape``. + """ + + # EBU Tech 3355 section 1.3.1 sets the neutral reflector level to 0.9 + # so the ColorChecker white patch generates peak white. + sd_reflector = SpectralDistribution( + np.full(len(shape.wavelengths), reflectance), + shape, + name=f"{reflectance:.0%} Flat Reflector", + ) + + with domain_range_scale("1"): + # EBU Tech 3355 sections 1.2 and 1.3.1, equations [18]-[19]. + return sd_to_XYZ( + sd_reflector, + msds_camera, + reshape_sd(sd_illuminant, shape, "Align", copy=False), + ) + + +def _camera_to_Lab(RGB: NDArrayFloat) -> tuple[NDArrayFloat, NDArrayBoolean]: + """ + Process white-balanced camera RGB values through the TLCI display pipeline. + """ + + # EBU Tech 3355 section 1.3.2, equations [20]-[22]. + RGB_matrix = np.matmul(RGB, MATRIX_TLCI2012_CAMERA.T) + # EBU Tech 3355 section 1.3.2, equations [23]-[25]. + RGB_saturation = np.matmul(RGB_matrix, MATRIX_TLCI2012_SATURATION.T) + # Section 1.5.1 excludes colours clipped in the mathematics. Negative + # camera RGB values are excluded here; display RGB values cannot become + # negative after the clipped OETF input below. + clipped = np.any(RGB_matrix < 0, axis=-1) | np.any(RGB_saturation < 0, axis=-1) + + # EBU Tech 3355 section 1.3.3, equation [26], produces the R_C' G_C' B_C' + # signals that drive the display. Keep the section 2 equation [61] + # headroom above nominal white, but cap at the full-scale display-drive + # signal implied by code value 255. + RGB_prime = np.clip( + oetf_BT709(np.clip(RGB_saturation, 0, None)), None, _STUDIO_SWING_WHITE + ) + # EBU Tech 3355 section 1.4.1, equation [28]. + RGB_display = spow(RGB_prime, _DISPLAY_GAMMA) + # EBU Tech 3355 section 1.4.2, equation [29]. + XYZ = np.matmul(RGB_display, MATRIX_TLCI2012_DISPLAY.T) + + # EBU Tech 3355 section 1.5, equations [30]-[33]. + return XYZ_to_Lab(XYZ, _D65_XY), clipped + + +def _Q_from_delta_E(delta_Es: NDArrayFloat) -> tuple[float, float]: + """ + Compute the *TLCI-2012* quality index and aggregate colour difference. + """ + + if len(delta_Es) == 0: + error = "All TLCI/TLMF samples were excluded by negative RGB clipping." + raise ValueError(error) + + # EBU Tech 3355 section 1.5.1, equations [58]-[59]. + delta_E_a = as_float_scalar(np.mean(delta_Es**4) ** 0.25) + Q_a = as_float_scalar(100.0 / (1.0 + (delta_E_a / _TLCI_K) ** _TLCI_P)) + + return Q_a, delta_E_a + + +def _tlci_pipeline( + sd_test: SpectralDistribution, + sd_reference: SpectralDistribution, + msds_camera: MultiSpectralDistributions, + normalise_test_luma_only: bool = False, +) -> tuple[NDArrayFloat, NDArrayBoolean]: + """ + Compute the per-sample colour differences and clipping flags for all 24 + *EBU Tech 3355* Appendix 4 test-colour samples. + """ + + shape = SPECTRAL_SHAPE_TLCI2012 + msds_camera = reshape_msds(msds_camera, shape, "Align", copy=False) + msds_tcs = reshape_msds(_MSDS_TCS_TLCI2012, shape, "Align", copy=False) + + RGB_test = _camera_RGB_illuminated(sd_test, msds_tcs, msds_camera, shape) + RGB_reference = _camera_RGB_illuminated(sd_reference, msds_tcs, msds_camera, shape) + + RGB_neutral_test = _camera_RGB_flat_reflector(sd_test, msds_camera, shape) + RGB_neutral_reference = _camera_RGB_flat_reflector(sd_reference, msds_camera, shape) + + # EBU Tech 3355 section 1.3.1 colour-balances TLCI test and reference + # luminaires independently. For TLMF, the test luminaire keeps the + # reference-luminaire colour balance and is normalised only so the + # camera luma signal is unity. Tech 3355 does not give a separate luma + # equation; derive it from the matrixed neutral camera signal using the + # display luminance row defined in section 1.4.2, equation [29]. + if normalise_test_luma_only: + RGB_neutral_test_matrix = np.matmul(RGB_neutral_test, MATRIX_TLCI2012_CAMERA.T) + RGB_neutral_reference_matrix = np.matmul( + RGB_neutral_reference, MATRIX_TLCI2012_CAMERA.T + ) + luma_test = np.dot(RGB_neutral_test_matrix, MATRIX_TLCI2012_DISPLAY[1]) + luma_reference = np.dot( + RGB_neutral_reference_matrix, MATRIX_TLCI2012_DISPLAY[1] + ) + RGB_test = RGB_test / RGB_neutral_reference + RGB_test *= luma_reference / luma_test + else: + RGB_test = RGB_test / RGB_neutral_test + + RGB_reference = RGB_reference / RGB_neutral_reference + + Lab_test, clipped_test = _camera_to_Lab(RGB_test) + Lab_reference, clipped_reference = _camera_to_Lab(RGB_reference) + + # EBU Tech 3355 section 1.5, equations [34]-[54], defines the + # CIEDE2000 colour-difference path with unity k factors; use Colour's + # existing CIE 2000 implementation for that standard calculation. + return ( + delta_E(Lab_test, Lab_reference, method="CIE 2000"), + clipped_test | clipped_reference, + ) + + +@typing.overload +def television_lighting_consistency_index( + sd_test: SpectralDistribution, + camera: str | None, + additional_data: Literal[True], +) -> ColourQuality_Specification_TLCI2012: ... + + +@typing.overload +def television_lighting_consistency_index( + sd_test: SpectralDistribution, + camera: str | None = None, + *, + additional_data: Literal[True], +) -> ColourQuality_Specification_TLCI2012: ... + + +@typing.overload +def television_lighting_consistency_index( + sd_test: SpectralDistribution, + camera: str | None = None, + *, + additional_data: Literal[False], +) -> float: ... + + +@typing.overload +def television_lighting_consistency_index( + sd_test: SpectralDistribution, + camera: str | None = None, + additional_data: Literal[False] = False, +) -> float: ... + + +def television_lighting_consistency_index( + sd_test: SpectralDistribution, + camera: str | None = None, + additional_data: bool = False, +) -> float | ColourQuality_Specification_TLCI2012: + """ + Compute the *Television Lighting Consistency Index* (TLCI-2012) for the + specified test spectral distribution. + + Parameters + ---------- + sd_test + Test spectral distribution. + camera + Camera sensitivity dataset name. If *None*, the *EBU Standard Camera* + defined by *EBU Tech 3355* is used. + additional_data + Whether to return the detailed + :class:`colour.quality.ColourQuality_Specification_TLCI2012` instead + of only the *TLCI-2012* score. + + Returns + ------- + :class:`float` or \ +:class:`colour.quality.ColourQuality_Specification_TLCI2012` + *TLCI-2012* score or detailed specification. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> from colour import SDS_ILLUMINANTS + >>> television_lighting_consistency_index( # doctest: +ELLIPSIS + ... SDS_ILLUMINANTS["FL2"] + ... ) + np.float64(29.49254175...) + """ + + camera = optional(camera, "EBU Standard Camera") + msds_camera = MSDS_CAMERA_SENSITIVITIES_TLCI2012[camera] + + sd_reference, CCT, D_uv = sd_reference_illuminant_TLCI2012(sd_test) + delta_Es, invalid = _tlci_pipeline(sd_test, sd_reference, msds_camera) + # EBU Tech 3355 section 1.5.1 uses only the first 18 coloured ColorChecker + # patches for TLCI, excluding the grey scale patches. + delta_Es, invalid = delta_Es[:18], invalid[:18] + delta_Es = delta_Es[~invalid] + + Q_a, delta_E_a = _Q_from_delta_E(delta_Es) + + if additional_data: + return ColourQuality_Specification_TLCI2012( + name=sd_test.name or "Test", + Q_a=Q_a, + delta_E_a=delta_E_a, + delta_E_s=delta_Es, + CCT=CCT, + D_uv=D_uv, + ) + + return Q_a + + +@typing.overload +def television_luminaire_matching_factor( + sd_test: SpectralDistribution, + sd_reference: SpectralDistribution, + additional_data: Literal[True], +) -> ColourQuality_Specification_TLMF2013: ... + + +@typing.overload +def television_luminaire_matching_factor( + sd_test: SpectralDistribution, + sd_reference: SpectralDistribution, + *, + additional_data: Literal[False], +) -> float: ... + + +@typing.overload +def television_luminaire_matching_factor( + sd_test: SpectralDistribution, + sd_reference: SpectralDistribution, + additional_data: Literal[False] = False, +) -> float: ... + + +def television_luminaire_matching_factor( + sd_test: SpectralDistribution, + sd_reference: SpectralDistribution, + additional_data: bool = False, +) -> float | ColourQuality_Specification_TLMF2013: + """ + Compute the *Television Luminaire Matching Factor* (TLMF-2013). + + Parameters + ---------- + sd_test + Test spectral distribution. + sd_reference + Reference spectral distribution. + additional_data + Whether to return the detailed + :class:`colour.quality.ColourQuality_Specification_TLMF2013` instead + of only the *TLMF-2013* score. + + Returns + ------- + :class:`float` or \ +:class:`colour.quality.ColourQuality_Specification_TLMF2013` + *TLMF-2013* score or detailed specification. + + References + ---------- + :cite:`EuropeanBroadcastingUnion2017` + + Examples + -------- + >>> from colour import SDS_ILLUMINANTS + >>> television_luminaire_matching_factor( # doctest: +ELLIPSIS + ... SDS_ILLUMINANTS["FL2"], SDS_ILLUMINANTS["D65"] + ... ) + np.float64(5.39310977...) + """ + + # EBU Tech 3355 section 1.5.1 uses all 24 ColorChecker patches for + # TLMF-2013 and section 1.3.1 normalises the test source by luma only. + delta_E_s, invalid = _tlci_pipeline( + sd_test, + sd_reference, + MSDS_CAMERA_SENSITIVITIES_TLCI2012["EBU Standard Camera"], + normalise_test_luma_only=True, + ) + delta_E_s = delta_E_s[~invalid] + + Q_a, delta_E_a = _Q_from_delta_E(delta_E_s) + + if additional_data: + return ColourQuality_Specification_TLMF2013( + name=f"{sd_test.name or 'Test'} / {sd_reference.name or 'Reference'}", + Q_a=Q_a, + delta_E_a=delta_E_a, + delta_E_s=delta_E_s, + ) + + return Q_a diff --git a/docs/colour.quality.rst b/docs/colour.quality.rst index fb4692ee77..ea93d10a90 100644 --- a/docs/colour.quality.rst +++ b/docs/colour.quality.rst @@ -81,3 +81,45 @@ Academy Spectral Similarity Index (SSI) :toctree: generated/ spectral_similarity_index + +Television Lighting Consistency Index (TLCI-2012) +------------------------------------------------- + +``colour`` + +.. currentmodule:: colour + +.. autosummary:: + :toctree: generated/ + + television_lighting_consistency_index + +``colour.quality`` + +.. currentmodule:: colour.quality + +.. autosummary:: + :toctree: generated/ + + ColourQuality_Specification_TLCI2012 + +Television Luminaire Matching Factor (TLMF-2013) +------------------------------------------------- + +``colour`` + +.. currentmodule:: colour + +.. autosummary:: + :toctree: generated/ + + television_luminaire_matching_factor + +``colour.quality`` + +.. currentmodule:: colour.quality + +.. autosummary:: + :toctree: generated/ + + ColourQuality_Specification_TLMF2013 diff --git a/docs/index.rst b/docs/index.rst index 14241809b8..49a1c8c073 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1622,6 +1622,34 @@ Academy Spectral Similarity Index (SSI) np.float64(94.0) +Television Lighting Consistency Index (TLCI) +******************************************** + +.. code-block:: python + + import colour + + colour.television_lighting_consistency_index(colour.SDS_ILLUMINANTS["FL2"]) + +.. code-block:: text + + np.float64(29.4691102039) + +Television Luminaire Matching Factor (TLMF) +******************************************* + +.. code-block:: python + + import colour + + colour.television_luminaire_matching_factor( + colour.SDS_ILLUMINANTS["FL2"], colour.SDS_ILLUMINANTS["D65"] + ) + +.. code-block:: text + + np.float64(5.3931097713) + Spectral Up-Sampling & Recovery - ``colour.recovery`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~