Coverage for plotting/quality.py: 68%

82 statements  

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

1""" 

2Colour Quality Plotting 

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

4 

5Define the colour quality plotting objects. 

6 

7- :func:`colour.plotting.plot_single_sd_colour_rendering_index_bars` 

8- :func:`colour.plotting.plot_multi_sds_colour_rendering_indexes_bars` 

9- :func:`colour.plotting.plot_single_sd_colour_quality_scale_bars` 

10- :func:`colour.plotting.plot_multi_sds_colour_quality_scales_bars` 

11""" 

12 

13from __future__ import annotations 

14 

15import typing 

16 

17if typing.TYPE_CHECKING: 

18 from collections.abc import ValuesView 

19 

20from itertools import cycle 

21 

22if typing.TYPE_CHECKING: 

23 from matplotlib.figure import Figure 

24 from matplotlib.axes import Axes 

25 

26import numpy as np 

27 

28from colour.colorimetry import ( 

29 MultiSpectralDistributions, 

30 SpectralDistribution, 

31 sds_and_msds_to_sds, 

32) 

33from colour.constants import DTYPE_FLOAT_DEFAULT 

34 

35if typing.TYPE_CHECKING: 

36 from colour.hints import ( 

37 Any, 

38 Dict, 

39 Literal, 

40 Sequence, 

41 Tuple, 

42 ) 

43 

44from colour.plotting import ( 

45 CONSTANTS_COLOUR_STYLE, 

46 XYZ_to_plotting_colourspace, 

47 artist, 

48 label_rectangles, 

49 override_style, 

50 render, 

51) 

52from colour.quality import ( 

53 COLOUR_QUALITY_SCALE_METHODS, 

54 ColourRendering_Specification_CQS, 

55 ColourRendering_Specification_CRI, 

56 colour_quality_scale, 

57 colour_rendering_index, 

58) 

59from colour.utilities import as_float_array, ones, validate_method 

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

70 "plot_single_sd_colour_rendering_index_bars", 

71 "plot_multi_sds_colour_rendering_indexes_bars", 

72 "plot_single_sd_colour_quality_scale_bars", 

73 "plot_multi_sds_colour_quality_scales_bars", 

74] 

75 

76 

77@override_style() 

78def plot_colour_quality_bars( 

79 specifications: Sequence[ 

80 ColourRendering_Specification_CQS | ColourRendering_Specification_CRI 

81 ], 

82 labels: bool = True, 

83 hatching: bool | None = None, 

84 hatching_repeat: int = 2, 

85 **kwargs: Any, 

86) -> Tuple[Figure, Axes]: 

87 """ 

88 Plot the colour quality data of the specified illuminants or light sources 

89 colour quality specifications. 

90 

91 Parameters 

92 ---------- 

93 specifications 

94 Array of illuminants or light sources colour quality 

95 specifications. 

96 labels 

97 Add labels above bars. 

98 hatching 

99 Use hatching for the bars. 

100 hatching_repeat 

101 Hatching pattern repeat. 

102 

103 Other Parameters 

104 ---------------- 

105 kwargs 

106 {:func:`colour.plotting.artist`, 

107 :func:`colour.plotting.quality.plot_colour_quality_bars`, 

108 :func:`colour.plotting.render`}, 

109 See the documentation of the previously listed definitions. 

110 

111 Returns 

112 ------- 

113 :class:`tuple` 

114 Current figure and axes. 

115 

116 Examples 

117 -------- 

118 >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES, SpectralShape 

119 >>> illuminant = SDS_ILLUMINANTS["FL2"] 

120 >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"] 

121 >>> light_source = light_source.copy().align(SpectralShape(360, 830, 1)) 

122 >>> cqs_i = colour_quality_scale(illuminant, additional_data=True) 

123 >>> cqs_l = colour_quality_scale(light_source, additional_data=True) 

124 >>> plot_colour_quality_bars([cqs_i, cqs_l]) # doctest: +ELLIPSIS 

125 (<Figure size ... with 1 Axes>, <...Axes...>) 

126 

127 .. image:: ../_static/Plotting_Plot_Colour_Quality_Bars.png 

128 :align: center 

129 :alt: plot_colour_quality_bars 

130 """ 

131 

132 settings: Dict[str, Any] = {"uniform": True} 

133 settings.update(kwargs) 

134 

135 _figure, axes = artist(**settings) 

136 

