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

75 statements  

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

1""" 

2:math:`J_za_zb_z` Colourspace 

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

4 

5Define the :math:`J_za_zb_z` colourspace transformations. 

6 

7- :func:`colour.models.IZAZBZ_METHODS` 

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

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

10- :func:`colour.XYZ_to_Jzazbz` 

11- :func:`colour.Jzazbz_to_XYZ` 

12 

13References 

14---------- 

15- :cite:`Safdar2017` : Safdar, M., Cui, G., Kim, Y. J., & Luo, M. R. (2017). 

16 Perceptually uniform color space for image signals including high dynamic 

17 range and wide gamut. Optics Express, 25(13), 15131. 

18 doi:10.1364/OE.25.015131 

19- :cite:`Safdar2021` : Safdar, M., Hardeberg, J. Y., & Ronnier Luo, M. 

20 (2021). ZCAM, a colour appearance model based on a high dynamic range 

21 uniform colour space. Optics Express, 29(4), 6036. doi:10.1364/OE.413659 

22""" 

23 

24from __future__ import annotations 

25 

26import typing 

27 

28import numpy as np 

29 

30from colour.algebra import vecmul 

31 

32if typing.TYPE_CHECKING: 

33 from colour.hints import ArrayLike, Domain1, Literal, NDArrayFloat, Range1 

34 

35from colour.models.rgb.transfer_functions import eotf_inverse_ST2084, eotf_ST2084 

36from colour.models.rgb.transfer_functions.st_2084 import CONSTANTS_ST2084 

37from colour.utilities import ( 

38 Structure, 

39 as_float_array, 

40 domain_range_scale, 

41 optional, 

42 tsplit, 

43 tstack, 

44 validate_method, 

45) 

46from colour.utilities.documentation import DocstringTuple, is_documentation_building 

47 

48__author__ = "Colour Developers" 

49__copyright__ = "Copyright 2013 Colour Developers" 

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

51__maintainer__ = "Colour Developers" 

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

53__status__ = "Production" 

54 

55__all__ = [ 

56 "CONSTANTS_JZAZBZ_SAFDAR2017", 

57 "CONSTANTS_JZAZBZ_SAFDAR2021", 

58 "MATRIX_JZAZBZ_XYZ_TO_LMS", 

59 "MATRIX_JZAZBZ_LMS_TO_XYZ", 

60 "MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017", 

61 "MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017", 

62 "MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021", 

63 "MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021", 

64 "IZAZBZ_METHODS", 

65 "XYZ_to_Izazbz", 

66 "Izazbz_to_XYZ", 

67 "XYZ_to_Jzazbz", 

68 "Jzazbz_to_XYZ", 

69] 

70 

71CONSTANTS_JZAZBZ_SAFDAR2017: Structure = Structure( 

72 b=1.15, g=0.66, d=-0.56, d_0=1.6295499532821566 * 10**-11 

73) 

74CONSTANTS_JZAZBZ_SAFDAR2017.update(CONSTANTS_ST2084) 

75CONSTANTS_JZAZBZ_SAFDAR2017.m_2 = 1.7 * 2523 / 2**5 

76""" 

77Constants for :math:`J_za_zb_z` colourspace and its variant of the perceptual 

78quantizer (PQ) from Dolby Laboratories. 

79 

80Notes 

81----- 

82- The :math:`m2` constant, i.e., the power factor has been re-optimized during 

83 the development of the :math:`J_za_zb_z` colourspace. 

84""" 

85 

86CONSTANTS_JZAZBZ_SAFDAR2021: Structure = Structure(**CONSTANTS_JZAZBZ_SAFDAR2017) 

87CONSTANTS_JZAZBZ_SAFDAR2021.d_0 = 3.7035226210190005 * 10**-11 

88""":math:`J_za_zb_z` colourspace constants for the *ZCAM* colour appearance model.""" 

89 

90MATRIX_JZAZBZ_XYZ_TO_LMS: NDArrayFloat = np.array( 

91 [ 

92 [0.41478972, 0.579999, 0.0146480], 

93 [-0.2015100, 1.120649, 0.0531008], 

94 [-0.0166008, 0.264800, 0.6684799], 

95 ] 

96) 

