Coverage for colour/quality/cfi2017.py: 100%

129 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 23:01 +1300

1""" 

2CIE 2017 Colour Fidelity Index 

3============================== 

4 

5Define the *CIE 2017 Colour Fidelity Index* (CFI) computation objects. 

6 

7- :class:`colour.quality.ColourRendering_Specification_CIE2017` 

8- :func:`colour.quality.colour_fidelity_index_CIE2017` 

9 

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""" 

15 

16from __future__ import annotations 

17 

18import os 

19import typing 

20from dataclasses import dataclass 

21 

22import numpy as np 

23 

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) 

41 

42if typing.TYPE_CHECKING: 

43 from colour.hints import ArrayLike, List, Literal, Tuple 

44 

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) 

60 

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" 

67 

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] 

80 

81SPECTRAL_SHAPE_CIE2017: SpectralShape = SpectralShape(380, 780, 1) 

82""" 

83Spectral shape for *CIE 2017 Colour Fidelity Index* (CFI) 

84standard. 

85""" 

86 

87ROOT_RESOURCES_CIE2017: str = os.path.join(os.path.dirname(__file__), "datasets") 

88"""*CIE 2017 Colour Fidelity Index* resources directory.""" 

89 

90_CACHE_TCS_CIE2017: dict = CACHE_REGISTRY.register_cache( 

91 f"{__name__}._CACHE_TCS_CIE2017" 

92) 

93 

94 

95@dataclass 

96class DataColorimetry_TCS_CIE2017: 

97 """ 

98 Store colorimetry data for *test colour samples* used in CIE 2017 

99 colour fidelity calculations. 

100 

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. 

105 

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 """ 

122 

123 name: str | list[str] 

124 XYZ: NDArrayFloat 

125 CAM: CAM_Specification_CIECAM02 

126 JMh: NDArrayFloat 

127 Jpapbp: NDArrayFloat 

128 

129 

130@dataclass 

131class ColourRendering_Specification_CIE2017: 

132 """ 

133 Define the *CIE 2017 Colour Fidelity Index* (CFI) colour quality 

134 specification. 

135 

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 """ 

155 

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 

164 

165 

166@typing.overload 

167def colour_fidelity_index_CIE2017( 

168 sd_test: SpectralDistribution, additional_data: Literal[True] = True 

169) -> ColourRendering_Specification_CIE2017: ... 

170 

171 

172@typing.overload 

173def colour_fidelity_index_CIE2017( 

174 sd_test: SpectralDistribution, *, additional_data: Literal[False] 

175) -> float: ... 

176 

177 

178@typing.overload 

179def colour_fidelity_index_CIE2017( 

180 sd_test: SpectralDistribution, additional_data: Literal[False] 

181) -> float: ... 

182 

183 

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. 

190 

191 Parameters 

192 ---------- 

193 sd_test 

194 Test spectral distribution. 

195 additional_data 

196 Whether to output additional data. 

197 

198 Returns 

199 ------- 

200 :class:`float` or \ 

201:class:`colour.quality.ColourRendering_Specification_CIE2017` 