137 bar_width = 0.5 

138 y_ticks_interval = 10 

139 count_s, count_Q_as = len(specifications), 0 

140 patterns = cycle(CONSTANTS_COLOUR_STYLE.hatch.patterns) 

141 if hatching is None: 

142 hatching = count_s != 1 

143 

144 for i, specification in enumerate(specifications): 

145 Q_a, Q_as, colorimetry_data = ( 

146 specification.Q_a, 

147 specification.Q_as, 

148 specification.colorimetry_data, 

149 ) 

150 

151 count_Q_as = len(Q_as) 

152 RGB = [ones(3)] + [ 

153 np.clip(XYZ_to_plotting_colourspace(x.XYZ), 0, 1) 

154 for x in colorimetry_data[0] 

155 ] 

156 

157 x = ( 

158 as_float_array( 

159 i 

160 + np.arange( 

161 0, 

162 (count_Q_as + 1) * (count_s + 1), 

163 (count_s + 1), 

164 dtype=DTYPE_FLOAT_DEFAULT, 

165 ) 

166 ) 

167 * bar_width 

168 ) 

169 y = as_float_array( 

170 [Q_a] + [s[1].Q_a for s in sorted(Q_as.items(), key=lambda s: s[0])] 

171 ) 

172 

173 bars = axes.bar( 

174 x, 

175 np.abs(y), 

176 color=RGB, 

177 width=bar_width, 

178 edgecolor=CONSTANTS_COLOUR_STYLE.colour.dark, 

179 label=specification.name, 

180 zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, 

181 ) 

182 

183 hatches = ( 

184 [next(patterns) * hatching_repeat] * (count_Q_as + 1) 

185 if hatching 

186 else list(np.where(y < 0, next(patterns), None)) # pyright: ignore 

187 ) 

188 

189 for j, bar in enumerate(bars.patches): 

190 bar.set_hatch(hatches[j]) 

191 

192 if labels: 

193 label_rectangles( 

194 [f"{y_v:.1f}" for y_v in y], 

195 bars, 

196 rotation="horizontal" if count_s == 1 else "vertical", 

197 offset=( 

198 0 if count_s == 1 else 3 / 100 * count_s + 65 / 1000, 

199 0.025, 

200 ), 

201 text_size=-5 / 7 * count_s + 12.5, 

202 axes=axes, 

203 ) 

204 

205 axes.axhline( 

206 y=100, 

207 color=CONSTANTS_COLOUR_STYLE.colour.dark, 

208 linestyle="--", 

209 zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, 

210 ) 

211 

212 axes.set_xticks( 

213 ( 

214 np.arange( 

215 0, 

216 (count_Q_as + 1) * (count_s + 1), 

217 (count_s + 1), 

218 dtype=DTYPE_FLOAT_DEFAULT, 

219 ) 

220 - bar_width 

221 ) 

222 * bar_width 

223 + (count_s * bar_width / 2) 

224 ) 

225 axes.set_xticklabels( 

226 ["Qa"] + [f"Q{index + 1}" for index in range(0, count_Q_as, 1)] 

227 ) 

228 axes.set_yticks(range(0, 100 + y_ticks_interval, y_ticks_interval)) 

229 

230 aspect = 1 / (120 / (bar_width + len(Q_as) + bar_width * 2)) 

231 bounding_box = ( 

232 -bar_width, 

233 ((count_Q_as + 1) * (count_s + 1)) / 2 - bar_width, 

234 0, 

235 120, 

236 ) 

237 

238 settings = { 

239 "axes": axes, 

240 "aspect": aspect, 

241 "bounding_box": bounding_box, 

242 "legend": hatching, 

243 "title": "Colour Quality", 

244 } 

245 settings.update(kwargs) 

246 

247 return render(**settings) 

248 

249 

250@override_style() 

251def plot_single_sd_colour_rendering_index_bars( 

252 sd: SpectralDistribution, **kwargs: Any 

253) -> Tuple[Figure, Axes]: 

