Coverage for appearance/atd95.py: 51%

1"""

2 ATD (1995) Colour Vision Model

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

4

5 Define the *ATD (1995)* colour vision model.

6

7- :class:`colour.CAM_Specification_ATD95`

8- :func:`colour.XYZ_to_ATD95`

9

10 Notes

11-----

12- According to *CIE TC1-34* definition of a colour appearance model, the

13 *ATD (1995)* model cannot be considered as a colour appearance model.

14 It was developed with different aims and is described as a model of

15 colour vision.

16

17 References

18----------

19- :cite:`Fairchild2013v` : Fairchild, M. D. (2013). ATD Model. In Color

20 Appearance Models (3rd ed., pp. 5852-5991). Wiley. ISBN:B00DAYO8E2

21- :cite:`Guth1995a` : Guth, S. L. (1995). Further applications of the ATD

22 model for color vision. In E. Walowit (Ed.), Proc. SPIE 2414,

23 Device-Independent Color Imaging II (Vol. 2414, pp. 12-26).

24 doi:10.1117/12.206546

25"""

26

27 from __future__ import annotations

28

29 from dataclasses import dataclass, field

30

31 import numpy as np

32

33 from colour.algebra import spow, vecmul

34 from colour.hints import Annotated, ArrayLike, Domain100, NDArrayFloat # noqa: TC001

35 from colour.utilities import (

36 MixinDataclassArithmetic,

37 as_float,

38 as_float_array,

39 from_range_degrees,

40 to_domain_100,

41 tsplit,

42 tstack,

43)

44

45 __author__ = "Colour Developers"

46 __copyright__ = "Copyright 2013 Colour Developers"

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

48 __maintainer__ = "Colour Developers"

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

50 __status__ = "Production"

51

52 __all__ = [

53 "CAM_ReferenceSpecification_ATD95",

54 "CAM_Specification_ATD95",

55 "XYZ_to_ATD95",

56 "luminance_to_retinal_illuminance",

57 "XYZ_to_LMS_ATD95",

58 "opponent_colour_dimensions",

59 "final_response",

60]

61

62

63@dataclass

64 class CAM_ReferenceSpecification_ATD95(MixinDataclassArithmetic):

65 """

66 Define the *ATD (1995)* colour vision model reference specification.

67

68 This specification contains field names consistent with the *Fairchild

69 (2013)* reference.

70

71 Parameters

72 ----------

73 H

74 *Hue* angle :math:`H` in degrees.

75 C

76 Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses

77 the terms saturation and chroma interchangeably. However, :math:`C`

78 represents a measure of saturation rather than chroma since it is

79 calculated relative to the achromatic response for the stimulus

80 rather than that of a similarly illuminated white.

81 Br

82 Correlate of *brightness* :math:`Br`.

83 A_1

84 First stage :math:`A_1` response.

85 T_1

86 First stage :math:`T_1` response.

87 D_1

88 First stage :math:`D_1` response.

89 A_2

90 Second stage :math:`A_2` response.

91 T_2

92 Second stage :math:`A_2` response.

93 D_2

94 Second stage :math:`D_2` response.

95

96 References

97 ----------

98 :cite:`Fairchild2013v`, :cite:`Guth1995a`

99 """

100

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

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

103 Br: float | NDArrayFloat | None = field(default_factory=lambda: None)

104 A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

105 T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

106 D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

107 A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

108 T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

109 D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

110

111

112@dataclass

113 class CAM_Specification_ATD95(MixinDataclassArithmetic):

114 """

115 Define the *ATD (1995)* colour vision model specification.

116

117 This specification provides a standardized interface for the *ATD (1995)*

118 model with field names consistent across all colour appearance models in

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

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

121 perceptual correlates.

122

123 Parameters

124 ----------

125 h

126 *Hue* angle :math:`H` in degrees.

127 C

128 Correlate of *saturation* :math:`C`. *Guth (1995)* incorrectly uses

129 the terms saturation and chroma interchangeably. However, :math:`C`

130 represents a measure of saturation rather than chroma since it is

131 measured relative to the achromatic response for the stimulus rather

132 than that of a similarly illuminated white.

133 Q

134 Correlate of *brightness* :math:`Br`.

135 A_1

136 First stage :math:`A_1` response.

137 T_1

138 First stage :math:`T_1` response.

139 D_1

140 First stage :math:`D_1` response.

141 A_2

142 Second stage :math:`A_2` response.

143 T_2

144 Second stage :math:`T_2` response.

145 D_2

146 Second stage :math:`D_2` response.

147

148 Notes

149 -----

150 - This specification is the one used in the current model

151 implementation.

152

153 References

154 ----------

155 :cite:`Fairchild2013v`, :cite:`Guth1995a`

156 """

157

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

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

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

161 A_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

162 T_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