202 *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f`. 

203 

204 References 

205 ---------- 

206 :cite:`CIETC1-902017` 

207 

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 """ 

215 

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 ) 

222 

223 raise ValueError(error) 

224 

225 shape = SpectralShape( 

226 SPECTRAL_SHAPE_CIE2017.start, 

227 SPECTRAL_SHAPE_CIE2017.end, 

228 sd_test.shape.interval, 

229 ) 

230 

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 ) 

237 

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) 

248 

249 if sd_test.shape.boundaries != shape.boundaries: 

250 sd_test.trim(shape) 

251 

252 CCT, D_uv = tsplit(CCT_reference_illuminant(sd_test)) 

253 sd_reference = sd_reference_illuminant(CCT, shape) 

254 

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 ) 

260 

261 sds_tcs = load_TCS_CIE2017(shape) 

262 

263 ( 

264 test_tcs_colorimetry_data, 

265 reference_tcs_colorimetry_data, 

266 ) = tcs_colorimetry_data([sd_test, sd_reference], sds_tcs, cmfs_10) 

267 

268 delta_E_s = euclidean_distance( 

269 test_tcs_colorimetry_data.Jpapbp, 

270 reference_tcs_colorimetry_data.Jpapbp, 

271 ) 

272 

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))) 

275 

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 ) 

287 

288 return R_f 

289 

290 

291def load_TCS_CIE2017(shape: SpectralShape) -> MultiSpectralDistributions: 

292 """ 

293 Load the *CIE 2017 Test Colour Samples* dataset appropriate for the 

294 specified spectral shape. 

295 

296 The datasets are cached and will not be loaded again on subsequent 

297 calls to this definition. 

298 

299 Parameters 

300 ---------- 

301 shape 

302 Spectral shape of the tested illuminant. 

303 

304 Returns 

305 ------- 

306 :class:`colour.MultiSpectralDistributions` 

307 *CIE 2017 Test Colour Samples* dataset. 

308 

309 Examples 

310 -------- 

311 >>> sds_tcs = load_TCS_CIE2017(SpectralShape(380, 780, 5)) 

312 >>> len(sds_tcs.labels) 

313 99 

314 """ 

315 

316 global _CACHE_TCS_CIE2017 # noqa: PLW0602 

317 

318 interval = shape.interval 

319 

320 attest( 

321 interval in (1, 5), 

322 "Spectral shape interval must be either 1nm or 5nm!", 

323 ) 

324 

325 filename = f"tcs_cfi2017_{as_int_scalar(interval)}_nm.csv.gz" 

326 

327 if is_caching_enabled() and filename in _CACHE_TCS_CIE2017: 

328 return _CACHE_TCS_CIE2017[filename] 

329 

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)] 

334 

335 tcs = MultiSpectralDistributions(data[:, 1:], data[:, 0], labels) 

336 

337 _CACHE_TCS_CIE2017[filename] = tcs 

338 

339 return tcs 

340 

341 

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. 

347 

348 Parameters 

349 ---------- 

350 sd 

351 Test spectral distribution. 

352 

353 Returns 

354 ------- 

355 :class:`numpy.ndarray` 

356 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. 

357 

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 """ 

365 

366 XYZ = sd_to_XYZ(sd.values, shape=sd.shape, method="Integration") 

367 

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) 

371 

372 

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. 

378 

379 Parameters 

380 ---------- 

381 CCT 

382 Correlated colour temperature :math:`T_{cp}`. 

383 shape 

384 Desired shape of the returned spectral distribution. 

385 

386 Returns 

387 ------- 

388 :class:`colour.SpectralDistribution` 

389 Reference illuminant for *CIE 2017 Colour Fidelity Index* (CFI) 

390 computation. 

391 

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 """ 

425 

426 if CCT <= 5000: 

427 sd_planckian = sd_blackbody(CCT, shape) 

428 

429 if CCT >= 4000: 

430 xy = CCT_to_xy_CIE_D(CCT) 

431 sd_daylight = sd_CIE_illuminant_D_series(xy, shape=shape) 

432 

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 ] 

444 

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 

456 

457 return sd_reference 

458 

459 

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. 

469 

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. 

479 

480 Returns 

481 ------- 

482 :class:`tuple` 

483 *Test colour samples* colorimetry data under the specified test 

484 light source or reference illuminant spectral distribution. 

485 

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 """ 

498 

499 if isinstance(sd_irradiance, SpectralDistribution): 

500 sd_irradiance = [sd_irradiance] 

501 

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) 

514 

515 Y_b = 20 

516 L_A = 100 

517 surround = VIEWING_CONDITIONS_CIECAM02["Average"] 

518 

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 ) 

524 

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 ) 

540 

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) 

549 

550 specification = as_float_array(specification).transpose((0, 2, 1)) 

551 specification = [CAM_Specification_CIECAM02(*t) for t in specification] 

552 

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 ) 

565 

566 

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. 

571 

572 Parameters 

573 ---------- 

574 delta_E 

575 Euclidean distance between two colours in *CAM02-UCS* colourspace. 

576 

577 Returns 

578 ------- 

579 :class:`numpy.ndarray` 

580 Corresponding *CIE 2017 Colour Fidelity Index* (CFI) :math:`R_f` 

581 value. 

582 """ 

583 

584 delta_E = as_float_array(delta_E) 

585 

586 c_f = 6.73 

587 

588 return as_float(10 * np.log1p(np.exp((100 - c_f * delta_E) / 10)))