254 """ 

255 Plot the *Colour Rendering Index* (CRI) of the specified illuminant or 

256 light source spectral distribution. 

257 

258 Parameters 

259 ---------- 

260 sd 

261 Illuminant or light source spectral distribution for which to plot 

262 the *Colour Rendering Index* (CRI). 

263 

264 Other Parameters 

265 ---------------- 

266 kwargs 

267 {:func:`colour.plotting.artist`, 

268 :func:`colour.plotting.quality.plot_colour_quality_bars`, 

269 :func:`colour.plotting.render`}, 

270 See the documentation of the previously listed definitions. 

271 

272 Returns 

273 ------- 

274 :class:`tuple` 

275 Current figure and axes. 

276 

277 Examples 

278 -------- 

279 >>> from colour import SDS_ILLUMINANTS 

280 >>> illuminant = SDS_ILLUMINANTS["FL2"] 

281 >>> plot_single_sd_colour_rendering_index_bars(illuminant) 

282 ... # doctest: +ELLIPSIS 

283 (<Figure size ... with 1 Axes>, <...Axes...>) 

284 

285 .. image:: ../_static/Plotting_\ 

286Plot_Single_SD_Colour_Rendering_Index_Bars.png 

287 :align: center 

288 :alt: plot_single_sd_colour_rendering_index_bars 

289 """ 

290 

291 return plot_multi_sds_colour_rendering_indexes_bars([sd], **kwargs) 

292 

293 

294@override_style() 

295def plot_multi_sds_colour_rendering_indexes_bars( 

296 sds: ( 

297 Sequence[SpectralDistribution | MultiSpectralDistributions] 

298 | SpectralDistribution 

299 | MultiSpectralDistributions 

300 | ValuesView 

301 ), 

302 **kwargs: Any, 

303) -> Tuple[Figure, Axes]: 

304 """ 

305 Plot the *Colour Rendering Index* (CRI) of the specified illuminants or 

306 light sources spectral distributions. 

307 

308 Parameters 

309 ---------- 

310 sds 

311 Spectral distributions or multi-spectral distributions to plot. 

312 `sds` can be a single :class:`colour.MultiSpectralDistributions` 

313 class instance, a list of :class:`colour.MultiSpectralDistributions` 

314 class instances or a list of :class:`colour.SpectralDistribution` class 

315 instances. 

316 

317 Other Parameters 

318 ---------------- 

319 kwargs 

320 {:func:`colour.plotting.artist`, 

321 :func:`colour.plotting.quality.plot_colour_quality_bars`, 

322 :func:`colour.plotting.render`}, 

323 See the documentation of the previously listed definitions. 

324 

325 Returns 

326 ------- 

327 :class:`tuple` 

328 Current figure and axes. 

329 

330 Examples 

331 -------- 

332 >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES 

333 >>> illuminant = SDS_ILLUMINANTS["FL2"] 

334 >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"] 

335 >>> plot_multi_sds_colour_rendering_indexes_bars( 

336 ... [illuminant, light_source] 

337 ... ) # doctest: +ELLIPSIS 

338 (<Figure size ... with 1 Axes>, <...Axes...>) 

339 

340 .. image:: ../_static/Plotting_\ 

341Plot_Multi_SDS_Colour_Rendering_Indexes_Bars.png 

342 :align: center 

343 :alt: plot_multi_sds_colour_rendering_indexes_bars 

344 """ 

345 

346 sds_converted = sds_and_msds_to_sds(sds) 

347 

348 settings: Dict[str, Any] = dict(kwargs) 

349 settings.update({"show": False}) 

350 

351 specifications = [ 

352 colour_rendering_index(sd, additional_data=True) for sd in sds_converted 

353 ] 

354 

355 # *colour rendering index* colorimetry data tristimulus values are 

356 # computed in [0, 100] domain however `plot_colour_quality_bars` expects 

357 # [0, 1] domain. As we want to keep `plot_colour_quality_bars` definition 

358 # agnostic from the colour quality data, we update the test sd 

359 # colorimetry data tristimulus values domain. 

360 for specification in specifications: 

361 colorimetry_data = specification.colorimetry_data 

362 for i in range(len(colorimetry_data[0])): 

363 colorimetry_data[0][i].XYZ /= 100 

364 

365 _figure, axes = plot_colour_quality_bars(specifications, **settings) 

366 

367 title = ( 

368 f"Colour Rendering Index - " 

369 f"{', '.join([sd.display_name for sd in sds_converted])}" 

370 ) 

371 

372 settings = {"axes": axes, "title": title} 

373 settings.update(kwargs) 

374 

375 return render(**settings) 

376 

377 

378@override_style() 

379def plot_single_sd_colour_quality_scale_bars( 

380 sd: SpectralDistribution, 

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

382 **kwargs: Any, 

383) -> Tuple[Figure, Axes]: 