97""" 

98:math:`J_za_zb_z` *CIE XYZ* tristimulus values to normalised cone responses 

99matrix. 

100""" 

101 

102MATRIX_JZAZBZ_LMS_TO_XYZ: NDArrayFloat = np.linalg.inv(MATRIX_JZAZBZ_XYZ_TO_LMS) 

103""" 

104:math:`J_za_zb_z` normalised cone responses to *CIE XYZ* tristimulus values 

105matrix. 

106""" 

107 

108MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017: NDArrayFloat = np.array( 

109 [ 

110 [0.500000, 0.500000, 0.000000], 

111 [3.524000, -4.066708, 0.542708], 

112 [0.199076, 1.096799, -1.295875], 

113 ] 

114) 

115""" 

116:math:`LMS_p` *SMPTE ST 2084:2014* encoded normalised cone responses to 

117:math:`I_za_zb_z` intermediate colourspace matrix. 

118""" 

119 

120MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017: NDArrayFloat = np.linalg.inv( 

121 MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017 

122) 

123""" 

124:math:`I_za_zb_z` intermediate colourspace to :math:`LMS_p` 

125*SMPTE ST 2084:2014* encoded normalised cone responses matrix. 

126""" 

127 

128MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021: NDArrayFloat = np.array( 

129 [ 

130 [0.000000, 1.000000, 0.000000], 

131 [3.524000, -4.066708, 0.542708], 

132 [0.199076, 1.096799, -1.295875], 

133 ] 

134) 

135""" 

136:math:`LMS_p` *SMPTE ST 2084:2014* encoded normalised cone responses to 

137:math:`I_za_zb_z` intermediate colourspace matrix. 

138 

139References 

140---------- 

141:cite:`Safdar2021` 

142""" 

143 

144MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021: NDArrayFloat = np.linalg.inv( 

145 MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021 

146) 

147""" 

148:math:`I_za_zb_z` intermediate colourspace to :math:`LMS_p` 

149*SMPTE ST 2084:2014* encoded normalised cone responses matrix. 

150 

151References 

152---------- 

153:cite:`Safdar2021` 

154""" 

155 

156IZAZBZ_METHODS: tuple = ("Safdar 2017", "Safdar 2021", "ZCAM") 

157if is_documentation_building(): # pragma: no cover 

158 IZAZBZ_METHODS = DocstringTuple(IZAZBZ_METHODS) 

159 IZAZBZ_METHODS.__doc__ = """ 

160Supported :math:`I_za_zb_z` computation methods. 

161 

162References 

163---------- 

164:cite:`Safdar2017`, :cite:`Safdar2021` 

165""" 

166 

167 

168def XYZ_to_Izazbz( 

169 XYZ_D65: ArrayLike, 

170 constants: Structure | None = None, 

171 method: (Literal["Safdar 2017", "Safdar 2021", "ZCAM"] | str) = "Safdar 2017", 

172) -> Range1: 

173 """ 

174 Convert from *CIE XYZ* tristimulus values to :math:`I_za_zb_z` 

175 colourspace. 

176 

177 Parameters 

178 ---------- 

179 XYZ_D65 

180 *CIE XYZ* tristimulus values under 

181 *CIE Standard Illuminant D Series D65*. 

182 constants 

183 :math:`J_za_zb_z` colourspace constants. 

184 method 

185 Computation method, *Safdar 2021* and *ZCAM* methods are equivalent. 

186 

187 Returns 

188 ------- 

189 :class:`numpy.ndarray` 

190 :math:`I_za_zb_z` colourspace array where :math:`I_z` is the 

191 achromatic response, :math:`a_z` is redness-greenness and 

192 :math:`b_z` is yellowness-blueness. 

193 

194 Warnings 

195 -------- 

196 The underlying *SMPTE ST 2084:2014* transfer function is an absolute 

197 transfer function. 

198 

199 Notes 

200 ----- 

201 - The underlying *SMPTE ST 2084:2014* transfer function is an 

202 absolute transfer function, thus the domain and range values for 

203 the *Reference* and *1* scales are only indicative that the data 

204 is not affected by scale transformations. The effective domain of 

205 *SMPTE ST 2084:2014* inverse electro-optical transfer function 

206 (EOTF) is [0.0001, 10000]. 

207 

208 +------------+-----------------------+------------------+ 

209 | **Domain** | **Scale - Reference** | **Scale - 1** | 

210 +============+=======================+==================+ 

211 | ``XYZ`` | ``UN`` | ``UN`` | 

212 +------------+-----------------------+------------------+ 

213 

214 +------------+-----------------------+------------------+ 

215 | **Range** | **Scale - Reference** | **Scale - 1** | 

216 +============+=======================+==================+ 

217 | ``Izazbz`` | 1 | 1 | 

218 +------------+-----------------------+------------------+ 

219 

220 References 

221 ---------- 

222 :cite:`Safdar2017`, :cite:`Safdar2021` 

223 

224 Examples 

225 -------- 

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

227 >>> XYZ_to_Izazbz(XYZ) # doctest: +ELLIPSIS 

228 array([ 0.0120779..., 0.0092430..., 0.0052600...]) 

229 """ 

