Coverage for appearance/zcam.py: 65%

136 statements  

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

1""" 

2ZCAM Colour Appearance Model 

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

4 

5Define the *ZCAM* colour appearance model for predicting perceptual colour 

6attributes under varying viewing conditions. 

7 

8- :class:`colour.appearance.InductionFactors_ZCAM` 

9- :attr:`colour.VIEWING_CONDITIONS_ZCAM` 

10- :class:`colour.CAM_Specification_ZCAM` 

11- :func:`colour.XYZ_to_ZCAM` 

12- :func:`colour.ZCAM_to_XYZ` 

13 

14References 

15---------- 

16- :cite:`Safdar2018` : Safdar, M., Hardeberg, J. Y., Kim, Y. J., & Luo, M. R. 

17 (2018). A Colour Appearance Model based on J z a z b z Colour Space. Color 

18 and Imaging Conference, 2018(1), 96-101. 

19 doi:10.2352/ISSN.2169-2629.2018.26.96 

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

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

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

23- :cite:`Zhai2018` : Zhai, Q., & Luo, M. R. (2018). Study of chromatic 

24 adaptation via neutral white matches on different viewing media. Optics 

25 Express, 26(6), 7724. doi:10.1364/OE.26.007724 

26""" 

27 

28from __future__ import annotations 

29 

30from dataclasses import astuple, dataclass, field 

31 

32import numpy as np 

33 

34from colour.adaptation import chromatic_adaptation_Zhai2018 

35from colour.algebra import sdiv, sdiv_mode, spow 

36from colour.appearance.ciecam02 import ( 

37 VIEWING_CONDITIONS_CIECAM02, 

38 degree_of_adaptation, 

39 hue_angle, 

40) 

41from colour.colorimetry import CCS_ILLUMINANTS 

42from colour.hints import ( # noqa: TC001 

43 Annotated, 

44 ArrayLike, 

45 Domain1, 

46 NDArrayFloat, 

47 Range1, 

48) 

49from colour.models import Izazbz_to_XYZ, XYZ_to_Izazbz, xy_to_XYZ 

50from colour.utilities import ( 

51 CanonicalMapping, 

52 MixinDataclassArithmetic, 

53 MixinDataclassIterable, 

54 as_float, 

55 as_float_array, 

56 as_int_array, 

57 domain_range_scale, 

58 from_range_1, 

59 from_range_degrees, 

60 has_only_nan, 

61 ones, 

62 to_domain_1, 

63 to_domain_degrees, 

64 tsplit, 

65 tstack, 

66) 

67 

68__author__ = "Colour Developers" 

69__copyright__ = "Copyright 2013 Colour Developers" 

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

71__maintainer__ = "Colour Developers" 

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

73__status__ = "Production" 

74 

75__all__ = [ 

76 "InductionFactors_ZCAM", 

77 "VIEWING_CONDITIONS_ZCAM", 

78 "CAM_Specification_ZCAM", 

79 "XYZ_to_ZCAM", 

80 "ZCAM_to_XYZ", 

81] 

82 

83 

84@dataclass(frozen=True) 

85class InductionFactors_ZCAM(MixinDataclassIterable): 

86 """ 

87 Define the *ZCAM* colour appearance model induction factors. 

88 

89 Parameters 

90 ---------- 

91 F_s 

92 Surround impact :math:`F_s`. 

93 F 

94 Maximum degree of adaptation :math:`F`. 

95 c 

96 Exponential non-linearity :math:`c`. 

97 N_c 

98 Chromatic induction factor :math:`N_c`. 

99 

100 Notes 

101 ----- 

102 - The *ZCAM* colour appearance model induction factors are inherited 

103 from the *CIECAM02* colour appearance model. 

104 

105 References 

106 ---------- 

107 :cite:`Safdar2021` 

108 """ 

109 

110 F_s: float 

111 F: float 

112 c: float 

113 N_c: float 

114 

115 

116VIEWING_CONDITIONS_ZCAM: CanonicalMapping = CanonicalMapping( 

117 { 

118 "Average": InductionFactors_ZCAM( 

119 0.69, *VIEWING_CONDITIONS_CIECAM02["Average"].values 

120 ), 

121 "Dim": InductionFactors_ZCAM(0.59, *VIEWING_CONDITIONS_CIECAM02["Dim"].values), 

122 "Dark": InductionFactors_ZCAM( 

123 0.525, *VIEWING_CONDITIONS_CIECAM02["Dark"].values 

124 ), 

125 } 

126) 

