Coverage for appearance/atd95.py: 51%

81 statements  

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

1""" 

2ATD (1995) Colour Vision Model 

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

4 

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

6 

7- :class:`colour.CAM_Specification_ATD95` 

8- :func:`colour.XYZ_to_ATD95` 

9 

10Notes 

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 

17References 

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 

27from __future__ import annotations 

28 

29from dataclasses import dataclass, field 

30 

31import numpy as np 

32 

33from colour.algebra import spow, vecmul 

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

35from 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 

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

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

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

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

236T_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 

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

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

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

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