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

83 statements  

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

1""" 

2CAM16-LCD, CAM16-SCD, and CAM16-UCS Colourspaces - Li et al. (2017) 

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

4 

5Define the *Li, Li, Wang, Zu, Luo, Cui, Melgosa, Brill and Pointer (2017)* 

6*CAM16-LCD*, *CAM16-SCD*, and *CAM16-UCS* colourspaces transformations. 

7 

8- :func:`colour.JMh_CAM16_to_CAM16LCD` 

9- :func:`colour.CAM16LCD_to_JMh_CAM16` 

10- :func:`colour.JMh_CAM16_to_CAM16SCD` 

11- :func:`colour.CAM16SCD_to_JMh_CAM16` 

12- :func:`colour.JMh_CAM16_to_CAM16UCS` 

13- :func:`colour.CAM16UCS_to_JMh_CAM16` 

14- :func:`colour.XYZ_to_CAM16LCD` 

15- :func:`colour.CAM16LCD_to_XYZ` 

16- :func:`colour.XYZ_to_CAM16SCD` 

17- :func:`colour.CAM16SCD_to_XYZ` 

18- :func:`colour.XYZ_to_CAM16UCS` 

19- :func:`colour.CAM16UCS_to_XYZ` 

20 

21References 

22---------- 

23- :cite:`Li2017` : Li, C., Li, Z., Wang, Z., Xu, Y., Luo, M. R., Cui, G., 

24 Melgosa, M., Brill, M. H., & Pointer, M. (2017). Comprehensive color 

25 solutions: CAM16, CAT16, and CAM16-UCS. Color Research & Application, 

26 42(6), 703-718. doi:10.1002/col.22131 

27""" 

28 

29from __future__ import annotations 

30 

31import re 

32import typing 

33from functools import partial 

34 

35if typing.TYPE_CHECKING: 

36 from colour.hints import ( 

37 Any, 

38 ArrayLike, 

39 Callable, 

40 Domain1, 

41 Domain100, 

42 Range1, 

43 Range100, 

44 ) 

45 

46from colour.hints import ArrayLike, NDArrayFloat, cast 

47from colour.models.cam02_ucs import ( 

48 COEFFICIENTS_UCS_LUO2006, 

49 CAM02LCD_to_JMh_CIECAM02, 

50 CAM02LCD_to_XYZ, 

51 CAM02SCD_to_JMh_CIECAM02, 

52 CAM02SCD_to_XYZ, 

53 CAM02UCS_to_JMh_CIECAM02, 

54 CAM02UCS_to_XYZ, 

55 JMh_CIECAM02_to_CAM02LCD, 

56 JMh_CIECAM02_to_CAM02SCD, 

57 JMh_CIECAM02_to_CAM02UCS, 

58 JMh_CIECAM02_to_UCS_Luo2006, 

59 UCS_Luo2006_to_JMh_CIECAM02, 

60 XYZ_to_CAM02LCD, 

61 XYZ_to_CAM02SCD, 

62 XYZ_to_CAM02UCS, 

63) 

64from colour.utilities import ( 

65 as_float_array, 

66 copy_definition, 

67 get_domain_range_scale, 

68 optional, 

69 tsplit, 

70 tstack, 

71) 

72 

73__author__ = "Colour Developers" 

74__copyright__ = "Copyright 2013 Colour Developers" 

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

76__maintainer__ = "Colour Developers" 

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

78__status__ = "Production" 

79 

80__all__ = [ 

81 "JMh_CAM16_to_UCS_Li2017", 

82 "UCS_Li2017_to_JMh_CAM16", 

83 "JMh_CAM16_to_CAM16LCD", 

84 "CAM16LCD_to_JMh_CAM16", 

85 "JMh_CAM16_to_CAM16SCD", 

86 "CAM16SCD_to_JMh_CAM16", 

87 "JMh_CAM16_to_CAM16UCS", 

88 "CAM16UCS_to_JMh_CAM16", 

89 "XYZ_to_UCS_Li2017", 

90 "UCS_Li2017_to_XYZ", 

91 "XYZ_to_CAM16LCD", 

92 "CAM16LCD_to_XYZ", 

93 "XYZ_to_CAM16SCD", 

94 "CAM16SCD_to_XYZ", 

95 "XYZ_to_CAM16UCS", 

96 "CAM16UCS_to_XYZ", 

97] 