230 

231 X_D65, Y_D65, Z_D65 = tsplit(as_float_array(XYZ_D65)) 

232 

233 method = validate_method(method, IZAZBZ_METHODS) 

234 

235 constants = optional( 

236 constants, 

237 ( 

238 CONSTANTS_JZAZBZ_SAFDAR2017 

239 if method == "safdar 2017" 

240 else CONSTANTS_JZAZBZ_SAFDAR2021 

241 ), 

242 ) 

243 

244 X_p_D65 = constants.b * X_D65 - (constants.b - 1) * Z_D65 

245 Y_p_D65 = constants.g * Y_D65 - (constants.g - 1) * X_D65 

246 

247 XYZ_p_D65 = tstack([X_p_D65, Y_p_D65, Z_D65]) 

248 

249 LMS = vecmul(MATRIX_JZAZBZ_XYZ_TO_LMS, XYZ_p_D65) 

250 

251 with domain_range_scale("ignore"): 

252 LMS_p = eotf_inverse_ST2084(LMS, 10000, constants) 

253 

254 if method == "safdar 2017": 

255 Izazbz = vecmul(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2017, LMS_p) 

256 else: 

257 Izazbz = vecmul(MATRIX_JZAZBZ_LMS_P_TO_IZAZBZ_SAFDAR2021, LMS_p) 

258 Izazbz[..., 0] -= constants.d_0 

259 

260 return Izazbz 

261 

262 

263def Izazbz_to_XYZ( 

264 Izazbz: Domain1, 

265 constants: Structure | None = None, 

266 method: (Literal["Safdar 2017", "Safdar 2021", "ZCAM"] | str) = "Safdar 2017", 

267) -> NDArrayFloat: 

268 """ 

269 Convert from :math:`I_za_zb_z` colourspace to *CIE XYZ* tristimulus 

270 values. 

271 

272 Parameters 

273 ---------- 

274 Izazbz 

275 :math:`I_za_zb_z` colourspace array where :math:`I_z` is the 

276 achromatic response, :math:`a_z` is redness-greenness and 

277 :math:`b_z` is yellowness-blueness. 

278 constants 

279 :math:`J_za_zb_z` colourspace constants. 

280 method 

281 Computation method, *Safdar 2021* and *ZCAM* methods are equivalent. 

282 

283 Returns 

284 ------- 

285 :class:`numpy.ndarray` 

286 *CIE XYZ* tristimulus values under 

287 *CIE Standard Illuminant D Series D65*. 

288 

289 Warnings 

290 -------- 

291 The underlying *SMPTE ST 2084:2014* transfer function is an absolute 

292 transfer function. 

293 

294 Notes 

295 ----- 

296 - The underlying *SMPTE ST 2084:2014* transfer function is an 

297 absolute transfer function, thus the domain and range values for 

298 the *Reference* and *1* scales are only indicative that the data 

299 is not affected by scale transformations. The effective domain of 

300 *SMPTE ST 2084:2014* inverse electro-optical transfer function 

301 (EOTF) is [0.0001, 10000]. 

302 

303 +------------+-----------------------+------------------+ 

304 | **Domain** | **Scale - Reference** | **Scale - 1** | 

305 +============+=======================+==================+ 

306 | ``Izazbz`` | 1 | 1 | 

307 +------------+-----------------------+------------------+ 

308 

309 +------------+-----------------------+------------------+ 

310 | **Range** | **Scale - Reference** | **Scale - 1** | 

311 +============+=======================+==================+ 

312 | ``XYZ`` | ``UN`` | ``UN`` | 

313 +------------+-----------------------+------------------+ 

314 

315 References 

316 ---------- 

317 :cite:`Safdar2017`, :cite:`Safdar2021` 

318 

319 Examples 

320 -------- 

321 >>> Izazbz = np.array([0.01207793, 0.00924302, 0.00526007]) 

322 >>> Izazbz_to_XYZ(Izazbz) # doctest: +ELLIPSIS 

323 array([ 0.2065401..., 0.1219723..., 0.0513696...]) 

324 """ 

