Coverage for appearance/zcam.py: 65%

1"""

2 ZCAM Colour Appearance Model

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

4

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

6 attributes 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

14 References

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

28 from __future__ import annotations

29

30 from dataclasses import astuple, dataclass, field

31

32 import numpy as np

33

34 from colour.adaptation import chromatic_adaptation_Zhai2018

35 from colour.algebra import sdiv, sdiv_mode, spow

36 from colour.appearance.ciecam02 import (

37 VIEWING_CONDITIONS_CIECAM02,

38 degree_of_adaptation,

39 hue_angle,

40)

41 from colour.colorimetry import CCS_ILLUMINANTS

42 from colour.hints import ( # noqa: TC001

43 Annotated,

44 ArrayLike,

45 Domain1,

46 NDArrayFloat,

47 Range1,

48)

49 from colour.models import Izazbz_to_XYZ, XYZ_to_Izazbz, xy_to_XYZ

50 from 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)

85 class 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

116 VIEWING_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)

127 VIEWING_CONDITIONS_ZCAM.__doc__ = """

128 Define the reference *ZCAM* colour appearance model

129 viewing conditions.

130

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

132 with corresponding induction factors. Each condition specifies a unique

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

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

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

136

137 Notes

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

143 References

144----------

145:cite:`Safdar2021`

146"""

147

148 HUE_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

156 class 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

207 class 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

328 TVS_D65: NDArrayFloat = xy_to_XYZ(

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

330)

331

332

333 def 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..., \

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

451 HC=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

537 def 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

753 def 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)