98 

99 

100def _UCS_Luo2006_callable_to_UCS_Li2017_docstring(callable_: Callable) -> str: 

101 """ 

102 Convert the docstring of the specified *Luo et al. (2006)* callable to 

103 conform to *Li et al. (2017)* conventions. 

104 

105 Parameters 

106 ---------- 

107 callable_ 

108 Callable whose docstring will be adapted. 

109 

110 Returns 

111 ------- 

112 :class:`str` 

113 Converted docstring following *Li et al. (2017)* conventions. 

114 """ 

115 

116 docstring = callable_.__doc__ 

117 # NOTE: Required for optimised python launch. 

118 docstring = optional(docstring, "") 

119 

120 docstring = docstring.replace("Luo et al. (2006)", "Li et al. (2017)") 

121 docstring = docstring.replace("CIECAM02", "CAM16") 

122 docstring = docstring.replace("CAM02", "CAM16") 

123 docstring = docstring.replace("Luo2006b", "Li2017") 

124 

125 match = re.match("(.*)Examples", docstring, re.DOTALL) 

126 if match is not None: 

127 docstring = match.group(1) 

128 

129 return docstring 

130 

131 

132JMh_CAM16_to_UCS_Li2017 = copy_definition( 

133 JMh_CIECAM02_to_UCS_Luo2006, "JMh_CAM16_to_UCS_Li2017" 

134) 

135JMh_CAM16_to_UCS_Li2017.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

136 JMh_CIECAM02_to_UCS_Luo2006 

137) 

138 

139UCS_Li2017_to_JMh_CAM16 = copy_definition( 

140 UCS_Luo2006_to_JMh_CIECAM02, "UCS_Li2017_to_JMh_CAM16" 

141) 

142UCS_Li2017_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

143 UCS_Luo2006_to_JMh_CIECAM02 

144) 

145 

146JMh_CAM16_to_CAM16LCD = partial( 

147 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] 

148) 

149JMh_CAM16_to_CAM16LCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

150 JMh_CIECAM02_to_CAM02LCD 

151) 

152 

153CAM16LCD_to_JMh_CAM16 = partial( 

154 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] 

155) 

156CAM16LCD_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

157 CAM02LCD_to_JMh_CIECAM02 

158) 

159 

160JMh_CAM16_to_CAM16SCD = partial( 

161 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] 

162) 

163JMh_CAM16_to_CAM16SCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

164 JMh_CIECAM02_to_CAM02SCD 

165) 

166 

167CAM16SCD_to_JMh_CAM16 = partial( 

168 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] 

169) 

170CAM16SCD_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

171 CAM02SCD_to_JMh_CIECAM02 

172) 

173 

174JMh_CAM16_to_CAM16UCS = partial( 

175 JMh_CAM16_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] 

176) 

177JMh_CAM16_to_CAM16UCS.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

178 JMh_CIECAM02_to_CAM02UCS 

179) 

180 

181CAM16UCS_to_JMh_CAM16 = partial( 

182 UCS_Li2017_to_JMh_CAM16, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] 

183) 

184CAM16UCS_to_JMh_CAM16.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring( 

185 CAM02UCS_to_JMh_CIECAM02 

186) 

187 

188 

189def XYZ_to_UCS_Li2017( 

190 XYZ: Domain1, 

191 coefficients: ArrayLike, 

192 **kwargs: Any, 

193) -> Range100: 