163 D_1: float | NDArrayFloat | None = field(default_factory=lambda: None)

164 A_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

165 T_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

166 D_2: float | NDArrayFloat | None = field(default_factory=lambda: None)

167

168

169 def XYZ_to_ATD95(

170 XYZ: Domain100,

171 XYZ_0: Domain100,

172 Y_0: ArrayLike,

173 k_1: ArrayLike,

174 k_2: ArrayLike,

175 sigma: ArrayLike = 300,

176) -> Annotated[CAM_Specification_ATD95, 360]:

177 """

178 Compute the *ATD (1995)* colour vision model correlates from the specified

179 *CIE XYZ* tristimulus values.

180

181 Parameters

182 ----------

183 XYZ

184 *CIE XYZ* tristimulus values of test sample / stimulus.

185 XYZ_0

186 *CIE XYZ* tristimulus values of reference white.

187 Y_0

188 Absolute adapting field luminance in :math:`cd/m^2`.

189 k_1

190 Application specific weight :math:`k_1`.

191 k_2

192 Application specific weight :math:`k_2`.

193 sigma

194 Constant :math:`\\sigma` varied to predict different types of data.

195

196 Returns

197 -------

198 :class:`colour.CAM_Specification_ATD95`

199 *ATD (1995)* colour vision model specification.

200

201 Notes

202 -----

203 +---------------------+-----------------------+---------------+

204 | **Domain** | **Scale - Reference** | **Scale - 1** |

205 +=====================+=======================+===============+

206 | ``XYZ`` | 100 | 1 |

207 +---------------------+-----------------------+---------------+

208 | ``XYZ_0`` | 100 | 1 |

209 +---------------------+-----------------------+---------------+

210

211 +---------------------+-----------------------+---------------+

212 | **Range** | **Scale - Reference** | **Scale - 1** |

213 +=====================+=======================+===============+

214 | ``specification.h`` | 360 | 1 |

215 +---------------------+-----------------------+---------------+

216

217 - For unrelated colours, there is only self-adaptation and :math:`k_1`

218 is set to 1.0 while :math:`k_2` is set to 0.0. For related colours

219 such as typical colorimetric applications, :math:`k_1` is set to 0.0

220 and :math:`k_2` is set to a value between 15 and 50 *(Guth, 1995)*.

221

222 References

223 ----------

224 :cite:`Fairchild2013v`, :cite:`Guth1995a`

225

226 Examples

227 --------

228 >>> XYZ = np.array([19.01, 20.00, 21.78])

229 >>> XYZ_0 = np.array([95.05, 100.00, 108.88])

230 >>> Y_0 = 318.31

231 >>> k_1 = 0.0

232 >>> k_2 = 50.0

233 >>> XYZ_to_ATD95(XYZ, XYZ_0, Y_0, k_1, k_2) # doctest: +ELLIPSIS

234 CAM_Specification_ATD95(h=1.9089869..., C=1.2064060..., Q=0.1814003..., \

235 A_1=0.1787931... T_1=0.0286942..., D_1=0.0107584..., A_2=0.0192182..., \

236 T_2=0.0205377..., D_2=0.0107584...)

237 """

238

239 XYZ = to_domain_100(XYZ)

240 XYZ_0 = to_domain_100(XYZ_0)

241 Y_0 = as_float_array(Y_0)

242 k_1 = as_float_array(k_1)

243 k_2 = as_float_array(k_2)

244 sigma = as_float_array(sigma)

245

246 XYZ = luminance_to_retinal_illuminance(XYZ, Y_0)

247 XYZ_0 = luminance_to_retinal_illuminance(XYZ_0, Y_0)

248

249 # Computing adaptation model.

250 LMS = XYZ_to_LMS_ATD95(XYZ)

251 XYZ_a = k_1[..., None] * XYZ + k_2[..., None] * XYZ_0

252 LMS_a = XYZ_to_LMS_ATD95(XYZ_a)

253

254 LMS_g = LMS * (sigma[..., None] / (sigma[..., None] + LMS_a))

255

256 # Computing opponent colour dimensions.

257 A_1, T_1, D_1, A_2, T_2, D_2 = tsplit(opponent_colour_dimensions(LMS_g))

258

259 # Computing the correlate of *brightness* :math:`Br`.

260 Br = spow(A_1**2 + T_1**2 + D_1**2, 0.5)

261

262 # Computing the correlate of *saturation* :math:`C`.

263 C = spow(T_2**2 + D_2**2, 0.5) / A_2

264

265 # Computing the *hue* :math:`H`. Note that the reference does not take the

266 # modulus of the :math:`H`, thus :math:`H` can exceed 360 degrees.

267 H = T_2 / D_2

268