127VIEWING_CONDITIONS_ZCAM.__doc__ = """ 

128Define the reference *ZCAM* colour appearance model 

129viewing conditions. 

130 

131Provide three standard viewing conditions (*Average*, *Dim*, and *Dark*) 

132with corresponding induction factors. Each condition specifies a unique 

133surround impact factor (:math:`F_s`) alongside inherited *CIECAM02* 

134parameters for maximum degree of adaptation (:math:`F`), exponential 

135non-linearity (:math:`c`), and chromatic induction factor (:math:`N_c`). 

136 

137Notes 

138----- 

139- The *ZCAM* viewing conditions inherit parameters from *CIECAM02* while 

140 introducing model-specific surround impact factors: 0.69 (*Average*), 

141 0.59 (*Dim*), and 0.525 (*Dark*). 

142 

143References 

144---------- 

145:cite:`Safdar2021` 

146""" 

147 

148HUE_DATA_FOR_HUE_QUADRATURE: dict = { 

149 "h_i": np.array([33.44, 89.29, 146.30, 238.36, 393.44]), 

150 "e_i": np.array([0.68, 0.64, 1.52, 0.77, 0.68]), 

151 "H_i": np.array([0.0, 100.0, 200.0, 300.0, 400.0]), 

152} 

153 

154 

155@dataclass 

156class CAM_ReferenceSpecification_ZCAM(MixinDataclassArithmetic): 

157 """ 

158 Define the *ZCAM* colour appearance model reference specification. 

159 

160 This specification contains field names consistent with the *Fairchild 

161 (2013)* reference. 

162 

163 Parameters 

164 ---------- 

165 J_z 

166 Correlate of *lightness* :math:`J_z`. 

167 C_z 

168 Correlate of *chroma* :math:`C_z`. 

169 h_z 

170 *Hue* angle :math:`h_z` in degrees. 

171 S_z 

172 Correlate of *saturation* :math:`S_z`. 

173 Q_z 

174 Correlate of *brightness* :math:`Q_z`. 

175 M_z 

176 Correlate of *colourfulness* :math:`M_z`. 

177 H 

178 *Hue* :math:`h` quadrature :math:`H`. 

179 H_z 

180 *Hue* :math:`h` composition :math:`H_z`. 

181 V_z 

182 Correlate of *vividness* :math:`V_z`. 

183 K_z 

184 Correlate of *blackness* :math:`K_z`. 

185 W_z 

186 Correlate of *whiteness* :math:`W_z`. 

187 

188 References 

189 ---------- 

190 :cite:`Safdar2021` 

191 """ 

192 

193 J_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

194 C_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

195 h_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

196 S_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

197 Q_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

198 M_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

199 H: float | NDArrayFloat | None = field(default_factory=lambda: None) 

200 H_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

201 V_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

202 K_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

203 W_z: float | NDArrayFloat | None = field(default_factory=lambda: None) 

204 

205 

206@dataclass 

207class CAM_Specification_ZCAM(MixinDataclassArithmetic): 

