Coverage for quality/cqs.py: 61%

1"""

2 Colour Quality Scale

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

4

5 Define the *Colour Quality Scale* (CQS) computation objects.

6

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

8- :func:`colour.colour_quality_scale`

9

10 References

11----------

12- :cite:`Davis2010a` : Davis, W., & Ohno, Y. (2010). Color quality scale.

13 Optical Engineering, 49(3), 033602. doi:10.1117/1.3360335

14- :cite:`Ohno2008a` : Ohno, Yoshiro, & Davis, W. (2008). NIST CQS simulation

15 (Version 7.4) [Computer software].

16 https://drive.google.com/file/d/1PsuU6QjUJjCX6tQyCud6ul2Tbs8rYWW9/view?\

17 usp=sharing

18- :cite:`Ohno2013` : Ohno, Yoshiro, & Davis, W. (2008). NIST CQS simulation

19 (Version 7.4) [Computer software].

20 https://drive.google.com/file/d/1PsuU6QjUJjCX6tQyCud6ul2Tbs8rYWW9/view?\

21 usp=sharing

22"""

23

24 from __future__ import annotations

25

26 import typing

27 from dataclasses import dataclass

28

29 import numpy as np

30

31 from colour.adaptation import chromatic_adaptation_VonKries

32 from colour.algebra import euclidean_distance, sdiv, sdiv_mode

33 from colour.colorimetry import (

34 CCS_ILLUMINANTS,

35 MSDS_CMFS,

36 SPECTRAL_SHAPE_DEFAULT,

37 MultiSpectralDistributions,

38 SpectralDistribution,

39 reshape_msds,

40 reshape_sd,

41 sd_blackbody,

42 sd_CIE_illuminant_D_series,

43 sd_to_XYZ,

44)

45

46 if typing.TYPE_CHECKING:

47 from colour.hints import (

48 ArrayLike,

49 Dict,

50 Literal,

51 NDArrayFloat,

52 Tuple,

53 )

54

55 from colour.hints import cast

56 from colour.models import Lab_to_LCHab # pyright: ignore

57 from colour.models import UCS_to_uv, XYZ_to_Lab, XYZ_to_UCS, XYZ_to_xy, xy_to_XYZ

58 from colour.quality.datasets.vs import INDEXES_TO_NAMES_VS, SDS_VS

59 from colour.temperature import CCT_to_xy_CIE_D, uv_to_CCT_Ohno2013

60 from colour.utilities import as_float_array, domain_range_scale, tsplit, validate_method

61 from colour.utilities.documentation import DocstringTuple, is_documentation_building

62

63 __author__ = "Colour Developers"

64 __copyright__ = "Copyright 2013 Colour Developers"

65 __license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"

66 __maintainer__ = "Colour Developers"

67 __email__ = "colour-developers@colour-science.org"

68 __status__ = "Production"

69

70 __all__ = [

71 "GAMUT_AREA_D65",

72 "DataColorimetry_VS",

73 "DataColourQualityScale_VS",

74 "ColourRendering_Specification_CQS",

75 "COLOUR_QUALITY_SCALE_METHODS",

76 "colour_quality_scale",

77 "gamut_area",

78 "vs_colorimetry_data",

79 "CCT_factor",

80 "scale_conversion",

81 "delta_E_RMS",

82 "colour_quality_scales",

83]

84

85 GAMUT_AREA_D65: int = 8210

86"""Gamut area for *CIE Illuminant D Series D65*."""

87

88

89@dataclass

90 class DataColorimetry_VS:

91 """

92 Store colorimetry data for *VS test colour samples*.

93

94 This dataclass encapsulates the colorimetric measurements and derived

95 values for Visual Spectrum (VS) test colour samples used in colour

96 quality evaluation.

97

98 Attributes

99 ----------

100 name

101 Sample identifier or designation.

102 XYZ

103 Tristimulus values under the test illuminant.

104 Lab

105 *CIE L\\*a\\*b\\** colour space coordinates.

106 C

107 Chroma values calculated from the *CIE L\\*a\\*b\\** coordinates.

108 """

109

110 name: str

111 XYZ: NDArrayFloat

112 Lab: NDArrayFloat

113 C: NDArrayFloat

114

115

116@dataclass

117 class DataColourQualityScale_VS:

