Coverage for colour/models/common.py: 100%

40 statements  

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

1""" 

2Common Colour Models Utilities 

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

4 

5Define utilities for common colour models and transformations. 

6 

7- :attr:`colour.COLOURSPACE_MODELS` 

8- :func:`colour.models.Jab_to_JCh` 

9- :func:`colour.models.JCh_to_Jab` 

10- :func:`colour.models.XYZ_to_Iab` 

11- :func:`colour.models.Iab_to_XYZ` 

12 

13References 

14---------- 

15- :cite:`CIETC1-482004m` : CIE TC 1-48. (2004). CIE 1976 uniform colour 

16 spaces. In CIE 015:2004 Colorimetry, 3rd Edition (p. 24). 

17 ISBN:978-3-901906-33-6 

18""" 

19 

20from __future__ import annotations 

21 

22import typing 

23 

24import numpy as np 

25 

26from colour.algebra import cartesian_to_polar, polar_to_cartesian, vecmul 

27 

28if typing.TYPE_CHECKING: 

29 from colour.hints import Callable 

30 

31from colour.hints import ( # noqa: TC001 

32 Annotated, 

33 ArrayLike, 

34 Domain1, 

35 NDArrayFloat, 

36 Range1, 

37) 

38from colour.utilities import ( 

39 CanonicalMapping, 

40 attest, 

41 from_range_1, 

42 from_range_degrees, 

43 to_domain_1, 

44 to_domain_degrees, 

45 tsplit, 

46 tstack, 

47) 

48from colour.utilities.documentation import DocstringTuple, is_documentation_building 

49 

50__author__ = "Colour Developers" 

51__copyright__ = "Copyright 2013 Colour Developers" 

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

53__maintainer__ = "Colour Developers" 

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

55__status__ = "Production" 

56 

57__all__ = [ 

58 "COLOURSPACE_MODELS", 

59 "COLOURSPACE_MODELS_AXIS_LABELS", 

60 "COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE", 

61 "Jab_to_JCh", 

62 "JCh_to_Jab", 

63 "XYZ_to_Iab", 

64 "Iab_to_XYZ", 

65] 

66 

67COLOURSPACE_MODELS: tuple = ( 

68 "CAM02LCD", 

69 "CAM02SCD", 

70 "CAM02UCS", 

71 "CAM16LCD", 

72 "CAM16SCD", 

73 "CAM16UCS", 

74 "CIE 1931", 

75 "CIE 1960 UCS", 

76 "CIE 1976 UCS", 

77 "CIE Lab", 

78 "CIE Luv", 

79 "CIE UCS", 

80 "CIE UVW", 

81 "CIE XYZ", 

82 "CIE xyY", 

83 "DIN99", 

84 "HCL", 

85 "hdr-CIELAB", 

86 "hdr-IPT", 

87 "HSL", 

88 "HSV", 

89 "Hunter Lab", 

90 "Hunter Rdab", 

91 "ICaCb", 

92 "ICtCp", 

93 "IHLS", 

94 "IPT Ragoo 2021", 

95 "IPT", 

96 "IgPgTg", 

97 "Jzazbz", 

98 "OSA UCS", 

99 "Oklab", 

100 "RGB", 

101 "sUCS", 

102 "YCbCr", 

103 "YCoCg", 

104 "Yrg", 

105) 

106if is_documentation_building(): # pragma: no cover 

107 COLOURSPACE_MODELS = DocstringTuple(COLOURSPACE_MODELS) 

108 COLOURSPACE_MODELS.__doc__ = """ 

109Colourspace models supporting a direct conversion to *CIE XYZ* tristimulus 

110values. 

111""" 

112 