208 """ 

209 Define the *ZCAM* colour appearance model specification. 

210 

211 This specification provides a standardized interface for the *ZCAM* model 

212 with field names consistent across all colour appearance models in 

213 :mod:`colour.appearance`. While the field names differ from the original 

214 *Fairchild (2013)* reference notation, they map directly to the model's 

215 perceptual correlates. 

216 

217 Parameters 

218 ---------- 

219 J 

220 *Lightness* :math:`J` is the "brightness of an area (:math:`Q`) judged 

221 relative to the brightness of a similarly illuminated area that appears 

222 to be white or highly transmitting (:math:`Q_w`)", i.e., 

223 :math:`J = (Q/Q_w)`. It is a visual scale with two well defined levels 

224 i.e., zero and 100 for a pure black and a reference white, 

225 respectively. Note that in HDR visual field, samples could have a 

226 higher luminance than that of the reference white, so the lightness 

227 could be over 100. Subscripts :math:`s` and :math:`w` are used to 

228 annotate the sample and the reference white, respectively. 

229 C 

230 *Chroma* :math:`C` is "colourfulness of an area (:math:`M`) judged as 

231 a proportion of the brightness of a similarly illuminated area that 

232 appears white or highly transmitting (:math:`Q_w`)", i.e., 

233 :math:`C = (M/Q_w)`. It is an open-end scale with origin as a colour 

234 in the neutral axis. It can be estimated as the magnitude of the 

235 chromatic difference between the test colour and a neutral colour 

236 having the lightness same as the test colour. 

237 h 

238 *Hue* angle :math:`h` is a scale ranged from :math:`0^{\\circ}` to 

239 :math:`360^{\\circ}` with the hues following rainbow sequence. The same 

240 distance between pairs of hues in a constant lightness and chroma shows 

241 the same perceived colour difference. 

242 s 

243 *Saturation* :math:`s` is the "colourfulness (:math:`M`) of an area 

244 judged in proportion to its brightness (:math:`Q`)", i.e., 

245 :math:`s = (M/Q)`. It can also be defined as the chroma of an area 

246 judged in proportion to its lightness, i.e., :math:`s = (C/J)`. It is 

247 an open-end scale with all neutral colours to have saturation of zero. 

248 For example, the red bricks in a building would exhibit different 

249 colours when illuminated by daylight. Those (directly) under daylight 

250 will appear to be bright and colourful, and those under shadow will 

251 appear darker and less colourful. However, the two areas have the same 

252 saturation. 

253 Q 

254 *Brightness* :math:`Q` is an "attribute of a visual perception 

255 according to which an area appears to emit, or reflect, more or less 

256 light". It is an open-end scale with origin as pure black or complete 

257 darkness. It is an absolute scale according to the illumination 

258 condition i.e., an increase of brightness of an object when the 

259 illuminance of light is increased. This is a visual phenomenon known as 

260 Stevens effect. 

261 M 

262 *Colourfulness* :math:`M` is an "attribute of a visual perception 

263 according to which the perceived colour of an area appears to be more 

264 or less chromatic". It is an open-end scale with origin as a neutral 

265 colour i.e., appearance of no hue. It is an absolute scale according to 

266 the illumination condition i.e., an increase of colourfulness of an 

267 object when the illuminance of light is increased. This is a visual 

268 phenomenon known as Hunt effect. 

269 H 

270 *Hue* :math:`h` quadrature :math:`H_C` is an "attribute of a visual 

271 perception according to which an area appears to be similar to one of 

272 the colours: red, yellow, green, and blue, or to a combination of 

273 adjacent pairs of these colours considered in a closed ring". It has 

274 a 0-400 scale, i.e., hue quadrature of 0, 100, 200, 300, and 400 

275 range from unitary red to, yellow, green, blue, and back to red, 

276 respectively. For example, a cyan colour consists of 50% green and 

277 50% blue, corresponding to a hue quadrature of 250. 

278 HC 

279 *Hue* :math:`h` composition :math:`H^C` used to define the hue 

280 appearance of a sample. Note that hue circles formed by the equal hue 

281 angle and equal hue composition appear to be quite different. 

282 V 

283 *Vividness* :math:`V` is an "attribute of colour used to indicate the 

284 degree of departure of the colour (of stimulus) from a neutral black 

285 colour", i.e., :math:`V = \\sqrt{J^2 + C^2}`. It is an open-end scale 

286 with origin at pure black. This reflects the visual phenomena of an 

287 object illuminated by a light to increase both the lightness and the 

288 chroma. 

289 K 

290 *Blackness* :math:`K` is a visual attribute according to which an area 

291 appears to contain more or less black content. It is a scale in the 

292 Natural Colour System (NCS) and can also be defined in resemblance to a 

293 pure black. It is an open-end scale with 100 as pure black (luminance 

294 of 0 :math:`cd/m^2`), i.e., 

295 :math:`K = (100 - \\sqrt{J^2 + C^2} = (100 - V)`. The visual effect can 

296 be illustrated by mixing a black to a colour pigment. The more black 

297 pigment is added, the higher blackness will be. A blacker colour will 

298 have less lightness and/or chroma than a less black colour. 

299 W 

300 *Whiteness* :math:`W` is a visual attribute according to which an area 

301 appears to contain more or less white content. It is a scale of the NCS 

302 and can also be defined in resemblance to a pure white. It is an 

303 open-end scale with 100 as reference white, i.e., 

304 :math:`W = (100 - \\sqrt{(100 - J)^2 + C^2} = (100 - D)`. The visual 

305 effect can be illustrated by mixing a white to a colour pigment. The 

306 more white pigment is added, the higher whiteness will be. A whiter 

307 colour will have a lower chroma and higher lightness than the less 

308 white colour. 

309 

310 References 

311 ---------- 

312 :cite:`Safdar2021` 

313 """ 

314 

315 J: float | NDArrayFloat | None = field(default_factory=lambda: None) 

316 C: float | NDArrayFloat | None = field(default_factory=lambda: None) 

317 h: float | NDArrayFloat | None = field(default_factory=lambda: None) 

318 s: float | NDArrayFloat | None = field(default_factory=lambda: None) 

319 Q: float | NDArrayFloat | None = field(default_factory=lambda: None) 

320 M: float | NDArrayFloat | None = field(default_factory=lambda: None) 

321 H: float | NDArrayFloat | None = field(default_factory=lambda: None) 

322 HC: float | NDArrayFloat | None = field(default_factory=lambda: None) 