325 

326 Izazbz = as_float_array(Izazbz) 

327 

328 method = validate_method(method, IZAZBZ_METHODS) 

329 

330 constants = optional( 

331 constants, 

332 ( 

333 CONSTANTS_JZAZBZ_SAFDAR2017 

334 if method == "safdar 2017" 

335 else CONSTANTS_JZAZBZ_SAFDAR2021 

336 ), 

337 ) 

338 

339 if method == "safdar 2017": 

340 LMS_p = vecmul(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2017, Izazbz) 

341 else: 

342 Izazbz[..., 0] += constants.d_0 

343 LMS_p = vecmul(MATRIX_JZAZBZ_IZAZBZ_TO_LMS_P_SAFDAR2021, Izazbz) 

344 

345 with domain_range_scale("ignore"): 

346 LMS = eotf_ST2084(LMS_p, 10000, constants) 

347 

348 X_p_D65, Y_p_D65, Z_p_D65 = tsplit(vecmul(MATRIX_JZAZBZ_LMS_TO_XYZ, LMS)) 

349 

350 X_D65 = (X_p_D65 + (constants.b - 1) * Z_p_D65) / constants.b 

351 Y_D65 = (Y_p_D65 + (constants.g - 1) * X_D65) / constants.g 

352 

353 return tstack([X_D65, Y_D65, Z_p_D65]) 

354 

355 

356def XYZ_to_Jzazbz( 

357 XYZ_D65: ArrayLike, constants: Structure = CONSTANTS_JZAZBZ_SAFDAR2017 

358) -> Range1: 

359 """ 

360 Convert from *CIE XYZ* tristimulus values to :math:`J_za_zb_z` 

361 colourspace. 

362 

363 Parameters 

364 ---------- 

365 XYZ_D65 

366 *CIE XYZ* tristimulus values under 

367 *CIE Standard Illuminant D Series D65*. 

368 constants 

369 :math:`J_za_zb_z` colourspace constants. 

370 

371 Returns 

372 ------- 

373 :class:`numpy.ndarray` 

374 :math:`J_za_zb_z` colourspace array where :math:`J_z` is 

375 Lightness, :math:`a_z` is redness-greenness and :math:`b_z` is 

376 yellowness-blueness. 

377 

378 Warnings 

379 -------- 

380 The underlying *SMPTE ST 2084:2014* transfer function is an absolute 

381 transfer function. 

382 

383 Notes 

384 ----- 

385 - The underlying *SMPTE ST 2084:2014* transfer function is an 

386 absolute transfer function, thus the domain and range values for 

387 the *Reference* and *1* scales are only indicative that the data 

388 is not affected by scale transformations. The effective domain of 

389 *SMPTE ST 2084:2014* inverse electro-optical transfer function 

390 (EOTF) is [0.0001, 10000]. 

391 domain of *SMPTE ST 2084:2014* inverse electro-optical transfer 

392 function (EOTF) is [0.0001, 10000]. 

393 

394 +------------+-----------------------+------------------+ 

395 | **Domain** | **Scale - Reference** | **Scale - 1** | 

396 +============+=======================+==================+ 

397 | ``XYZ`` | ``UN`` | ``UN`` | 

398 +------------+-----------------------+------------------+ 

399 

400 +------------+-----------------------+------------------+ 

401 | **Range** | **Scale - Reference** | **Scale - 1** | 

402 +============+=======================+==================+ 

403 | ``Jzazbz`` | 1 | 1 | 

404 +------------+-----------------------+------------------+ 

405 

406 References 

407 ---------- 

408 :cite:`Safdar2017` 

409 

410 Examples 

411 -------- 

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

413 >>> XYZ_to_Jzazbz(XYZ) # doctest: +ELLIPSIS 

414 array([ 0.0053504..., 0.0092430..., 0.0052600...]) 

415 """ 