113COLOURSPACE_MODELS_AXIS_LABELS: CanonicalMapping = CanonicalMapping( 

114 { 

115 "CAM02LCD": ("$J^'$", "$a^'$", "$b^'$"), 

116 "CAM02SCD": ("$J^'$", "$a^'$", "$b^'$"), 

117 "CAM02UCS": ("$J^'$", "$a^'$", "$b^'$"), 

118 "CAM16LCD": ("$J^'$", "$a^'$", "$b^'$"), 

119 "CAM16SCD": ("$J^'$", "$a^'$", "$b^'$"), 

120 "CAM16UCS": ("$J^'$", "$a^'$", "$b^'$"), 

121 "CIE 1931": ("x", "y", "Y"), 

122 "CIE 1960 UCS": ("$u^'$", "$v^'$", "$L^*$"), 

123 "CIE 1976 UCS": ("$u^'$", "$v^'$", "$L^*$"), 

124 "CIE Lab": ("$L^*$", "$a^*$", "$b^*$"), 

125 "CIE Luv": ("$L^*$", "$u^'$", "$v^'$"), 

126 "CIE UCS": ("U", "V", "W"), 

127 "CIE UVW": ("U", "V", "W"), 

128 "CIE XYZ": ("X", "Y", "Z"), 

129 "CIE xyY": ("x", "y", "Y"), 

130 "DIN99": ("$L_{99}$", "$a_{99}$", "$b_{99}$"), 

131 "HCL": ("H", "C", "L"), 

132 "hdr-CIELAB": ("L hdr", "a hdr", "b hdr"), 

133 "hdr-IPT": ("I hdr", "P hdr", "T hdr"), 

134 "HSL": ("H", "S", "L"), 

135 "HSV": ("H", "S", "V"), 

136 "Hunter Lab": ("$L^*$", "$a^*$", "$b^*$"), 

137 "Hunter Rdab": ("Rd", "a", "b"), 

138 "ICaCb": ("$I$", "$C_a$", "$C_b$"), 

139 "ICtCp": ("$I$", "$C_T$", "$C_P$"), 

140 "IHLS": ("H", "Y", "S"), 

141 "IPT Ragoo 2021": ("I", "P", "T"), 

142 "IPT": ("I", "P", "T"), 

143 "IgPgTg": ("$I_G$", "$P_G$", "$T_G$"), 

144 "Jzazbz": ("$J_z$", "$a_z$", "$b_z$"), 

145 "OSA UCS": ("L", "j", "g"), 

146 "Oklab": ("$L$", "$a$", "$b$"), 

147 "RGB": ("R", "G", "B"), 

148 "sUCS": ("I", "a", "b"), 

149 "YCbCr": ("Y", "$C_b$", "$C_r$"), 

150 "YCoCg": ("Y", "$C_o$", "$C_g$"), 

151 "Yrg": ("Y", "r", "g"), 

152 } 

153) 

154"""Colourspace models labels mapping.""" 

155 

156attest(tuple(COLOURSPACE_MODELS_AXIS_LABELS.keys()) == COLOURSPACE_MODELS) 

157 

158COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE: CanonicalMapping = ( 

159 CanonicalMapping( 

160 { 

161 "CAM02LCD": np.array([100, 100, 100]), 

162 "CAM02SCD": np.array([100, 100, 100]), 

163 "CAM02UCS": np.array([100, 100, 100]), 

164 "CAM16LCD": np.array([100, 100, 100]), 

165 "CAM16SCD": np.array([100, 100, 100]), 

166 "CAM16UCS": np.array([100, 100, 100]), 

167 "CIE 1931": np.array([1, 1, 1]), 

168 "CIE 1960 UCS": np.array([1, 1, 1]), 

169 "CIE 1976 UCS": np.array([1, 1, 100]), 

170 "CIE Lab": np.array([100, 100, 100]), 

171 "CIE Luv": np.array([100, 100, 100]), 

172 "CIE UCS": np.array([1, 1, 1]), 

173 "CIE UVW": np.array([100, 100, 100]), 

174 "CIE XYZ": np.array([1, 1, 1]), 

175 "CIE xyY": np.array([1, 1, 1]), 

176 "DIN99": np.array([100, 100, 100]), 

177 "HCL": np.array([1, 1, 1]), 

178 "hdr-CIELAB": np.array([100, 100, 100]), 

179 "hdr-IPT": np.array([100, 100, 100]), 

180 "HSL": np.array([1, 1, 1]), 

181 "HSV": np.array([1, 1, 1]), 

182 "Hunter Lab": np.array([100, 100, 100]), 

183 "Hunter Rdab": np.array([100, 100, 100]), 

184 "ICaCb": np.array([1, 1, 1]), 

185 "ICtCp": np.array([1, 1, 1]), 

186 "IHLS": np.array([1, 1, 1]), 

187 "IPT Ragoo 2021": np.array([1, 1, 1]), 

188 "IPT": np.array([1, 1, 1]), 

189 "IgPgTg": np.array([1, 1, 1]), 

190 "Jzazbz": np.array([1, 1, 1]), 

191 "OSA UCS": np.array([100, 100, 100]), 

192 "Oklab": np.array([1, 1, 1]), 

193 "RGB": np.array([1, 1, 1]), 

194 "sUCS": np.array([100, 100, 100]), 

195 "YCbCr": np.array([1, 1, 1]), 

196 "YCoCg": np.array([1, 1, 1]), 

197 "Yrg": np.array([1, 1, 1]), 

198 } 

199 ) 

200) 