323 V: float | NDArrayFloat | None = field(default_factory=lambda: None) 

324 K: float | NDArrayFloat | None = field(default_factory=lambda: None) 

325 W: float | NDArrayFloat | None = field(default_factory=lambda: None) 

326 

327 

328TVS_D65: NDArrayFloat = xy_to_XYZ( 

329 CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["D65"] 

330) 

331 

332 

333def XYZ_to_ZCAM( 

334 XYZ: Domain1, 

335 XYZ_w: Domain1, 

336 L_A: ArrayLike, 

337 Y_b: ArrayLike, 

338 surround: InductionFactors_ZCAM = VIEWING_CONDITIONS_ZCAM["Average"], 

339 discount_illuminant: bool = False, 

340 compute_H: bool = True, 

341) -> Annotated[CAM_Specification_ZCAM, (1, 1, 360, 1, 1, 1, 400, 1, 1, 1)]: 

342 """ 

343 Compute the *ZCAM* colour appearance model correlates from the specified 

344 *CIE XYZ* tristimulus values. 

345 

346 Parameters 

347 ---------- 

348 XYZ 

349 Absolute *CIE XYZ* tristimulus values of test sample / stimulus. 

350 XYZ_w 

351 Absolute *CIE XYZ* tristimulus values of the white under reference 

352 illuminant. 

353 L_A 

354 Test adapting field *luminance* :math:`L_A` in :math:`cd/m^2` such as 

355 :math:`L_A = L_w * Y_b / 100` (where :math:`L_w` is luminance of the 

356 reference white and :math:`Y_b` is the background luminance factor). 

357 Y_b 

358 Luminous factor of background :math:`Y_b` such as 

359 :math:`Y_b = 100 * L_b / L_w` where :math:`L_w` is the luminance of 

360 the light source and :math:`L_b` is the luminance of the background. 

361 For viewing images, :math:`Y_b` can be the average :math:`Y` value 

362 for the pixels in the entire image, or frequently, a :math:`Y` value 

363 of 20, approximating an :math:`L^*` of 50 is used. 

364 surround 

365 Surround viewing conditions induction factors. 

366 discount_illuminant 

367 Truth value indicating if the illuminant should be discounted. 

368 compute_H 

369 Whether to compute *Hue* :math:`h` quadrature :math:`H`. :math:`H` 

370 is rarely used, and expensive to compute. 

371 

372 Returns 

373 ------- 

374 :class:`colour.CAM_Specification_ZCAM` 

375 *ZCAM* colour appearance model specification. 

376 

377 Warnings 

378 -------- 

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

380 transfer function. 

381 

382 Notes 

383 ----- 

384 - *Safdar, Hardeberg and Luo (2021)* does not specify how the 

385 chromatic adaptation to *CIE Standard Illuminant D65* in *Step 0* 

386 should be performed. A one-step *Von Kries* chromatic adaptation 

387 transform is not symmetrical or transitive when a degree of 

388 adaptation is involved. *Safdar, Hardeberg and Luo (2018)* uses 

389 *Zhai and Luo (2018)* two-steps chromatic adaptation transform, thus 

390 it seems sensible to adopt this transform for the *ZCAM* colour 

391 appearance model until more information is available. It is worth 

392 noting that a one-step *Von Kries* chromatic adaptation transform 

393 with support for degree of adaptation produces values closer to the 

394 supplemental document compared to the *Zhai and Luo (2018)* 

395 two-steps chromatic adaptation transform but then the *ZCAM* colour 

396 appearance model does not round-trip properly. 

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

398 transfer function, thus the domain and range values for the 

399 *Reference* and *1* scales are only indicative that the data is not 

400 affected by scale transformations. 

401 

402 +----------------------+-----------------------+---------------+ 

403 | **Domain** | **Scale - Reference** | **Scale - 1** | 

404 +======================+=======================+===============+ 

405 | ``XYZ`` | UN | UN | 

406 +----------------------+-----------------------+---------------+ 

407 | ``XYZ_w`` | UN | UN | 

408 +----------------------+-----------------------+---------------+ 

409 

410 +----------------------+-----------------------+---------------+ 

411 | **Range** | **Scale - Reference** | **Scale - 1** | 

412 +======================+=======================+===============+ 

413 | ``specification.J`` | UN | 1 | 

414 +----------------------+-----------------------+---------------+ 

415 | ``specification.C`` | UN | 1 | 

416 +----------------------+-----------------------+---------------+ 

417 | ``specification.h`` | 360 | 1 | 

418 +----------------------+-----------------------+---------------+ 

419 | ``specification.s`` | UN | 1 | 

420 +----------------------+-----------------------+---------------+ 

421 | ``specification.Q`` | UN | 1 | 

422 +----------------------+-----------------------+---------------+ 

423 | ``specification.M`` | UN | 1 | 

424 +----------------------+-----------------------+---------------+ 

425 | ``specification.H`` | 400 | 1 | 

426 +----------------------+-----------------------+---------------+ 

427 | ``specification.HC`` | UN | 1 | 

428 +----------------------+-----------------------+---------------+ 

429 | ``specification.V`` | UN | 1 | 

430 +----------------------+-----------------------+---------------+ 

431 | ``specification.K`` | UN | 1 | 

432 +----------------------+-----------------------+---------------+ 

433 | ``specification.H`` | UN | 1 | 

434 +----------------------+-----------------------+---------------+ 

435 

436 References 

437 ---------- 

438 :cite:`Safdar2018`, :cite:`Safdar2021`, :cite:`Zhai2018` 

439 

440 Examples 

441 -------- 

442 >>> XYZ = np.array([185, 206, 163]) 

443 >>> XYZ_w = np.array([256, 264, 202]) 

444 >>> L_A = 264 

445 >>> Y_b = 100 

446 >>> surround = VIEWING_CONDITIONS_ZCAM["Average"] 

447 >>> XYZ_to_ZCAM(XYZ, XYZ_w, L_A, Y_b, surround) 

448 ... # doctest: +ELLIPSIS 

449 CAM_Specification_ZCAM(J=92.2504437..., C=3.0216926..., h=196.3245737..., \ 

450s=19.1319556..., Q=321.3408463..., M=10.5256217..., H=237.6114442..., \ 

451HC=None, V=34.7006776..., K=25.8835968..., W=91.6821728...) 

452 """ 

