Coverage for blindness/machado2009.py: 74%

76 statements  

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

1""" 

2Simulation of CVD - Machado, Oliveira and Fernandes (2009) 

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

4 

5Define the *Machado et al. (2009)* physiologically-based model for 

6simulation of colour vision deficiency. 

7 

8- :func:`colour.msds_cmfs_anomalous_trichromacy_Machado2009` 

9- :func:`colour.matrix_anomalous_trichromacy_Machado2009` 

10- :func:`colour.matrix_cvd_Machado2009` 

11 

12References 

13---------- 

14- :cite:`Colblindora` : Colblindor. (n.d.). Deuteranopia - Red-Green Color 

15 Blindness. Retrieved July 4, 2015, from 

16 http://www.color-blindness.com/deuteranopia-red-green-color-blindness/ 

17- :cite:`Colblindorb` : Colblindor. (n.d.). Protanopia - Red-Green Color 

18 Blindness. Retrieved July 4, 2015, from 

19 http://www.color-blindness.com/protanopia-red-green-color-blindness/ 

20- :cite:`Colblindorc` : Colblindor. (n.d.). Tritanopia - Blue-Yellow Color 

21 Blindness. Retrieved July 4, 2015, from 

22 http://www.color-blindness.com/tritanopia-blue-yellow-color-blindness/ 

23- :cite:`Machado2009` : Machado, G.M., Oliveira, M. M., & Fernandes, L. 

24 (2009). A Physiologically-based Model for Simulation of Color Vision 

25 Deficiency. IEEE Transactions on Visualization and Computer Graphics, 

26 15(6), 1291-1298. doi:10.1109/TVCG.2009.113 

27""" 

28 

29from __future__ import annotations 

30 

31import typing 

32 

33import numpy as np 

34 

35from colour.algebra import vecmul 

36from colour.blindness import CVD_MATRICES_MACHADO2010 

37 

38if typing.TYPE_CHECKING: 

39 from colour.characterisation import RGB_DisplayPrimaries 

40 

41from colour.colorimetry import LMS_ConeFundamentals, SpectralShape, reshape_msds 

42 

43if typing.TYPE_CHECKING: 

44 from colour.hints import ArrayLike, Literal, NDArrayFloat 

45 

46from colour.utilities import ( 

47 as_float_array, 

48 as_int_scalar, 

49 tsplit, 

50 tstack, 

51 usage_warning, 

52) 

53 

54__author__ = "Colour Developers" 

55__copyright__ = "Copyright 2013 Colour Developers" 

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

57__maintainer__ = "Colour Developers" 

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

59__status__ = "Production" 

60 

61__all__ = [ 

62 "MATRIX_LMS_TO_WSYBRG", 

63 "matrix_RGB_to_WSYBRG", 

64 "msds_cmfs_anomalous_trichromacy_Machado2009", 

65 "matrix_anomalous_trichromacy_Machado2009", 

66 "matrix_cvd_Machado2009", 

67] 

68 

69MATRIX_LMS_TO_WSYBRG: NDArrayFloat = np.array( 

70 [ 

71 [0.600, 0.400, 0.000], 

72 [0.240, 0.105, -0.700], 

73 [1.200, -1.600, 0.400], 

74 ] 

75) 

76""" 

77Ingling and Tsou (1977) matrix converting from cones responses to 

78opponent-colour space. 

79""" 

80 

81 

82def matrix_RGB_to_WSYBRG( 

83 cmfs: LMS_ConeFundamentals, primaries: RGB_DisplayPrimaries 

84) -> NDArrayFloat: 

85 """ 

86 Compute the matrix for transforming from *RGB* colourspace to 

87 opponent-colour space using *Machado et al. (2009)* method. 

88 

89 Parameters 

90 ---------- 

91 cmfs 

92 *LMS* cone fundamentals colour matching functions. 

93 primaries 

94 *RGB* display primaries tri-spectral distributions. 

95 

96 Returns 

97 ------- 

98 :class:`numpy.ndarray` 

99 Matrix transforming from *RGB* colourspace to opponent-colour 

100 space. 

101 

102 Examples 

103 -------- 

104 >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES 

105 >>> from colour.colorimetry import MSDS_CMFS_LMS 

106 >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] 

107 >>> d_LMS = np.array([15, 0, 0]) 

108 >>> primaries = MSDS_DISPLAY_PRIMARIES["Apple Studio Display"] 

109 >>> matrix_RGB_to_WSYBRG(cmfs, primaries) # doctest: +ELLIPSIS 

110 array([[ 0.2126535..., 0.6704626..., 0.1168838...], 

111 [ 4.7095295..., 12.4862869..., -16.1958165...], 

112 [-11.1518474..., 15.2534789..., -3.1016315...]]) 

113 """ 