201"""Colourspace models domain-range scale **'1'** to **'Reference'** mapping.""" 

202 

203 

204def Jab_to_JCh(Jab: Domain1) -> Annotated[NDArrayFloat, (1, 1, 360)]: 

205 """ 

206 Convert from *Jab* colour representation to *JCh* colour representation. 

207 

208 This definition performs conversion from *CIE L\\*a\\*b\\** colourspace to 

209 *CIE L\\*C\\*Hab* colourspace and other similar conversions. It implements 

210 a generic transformation from *lightness* :math:`J`, :math:`a` and 

211 :math:`b` opponent colour dimensions to the correlates of *lightness* 

212 :math:`J`, chroma :math:`C` and hue angle :math:`h`. 

213 

214 Parameters 

215 ---------- 

216 Jab 

217 *Jab* colour representation array. 

218 

219 Returns 

220 ------- 

221 :class:`numpy.ndarray` 

222 *JCh* colour representation array. 

223 

224 Notes 

225 ----- 

226 +------------+-----------------------+-----------------+ 

227 | **Domain** | **Scale - Reference** | **Scale - 1** | 

228 +============+=======================+=================+ 

229 | ``Jab`` | 1 | 1 | 

230 +------------+-----------------------+-----------------+ 

231 

232 +------------+-----------------------+-----------------+ 

233 | **Range** | **Scale - Reference** | **Scale - 1** | 

234 +============+=======================+=================+ 

235 | ``JCh`` | ``J`` : 1 | ``J`` : 1 | 

236 | | | | 

237 | | ``C`` : 1 | ``C`` : 1 | 

238 | | | | 

239 | | ``h`` : 360 | ``h`` : 1 | 

240 +------------+-----------------------+-----------------+ 

241 

242 References 

243 ---------- 

244 :cite:`CIETC1-482004m` 

245 

246 Examples 

247 -------- 

248 >>> Jab = np.array([41.52787529, 52.63858304, 26.92317922]) 

249 >>> Jab_to_JCh(Jab) # doctest: +ELLIPSIS 

250 array([ 41.5278752..., 59.1242590..., 27.0884878...]) 

251 """ 

252 

253 L, a, b = tsplit(Jab) 

254 

255 C, h = tsplit(cartesian_to_polar(tstack([a, b]))) 

256 

257 return tstack([L, C, from_range_degrees(np.degrees(h) % 360)]) 

258 

259 

260def JCh_to_Jab( 

261 JCh: Annotated[ArrayLike, (1, 1, 360)], 

262) -> Range1: 

263 """ 

264 Convert from *JCh* colour representation to *Jab* colour representation. 

265 

266 This definition performs conversion from *CIE L\\*C\\*Hab* colourspace to 

267 *CIE L\\*a\\*b\\** colourspace and other similar conversions. It implements 

268 a generic transformation from the correlates of *lightness* :math:`J`, 

269 chroma :math:`C` and hue angle :math:`h` to *lightness* :math:`J`, 

270 :math:`a` and :math:`b` opponent colour dimensions 

271 

272 Parameters 

273 ---------- 

274 JCh 

275 *JCh* colour representation array. 

276 

277 Returns 

278 ------- 

279 :class:`numpy.ndarray` 

280 *Jab* colour representation array. 

281 

282 Notes 

283 ----- 

284 +-------------+-----------------------+-----------------+ 

285 | **Domain** | **Scale - Reference** | **Scale - 1** | 

286 +=============+=======================+=================+ 

287 | ``JCh`` | ``J`` : 1 | ``J`` : 1 | 

288 | | | | 

289 | | ``C`` : 1 | ``C`` : 1 | 

290 | | | | 

291 | | ``h`` : 360 | ``h`` : 1 | 

292 +-------------+-----------------------+-----------------+ 

293 

294 +-------------+-----------------------+-----------------+ 

295 | **Range** | **Scale - Reference** | **Scale - 1** | 

296 +=============+=======================+=================+ 

297 | ``Jab`` | 1 | 1 | 

298 +-------------+-----------------------+-----------------+ 

299 

300 References 

301 ---------- 

302 :cite:`CIETC1-482004m` 

303 

304 Examples 

305 -------- 

306 >>> JCh = np.array([41.52787529, 59.12425901, 27.08848784]) 

307 >>> JCh_to_Jab(JCh) # doctest: +ELLIPSIS 

308 array([ 41.5278752..., 52.6385830..., 26.9231792...]) 

309 """ 