453 

454 XYZ = to_domain_1(XYZ) 

455 XYZ_w = to_domain_1(XYZ_w) 

456 _X_w, Y_w, _Z_w = tsplit(XYZ_w) 

457 L_A = as_float_array(L_A) 

458 Y_b = as_float_array(Y_b) 

459 

460 F_s, F, _c, _N_c = surround.values 

461 

462 # Step 0 (Forward) - Chromatic adaptation from reference illuminant to 

463 # "CIE Standard Illuminant D65" illuminant using "CAT02". 

464 # Computing degree of adaptation :math:`D`. 

465 D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape) 

466 

467 XYZ_D65 = chromatic_adaptation_Zhai2018( 

468 XYZ, XYZ_w, TVS_D65, D, D, transform="CAT02" 

469 ) 

470 

471 # Step 1 (Forward) - Computing factors related with viewing conditions and 

472 # independent of the test stimulus. 

473 # Background factor :math:`F_b` 

474 F_b = np.sqrt(Y_b / Y_w) 

475 # Luminance level adaptation factor :math:`F_L` 

476 F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A)) 

477 

478 # Step 2 (Forward) - Computing achromatic response (:math:`I_z` and 

479 # :math:`I_{z,w}`), redness-greenness (:math:`a_z` and :math:`a_{z,w}`), 

480 # and yellowness-blueness (:math:`b_z`, :math:`b_{z,w}`). 

481 with domain_range_scale("ignore"): 

482 I_z, a_z, b_z = tsplit(XYZ_to_Izazbz(XYZ_D65, method="Safdar 2021")) 

483 I_z_w, _a_z_w, _b_z_w = tsplit(XYZ_to_Izazbz(XYZ_w, method="Safdar 2021")) 

484 

485 # Step 3 (Forward) - Computing hue angle :math:`h_z` 

486 h_z = hue_angle(a_z, b_z) 

487 

488 # Step 4 (Forward) - Computing hue quadrature :math:`H`. 

489 H = hue_quadrature(h_z) if compute_H else np.full(h_z.shape, np.nan) 

490 

491 # Computing eccentricity factor :math:`e_z`. 

492 e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360)) 

493 

494 # Step 5 (Forward) - Computing brightness :math:`Q_z`, 

495 # lightness :math:`J_z`, colourfulness :math`M_z`, and chroma :math:`C_z` 

496 Q_z_p = (1.6 * F_s) / (F_b**0.12) 

497 Q_z_m = F_s**2.2 * F_b**0.5 * spow(F_L, 0.2) 

498 Q_z = 2700 * spow(I_z, Q_z_p) * Q_z_m 

499 Q_z_w = 2700 * spow(I_z_w, Q_z_p) * Q_z_m 

500 

501 J_z = 100 * Q_z / Q_z_w 

502 

503 M_z = ( 

504 100 

505 * (a_z**2 + b_z**2) ** 0.37 

506 * ((spow(e_z, 0.068) * spow(F_L, 0.2)) / (F_b**0.1 * spow(I_z_w, 0.78))) 

507 ) 

