Coverage for quality/cfi2017.py: 62%

1"""

2 CIE 2017 Colour Fidelity Index

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

4

5 Define 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

10 References

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

16 from __future__ import annotations

17

18 import os

19 import typing

20 from dataclasses import dataclass

21

22 import numpy as np

23

24 from colour.algebra import Extrapolator, euclidean_distance, linstep_function

25 from colour.appearance import (

26 VIEWING_CONDITIONS_CIECAM02,

27 CAM_Specification_CIECAM02,

28 XYZ_to_CIECAM02,

29)

30 from 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

42 if typing.TYPE_CHECKING:

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

44

45 from colour.hints import NDArrayFloat, cast

46 from colour.models import JMh_CIECAM02_to_CAM02UCS, UCS_to_uv, XYZ_to_UCS

47 from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Ohno2013

48 from 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

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

82"""

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

84 standard.

85"""

86

87 ROOT_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

96 class 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

131 class 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

167 def colour_fidelity_index_CIE2017(

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

169) -> ColourRendering_Specification_CIE2017: ...

170

171

172@typing.overload

173 def colour_fidelity_index_CIE2017(

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

175) -> float: ...

176

177

178@typing.overload

179 def colour_fidelity_index_CIE2017(

180 sd_test: SpectralDistribution, additional_data: Literal[False]

181) -> float: ...

182

183

184 def 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

291 def 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

342 def 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

373 def 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

460 def 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

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