118 """

119 Store colour quality scale data for *VS test colour samples*.

120

121 This dataclass encapsulates the colour quality metrics computed for VS

122 (Visual Samples) test colour samples, including quality assessment and

123 colour difference measurements used in colour rendering evaluations.

124

125 Attributes

126 ----------

127 name

128 Identifier or descriptor for the test colour sample.

129 Q_a

130 Colour quality scale value for the sample.

131 D_C_ab

132 Chroma difference in *CIE L\\*a\\*b\\** colourspace.

133 D_E_ab

134 Total colour difference in *CIE L\\*a\\*b\\** colourspace.

135 """

136

137 name: str

138 Q_a: float

139 D_C_ab: float

140 D_E_ab: float

141 D_Ep_ab: float

142

143

144@dataclass

145 class ColourRendering_Specification_CQS:

146 """

147 Define the *Colour Quality Scale* (CQS) colour rendering (quality)

148 specification.

149

150 Parameters

151 ----------

152 name

153 Name of the test spectral distribution.

154 Q_a

155 Colour quality scale :math:`Q_a`.

156 Q_f

157 Colour fidelity scale :math:`Q_f` intended to evaluate the

158 fidelity of object colour appearances (compared to the reference

159 illuminant of the same correlated colour temperature and

160 illuminance).

161 Q_p

162 Colour preference scale :math:`Q_p` similar to colour quality

163 scale :math:`Q_a` but placing additional weight on preference of

164 object colour appearance, set to *None* in *NIST CQS 9.0* method.

165 This metric is based on the notion that increases in chroma are

166 generally preferred and should be rewarded.

167 Q_g

168 Gamut area scale :math:`Q_g` representing the relative gamut

169 formed by the (:math:`a^*`, :math:`b^*`) coordinates of the 15

170 samples illuminated by the test light source in the

171 *CIE L\\*a\\*b\\** object colourspace.

172 Q_d

173 Relative gamut area scale :math:`Q_d`, set to *None* in

174 *NIST CQS 9.0* method.

175 Q_as

176 Individual *Colour Quality Scale* (CQS) data for each sample.

177 colorimetry_data

178 Colorimetry data for the test and reference computations.

179

180 References

181 ----------

182 :cite:`Davis2010a`, :cite:`Ohno2008a`, :cite:`Ohno2013`

183 """

184

185 name: str

186 Q_a: float

187 Q_f: float

188 Q_p: float | None

189 Q_g: float

190 Q_d: float | None

191 Q_as: Dict[int, DataColourQualityScale_VS]

192 colorimetry_data: Tuple[

193 Tuple[DataColorimetry_VS, ...], Tuple[DataColorimetry_VS, ...]

194 ]

195

196

197 COLOUR_QUALITY_SCALE_METHODS: tuple = ("NIST CQS 7.4", "NIST CQS 9.0")

198 if is_documentation_building(): # pragma: no cover

199 COLOUR_QUALITY_SCALE_METHODS = DocstringTuple(COLOUR_QUALITY_SCALE_METHODS)

200 COLOUR_QUALITY_SCALE_METHODS.__doc__ = """

201 Supported *Colour Quality Scale* (CQS) computation methods.

202

203 References

204----------

205:cite:`Davis2010a`, :cite:`Ohno2008a`, :cite:`Ohno2013`

206"""

207

208

209@typing.overload

210 def colour_quality_scale(

211 sd_test: SpectralDistribution,

212 *,

213 additional_data: Literal[False],

214 method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ...,

215) -> float: ...

216

217

218@typing.overload

219 def colour_quality_scale(

220 sd_test: SpectralDistribution,

221 additional_data: Literal[True] = True,

222 method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ...,

223) -> ColourRendering_Specification_CQS: ...

224

225

226@typing.overload

227 def colour_quality_scale(

228 sd_test: SpectralDistribution,

229 additional_data: Literal[False],

230 method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = ...,

231) -> float: ...

232

233

234 def colour_quality_scale(

235 sd_test: SpectralDistribution,

236 additional_data: bool = False,

237 method: Literal["NIST CQS 7.4", "NIST CQS 9.0"] | str = "NIST CQS 9.0",

238) -> float | ColourRendering_Specification_CQS:

239 """

240 Compute the *Colour Quality Scale* (CQS) of the specified spectral

241 distribution using the specified method.

242

243 Parameters

244 ----------

245 sd_test

246 Test spectral distribution.

247 additional_data

248 Whether to output additional data.

249 method

250 Computation method.

251

252 Returns

253 -------

254 :class:`float` or :class:`colour.quality.ColourRendering_Specification_CQS`

255 *Colour Quality Scale* (CQS).

256

257 References

258 ----------

259 :cite:`Davis2010a`, :cite:`Ohno2008a`, :cite:`Ohno2013`

260

261 Examples

262 --------

263 >>> from colour import SDS_ILLUMINANTS

264 >>> sd = SDS_ILLUMINANTS["FL2"]

265 >>> colour_quality_scale(sd) # doctest: +ELLIPSIS

266 64.1118220...

267 """

268

269 method = validate_method(method, tuple(COLOUR_QUALITY_SCALE_METHODS))

270

271 cmfs = reshape_msds(

272 MSDS_CMFS["CIE 1931 2 Degree Standard Observer"],

273 SPECTRAL_SHAPE_DEFAULT,

274 copy=False,

275 )

276

277 shape = cmfs.shape

278 sd_test = reshape_sd(sd_test, shape, copy=False)

279 vs_sds = {

280 sd.name: reshape_sd(sd, shape, copy=False) for sd in SDS_VS[method].values()

281 }

282

283 with domain_range_scale("1"):

284 XYZ = sd_to_XYZ(sd_test, cmfs)

285

286 uv = UCS_to_uv(XYZ_to_UCS(XYZ))

287 CCT, _D_uv = uv_to_CCT_Ohno2013(uv)

288

289 if CCT < 5000:

290 sd_reference = sd_blackbody(CCT, shape)

291 else:

292 xy = CCT_to_xy_CIE_D(CCT)

293 sd_reference = sd_CIE_illuminant_D_series(xy)

294 sd_reference.align(shape)

295

296 test_vs_colorimetry_data = vs_colorimetry_data(

297 sd_test, sd_reference, vs_sds, cmfs, chromatic_adaptation=True

298 )

299

300 reference_vs_colorimetry_data = vs_colorimetry_data(

301 sd_reference, sd_reference, vs_sds, cmfs

302 )

303

304 CCT_f: float

305 if method == "nist cqs 9.0":

306 CCT_f = 1

307 scaling_f = 3.2

308 else:

309 XYZ_r = sd_to_XYZ(sd_reference, cmfs)

310 XYZ_r /= XYZ_r[1]

311 CCT_f = CCT_factor(reference_vs_colorimetry_data, XYZ_r)

312 scaling_f = 3.104

313

314 Q_as = colour_quality_scales(

315 test_vs_colorimetry_data,

316 reference_vs_colorimetry_data,

317 scaling_f,

318 CCT_f,

319 )

320

321 D_E_RMS = delta_E_RMS(Q_as, "D_E_ab")

322 D_Ep_RMS = delta_E_RMS(Q_as, "D_Ep_ab")

323

324 Q_a = scale_conversion(D_Ep_RMS, CCT_f, scaling_f)

325

326 scaling_f = 2.93 * 1.0343 if method == "nist cqs 9.0" else 2.928

327

328 Q_f = scale_conversion(D_E_RMS, CCT_f, scaling_f)

329

330 G_t = gamut_area([vs_CQS_data.Lab for vs_CQS_data in test_vs_colorimetry_data])

331 G_r = gamut_area([vs_CQS_data.Lab for vs_CQS_data in reference_vs_colorimetry_data])

332

333 Q_g = G_t / GAMUT_AREA_D65 * 100

334

335 if method == "nist cqs 9.0":

336 Q_p = Q_d = None

337 else:

338 p_delta_C = cast(

339 "float",

340 np.average([max(0, sample_data.D_C_ab) for sample_data in Q_as.values()]),

341 )

342 Q_p = 100 - 3.6 * (D_Ep_RMS - p_delta_C)

343 Q_d = G_t / G_r * CCT_f * 100

344

345 if additional_data:

346 return ColourRendering_Specification_CQS(

347 sd_test.name,

348 Q_a,

349 Q_f,

350 Q_p,

351 Q_g,

352 Q_d,

353 Q_as,

354 (test_vs_colorimetry_data, reference_vs_colorimetry_data),

355 )

356

357 return Q_a

358

359

360 def gamut_area(Lab: ArrayLike) -> float:

361 """

362 Compute the gamut area :math:`G` covered by the specified

363 *CIE L\\*a\\*b\\** colourspace matrices.

364

365 Parameters

366 ----------

367 Lab

368 *CIE L\\*a\\*b\\** colourspace matrices.

369

370 Returns

371 -------

372 :class:`float`

373 Gamut area :math:`G`.

374

375 Examples

376 --------

377 >>> Lab = [

378 ... np.array([39.94996006, 34.59018231, -19.86046321]),

379 ... np.array([38.88395498, 21.44348519, -34.87805301]),

380 ... np.array([36.60576301, 7.06742454, -43.21461177]),

381 ... np.array([46.60142558, -15.90481586, -34.64616865]),

382 ... np.array([56.50196523, -29.54655550, -20.50177194]),

383 ... np.array([55.73912101, -43.39520959, -5.08956953]),

384 ... np.array([56.20776870, -53.68997662, 20.21134410]),

385 ... np.array([66.16683122, -38.64600327, 42.77396631]),

386 ... np.array([76.72952110, -23.92148210, 61.04740432]),

387 ... np.array([82.85370708, -3.98679065, 75.43320144]),

388 ... np.array([69.26458861, 13.11066359, 68.83858372]),

389 ... np.array([69.63154351, 28.24532497, 59.45609803]),

390 ... np.array([61.26281449, 40.87950839, 44.97606172]),

391 ... np.array([41.62567821, 57.34129516, 27.46718170]),

392 ... np.array([40.52565174, 48.87449192, 3.45121680]),

393 ... ]

394 >>> gamut_area(Lab) # doctest: +ELLIPSIS

395 8335.9482018...

396 """

397

398 Lab = as_float_array(Lab)

399 Lab_s = np.roll(np.copy(Lab), -3)

400

401 _L, a, b = tsplit(Lab)

402 _L_s, a_s, b_s = tsplit(Lab_s)

403

404 A = np.linalg.norm(Lab[..., 1:3], axis=-1)

405 B = np.linalg.norm(Lab_s[..., 1:3], axis=-1)

406 C = np.linalg.norm(np.dstack([a_s - a, b_s - b]), axis=-1)

407 t = (A + B + C) / 2

408 S = np.sqrt(t * (t - A) * (t - B) * (t - C))

409

410 return np.sum(S)

411

412

413 def vs_colorimetry_data(

414 sd_test: SpectralDistribution,

415 sd_reference: SpectralDistribution,

416 sds_vs: Dict[str, SpectralDistribution],

417 cmfs: MultiSpectralDistributions,

418 chromatic_adaptation: bool = False,

419) -> Tuple[DataColorimetry_VS, ...]:

420 """

421 Compute the *VS test colour samples* colorimetry data.

422

423 Parameters

424 ----------

425 sd_test

426 Test spectral distribution.

427 sd_reference

428 Reference spectral distribution.

429 sds_vs

430 *VS test colour samples* spectral reflectance distributions.

431 cmfs

432 Standard observer colour matching functions.

433 chromatic_adaptation

434 Whether to perform chromatic adaptation.

435

436 Returns

437 -------

438 :class:`tuple`

439 *VS test colour samples* colorimetry data.

440 """

441

442 XYZ_t = sd_to_XYZ(sd_test, cmfs)

443

444 with sdiv_mode():

445 XYZ_t = sdiv(XYZ_t, XYZ_t[1])

446

447 XYZ_r = sd_to_XYZ(sd_reference, cmfs)

448

449 with sdiv_mode():

450 XYZ_r = sdiv(XYZ_r, XYZ_r[1])

451

452 xy_r = XYZ_to_xy(XYZ_r)

453

454 vs_data = []

455 for _key, value in sorted(INDEXES_TO_NAMES_VS.items()):

456 sd_vs = sds_vs[value]

457

458 with domain_range_scale("1"):

459 XYZ_vs = sd_to_XYZ(sd_vs, cmfs, sd_test)

460

461 if chromatic_adaptation:

462 XYZ_vs = chromatic_adaptation_VonKries(

463 XYZ_vs, XYZ_t, XYZ_r, transform="CMCCAT2000"

464 )

465

466 Lab_vs = XYZ_to_Lab(XYZ_vs, illuminant=xy_r)

467 _L_vs, C_vs, _Hab = Lab_to_LCHab(Lab_vs)

468