508 

509 C_z = 100 * M_z / Q_z_w 

510 

511 # Step 6 (Forward) - Computing saturation :math:`S_z`, 

512 # vividness :math:`V_z`, blackness :math:`K_z`, and whiteness :math:`W_z`. 

513 with sdiv_mode(): 

514 S_z = 100 * spow(F_L, 0.6) * np.sqrt(sdiv(M_z, Q_z)) 

515 

516 V_z = np.sqrt((J_z - 58) ** 2 + 3.4 * C_z**2) 

517 

518 K_z = 100 - 0.8 * np.sqrt(J_z**2 + 8 * C_z**2) 

519 

520 W_z = 100 - np.sqrt((100 - J_z) ** 2 + C_z**2) 

521 

522 return CAM_Specification_ZCAM( 

523 J=as_float(from_range_1(J_z)), 

524 C=as_float(from_range_1(C_z)), 

525 h=as_float(from_range_degrees(h_z)), 

526 s=as_float(from_range_1(S_z)), 

527 Q=as_float(from_range_1(Q_z)), 

528 M=as_float(from_range_1(M_z)), 

529 H=as_float(from_range_degrees(H, 400)), 

530 HC=None, 

531 V=as_float(from_range_1(V_z)), 

532 K=as_float(from_range_1(K_z)), 

533 W=as_float(from_range_1(W_z)), 

534 ) 

535 

536 

537def ZCAM_to_XYZ( 

538 specification: Annotated[ 

539 CAM_Specification_ZCAM, (1, 1, 360, 1, 1, 1, 400, 1, 1, 1) 

540 ], 

541 XYZ_w: Domain1, 

542 L_A: ArrayLike, 

543 Y_b: ArrayLike, 

544 surround: InductionFactors_ZCAM = VIEWING_CONDITIONS_ZCAM["Average"], 

545 discount_illuminant: bool = False, 

546) -> Range1: 