269 return CAM_Specification_ATD95(

270 h=as_float(from_range_degrees(H)),

271 C=C,

272 Q=Br,

273 A_1=A_1,

274 T_1=T_1,

275 D_1=D_1,

276 A_2=A_2,

277 T_2=T_2,

278 D_2=D_2,

279 )

280

281

282 def luminance_to_retinal_illuminance(XYZ: ArrayLike, Y_c: ArrayLike) -> NDArrayFloat:

283 """

284 Convert luminance in :math:`cd/m^2` to retinal illuminance in trolands.

285

286 This function converts photometric luminance values to retinal illuminance

287 by applying a power transformation that accounts for pupil area effects

288 under the specified adapting field luminance conditions.

289

290 Parameters

291 ----------

292 XYZ

293 *CIE XYZ* tristimulus values in photometric units.

294 Y_c

295 Absolute adapting field luminance in :math:`cd/m^2`.

296

297 Returns

298 -------

299 :class:`numpy.ndarray`

300 Retinal illuminance values in trolands corresponding to the

301 tristimulus values.

302

303 Examples

304 --------

305 >>> XYZ = np.array([19.01, 20.00, 21.78])

306 >>> Y_0 = 318.31

307 >>> luminance_to_retinal_illuminance(XYZ, Y_0) # doctest: +ELLIPSIS

308 array([ 479.4445924..., 499.3174313..., 534.5631673...])

309 """

310

311 XYZ = as_float_array(XYZ)

312 Y_c = as_float_array(Y_c)

313

314 return 18 * spow(Y_c[..., None] * XYZ / 100, 0.8)

315

316

317 def XYZ_to_LMS_ATD95(XYZ: ArrayLike) -> NDArrayFloat:

318 """

319 Convert *CIE XYZ* tristimulus values to *LMS* cone responses using the

320 *ATD95* colour appearance model.

321

322 Parameters

323 ----------

324 XYZ

325 *CIE XYZ* tristimulus values.

326

327 Returns

328 -------

329 :class:`numpy.ndarray`

330 *LMS* cone responses.

331

332 Examples

333 --------

334 >>> XYZ = np.array([19.01, 20.00, 21.78])

335 >>> XYZ_to_LMS_ATD95(XYZ) # doctest: +ELLIPSIS

336 array([ 6.2283272..., 7.4780666..., 3.8859772...])

337 """

338

339 LMS = vecmul(

340 [

341 [0.2435, 0.8524, -0.0516],

342 [-0.3954, 1.1642, 0.0837],

343 [0.0000, 0.0400, 0.6225],

344 ],

345 XYZ,

346 )

347 LMS *= np.array([0.66, 1.0, 0.43])

348

349 LMS_p = spow(LMS, 0.7)

350 LMS_p += np.array([0.024, 0.036, 0.31])

351

352 return LMS_p

353

354

355 def opponent_colour_dimensions(LMS_g: ArrayLike) -> NDArrayFloat:

356 """

357 Compute opponent colour dimensions from the specified post-adaptation cone

358 signals.

359

360 Parameters

361 ----------

362 LMS_g

363 Post-adaptation cone signals.

364

365 Returns

366 -------

367 :class:`numpy.ndarray`

368 Opponent colour dimensions.

369

370 Examples

371 --------

372 >>> LMS_g = np.array([6.95457922, 7.08945043, 6.44069316])

373 >>> opponent_colour_dimensions(LMS_g) # doctest: +ELLIPSIS

374 array([ 0.1787931..., 0.0286942..., 0.0107584..., 0.0192182..., ...])

375 """

376

377 L_g, M_g, S_g = tsplit(LMS_g)

378

379 A_1i = 3.57 * L_g + 2.64 * M_g

380 T_1i = 7.18 * L_g - 6.21 * M_g

381 D_1i = -0.7 * L_g + 0.085 * M_g + S_g

382 A_2i = 0.09 * A_1i

383 T_2i = 0.43 * T_1i + 0.76 * D_1i

384 D_2i = D_1i

385

386 A_1 = final_response(A_1i)

387 T_1 = final_response(T_1i)

388 D_1 = final_response(D_1i)

389 A_2 = final_response(A_2i)

390 T_2 = final_response(T_2i)

391 D_2 = final_response(D_2i)

392

393 return tstack([A_1, T_1, D_1, A_2, T_2, D_2])

394

395

396 def final_response(value: ArrayLike) -> NDArrayFloat:

397 """

398 Compute the final response of the specified opponent colour dimension.

399

400 Parameters

401 ----------

402 value

403 Opponent colour dimension.

404

405 Returns

406 -------

407 :class:`numpy.ndarray`

408 Final response of the opponent colour dimension.

409

410 Examples

411 --------

412 >>> final_response(43.54399695501678) # doctest: +ELLIPSIS

413 0.1787931...

414 """

415

416 value = as_float_array(value)

417

418 return as_float(value / (200 + np.abs(value)))