310 

311 L, C, h = tsplit(JCh) 

312 

313 a, b = tsplit(polar_to_cartesian(tstack([C, np.radians(to_domain_degrees(h))]))) 

314 

315 return tstack([L, a, b]) 

316 

317 

318def XYZ_to_Iab( 

319 XYZ: Domain1, 

320 LMS_to_LMS_p_callable: Callable, 

321 matrix_XYZ_to_LMS: ArrayLike, 

322 matrix_LMS_p_to_Iab: ArrayLike, 

323) -> Range1: 

324 """ 

325 Convert from *CIE XYZ* tristimulus values to *IPT*-like :math:`Iab` 

326 colour representation. 

327 

328 Perform conversion from *CIE XYZ* tristimulus values to *IPT* 

329 colourspace and other similar conversions. It implements a generic 

330 transformation from *CIE XYZ* tristimulus values to *lightness* 

331 :math:`I`, :math:`a` representing the red-green dimension (the 

332 dimension lost by protanopes), and :math:`b` representing the 

333 yellow-blue dimension (the dimension lost by tritanopes). 

334 

335 Parameters 

336 ---------- 

337 XYZ 

338 *CIE XYZ* tristimulus values. 

339 LMS_to_LMS_p_callable 

340 Callable applying the forward non-linearity to the :math:`LMS` 

341 colourspace array. 

342 matrix_XYZ_to_LMS 

343 Matrix converting from *CIE XYZ* tristimulus values to 

344 :math:`LMS` colourspace. 

345 matrix_LMS_p_to_Iab 

346 Matrix converting from non-linear :math:`LMS_p` colourspace to 

347 *IPT*-like :math:`Iab` colour representation. 

348 

349 Returns 

350 ------- 

351 :class:`numpy.ndarray` 

352 *IPT*-like :math:`Iab` colour representation. 

353 

354 Notes 

355 ----- 

356 +------------+-----------------------+-----------------+ 

357 | **Domain** | **Scale - Reference** | **Scale - 1** | 

358 +============+=======================+=================+ 

359 | ``XYZ`` | 1 | 1 | 

360 +------------+-----------------------+-----------------+ 

361 

362 +------------+-----------------------+-----------------+ 

363 | **Range** | **Scale - Reference** | **Scale - 1** | 

364 +============+=======================+=================+ 

365 | ``Iab`` | 1 | 1 | 

366 +------------+-----------------------+-----------------+ 

367 

368 Examples 

369 -------- 

370 >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952]) 

371 >>> LMS_to_LMS_p = lambda x: x**0.43 

372 >>> M_XYZ_to_LMS = np.array( 

373 ... [ 

374 ... [0.4002, 0.7075, -0.0807], 

375 ... [-0.2280, 1.1500, 0.0612], 

376 ... [0.0000, 0.0000, 0.9184], 

377 ... ] 

378 ... ) 

379 >>> M_LMS_p_to_Iab = np.array( 

380 ... [ 

381 ... [0.4000, 0.4000, 0.2000], 

382 ... [4.4550, -4.8510, 0.3960], 

383 ... [0.8056, 0.3572, -1.1628], 

384 ... ] 

385 ... ) 

386 >>> XYZ_to_Iab(XYZ, LMS_to_LMS_p, M_XYZ_to_LMS, M_LMS_p_to_Iab) 

387 ... # doctest: +ELLIPSIS 

388 array([ 0.3842619..., 0.3848730..., 0.1888683...]) 

389 """ 