547 """ 

548 Convert the *ZCAM* specification to *CIE XYZ* tristimulus values. 

549 

550 Parameters 

551 ---------- 

552 specification 

553 *ZCAM* colour appearance model specification. 

554 Correlate of *lightness* :math:`J`, correlate of *chroma* :math:`C` or 

555 correlate of *colourfulness* :math:`M` and *hue* angle :math:`h` in 

556 degrees must be specified, e.g., :math:`JCh` or :math:`JMh`. 

557 XYZ_w 

558 Absolute *CIE XYZ* tristimulus values of the white under reference 

559 illuminant. 

560 L_A 

561 Test adapting field *luminance* :math:`L_A` in :math:`cd/m^2` such as 

562 :math:`L_A = L_w * Y_b / 100` (where :math:`L_w` is luminance of the 

563 reference white and :math:`Y_b` is the background luminance factor). 

564 Y_b 

565 Luminous factor of background :math:`Y_b` such as 

566 :math:`Y_b = 100 x L_b / L_w` where :math:`L_w` is the luminance of 

567 the light source and :math:`L_b` is the luminance of the background. 

568 For viewing images, :math:`Y_b` can be the average :math:`Y` value for 

569 the pixels in the entire image, or frequently, a :math:`Y` value of 

570 20, approximating an :math:`L^*` of 50 is used. 

571 surround 

572 Surround viewing conditions induction factors. 

573 discount_illuminant 

574 Truth value indicating if the illuminant should be discounted. 

575 

576 Returns 

577 ------- 

578 :class:`numpy.ndarray` 

579 *CIE XYZ* tristimulus values. 

580 

581 Raises 

582 ------ 

583 ValueError 

584 If neither :math:`C` or :math:`M` correlates have been defined in the 

585 ``specification`` argument. 

586 

587 Warnings 

588 -------- 

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

590 transfer function. 

591 

592 Notes 

593 ----- 

594 - *Safdar, Hardeberg and Luo (2021)* does not specify how the 

595 chromatic adaptation to *CIE Standard Illuminant D65* in *Step 0* 

596 should be performed. A one-step *Von Kries* chromatic adaptation 

597 transform is not symmetrical or transitive when a degree of 

598 adaptation is involved. *Safdar, Hardeberg and Luo (2018)* uses 

599 *Zhai and Luo (2018)* two-steps chromatic adaptation transform, thus 

600 it seems sensible to adopt this transform for the *ZCAM* colour 

601 appearance model until more information is available. It is worth 

602 noting that a one-step *Von Kries* chromatic adaptation transform 

603 with support for degree of adaptation produces values closer to the 

604 supplemental document compared to the *Zhai and Luo (2018)* 

605 two-steps chromatic adaptation transform but then the *ZCAM* colour 

606 appearance model does not round-trip properly. 

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

608 transfer function, thus the domain and range values for the 

609 *Reference* and *1* scales are only indicative that the data is not 

610 affected by scale transformations. 

611 - *Step 4* of the inverse model uses a rounded exponent of 1.3514 

612 preventing the model to round-trip properly. Given that this 

613 implementation takes some liberties with respect to the chromatic 

614 adaptation transform to use, it was deemed appropriate to use an 

615 exponent value that enables the *ZCAM* colour appearance model to 

616 round-trip. 

617 

618 +----------------------+-----------------------+---------------+ 

619 | **Domain** | **Scale - Reference** | **Scale - 1** | 

620 +======================+=======================+===============+ 

621 | ``specification.J`` | UN | UN | 

622 +----------------------+-----------------------+---------------+ 

623 | ``specification.C`` | UN | UN | 

624 +----------------------+-----------------------+---------------+ 

625 | ``specification.h`` | 360 | 1 | 

626 +----------------------+-----------------------+---------------+ 

627 | ``specification.s`` | UN | UN | 

628 +----------------------+-----------------------+---------------+ 

629 | ``specification.Q`` | UN | UN | 

630 +----------------------+-----------------------+---------------+ 

631 | ``specification.M`` | UN | UN | 

632 +----------------------+-----------------------+---------------+ 

633 | ``specification.H`` | 400 | 1 | 

634 +----------------------+-----------------------+---------------+ 

635 | ``specification.HC`` | UN | UN | 

636 +----------------------+-----------------------+---------------+ 

637 | ``specification.V`` | UN | UN | 

638 +----------------------+-----------------------+---------------+ 

639 | ``specification.K`` | UN | UN | 

640 +----------------------+-----------------------+---------------+ 

641 | ``specification.H`` | UN | UN | 

642 +----------------------+-----------------------+---------------+ 

643 | ``XYZ_w`` | UN | UN | 

644 +----------------------+-----------------------+---------------+ 

645 

646 +----------------------+-----------------------+---------------+ 

647 | **Range** | **Scale - Reference** | **Scale - 1** | 

648 +======================+=======================+===============+ 

649 | ``XYZ`` | UN | UN | 

650 +----------------------+-----------------------+---------------+ 

651 

652 References 

653 ---------- 

654 :cite:`Safdar2018`, :cite:`Safdar2021`, :cite:`Zhai2018` 

655 

656 Examples 

657 -------- 

658 >>> specification = CAM_Specification_ZCAM( 

659 ... J=92.250443780723629, C=3.0216926733329013, h=196.32457375575581 

660 ... ) 

661 >>> XYZ_w = np.array([256, 264, 202]) 

662 >>> L_A = 264 

663 >>> Y_b = 100 

664 >>> surround = VIEWING_CONDITIONS_ZCAM["Average"] 

665 >>> ZCAM_to_XYZ(specification, XYZ_w, L_A, Y_b, surround) 

666 ... # doctest: +ELLIPSIS 

667 array([ 185., 206., 163.]) 

668 """ 

669 

670 J_z, C_z, h_z, _S_z, _Q_z, M_z, _H, _H_Z, _V_z, _K_z, _W_z = astuple(specification) 

671 

672 J_z = to_domain_1(J_z) 

673 C_z = to_domain_1(C_z) 

674 h_z = to_domain_degrees(h_z) 

675 M_z = to_domain_1(M_z) 

676 

677 XYZ_w = to_domain_1(XYZ_w) 

678 _X_w, Y_w, _Z_w = tsplit(XYZ_w) 

679 L_A = as_float_array(L_A) 

680 Y_b = as_float_array(Y_b) 

681 

682 F_s, F, c, N_c = surround.values 

683 

684 # Step 0 (Forward) - Chromatic adaptation from reference illuminant to 

685 # "CIE Standard Illuminant D65" illuminant using "CAT02". 

686 # Computing degree of adaptation :math:`D`. 

687 D = degree_of_adaptation(F, L_A) if not discount_illuminant else ones(L_A.shape) 

688 

689 # Step 1 (Forward) - Computing factors related with viewing conditions and 

690 # independent of the test stimulus. 

691 # Background factor :math:`F_b` 

692 F_b = np.sqrt(Y_b / Y_w) 

693 # Luminance level adaptation factor :math:`F_L` 

694 F_L = 0.171 * spow(L_A, 1 / 3) * (1 - np.exp(-48 / 9 * L_A)) 

695 

696 # Step 2 (Forward) - Computing achromatic response (:math:`I_{z,w}`), 

697 # redness-greenness (:math:`a_{z,w}`), and yellowness-blueness 