114 

115 wavelengths = cmfs.wavelengths 

116 WSYBRG = vecmul(MATRIX_LMS_TO_WSYBRG, cmfs.values) 

117 WS, YB, RG = tsplit(WSYBRG) 

118 

119 primaries = reshape_msds( 

120 primaries, 

121 cmfs.shape, 

122 copy=False, 

123 extrapolator_kwargs={"method": "Constant", "left": 0, "right": 0}, 

124 ) 

125 

126 R, G, B = tsplit(primaries.values) 

127 

128 WS_R = np.trapezoid(R * WS, wavelengths) 

129 WS_G = np.trapezoid(G * WS, wavelengths) 

130 WS_B = np.trapezoid(B * WS, wavelengths) 

131 

132 YB_R = np.trapezoid(R * YB, wavelengths) 

133 YB_G = np.trapezoid(G * YB, wavelengths) 

134 YB_B = np.trapezoid(B * YB, wavelengths) 

135 

136 RG_R = np.trapezoid(R * RG, wavelengths) 

137 RG_G = np.trapezoid(G * RG, wavelengths) 

138 RG_B = np.trapezoid(B * RG, wavelengths) 

139 

140 M_G = as_float_array( 

141 [ 

142 [WS_R, WS_G, WS_B], 

143 [YB_R, YB_G, YB_B], 

144 [RG_R, RG_G, RG_B], 

145 ] 

146 ) 

147 

148 M_G /= np.sum(M_G, axis=-1)[:, None] 

149 

150 return M_G 

151 

152 

153def msds_cmfs_anomalous_trichromacy_Machado2009( 

154 cmfs: LMS_ConeFundamentals, d_LMS: ArrayLike 

155) -> LMS_ConeFundamentals: 

156 """ 

157 Shift the specified *LMS* cone fundamentals colour matching functions 

158 with the specified :math:`\\Delta_{LMS}` shift amount in nanometers to 

159 simulate anomalous trichromacy using *Machado et al. (2009)* method. 

160 

161 Parameters 

162 ---------- 

163 cmfs 

164 *LMS* cone fundamentals colour matching functions. 

165 d_LMS 

166 :math:`\\Delta_{LMS}` wavelength shift amount in nanometers for each 

167 cone type. 

168 

169 Notes 

170 ----- 

171 - Input *LMS* cone fundamentals colour matching functions interval is 

172 expected to be 1 nanometer, incompatible input will be interpolated 

173 at 1 nanometer interval. 

174 - Input :math:`\\Delta_{LMS}` shift amount is in domain [0, 20]. 

175 

176 Returns 

177 ------- 

178 :class:`colour.LMS_ConeFundamentals` 

179 Anomalous trichromacy *LMS* cone fundamentals colour matching 

180 functions. 

181 

182 Warnings 

183 -------- 

184 *Machado et al. (2009)* simulation of tritanomaly is based on the shift 

185 paradigm as an approximation to the actual phenomenon and restrain the 

186 model from trying to model tritanopia. 

187 The pre-generated matrices are using a shift value in domain [5, 59] 

188 contrary to the domain [0, 20] used for protanomaly and deuteranomaly 

189 simulation. 

190 

191 References 

192 ---------- 

193 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, 

194 :cite:`Machado2009` 

195 

196 Examples 

197 -------- 

198 >>> from colour.colorimetry import MSDS_CMFS_LMS 

199 >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] 

200 >>> cmfs[450] 

201 array([ 0.0498639, 0.0870524, 0.955393 ]) 

202 >>> msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, np.array([15, 0, 0]))[ 

203 ... 450 

204 ... ] # doctest: +ELLIPSIS 

205 array([ 0.0891288..., 0.0870524 , 0.955393 ]) 

206 """ 