390 

391 XYZ = to_domain_1(XYZ) 

392 

393 LMS = vecmul(matrix_XYZ_to_LMS, XYZ) 

394 LMS_p = LMS_to_LMS_p_callable(LMS) 

395 Iab = vecmul(matrix_LMS_p_to_Iab, LMS_p) 

396 

397 return from_range_1(Iab) 

398 

399 

400def Iab_to_XYZ( 

401 Iab: Domain1, 

402 LMS_p_to_LMS_callable: Callable, 

403 matrix_Iab_to_LMS_p: ArrayLike, 

404 matrix_LMS_to_XYZ: ArrayLike, 

405) -> Range1: 

406 """ 

407 Convert from *IPT*-like :math:`Iab` colour representation to *CIE XYZ* 

408 tristimulus values. 

409 

410 Perform conversion from *IPT* colourspace to *CIE XYZ* tristimulus 

411 values and other similar conversions. It implements a generic 

412 transformation from *lightness* :math:`I`, :math:`a` representing the 

413 red-green dimension (the dimension lost by protanopes), and :math:`b` 

414 representing the yellow-blue dimension (the dimension lost by tritanopes) 

415 to *CIE XYZ* tristimulus values. 

416 

417 Parameters 

418 ---------- 

419 Iab 

420 *IPT*-like :math:`Iab` colour representation. 

421 LMS_p_to_LMS_callable 

422 Callable applying the reverse non-linearity to the :math:`LMS_p` 

423 colourspace array. 

424 matrix_Iab_to_LMS_p 

425 Matrix converting from *IPT*-like :math:`Iab` colour 

426 representation to non-linear :math:`LMS_p` colourspace. 

427 matrix_LMS_to_XYZ 

428 Matrix converting from :math:`LMS` colourspace to *CIE XYZ* 

429 tristimulus values. 

430 

431 Returns 

432 ------- 

433 :class:`numpy.ndarray` 

434 *CIE XYZ* tristimulus values. 

435 

436 Notes 

437 ----- 

438 +------------+-----------------------+-----------------+ 

439 | **Domain** | **Scale - Reference** | **Scale - 1** | 

440 +============+=======================+=================+ 

441 | ``Iab`` | 1 | 1 | 

442 +------------+-----------------------+-----------------+ 

443 

444 +------------+-----------------------+-----------------+ 

445 | **Range** | **Scale - Reference** | **Scale - 1** | 

446 +============+=======================+=================+ 

447 | ``XYZ`` | 1 | 1 | 

448 +------------+-----------------------+-----------------+ 

449 

450 Examples 

451 -------- 

452 >>> Iab = np.array([0.38426191, 0.38487306, 0.18886838]) 

453 >>> LMS_p_to_LMS = lambda x: x ** (1 / 0.43) 

454 >>> M_Iab_to_LMS_p = np.linalg.inv( 

455 ... np.array( 

456 ... [ 

457 ... [0.4000, 0.4000, 0.2000], 

458 ... [4.4550, -4.8510, 0.3960], 

459 ... [0.8056, 0.3572, -1.1628], 

460 ... ] 

461 ... ) 

462 ... ) 

463 >>> M_LMS_to_XYZ = np.linalg.inv( 

464 ... np.array( 

465 ... [ 

466 ... [0.4002, 0.7075, -0.0807], 

467 ... [-0.2280, 1.1500, 0.0612], 

468 ... [0.0000, 0.0000, 0.9184], 

469 ... ] 

470 ... ) 

471 ... ) 

472 >>> Iab_to_XYZ(Iab, LMS_p_to_LMS, M_Iab_to_LMS_p, M_LMS_to_XYZ) 

473 ... # doctest: +ELLIPSIS 

474 array([ 0.2065400..., 0.1219722..., 0.0513695...]) 

475 """ 

476 

477 Iab = to_domain_1(Iab) 

478 

479 LMS = vecmul(matrix_Iab_to_LMS_p, Iab) 

480 LMS_p = LMS_p_to_LMS_callable(LMS) 

481 XYZ = vecmul(matrix_LMS_to_XYZ, LMS_p) 

482 

483 return from_range_1(XYZ)