384 """ 

385 Plot the *Colour Quality Scale* (CQS) of the specified illuminant or 

386 light source spectral distribution. 

387 

388 Parameters 

389 ---------- 

390 sd 

391 Illuminant or light source spectral distribution for which to plot 

392 the *Colour Quality Scale* (CQS). 

393 method 

394 *Colour Quality Scale* (CQS) computation method. 

395 

396 Other Parameters 

397 ---------------- 

398 kwargs 

399 {:func:`colour.plotting.artist`, 

400 :func:`colour.plotting.quality.plot_colour_quality_bars`, 

401 :func:`colour.plotting.render`}, 

402 See the documentation of the previously listed definitions. 

403 

404 Returns 

405 ------- 

406 :class:`tuple` 

407 Current figure and axes. 

408 

409 Examples 

410 -------- 

411 >>> from colour import SDS_ILLUMINANTS 

412 >>> illuminant = SDS_ILLUMINANTS["FL2"] 

413 >>> plot_single_sd_colour_quality_scale_bars(illuminant) 

414 ... # doctest: +ELLIPSIS 

415 (<Figure size ... with 1 Axes>, <...Axes...>) 

416 

417 .. image:: ../_static/Plotting_\ 

418Plot_Single_SD_Colour_Quality_Scale_Bars.png 

419 :align: center 

420 :alt: plot_single_sd_colour_quality_scale_bars 

421 """ 

422 

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

424 

425 return plot_multi_sds_colour_quality_scales_bars([sd], method, **kwargs) 

426 

427 

428@override_style() 

429def plot_multi_sds_colour_quality_scales_bars( 

430 sds: ( 

431 Sequence[SpectralDistribution | MultiSpectralDistributions] 

432 | SpectralDistribution 

433 | MultiSpectralDistributions 

434 | ValuesView 

435 ), 

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

437 **kwargs: Any, 

438) -> Tuple[Figure, Axes]: 

439 """ 

440 Plot the *Colour Quality Scale* (CQS) of the specified illuminants or light 

441 sources spectral distributions. 

442 

443 Parameters 

444 ---------- 

445 sds 

446 Spectral distributions or multi-spectral distributions to plot. 

447 `sds` can be a single :class:`colour.MultiSpectralDistributions` 

448 class instance, a list of :class:`colour.MultiSpectralDistributions` 

449 class instances or a list of :class:`colour.SpectralDistribution` class 

450 instances. 

451 method 

452 *Colour Quality Scale* (CQS) computation method. 

453 

454 Other Parameters 

455 ---------------- 

456 kwargs 

457 {:func:`colour.plotting.artist`, 

458 :func:`colour.plotting.quality.plot_colour_quality_bars`, 

459 :func:`colour.plotting.render`}, 

460 See the documentation of the previously listed definitions. 

461 

462 Returns 

463 ------- 

464 :class:`tuple` 

465 Current figure and axes. 

466 

467 Examples 

468 -------- 

469 >>> from colour import SDS_ILLUMINANTS, SDS_LIGHT_SOURCES 

470 >>> illuminant = SDS_ILLUMINANTS["FL2"] 

471 >>> light_source = SDS_LIGHT_SOURCES["Kinoton 75P"] 

472 >>> plot_multi_sds_colour_quality_scales_bars([illuminant, light_source]) 

473 ... # doctest: +ELLIPSIS 

474 (<Figure size ... with 1 Axes>, <...Axes...>) 

475 

476 .. image:: ../_static/Plotting_\ 

477Plot_Multi_SDS_Colour_Quality_Scales_Bars.png 

478 :align: center 

479 :alt: plot_multi_sds_colour_quality_scales_bars 

480 """ 

481 

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

483 

484 sds_converted = sds_and_msds_to_sds(sds) 

485 

486 settings: Dict[str, Any] = dict(kwargs) 

487 settings.update({"show": False}) 

488 

489 specifications = [colour_quality_scale(sd, True, method) for sd in sds_converted] 

490 

491 _figure, axes = plot_colour_quality_bars(specifications, **settings) 

492 

493 title = ( 

494 f"Colour Quality Scale - {', '.join([sd.display_name for sd in sds_converted])}" 

495 ) 

496 

497 settings = {"axes": axes, "title": title} 

498 settings.update(kwargs) 

499 

500 return render(**settings)