207 

208 cmfs = cmfs.copy() 

209 

210 if cmfs.shape.interval != 1: 

211 cmfs.interpolate(SpectralShape(cmfs.shape.start, cmfs.shape.end, 1)) 

212 

213 cmfs.extrapolator_kwargs = {"method": "Constant", "left": 0, "right": 0} 

214 

215 L, M, _S = tsplit(cmfs.values) 

216 d_L, d_M, d_S = tsplit(d_LMS) 

217 

218 if d_S != 0: 

219 usage_warning( 

220 '"Machado et al. (2009)" simulation of tritanomaly is based on ' 

221 "the shift paradigm as an approximation to the actual phenomenon " 

222 "and restrain the model from trying to model tritanopia.\n" 

223 "The pre-generated matrices are using a shift value in domain " 

224 "[5, 59] contrary to the domain [0, 20] used for protanomaly and " 

225 "deuteranomaly simulation." 

226 ) 

227 

228 area_L = np.trapezoid(L, cmfs.wavelengths) 

229 area_M = np.trapezoid(M, cmfs.wavelengths) 

230 

231 def alpha(x: NDArrayFloat) -> NDArrayFloat: 

232 """Compute :math:`alpha` factor.""" 

233 

234 return (20 - x) / 20 

235 

236 # Corrected equations as per: 

237 # http://www.inf.ufrgs.br/~oliveira/pubs_files/ 

238 # CVD_Simulation/CVD_Simulation.html#Errata 

239 L_a = alpha(d_L) * L + 0.96 * area_L / area_M * (1 - alpha(d_L)) * M 

240 M_a = alpha(d_M) * M + 1 / 0.96 * area_M / area_L * (1 - alpha(d_M)) * L 

241 S_a = cmfs[cmfs.wavelengths - d_S][:, 2] 

242 

243 LMS_a = tstack([L_a, M_a, S_a]) 

244 cmfs[cmfs.wavelengths] = LMS_a 

245 

246 severity = f"{d_L}, {d_M}, {d_S}" 

247 template = "{0} - Anomalous Trichromacy ({1})" 

248 cmfs.name = template.format(cmfs.name, severity) 

249 cmfs.display_name = template.format(cmfs.display_name, severity) 

250 

251 return cmfs 

252 

253 

254def matrix_anomalous_trichromacy_Machado2009( 

255 cmfs: LMS_ConeFundamentals, 

256 primaries: RGB_DisplayPrimaries, 

257 d_LMS: ArrayLike, 

258) -> NDArrayFloat: 

259 """ 

260 Compute the *Machado et al. (2009)* colour vision deficiency matrix for 

261 anomalous trichromacy simulation. 

262 primaries tri-spectral distributions with the specified :math:`\\Delta_{LMS}` shift 

263 amount in nanometers to simulate anomalous trichromacy. 

264 

265 Parameters 

266 ---------- 

267 cmfs 

268 *LMS* cone fundamentals colour matching functions. 

269 primaries 

270 *RGB* display primaries tri-spectral distributions. 

271 d_LMS 

272 :math:`\\Delta_{LMS}` wavelength shift amount in nanometers for each 

273 cone type. 

274 

275 Returns 

276 ------- 

277 :class:`numpy.ndarray` 

278 Anomalous trichromacy transformation matrix. 

279 

280 Notes 

281 ----- 

282 - Input *LMS* cone fundamentals colour matching functions interval is 

283 expected to be 1 nanometer, incompatible input will be interpolated 

284 at 1 nanometer interval. 

285 - Input :math:`\\Delta_{LMS}` shift amount is in domain [0, 20]. 

286 

287 References 

288 ---------- 

289 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, 

290 :cite:`Machado2009` 

291 

292 Examples 

293 -------- 

294 >>> from colour.characterisation import MSDS_DISPLAY_PRIMARIES 

295 >>> from colour.colorimetry import MSDS_CMFS_LMS 

296 >>> cmfs = MSDS_CMFS_LMS["Stockman & Sharpe 2 Degree Cone Fundamentals"] 

297 >>> d_LMS = np.array([15, 0, 0]) 

298 >>> primaries = MSDS_DISPLAY_PRIMARIES["Apple Studio Display"] 

299 >>> matrix_anomalous_trichromacy_Machado2009(cmfs, primaries, d_LMS) 

300 ... # doctest: +ELLIPSIS 

301 array([[-0.2777465..., 2.6515008..., -1.3737543...], 

302 [ 0.2718936..., 0.2004786..., 0.5276276...], 

303 [ 0.0064404..., 0.2592157..., 0.7343437...]]) 

304 """ 