194 """ 

195 Convert from *CIE XYZ* tristimulus values to one of the *Li et al. 

196 (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS* colourspaces 

197 :math:`J'a'b'` array. 

198 

199 Parameters 

200 ---------- 

201 XYZ 

202 *CIE XYZ* tristimulus values. 

203 coefficients 

204 Coefficients of one of the *Li et al. (2017)* *CAM16-LCD*, 

205 *CAM16-SCD*, or *CAM16-UCS* colourspaces. 

206 

207 Other Parameters 

208 ---------------- 

209 kwargs 

210 {:func:`colour.XYZ_to_CAM16`}, 

211 See the documentation of the previously listed definition. The 

212 default viewing conditions are those of *IEC 61966-2-1:1999*, 

213 i.e., *sRGB* 64 Lux ambient illumination, 80 :math:`cd/m^2`, 

214 adapting field luminance about 20% of a white object in the 

215 scene. 

216 

217 Returns 

218 ------- 

219 :class:`numpy.ndarray` 

220 *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS* 

221 colourspaces :math:`J'a'b'` array. 

222 

223 Warnings 

224 -------- 

225 The ``XYZ_w`` parameter for :func:`colour.XYZ_to_CAM16` definition must 

226 be specified in the same domain-range scale as the ``XYZ`` parameter. 

227 

228 Notes 

229 ----- 

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

231 | **Domain** | **Scale - Reference** | **Scale - 1** | 

232 +============+========================+==================+ 

233 | ``XYZ`` | 1 | 1 | 

234 +------------+------------------------+------------------+ 

235 

236 +------------+------------------------+------------------+ 

237 | **Range** | **Scale - Reference** | **Scale - 1** | 

238 +============+========================+==================+ 

239 | ``Jpapbp`` | 100 | 1 | 

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

241 

242 Examples 

243 -------- 

244 >>> import numpy as np 

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

246 >>> XYZ_to_UCS_Li2017(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) 

247 ... # doctest: +ELLIPSIS 

248 array([ 46.0658603..., 41.0758649..., 14.5102582...]) 

249 

250 >>> from colour.appearance import CAM_KWARGS_CIECAM02_sRGB 

251 >>> XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] 

252 >>> XYZ_to_UCS_Li2017(XYZ, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], XYZ_w=XYZ_w / 100) 

253 ... # doctest: +ELLIPSIS 

254 array([ 46.0658603..., 41.0758649..., 14.5102582...]) 

255 """ 

256 

257 from colour.appearance import ( # noqa: PLC0415 

258 CAM_KWARGS_CIECAM02_sRGB, 

259 XYZ_to_CAM16, 

260 ) 

261 

262 domain_range_reference = get_domain_range_scale() == "reference" 

263 

264 settings = CAM_KWARGS_CIECAM02_sRGB.copy() 

265 settings.update(**kwargs) 

266 XYZ_w = kwargs.get("XYZ_w") 

267 if XYZ_w is not None and domain_range_reference: 

268 settings["XYZ_w"] = XYZ_w * 100 

269 

270 if domain_range_reference: 

271 XYZ = as_float_array(XYZ) * 100 

272 

273 specification = XYZ_to_CAM16(XYZ, **settings) 

274 JMh = tstack( 

275 [ 

276 cast("NDArrayFloat", specification.J), 

277 cast("NDArrayFloat", specification.M), 

278 cast("NDArrayFloat", specification.h), 

279 ] 

280 ) 

281 

282 return JMh_CAM16_to_UCS_Li2017(JMh, coefficients) 

283 

284 

285def UCS_Li2017_to_XYZ( 

286 Jpapbp: Domain100, 

287 coefficients: ArrayLike, 

288 **kwargs: Any, 

289) -> Range1: 