469 vs_data.append(DataColorimetry_VS(sd_vs.name, XYZ_vs, Lab_vs, C_vs))

470

471 return tuple(vs_data)

472

473

474 def CCT_factor(

475 reference_data: Tuple[DataColorimetry_VS, ...], XYZ_r: ArrayLike

476) -> float:

477 """

478 Compute the correlated colour temperature factor that penalizes lamps

479 with extremely low correlated colour temperatures.

480

481 Parameters

482 ----------

483 reference_data

484 Reference colorimetry data.

485 XYZ_r

486 *CIE XYZ* tristimulus values for reference.

487

488 Returns

489 -------

490 :class:`float`

491 Correlated colour temperature factor.

492 """

493

494 xy_w = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"]

495 XYZ_w = xy_to_XYZ(xy_w)

496

497 Lab = XYZ_to_Lab(

498 chromatic_adaptation_VonKries(

499 [colorimetry_data.XYZ for colorimetry_data in reference_data],

500 XYZ_r,

501 XYZ_w,

502 transform="CMCCAT2000",

503 ),

504 illuminant=xy_w,

505 )

506

507 G_r = gamut_area(Lab) / GAMUT_AREA_D65

508

509 return min(G_r, 1)

510

511

512 def scale_conversion(D_E_ab: float, CCT_f: float, scaling_f: float) -> float:

513 """

514 Compute the *Colour Quality Scale* (CQS) for the specified

515 :math:`\\Delta E_{ab}` value and correlated colour temperature

516 penalizing factor.

517

518 Parameters

519 ----------

520 D_E_ab

521 :math:`\\Delta E_{ab}` value.

522 CCT_f

523 Correlated colour temperature penalizing factor.

524 scaling_f

525 Scaling factor constant.

526

527 Returns

528 -------

529 :class:`float`

530 *Colour Quality Scale* (CQS).

531 """

532

533 return 10 * np.log1p(np.exp((100 - scaling_f * D_E_ab) / 10)) * CCT_f

534

535

536 def delta_E_RMS(

537 CQS_data: Dict[int, DataColourQualityScale_VS], attribute: str

538) -> float:

539 """

540 Compute the root-mean-square average for the specified *Colour Quality

541 Scale* (CQS) data using the specified colorimetry attribute.

542

543 Parameters

544 ----------

545 CQS_data

546 *Colour Quality Scale* (CQS) data.

547 attribute

548 Colorimetry data attribute to use for computing the

549 root-mean-square average.

550

551 Returns

552 -------

553 :class:`float`

554 Root-mean-square average.

555 """

556

557 return np.sqrt(

558 1

559 / len(CQS_data)

560 * np.sum(

561 [getattr(sample_data, attribute) ** 2 for sample_data in CQS_data.values()]

562 )

563 )

564

565

566 def colour_quality_scales(

567 test_data: Tuple[DataColorimetry_VS, ...],

568 reference_data: Tuple[DataColorimetry_VS, ...],

569 scaling_f: float,

570 CCT_f: float,

571) -> Dict[int, DataColourQualityScale_VS]:

572 """

573 Compute the *VS test colour samples* rendering scales.

574

575 Parameters

576 ----------

577 test_data

578 Test data for the VS colour samples.

579 reference_data

580 Reference data for the VS colour samples.

581 scaling_f

582 Scaling factor constant for normalizing the colour rendering

583 scales.

584 CCT_f

585 Factor penalizing light sources with extremely low correlated

586 colour temperatures.

587

588 Returns

589 -------

590 :class:`dict`

591 *VS test colour samples* colour rendering scales.

592 """

593

594 Q_as = {}

595 for i in range(len(test_data)):

596 D_C_ab = cast("float", test_data[i].C - reference_data[i].C)

597 D_E_ab = cast(

598 "float", euclidean_distance(test_data[i].Lab, reference_data[i].Lab)

599 )

600 D_Ep_ab = cast(

601 "float", np.sqrt(D_E_ab**2 - D_C_ab**2) if D_C_ab > 0 else D_E_ab

602 )

603

604 Q_a = scale_conversion(D_Ep_ab, CCT_f, scaling_f)

605 Q_as[i + 1] = DataColourQualityScale_VS(

606 test_data[i].name, Q_a, D_C_ab, D_E_ab, D_Ep_ab

607 )

608

609 return Q_as