Coverage for quality/cfi2017.py: 62%
123 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2CIE 2017 Colour Fidelity Index
3==============================
5Define the *CIE 2017 Colour Fidelity Index* (CFI) computation objects.
7- :class:`colour.quality.ColourRendering_Specification_CIE2017`
8- :func:`colour.quality.colour_fidelity_index_CIE2017`
10References
11----------
12- :cite:`CIETC1-902017` : CIE TC 1-90. (2017). CIE 2017 colour fidelity index
13 for accurate scientific use. CIE Central Bureau. ISBN:978-3-902842-61-9
14"""
16from __future__ import annotations
18import os
19import typing
20from dataclasses import dataclass
22import numpy as np
24from colour.algebra import Extrapolator, euclidean_distance, linstep_function
25from colour.appearance import (
26 VIEWING_CONDITIONS_CIECAM02,
27 CAM_Specification_CIECAM02,
28 XYZ_to_CIECAM02,
29)
30from colour.colorimetry import (
31 MSDS_CMFS,
32 MultiSpectralDistributions,
33 SpectralDistribution,
34 SpectralShape,
35 msds_to_XYZ,
36 reshape_msds,
37 sd_blackbody,
38 sd_CIE_illuminant_D_series,
39 sd_to_XYZ,
40)
42if typing.TYPE_CHECKING:
43 from colour.hints import ArrayLike, List, Literal, Tuple
45from colour.hints import NDArrayFloat, cast
46from colour.models import JMh_CIECAM02_to_CAM02UCS, UCS_to_uv, XYZ_to_UCS
47from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Ohno2013
48from colour.utilities import (
49 CACHE_REGISTRY,
50 as_float,
51 as_float_array,
52 as_float_scalar,
53 as_int_scalar,
54 attest,
55 is_caching_enabled,
56 tsplit,
57 tstack,
58 usage_warning,
59)
61__author__ = "Colour Developers"
62__copyright__ = "Copyright 2013 Colour Developers"
63__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
64__maintainer__ = "Colour Developers"
65__email__ = "colour-developers@colour-science.org"
66__status__ = "Production"
68__all__ = [
69 "SPECTRAL_SHAPE_CIE2017",
70 "ROOT_RESOURCES_CIE2017",
71 "DataColorimetry_TCS_CIE2017",
72 "ColourRendering_Specification_CIE2017",
73 "colour_fidelity_index_CIE2017",
74 "load_TCS_CIE2017",
75 "CCT_reference_illuminant",
76 "sd_reference_illuminant",
77 "tcs_colorimetry_data",
78 "delta_E_to_R_f",
79]
81SPECTRAL_SHAPE_CIE2017: SpectralShape = SpectralShape(380, 780, 1)
82"""
83Spectral shape for *CIE 2017 Colour Fidelity Index* (CFI)
84standard.
85"""
87ROOT_RESOURCES_CIE2017: str = os.path.join(os.path.dirname(__file__), "datasets")
88"""*CIE 2017 Colour Fidelity Index* resources directory."""
90_CACHE_TCS_CIE2017: dict = CACHE_REGISTRY.register_cache(
91 f"{__name__}._CACHE_TCS_CIE2017"
92)
95@dataclass
96class DataColorimetry_TCS_CIE2017:
97 """
98 Store colorimetry data for *test colour samples* used in CIE 2017
99 colour fidelity calculations.
101 This dataclass encapsulates the colorimetric properties of test colour
102 samples as specified by CIE 2017, including their tristimulus values,
103 colour appearance model specifications, and perceptual colour
104 coordinates in both cylindrical and rectangular representations.
106 Attributes
107 ----------
108 name
109 Identifier(s) for the test colour sample(s).
110 XYZ
111 CIE XYZ tristimulus values of the test colour samples.
112 CAM
113 CIECAM02 colour appearance model specification containing the
114 complete appearance correlates.
115 JMh
116 Perceptual colour coordinates in cylindrical representation with
117 *lightness* (J), *colourfulness* (M), and *hue angle* (h).
118 Jpapbp
119 Perceptual colour coordinates in rectangular representation with
120 *lightness* (J) and opponent colour dimensions (a', b').
121 """
123 name: str | list[str]
124 XYZ: NDArrayFloat
125 CAM: CAM_Specification_CIECAM02
126 JMh: NDArrayFloat
127 Jpapbp: NDArrayFloat
130@dataclass
131class ColourRendering_Specification_CIE2017:
132 """
133 Define the *CIE 2017 Colour Fidelity Index* (CFI) colour quality
134 specification.
136 Parameters
137 ----------
138 name
139 Name of the test spectral distribution.
140 sd_reference
141 Spectral distribution of the reference illuminant.
142 R_f
143 *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`.
144 R_s
145 Individual *colour fidelity indexes* data for each sample.
146 CCT
147 Correlated colour temperature :math:`T_{cp}`.
148 D_uv
149 Distance from the Planckian locus :math:`\\Delta_{uv}`.
150 colorimetry_data
151 Colorimetry data for the test and reference computations.
152 delta_E_s
153 Colour shifts of samples.
154 """
156 name: str
157 sd_reference: SpectralDistribution
158 R_f: float
159 R_s: NDArrayFloat
160 CCT: float
161 D_uv: float
162 colorimetry_data: Tuple[DataColorimetry_TCS_CIE2017, DataColorimetry_TCS_CIE2017]
163 delta_E_s: NDArrayFloat
166@typing.overload
167def colour_fidelity_index_CIE2017(
168 sd_test: SpectralDistribution, additional_data: Literal[True] = True
169) -> ColourRendering_Specification_CIE2017: ...
172@typing.overload
173def colour_fidelity_index_CIE2017(
174 sd_test: SpectralDistribution, *, additional_data: Literal[False]
175) -> float: ...
178@typing.overload
179def colour_fidelity_index_CIE2017(
180 sd_test: SpectralDistribution, additional_data: Literal[False]
181) -> float: ...
184def colour_fidelity_index_CIE2017(
185 sd_test: SpectralDistribution, additional_data: bool = False
186) -> float | ColourRendering_Specification_CIE2017:
187 """
188 Compute the *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` of the
189 specified spectral distribution.
191 Parameters
192 ----------
193 sd_test
194 Test spectral distribution.
195 additional_data
196 Whether to output additional data.
198 Returns
199 -------
200 :class:`float` or \
201:class:`colour.quality.ColourRendering_Specification_CIE2017`
202 *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`.
204 References
205 ----------
206 :cite:`CIETC1-902017`
208 Examples
209 --------
210 >>> from colour.colorimetry import SDS_ILLUMINANTS
211 >>> sd = SDS_ILLUMINANTS["FL2"]
212 >>> colour_fidelity_index_CIE2017(sd) # doctest: +ELLIPSIS
213 70.1208244...
214 """
216 if sd_test.shape.interval > 5:
217 error = (
218 "Test spectral distribution interval is greater than "
219 "5nm which is the maximum recommended value "
220 'for computing the "CIE 2017 Colour Fidelity Index"!'
221 )
223 raise ValueError(error)
225 shape = SpectralShape(
226 SPECTRAL_SHAPE_CIE2017.start,
227 SPECTRAL_SHAPE_CIE2017.end,
228 sd_test.shape.interval,
229 )
231 if sd_test.shape.start > 380 or sd_test.shape.end < 780:
232 usage_warning(
233 "Test spectral distribution shape does not span the "
234 "recommended 380-780nm range, missing values will be "
235 "filled with zeros!"
236 )
238 # NOTE: "CIE 2017 Colour Fidelity Index" standard recommends filling
239 # missing values with zeros.
240 sd_test = sd_test.copy()
241 sd_test.extrapolator = Extrapolator
242 sd_test.extrapolator_kwargs = {
243 "method": "constant",
244 "left": 0,
245 "right": 0,
246 }
247 sd_test.align(shape=shape)
249 if sd_test.shape.boundaries != shape.boundaries:
250 sd_test.trim(shape)
252 CCT, D_uv = tsplit(CCT_reference_illuminant(sd_test))
253 sd_reference = sd_reference_illuminant(CCT, shape)
255 # NOTE: All computations except CCT calculation use the
256 # "CIE 1964 10 Degree Standard Observer".
257 cmfs_10 = reshape_msds(
258 MSDS_CMFS["CIE 1964 10 Degree Standard Observer"], shape, copy=False
259 )
261 sds_tcs = load_TCS_CIE2017(shape)
263 (
264 test_tcs_colorimetry_data,
265 reference_tcs_colorimetry_data,
266 ) = tcs_colorimetry_data([sd_test, sd_reference], sds_tcs, cmfs_10)
268 delta_E_s = euclidean_distance(
269 test_tcs_colorimetry_data.Jpapbp,
270 reference_tcs_colorimetry_data.Jpapbp,
271 )
273 R_s = delta_E_to_R_f(delta_E_s)
274 R_f = cast("float", delta_E_to_R_f(np.average(delta_E_s)))
276 if additional_data:
277 return ColourRendering_Specification_CIE2017(
278 sd_test.name,
279 sd_reference,
280 R_f,
281 R_s,
282 CCT,
283 D_uv,
284 (test_tcs_colorimetry_data, reference_tcs_colorimetry_data),
285 delta_E_s,
286 )
288 return R_f
291def load_TCS_CIE2017(shape: SpectralShape) -> MultiSpectralDistributions:
292 """
293 Load the *CIE 2017 Test Colour Samples* dataset appropriate for the
294 specified spectral shape.
296 The datasets are cached and will not be loaded again on subsequent
297 calls to this definition.
299 Parameters
300 ----------
301 shape
302 Spectral shape of the tested illuminant.
304 Returns
305 -------
306 :class:`colour.MultiSpectralDistributions`
307 *CIE 2017 Test Colour Samples* dataset.
309 Examples
310 --------
311 >>> sds_tcs = load_TCS_CIE2017(SpectralShape(380, 780, 5))
312 >>> len(sds_tcs.labels)
313 99
314 """
316 global _CACHE_TCS_CIE2017 # noqa: PLW0602
318 interval = shape.interval
320 attest(
321 interval in (1, 5),
322 "Spectral shape interval must be either 1nm or 5nm!",
323 )
325 filename = f"tcs_cfi2017_{as_int_scalar(interval)}_nm.csv.gz"
327 if is_caching_enabled() and filename in _CACHE_TCS_CIE2017:
328 return _CACHE_TCS_CIE2017[filename]
330 data = np.genfromtxt(
331 str(os.path.join(ROOT_RESOURCES_CIE2017, filename)), delimiter=","
332 )
333 labels = [f"TCS{i} (CIE 2017)" for i in range(99)]
335 tcs = MultiSpectralDistributions(data[:, 1:], data[:, 0], labels)
337 _CACHE_TCS_CIE2017[filename] = tcs
339 return tcs
342def CCT_reference_illuminant(sd: SpectralDistribution) -> NDArrayFloat:
343 """
344 Compute the reference illuminant correlated colour temperature
345 :math:`T_{cp}` and :math:`\\Delta_{uv}` for the specified test spectral
346 distribution using the *Ohno (2013)* method.
348 Parameters
349 ----------
350 sd
351 Test spectral distribution.
353 Returns
354 -------
355 :class:`numpy.ndarray`
356 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.
358 Examples
359 --------
360 >>> from colour import SDS_ILLUMINANTS
361 >>> sd = SDS_ILLUMINANTS["FL2"]
362 >>> CCT_reference_illuminant(sd) # doctest: +ELLIPSIS
363 array([ 4.2244776...e+03, 1.7885608...e-03])
364 """
366 XYZ = sd_to_XYZ(sd.values, shape=sd.shape, method="Integration")
368 # NOTE: Use "CFI2017" and "TM30" recommended temperature range of 1,000K to
369 # 25,000K for performance.
370 return uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)), start=1000, end=25000)
373def sd_reference_illuminant(CCT: float, shape: SpectralShape) -> SpectralDistribution:
374 """
375 Compute the reference illuminant for the specified correlated colour
376 temperature :math:`T_{cp}` for use in *CIE 2017 Colour Fidelity Index*
377 (CFI) computation.
379 Parameters
380 ----------
381 CCT
382 Correlated colour temperature :math:`T_{cp}`.
383 shape
384 Desired shape of the returned spectral distribution.
386 Returns
387 -------
388 :class:`colour.SpectralDistribution`
389 Reference illuminant for *CIE 2017 Colour Fidelity Index* (CFI)
390 computation.
392 Examples
393 --------
394 >>> from colour.utilities import numpy_print_options
395 >>> with numpy_print_options(suppress=True):
396 ... sd_reference_illuminant( # doctest: +ELLIPSIS
397 ... 4224.469705295263300, SpectralShape(380, 780, 20)
398 ... )
399 SpectralDistribution([[ 380. , 0.0034089...],
400 [ 400. , 0.0044208...],
401 [ 420. , 0.0053260...],
402 [ 440. , 0.0062857...],
403 [ 460. , 0.0072767...],
404 [ 480. , 0.0080207...],
405 [ 500. , 0.0086590...],
406 [ 520. , 0.0092242...],
407 [ 540. , 0.0097686...],
408 [ 560. , 0.0101444...],
409 [ 580. , 0.0104475...],
410 [ 600. , 0.0107642...],
411 [ 620. , 0.0110439...],
412 [ 640. , 0.0112535...],
413 [ 660. , 0.0113922...],
414 [ 680. , 0.0115185...],
415 [ 700. , 0.0113155...],
416 [ 720. , 0.0108192...],
417 [ 740. , 0.0111582...],
418 [ 760. , 0.0101299...],
419 [ 780. , 0.0105638...]],
420 SpragueInterpolator,
421 {},
422 Extrapolator,
423 {'method': 'Constant', 'left': None, 'right': None})
424 """
426 if CCT <= 5000:
427 sd_planckian = sd_blackbody(CCT, shape)
429 if CCT >= 4000:
430 xy = CCT_to_xy_CIE_D(CCT)
431 sd_daylight = sd_CIE_illuminant_D_series(xy, shape=shape)
433 if CCT < 4000:
434 sd_reference = sd_planckian
435 elif 4000 <= CCT <= 5000:
436 # Planckian and daylight illuminant must be normalised so that the
437 # mixture isn't biased.
438 sd_planckian /= sd_to_XYZ(
439 sd_planckian.values, shape=shape, method="Integration"
440 )[1]
441 sd_daylight /= sd_to_XYZ(sd_daylight.values, shape=shape, method="Integration")[
442 1
443 ]
445 # Mixture: 4200K should be 80% Planckian, 20% CIE Illuminant D Series.
446 m = (CCT - 4000) / 1000
447 values = linstep_function(m, sd_planckian.values, sd_daylight.values)
448 name = (
449 f"{as_int_scalar(CCT)}K "
450 f"Blackbody & CIE Illuminant D Series Mixture - "
451 f"{as_float_scalar(100 * m):.1f}%"
452 )
453 sd_reference = SpectralDistribution(values, shape.wavelengths, name=name)
454 elif CCT > 5000:
455 sd_reference = sd_daylight
457 return sd_reference
460def tcs_colorimetry_data(
461 sd_irradiance: SpectralDistribution | List[SpectralDistribution],
462 sds_tcs: MultiSpectralDistributions,
463 cmfs: MultiSpectralDistributions,
464) -> Tuple[DataColorimetry_TCS_CIE2017, ...]:
465 """
466 Compute the *test colour samples* colorimetry data under the specified
467 test light source or reference illuminant spectral distribution for the
468 *CIE 2017 Colour Fidelity Index* (CFI) computations.
470 Parameters
471 ----------
472 sd_irradiance
473 Test light source or reference illuminant spectral distribution,
474 i.e., the irradiance emitter.
475 sds_tcs
476 *Test colour samples* spectral reflectance distributions.
477 cmfs
478 Standard observer colour matching functions.
480 Returns
481 -------
482 :class:`tuple`
483 *Test colour samples* colorimetry data under the specified test
484 light source or reference illuminant spectral distribution.
486 Examples
487 --------
488 >>> from colour.colorimetry import SDS_ILLUMINANTS
489 >>> sd = SDS_ILLUMINANTS["FL2"]
490 >>> shape = SpectralShape(380, 780, 5)
491 >>> cmfs = MSDS_CMFS["CIE 1964 10 Degree Standard Observer"].copy().align(shape)
492 >>> test_tcs_colorimetry_data = tcs_colorimetry_data(
493 ... sd, load_TCS_CIE2017(shape), cmfs
494 ... )
495 >>> len(test_tcs_colorimetry_data)
496 1
497 """
499 if isinstance(sd_irradiance, SpectralDistribution):
500 sd_irradiance = [sd_irradiance]
502 XYZ_w = np.full((len(sd_irradiance), 3), np.nan)
503 for idx, sd in enumerate(sd_irradiance):
504 XYZ_t = sd_to_XYZ(
505 sd.values,
506 cmfs,
507 shape=sd.shape,
508 method="Integration",
509 )
510 k = 100 / XYZ_t[1]
511 XYZ_w[idx] = k * XYZ_t
512 sd_irradiance[idx] = sd_irradiance[idx].copy() * k
513 XYZ_w = as_float_array(XYZ_w)
515 Y_b = 20
516 L_A = 100
517 surround = VIEWING_CONDITIONS_CIECAM02["Average"]
519 sds_tcs_t = np.tile(np.transpose(sds_tcs.values), (len(sd_irradiance), 1, 1))
520 sds_tcs_t = sds_tcs_t * np.reshape(
521 as_float_array([sd.values for sd in sd_irradiance]),
522 (len(sd_irradiance), 1, len(sd_irradiance[0])),
523 )
525 XYZ = msds_to_XYZ(
526 sds_tcs_t,
527 cmfs,
528 method="Integration",
529 shape=sds_tcs.shape,
530 )
531 specification = XYZ_to_CIECAM02(
532 XYZ,
533 np.reshape(XYZ_w, (len(sd_irradiance), 1, 3)),
534 L_A,
535 Y_b,
536 surround,
537 discount_illuminant=True,
538 compute_H=False,
539 )
541 JMh = tstack(
542 [
543 cast("NDArrayFloat", specification.J),
544 cast("NDArrayFloat", specification.M),
545 cast("NDArrayFloat", specification.h),
546 ]
547 )
548 Jpapbp = JMh_CIECAM02_to_CAM02UCS(JMh)
550 specification = as_float_array(specification).transpose((0, 2, 1))
551 specification = [CAM_Specification_CIECAM02(*t) for t in specification]
553 return tuple(
554 [
555 DataColorimetry_TCS_CIE2017(
556 sds_tcs.display_labels,
557 XYZ[sd_idx],
558 specification[sd_idx],
559 JMh[sd_idx],
560 Jpapbp[sd_idx],
561 )
562 for sd_idx in range(len(sd_irradiance))
563 ]
564 )
567def delta_E_to_R_f(delta_E: ArrayLike) -> NDArrayFloat:
568 """
569 Convert colour-appearance difference to *CIE 2017 Colour Fidelity Index*
570 (CFI) :math:`R_f` value.
572 Parameters
573 ----------
574 delta_E
575 Euclidean distance between two colours in *CAM02-UCS* colourspace.
577 Returns
578 -------
579 :class:`numpy.ndarray`
580 Corresponding *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`
581 value.
582 """
584 delta_E = as_float_array(delta_E)
586 c_f = 6.73
588 return as_float(10 * np.log1p(np.exp((100 - c_f * delta_E) / 10)))