305 

306 if cmfs.shape.interval != 1: 

307 cmfs = reshape_msds( 

308 cmfs, 

309 SpectralShape(cmfs.shape.start, cmfs.shape.end, 1), 

310 "Interpolate", 

311 copy=False, 

312 ) 

313 

314 M_n = matrix_RGB_to_WSYBRG(cmfs, primaries) 

315 cmfs_a = msds_cmfs_anomalous_trichromacy_Machado2009(cmfs, d_LMS) 

316 M_a = matrix_RGB_to_WSYBRG(cmfs_a, primaries) 

317 

318 return np.matmul(np.linalg.inv(M_n), M_a) 

319 

320 

321def matrix_cvd_Machado2009( 

322 deficiency: Literal["Deuteranomaly", "Protanomaly", "Tritanomaly"] | str, 

323 severity: float, 

324) -> NDArrayFloat: 

325 """ 

326 Compute the *Machado et al. (2009)* colour vision deficiency matrix for 

327 the specified deficiency and severity using pre-computed matrices. 

328 

329 Parameters 

330 ---------- 

331 deficiency 

332 Colour vision deficiency type: 

333 

334 - *Protanomaly*: Defective long-wavelength cones (L-cones) with 

335 reduced sensitivity. Complete absence of L-cones is 

336 *Protanopia* or *red-dichromacy*. 

337 - *Deuteranomaly*: Defective medium-wavelength cones (M-cones) 

338 with peak sensitivity shifted towards red-sensitive cones. 

339 Complete absence of M-cones is *Deuteranopia*. 

340 - *Tritanomaly*: Defective short-wavelength cones (S-cones), 

341 representing an alleviated form of blue-yellow colour 

342 blindness. Complete absence of S-cones is *Tritanopia*. 

343 severity 

344 Severity of the colour vision deficiency in domain [0, 1]. 

345 

346 Returns 

347 ------- 

348 :class:`numpy.ndarray` 

349 Colour vision deficiency matrix. 

350 

351 References 

352 ---------- 

353 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`, 

354 :cite:`Machado2009` 

355 

356 Examples 

357 -------- 

358 >>> matrix_cvd_Machado2009("Protanomaly", 0.15) # doctest: +ELLIPSIS 

359 array([[ 0.7869875..., 0.2694875..., -0.0564735...], 

360 [ 0.0431695..., 0.933774 ..., 0.023058 ...], 

361 [-0.004238 ..., -0.0024515..., 1.0066895...]]) 

362 """ 

363 

364 if deficiency.lower() == "tritanomaly": 

365 usage_warning( 

366 '"Machado et al. (2009)" simulation of tritanomaly is based on ' 

367 "the shift paradigm as an approximation to the actual phenomenon " 

368 "and restrain the model from trying to model tritanopia.\n" 

369 "The pre-generated matrices are using a shift value in domain " 

370 "[5, 59] contrary to the domain [0, 20] used for protanomaly and " 

371 "deuteranomaly simulation." 

372 ) 

373 

374 matrices = CVD_MATRICES_MACHADO2010[deficiency] 

375 samples = np.array(sorted(matrices.keys())) 

376 index = as_int_scalar( 

377 np.clip(np.searchsorted(samples, severity), 0, len(samples) - 1) 

378 ) 

379 

380 a = samples[index] 

381 b = samples[min(index + 1, len(samples) - 1)] 

382 

383 m1, m2 = matrices[a], matrices[b] 

384 

385 if a == b: 

386 # 1.0 severity colour vision deficiency matrix, returning directly. 

387 return m1 

388 

389 return m1 + (severity - a) * ((m2 - m1) / (b - a))