Coverage for colour/blindness/machado2009.py: 100%
76 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-15 19:01 +1300
1"""
2Simulation of CVD - Machado, Oliveira and Fernandes (2009)
3==========================================================
5Define the *Machado et al. (2009)* physiologically-based model for
6simulation of colour vision deficiency.
8- :func:`colour.msds_cmfs_anomalous_trichromacy_Machado2009`
9- :func:`colour.matrix_anomalous_trichromacy_Machado2009`
10- :func:`colour.matrix_cvd_Machado2009`
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"""
29from __future__ import annotations
31import typing
33import numpy as np
35from colour.algebra import vecmul
36from colour.blindness import CVD_MATRICES_MACHADO2010
38if typing.TYPE_CHECKING:
39 from colour.characterisation import RGB_DisplayPrimaries
41from colour.colorimetry import LMS_ConeFundamentals, SpectralShape, reshape_msds
43if typing.TYPE_CHECKING:
44 from colour.hints import ArrayLike, Literal, NDArrayFloat
46from colour.utilities import (
47 as_float_array,
48 as_int_scalar,
49 tsplit,
50 tstack,
51 usage_warning,
52)
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"
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]
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"""
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.
89 Parameters
90 ----------
91 cmfs
92 *LMS* cone fundamentals colour matching functions.
93 primaries
94 *RGB* display primaries tri-spectral distributions.
96 Returns
97 -------
98 :class:`numpy.ndarray`
99 Matrix transforming from *RGB* colourspace to opponent-colour
100 space.
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 """
115 wavelengths = cmfs.wavelengths
116 WSYBRG = vecmul(MATRIX_LMS_TO_WSYBRG, cmfs.values)
117 WS, YB, RG = tsplit(WSYBRG)
119 primaries = reshape_msds(
120 primaries,
121 cmfs.shape,
122 copy=False,
123 extrapolator_kwargs={"method": "Constant", "left": 0, "right": 0},
124 )
126 R, G, B = tsplit(primaries.values)
128 WS_R = np.trapezoid(R * WS, wavelengths)
129 WS_G = np.trapezoid(G * WS, wavelengths)
130 WS_B = np.trapezoid(B * WS, wavelengths)
132 YB_R = np.trapezoid(R * YB, wavelengths)
133 YB_G = np.trapezoid(G * YB, wavelengths)
134 YB_B = np.trapezoid(B * YB, wavelengths)
136 RG_R = np.trapezoid(R * RG, wavelengths)
137 RG_G = np.trapezoid(G * RG, wavelengths)
138 RG_B = np.trapezoid(B * RG, wavelengths)
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 )
148 M_G /= np.sum(M_G, axis=-1)[:, None]
150 return M_G
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.
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.
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].
176 Returns
177 -------
178 :class:`colour.LMS_ConeFundamentals`
179 Anomalous trichromacy *LMS* cone fundamentals colour matching
180 functions.
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.
191 References
192 ----------
193 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`,
194 :cite:`Machado2009`
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 """
208 cmfs = cmfs.copy()
210 if cmfs.shape.interval != 1:
211 cmfs.interpolate(SpectralShape(cmfs.shape.start, cmfs.shape.end, 1))
213 cmfs.extrapolator_kwargs = {"method": "Constant", "left": 0, "right": 0}
215 L, M, _S = tsplit(cmfs.values)
216 d_L, d_M, d_S = tsplit(d_LMS)
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 )
228 area_L = np.trapezoid(L, cmfs.wavelengths)
229 area_M = np.trapezoid(M, cmfs.wavelengths)
231 def alpha(x: NDArrayFloat) -> NDArrayFloat:
232 """Compute :math:`alpha` factor."""
234 return (20 - x) / 20
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]
243 LMS_a = tstack([L_a, M_a, S_a])
244 cmfs[cmfs.wavelengths] = LMS_a
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)
251 return cmfs
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.
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.
275 Returns
276 -------
277 :class:`numpy.ndarray`
278 Anomalous trichromacy transformation matrix.
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].
287 References
288 ----------
289 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`,
290 :cite:`Machado2009`
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 """
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 )
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)
318 return np.matmul(np.linalg.inv(M_n), M_a)
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.
329 Parameters
330 ----------
331 deficiency
332 Colour vision deficiency type:
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].
346 Returns
347 -------
348 :class:`numpy.ndarray`
349 Colour vision deficiency matrix.
351 References
352 ----------
353 :cite:`Colblindorb`, :cite:`Colblindora`, :cite:`Colblindorc`,
354 :cite:`Machado2009`
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 """
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 )
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 )
380 a = samples[index]
381 b = samples[min(index + 1, len(samples) - 1)]
383 m1, m2 = matrices[a], matrices[b]
385 if a == b:
386 # 1.0 severity colour vision deficiency matrix, returning directly.
387 return m1
389 return m1 + (severity - a) * ((m2 - m1) / (b - a))