416 

417 XYZ_D65 = as_float_array(XYZ_D65) 

418 

419 with domain_range_scale("ignore"): 

420 I_z, a_z, b_z = tsplit( 

421 XYZ_to_Izazbz(XYZ_D65, CONSTANTS_JZAZBZ_SAFDAR2017, "Safdar 2017") 

422 ) 

423 

424 J_z = ((1 + constants.d) * I_z) / (1 + constants.d * I_z) - constants.d_0 

425 

426 return tstack([J_z, a_z, b_z]) 

427 

428 

429def Jzazbz_to_XYZ( 

430 Jzazbz: Domain1, constants: Structure = CONSTANTS_JZAZBZ_SAFDAR2017 

431) -> NDArrayFloat: 

432 """ 

433 Convert from :math:`J_za_zb_z` colourspace to *CIE XYZ* tristimulus 

434 values. 

435 

436 Parameters 

437 ---------- 

438 Jzazbz 

439 :math:`J_za_zb_z` colourspace array where :math:`J_z` is Lightness, 

440 :math:`a_z` is redness-greenness and :math:`b_z` is 

441 yellowness-blueness. 

442 constants 

443 :math:`J_za_zb_z` colourspace constants. 

444 

445 Returns 

446 ------- 

447 :class:`numpy.ndarray` 

448 *CIE XYZ* tristimulus values under 

449 *CIE Standard Illuminant D Series D65*. 

450 

451 Warnings 

452 -------- 

453 The underlying *SMPTE ST 2084:2014* transfer function is an absolute 

454 transfer function. 

455 

456 Notes 

457 ----- 

458 - The underlying *SMPTE ST 2084:2014* transfer function is an 

459 absolute transfer function, thus the domain and range values for 

460 the *Reference* and *1* scales are only indicative that the data 

461 is not affected by scale transformations. The effective domain of 

462 *SMPTE ST 2084:2014* inverse electro-optical transfer function 

463 (EOTF) is [0.0001, 10000]. 

464 

465 +------------+-----------------------+------------------+ 

466 | **Domain** | **Scale - Reference** | **Scale - 1** | 

467 +============+=======================+==================+ 

468 | ``Jzazbz`` | 1 | 1 | 

469 +------------+-----------------------+------------------+ 

470 

471 +------------+-----------------------+------------------+ 

472 | **Range** | **Scale - Reference** | **Scale - 1** | 

473 +============+=======================+==================+ 

474 | ``XYZ`` | ``UN`` | ``UN`` | 

475 +------------+-----------------------+------------------+ 

476 

477 References 

478 ---------- 

479 :cite:`Safdar2017` 

480 

481 Examples 

482 -------- 

483 >>> Jzazbz = np.array([0.00535048, 0.00924302, 0.00526007]) 

484 >>> Jzazbz_to_XYZ(Jzazbz) # doctest: +ELLIPSIS 

485 array([ 0.2065402..., 0.1219723..., 0.0513696...]) 

486 """ 

487 

488 J_z, a_z, b_z = tsplit(as_float_array(Jzazbz)) 

489 

490 I_z = (J_z + constants.d_0) / ( 

491 1 + constants.d - constants.d * (J_z + constants.d_0) 

492 ) 

493 

494 with domain_range_scale("ignore"): 

495 return Izazbz_to_XYZ( 

496 tstack([I_z, a_z, b_z]), CONSTANTS_JZAZBZ_SAFDAR2017, "Safdar 2017" 

497 )