698 # (:math:`b_{z,w}`). 

699 with domain_range_scale("ignore"): 

700 I_z_w, _A_z_w, _B_z_w = tsplit(XYZ_to_Izazbz(XYZ_w, method="Safdar 2021")) 

701 

702 # Step 1 (Inverse) - Computing achromatic response (:math:`I_z`). 

703 Q_z_p = (1.6 * F_s) / spow(F_b, 0.12) 

704 Q_z_m = spow(F_s, 2.2) * spow(F_b, 0.5) * spow(F_L, 0.2) 

705 Q_z_w = 2700 * spow(I_z_w, Q_z_p) * Q_z_m 

706 

707 I_z_p = spow(F_b, 0.12) / (1.6 * F_s) 

708 I_z_d = 2700 * 100 * Q_z_m 

709 

710 I_z = spow((J_z * Q_z_w) / I_z_d, I_z_p) 

711 

712 # Step 2 (Inverse) - Computing chroma :math:`C_z`. 

713 if has_only_nan(M_z) and not has_only_nan(C_z): 

714 M_z = (C_z * Q_z_w) / 100 

715 elif has_only_nan(M_z): 

716 error = ( 

717 'Either "C" or "M" correlate must be defined in ' 

718 'the "CAM_Specification_ZCAM" argument!' 

719 ) 

720 

721 raise ValueError(error) 

722 

723 # Step 3 (Inverse) - Computing hue angle :math:`h_z` 

724 # :math:`h_z` is currently required as an input. 

725 

726 # Computing eccentricity factor :math:`e_z`. 

727 e_z = 1.015 + np.cos(np.radians(89.038 + h_z % 360)) 

728 h_z_r = np.radians(h_z) 

729 

730 # Step 4 (Inverse) - Computing redness-greenness (:math:`a_z`), and 

731 # yellowness-blueness (:math:`b_z`). 

732 # C_z_p_e = 1.3514 

733 C_z_p_e = 50 / 37 

734 C_z_p = spow( 

735 (M_z * spow(I_z_w, 0.78) * spow(F_b, 0.1)) 

736 / (100 * spow(e_z, 0.068) * spow(F_L, 0.2)), 

737 C_z_p_e, 

738 ) 

739 a_z = C_z_p * np.cos(h_z_r) 

740 b_z = C_z_p * np.sin(h_z_r) 

741 

742 # Step 5 (Inverse) - Computing tristimulus values :math:`XYZ_{D65}`. 

743 with domain_range_scale("ignore"): 

744 XYZ_D65 = Izazbz_to_XYZ(tstack([I_z, a_z, b_z]), method="Safdar 2021") 

745 

746 XYZ = chromatic_adaptation_Zhai2018( 

747 XYZ_D65, TVS_D65, XYZ_w, D, D, transform="CAT02" 

748 ) 

749 

750 return from_range_1(XYZ) 

751 

752 

753def hue_quadrature(h: ArrayLike) -> NDArrayFloat: 

754 """ 

755 Compute the hue quadrature from the specified hue :math:`h` angle in 

756 degrees. 

757 

758 Parameters 

759 ---------- 

760 h 

761 Hue :math:`h` angle in degrees. 

762 

763 Returns 

764 ------- 

765 :class:`numpy.ndarray` 

766 Hue quadrature. 

767 

768 Examples 

769 -------- 

770 >>> hue_quadrature(196.3185839) # doctest: +ELLIPSIS 

771 237.6052911... 

772 """ 

773 

774 h = as_float_array(h) 

775 

776 h_i = HUE_DATA_FOR_HUE_QUADRATURE["h_i"] 

777 e_i = HUE_DATA_FOR_HUE_QUADRATURE["e_i"] 

778 H_i = HUE_DATA_FOR_HUE_QUADRATURE["H_i"] 

779 

780 # :math:`h_p` = :math:`h_z` + 360 if :math:`h_z` < :math:`h_1, i.e., h_i[0] 

781 h[h <= h_i[0]] += 360 

782 # *np.searchsorted* returns an erroneous index if a *nan* is used as input. 

783 h[np.asarray(np.isnan(h))] = 0 

784 i = as_int_array(np.searchsorted(h_i, h, side="left") - 1) 

785 

786 h_ii = h_i[i] 

787 e_ii = e_i[i] 

788 H_ii = H_i[i] 

789 h_ii1 = h_i[i + 1] 

790 e_ii1 = e_i[i + 1] 

791 

792 h_h_ii = h - h_ii 

793 

794 H = H_ii + (100 * h_h_ii / e_ii) / (h_h_ii / e_ii + (h_ii1 - h) / e_ii1) 

795 

796 return as_float(H)