290 """ 

291 Convert from one of the *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or 

292 *CAM16-UCS* colourspaces :math:`J'a'b'` array to *CIE XYZ* tristimulus 

293 values. 

294 

295 Parameters 

296 ---------- 

297 Jpapbp 

298 *Li et al. (2017)* *CAM16-LCD*, *CAM16-SCD*, or *CAM16-UCS* 

299 colourspaces :math:`J'a'b'` array. 

300 coefficients 

301 Coefficients of one of the *Li et al. (2017)* *CAM16-LCD*, 

302 *CAM16-SCD*, or *CAM16-UCS* colourspaces. 

303 

304 Other Parameters 

305 ---------------- 

306 kwargs 

307 {:func:`colour.CAM16_to_XYZ`}, 

308 See the documentation of the previously listed definition. The 

309 default viewing conditions are those of *IEC 61966-2-1:1999*, 

310 i.e., *sRGB* 64 Lux ambient illumination, 80 :math:`cd/m^2`, 

311 adapting field luminance about 20% of a white object in the 

312 scene. 

313 

314 Returns 

315 ------- 

316 :class:`numpy.ndarray` 

317 *CIE XYZ* tristimulus values. 

318 

319 Warnings 

320 -------- 

321 The ``XYZ_w`` parameter for :func:`colour.XYZ_to_CAM16` definition must 

322 be specified in the same domain-range scale as the ``XYZ`` parameter. 

323 

324 Notes 

325 ----- 

326 +------------+------------------------+------------------+ 

327 | **Domain** | **Scale - Reference** | **Scale - 1** | 

328 +============+========================+==================+ 

329 | ``Jpapbp`` | 100 | 1 | 

330 +------------+------------------------+------------------+ 

331 

332 +------------+------------------------+------------------+ 

333 | **Range** | **Scale - Reference** | **Scale - 1** | 

334 +============+========================+==================+ 

335 | ``XYZ`` | 1 | 1 | 

336 +------------+------------------------+------------------+ 

337 

338 Examples 

339 -------- 

340 >>> import numpy as np 

341 >>> Jpapbp = np.array([46.06586037, 41.07586491, 14.51025828]) 

342 >>> UCS_Li2017_to_XYZ(Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"]) 

343 ... # doctest: +ELLIPSIS 

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

345 

346 >>> from colour.appearance import CAM_KWARGS_CIECAM02_sRGB 

347 >>> XYZ_w = CAM_KWARGS_CIECAM02_sRGB["XYZ_w"] 

348 >>> UCS_Li2017_to_XYZ( 

349 ... Jpapbp, COEFFICIENTS_UCS_LUO2006["CAM02-LCD"], XYZ_w=XYZ_w / 100 

350 ... ) 

351 ... # doctest: +ELLIPSIS 

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

353 """ 

354 

355 from colour.appearance import ( # noqa: PLC0415 

356 CAM16_to_XYZ, 

357 CAM_KWARGS_CIECAM02_sRGB, 

358 CAM_Specification_CAM16, 

359 ) 

360 

361 domain_range_reference = get_domain_range_scale() == "reference" 

362 

363 settings = CAM_KWARGS_CIECAM02_sRGB.copy() 

364 settings.update(**kwargs) 

365 XYZ_w = kwargs.get("XYZ_w") 

366 

367 if XYZ_w is not None and domain_range_reference: 

368 settings["XYZ_w"] = XYZ_w * 100 

369 

370 J, M, h = tsplit(UCS_Li2017_to_JMh_CAM16(Jpapbp, coefficients)) 

371 

372 specification = CAM_Specification_CAM16(J=J, M=M, h=h) 

373 

374 XYZ = CAM16_to_XYZ(specification, **settings) 

375 

376 if domain_range_reference: 

377 XYZ /= 100 

378 

379 return XYZ 

380 

381 

382XYZ_to_CAM16LCD = partial( 

383 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] 

384) 

385XYZ_to_CAM16LCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02LCD) 

386XYZ_to_CAM16LCD.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy() 

387 

388CAM16LCD_to_XYZ = partial( 

389 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-LCD"] 

390) 

391CAM16LCD_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02LCD_to_XYZ) 

392 

393XYZ_to_CAM16SCD = partial( 

394 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] 

395) 

396XYZ_to_CAM16SCD.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02SCD) 

397XYZ_to_CAM16SCD.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy() 

398 

399CAM16SCD_to_XYZ = partial( 

400 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-SCD"] 

401) 

402CAM16SCD_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02SCD_to_XYZ) 

403 

404XYZ_to_CAM16UCS = partial( 

405 XYZ_to_UCS_Li2017, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] 

406) 

407XYZ_to_CAM16UCS.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(XYZ_to_CAM02UCS) 

408XYZ_to_CAM16UCS.__annotations__ = XYZ_to_UCS_Li2017.__annotations__.copy() 

409 

410CAM16UCS_to_XYZ = partial( 

411 UCS_Li2017_to_XYZ, coefficients=COEFFICIENTS_UCS_LUO2006["CAM02-UCS"] 

412) 

413CAM16UCS_to_XYZ.__doc__ = _UCS_Luo2006_callable_to_UCS_Li2017_docstring(CAM02UCS_to_XYZ)