Coverage for colour/contrast/barten1999.py: 100%
60 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"""
2Barten (1999) Contrast Sensitivity Function
3===========================================
5Define the *Barten (1999)* contrast sensitivity function model for predicting
6human visual contrast perception thresholds.
8- :func:`colour.contrast.contrast_sensitivity_function_Barten1999`
10References
11----------
12- :cite:`Barten1999` : Barten, P. G. (1999). Contrast Sensitivity of the
13 Human Eye and Its Effects on Image Quality. SPIE. doi:10.1117/3.353254
14- :cite:`Barten2003` : Barten, P. G. J. (2003). Formula for the contrast
15 sensitivity of the human eye. In Y. Miyake & D. R. Rasmussen (Eds.),
16 Proceedings of SPIE (Vol. 5294, pp. 231-238). doi:10.1117/12.537476
17- :cite:`Cowan2004` : Cowan, M., Kennel, G., Maier, T., & Walker, B. (2004).
18 Contrast Sensitivity Experiment to Determine the Bit Depth for Digital
19 Cinema. SMPTE Motion Imaging Journal, 113(9), 281-292. doi:10.5594/j11549
20- :cite:`InternationalTelecommunicationUnion2015` : International
21 Telecommunication Union. (2015). Report ITU-R BT.2246-4 - The present
22 state of ultra-high definition television BT Series Broadcasting service
23 (Vol. 5, pp. 1-92).
24 https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2246-4-2015-PDF-E.pdf
25- :cite:`Watson2012` : Watson, A. B., & Yellott, J. I. (2012). A unified
26 formula for light-adapted pupil size. Journal of Vision, 12(10), 12.
27 doi:10.1167/12.10.12
28"""
30from __future__ import annotations
32import typing
34import numpy as np
36if typing.TYPE_CHECKING:
37 from colour.hints import ArrayLike, NDArrayFloat
39from colour.utilities import as_float, as_float_array
41__author__ = "Colour Developers"
42__copyright__ = "Copyright 2013 Colour Developers"
43__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
44__maintainer__ = "Colour Developers"
45__email__ = "colour-developers@colour-science.org"
46__status__ = "Production"
48__all__ = [
49 "optical_MTF_Barten1999",
50 "pupil_diameter_Barten1999",
51 "sigma_Barten1999",
52 "retinal_illuminance_Barten1999",
53 "maximum_angular_size_Barten1999",
54 "SIGMA_DEFAULT",
55 "E_DEFAULT",
56 "contrast_sensitivity_function_Barten1999",
57]
60def optical_MTF_Barten1999(u: ArrayLike, sigma: ArrayLike = 0.01) -> NDArrayFloat:
61 """
62 Compute the optical modulation transfer function (MTF) :math:`M_{opt}` of
63 the eye using *Barten (1999)* method.
65 Parameters
66 ----------
67 u
68 Spatial frequency :math:`u`, the cycles per degree.
69 sigma
70 Standard deviation :math:`\\sigma` of the line-spread function
71 resulting from the convolution of the different elements of the
72 convolution process.
74 Returns
75 -------
76 :class:`numpy.ndarray`
77 Optical modulation transfer function (MTF) :math:`M_{opt}` of the eye.
79 References
80 ----------
81 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
82 :cite:`InternationalTelecommunicationUnion2015`,
84 Examples
85 --------
86 >>> optical_MTF_Barten1999(4, 0.01) # doctest: +ELLIPSIS
87 0.9689107...
88 """
90 u = as_float_array(u)
91 sigma = as_float_array(sigma)
93 return as_float(np.exp(-2 * np.pi**2 * sigma**2 * u**2))
96def pupil_diameter_Barten1999(
97 L: ArrayLike,
98 X_0: ArrayLike = 60,
99 Y_0: ArrayLike | None = None,
100) -> NDArrayFloat:
101 """
102 Compute the pupil diameter for the specified luminance and object or
103 stimulus angular size using the *Barten (1999)* method.
105 Parameters
106 ----------
107 L
108 Average luminance :math:`L` in :math:`cd/m^2`.
109 X_0
110 Angular size of the object :math:`X_0` in degrees in the x direction.
111 Y_0
112 Angular size of the object :math:`Y_0` in degrees in the y direction.
114 Returns
115 -------
116 :class:`numpy.ndarray`
117 Pupil diameter.
119 References
120 ----------
121 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
122 :cite:`InternationalTelecommunicationUnion2015`, :cite:`Watson2012`
124 Notes
125 -----
126 - The *Log* function is using base 10 as indicated by :cite:`Watson2012`.
128 Examples
129 --------
130 >>> pupil_diameter_Barten1999(100, 60, 60) # doctest: +ELLIPSIS
131 2.7931307...
132 """
134 L = as_float_array(L)
135 X_0 = as_float_array(X_0)
136 Y_0 = X_0 if Y_0 is None else as_float_array(Y_0)
138 return as_float(5 - 3 * np.tanh(0.4 * np.log10(L * X_0 * Y_0 / 40**2)))
141def sigma_Barten1999(
142 sigma_0: ArrayLike = 0.5 / 60,
143 C_ab: ArrayLike = 0.08 / 60,
144 d: ArrayLike = 2.1,
145) -> NDArrayFloat:
146 """
147 Compute the standard deviation :math:`\\sigma` of the line-spread
148 function resulting from the convolution of the different elements of
149 the convolution process using *Barten (1999)* method.
151 The :math:`\\sigma` quantity depends on the pupil diameter :math:`d` of
152 the eye lens. For very small pupil diameters, :math:`\\sigma` increases
153 inversely proportionally with pupil size because of diffraction, and
154 for large pupil diameters, :math:`\\sigma` increases about linearly
155 with pupil size because of chromatic aberration and other aberrations.
157 Parameters
158 ----------
159 sigma_0
160 Constant :math:`\\sigma_{0}` in degrees.
161 C_ab
162 Spherical aberration of the eye :math:`C_{ab}` in
163 :math:`degrees\\div mm`.
164 d
165 Pupil diameter :math:`d` in millimeters.
167 Returns
168 -------
169 :class:`numpy.ndarray`
170 Standard deviation :math:`\\sigma` of the line-spread function
171 resulting from the convolution of the different elements of the
172 convolution process.
174 Warnings
175 --------
176 This definition expects :math:`\\sigma_{0}` and :math:`C_{ab}` to be
177 specified in degrees and :math:`degrees\\div mm` respectively. However,
178 in the literature, the values for :math:`\\sigma_{0}` and :math:`C_{ab}`
179 are usually specified in :math:`arc min` and :math:`arc min\\div mm`
180 respectively, thus they need to be divided by 60.
182 References
183 ----------
184 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
185 :cite:`InternationalTelecommunicationUnion2015`,
187 Examples
188 --------
189 >>> sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1) # doctest: +ELLIPSIS
190 0.0087911...
191 """
193 sigma_0 = as_float_array(sigma_0)
194 C_ab = as_float_array(C_ab)
195 d = as_float_array(d)
197 return as_float(np.hypot(sigma_0, C_ab * d))
200def retinal_illuminance_Barten1999(
201 L: ArrayLike,
202 d: ArrayLike = 2.1,
203 apply_stiles_crawford_effect_correction: bool = True,
204) -> NDArrayFloat:
205 """
206 Compute the retinal illuminance :math:`E` in Trolands for the specified
207 average luminance :math:`L` and pupil diameter :math:`d` using the
208 *Barten (1999)* method.
210 Parameters
211 ----------
212 L
213 Average luminance :math:`L` in :math:`cd/m^2`.
214 d
215 Pupil diameter :math:`d` in millimeters.
216 apply_stiles_crawford_effect_correction
217 Whether to apply the correction for the *Stiles-Crawford* effect.
219 Returns
220 -------
221 :class:`numpy.ndarray`
222 Retinal illuminance :math:`E` in Trolands.
224 Notes
225 -----
226 - This definition is designed for photopic viewing conditions and
227 applies the *Stiles-Crawford* effect correction by default. This
228 effect accounts for the directional sensitivity of cone cells,
229 which exhibit reduced response to light entering from the edge
230 of the pupil.
232 References
233 ----------
234 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
235 :cite:`InternationalTelecommunicationUnion2015`,
237 Examples
238 --------
239 >>> retinal_illuminance_Barten1999(100, 2.1) # doctest: +ELLIPSIS
240 330.4115803...
241 >>> retinal_illuminance_Barten1999(100, 2.1, False) # doctest: +ELLIPSIS
242 346.3605900...
243 """
245 d = as_float_array(d)
246 L = as_float_array(L)
248 E = (np.pi * d**2) / 4 * L
250 if apply_stiles_crawford_effect_correction:
251 E *= 1 - (d / 9.7) ** 2 + (d / 12.4) ** 4
253 return as_float(E)
256def maximum_angular_size_Barten1999(
257 u: ArrayLike,
258 X_0: ArrayLike = 60,
259 X_max: ArrayLike = 12,
260 N_max: ArrayLike = 15,
261) -> NDArrayFloat:
262 """
263 Compute the maximum angular size :math:`X` of the object considered using
264 *Barten (1999)* method.
266 Parameters
267 ----------
268 u
269 Spatial frequency :math:`u`, the cycles per degree.
270 X_0
271 Angular size :math:`X_0` in degrees of the object in the x direction.
272 X_max
273 Maximum angular size :math:`X_{max}` in degrees of the integration
274 area in the x direction.
275 N_max
276 Maximum number of cycles :math:`N_{max}` over which the eye can
277 integrate the information.
279 Returns
280 -------
281 :class:`numpy.ndarray`
282 Maximum angular size :math:`X` of the object considered.
284 References
285 ----------
286 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
287 :cite:`InternationalTelecommunicationUnion2015`,
289 Examples
290 --------
291 >>> maximum_angular_size_Barten1999(4) # doctest: +ELLIPSIS
292 3.5729480...
293 """
295 u = as_float_array(u)
296 X_0 = as_float_array(X_0)
297 X_max = as_float_array(X_max)
298 N_max = as_float_array(N_max)
300 return as_float((1 / X_0**2 + 1 / X_max**2 + u**2 / N_max**2) ** -0.5)
303SIGMA_DEFAULT: NDArrayFloat = sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1)
304"""
305Default standard deviation :math:`\\sigma`.
306"""
308E_DEFAULT: NDArrayFloat = retinal_illuminance_Barten1999(20, 2.1)
309"""
310Default retinal illuminance :math:`E` in Trolands.
311"""
314def contrast_sensitivity_function_Barten1999(
315 u: ArrayLike,
316 sigma: ArrayLike = SIGMA_DEFAULT,
317 k: ArrayLike = 3.0,
318 T: ArrayLike = 0.1,
319 X_0: ArrayLike = 60,
320 Y_0: ArrayLike | None = None,
321 X_max: ArrayLike = 12,
322 Y_max: ArrayLike | None = None,
323 N_max: ArrayLike = 15,
324 n: ArrayLike = 0.03,
325 p: ArrayLike = 1.2274 * 10**6,
326 E: ArrayLike = E_DEFAULT,
327 phi_0: ArrayLike = 3 * 10**-8,
328 u_0: ArrayLike = 7,
329) -> NDArrayFloat:
330 """
331 Compute the contrast sensitivity :math:`S` of the human eye according
332 to the contrast sensitivity function (CSF) described by *Barten (1999)*.
334 Contrast sensitivity is defined as the inverse of the modulation
335 threshold of a sinusoidal luminance pattern. The modulation threshold
336 of this pattern is generally defined by 50% probability of detection.
337 The contrast sensitivity function or CSF gives the contrast sensitivity
338 as a function of spatial frequency. In the CSF, the spatial frequency
339 is expressed in angular units with respect to the eye. It reaches a
340 maximum between 1 and 10 cycles per degree with a fall-off at higher
341 and lower spatial frequencies.
343 Parameters
344 ----------
345 u
346 Spatial frequency :math:`u`, the cycles per degree.
347 sigma
348 Standard deviation :math:`\\sigma` of the line-spread function
349 resulting from the convolution of the different elements of the
350 convolution process.
351 k
352 Signal-to-noise (SNR) ratio :math:`k`.
353 T
354 Integration time :math:`T` in seconds of the eye.
355 X_0
356 Angular size :math:`X_0` in degrees of the object in the x
357 direction.
358 Y_0
359 Angular size :math:`Y_0` in degrees of the object in the y
360 direction.
361 X_max
362 Maximum angular size :math:`X_{max}` in degrees of the integration
363 area in the x direction.
364 Y_max
365 Maximum angular size :math:`Y_{max}` in degrees of the integration
366 area in the y direction.
367 N_max
368 Maximum number of cycles :math:`N_{max}` over which the eye can
369 integrate the information.
370 n
371 Quantum efficiency of the eye :math:`n`.
372 p
373 Photon conversion factor :math:`p` in
374 :math:`photons\\div seconds\\div degrees^2\\div Trolands` that
375 depends on the light source.
376 E
377 Retinal illuminance :math:`E` in Trolands.
378 phi_0
379 Spectral density :math:`\\phi_0` in :math:`seconds degrees^2` of
380 the neural noise.
381 u_0
382 Spatial frequency :math:`u_0` in :math:`cycles\\div degrees` above
383 which the lateral inhibition ceases.
385 Returns
386 -------
387 :class:`numpy.ndarray`
388 Contrast sensitivity :math:`S`.
390 Warnings
391 --------
392 This definition expects :math:`\\sigma_{0}` and :math:`C_{ab}` used in
393 the computation of :math:`\\sigma` to be specified in degrees and
394 :math:`degrees\\div mm` respectively. However, in the literature, the
395 values for :math:`\\sigma_{0}` and :math:`C_{ab}` are usually specified
396 in :math:`arc min` and :math:`arc min\\div mm` respectively, thus they
397 need to be divided by 60.
399 Notes
400 -----
401 - The formula holds for bilateral viewing and for equal dimensions of
402 the object in x and y direction. For monocular vision, the contrast
403 sensitivity is a factor :math:`\\sqrt{2}` smaller.
404 - *Barten (1999)* CSF default values for the :math:`k`,
405 :math:`\\sigma_{0}`, :math:`C_{ab}`, :math:`T`, :math:`X_{max}`,
406 :math:`N_{max}`, :math:`n`, :math:`\\phi_{0}` and :math:`u_0`
407 constants are valid for a standard observer with good vision and
408 with an age between 20 and 30 years.
409 - The other constants have been filled using reference data from
410 *Figure 31* in :cite:`InternationalTelecommunicationUnion2015` but
411 must be adapted to the current use case.
412 - The product of :math:`u`, the cycles per degree, and :math:`X_0`,
413 the number of degrees, gives the number of cycles :math:`P_c` in a
414 pattern. Therefore, :math:`X_0` can be made a variable dependent on
415 :math:`u` such as :math:`X_0 = P_c / u`.
417 References
418 ----------
419 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,
420 :cite:`InternationalTelecommunicationUnion2015`,
422 Examples
423 --------
424 >>> contrast_sensitivity_function_Barten1999(4) # doctest: +ELLIPSIS
425 360.8691122...
427 Reproducing *Figure 31* in \
428:cite:`InternationalTelecommunicationUnion2015` illustrating the minimum
429 detectable contrast according to *Barten (1999)* model with the assumed
430 conditions for UHDTV applications. The minimum detectable contrast
431 :math:`MDC` is then defined as follows::
433 :math:`MDC = 1 / CSF * 2 * (1 / 1.27)`
435 where :math:`2` is used for the conversion from modulation to contrast and
436 :math:`1 / 1.27` is used for the conversion from sinusoidal to rectangular
437 waves.
439 >>> from scipy.optimize import fmin
440 >>> settings_BT2246 = {
441 ... "k": 3.0,
442 ... "T": 0.1,
443 ... "X_max": 12,
444 ... "N_max": 15,
445 ... "n": 0.03,
446 ... "p": 1.2274 * 10**6,
447 ... "phi_0": 3 * 10**-8,
448 ... "u_0": 7,
449 ... }
450 >>>
451 >>> def maximise_spatial_frequency(L):
452 ... maximised_spatial_frequency = []
453 ... for L_v in L:
454 ... X_0 = 60
455 ... d = pupil_diameter_Barten1999(L_v, X_0)
456 ... sigma = sigma_Barten1999(0.5 / 60, 0.08 / 60, d)
457 ... E = retinal_illuminance_Barten1999(L_v, d, True)
458 ... maximised_spatial_frequency.append(
459 ... fmin(
460 ... lambda x: (
461 ... -contrast_sensitivity_function_Barten1999(
462 ... u=x,
463 ... sigma=sigma,
464 ... X_0=X_0,
465 ... E=E,
466 ... **settings_BT2246
467 ... )
468 ... ),
469 ... 0,
470 ... disp=False,
471 ... )[0]
472 ... )
473 ... return as_float(np.array(maximised_spatial_frequency))
474 ...
475 >>>
476 >>> L = np.logspace(np.log10(0.01), np.log10(100), 10)
477 >>> X_0 = Y_0 = 60
478 >>> d = pupil_diameter_Barten1999(L, X_0, Y_0)
479 >>> sigma = sigma_Barten1999(0.5 / 60, 0.08 / 60, d)
480 >>> E = retinal_illuminance_Barten1999(L, d)
481 >>> u = maximise_spatial_frequency(L)
482 >>> (
483 ... 1
484 ... / contrast_sensitivity_function_Barten1999(
485 ... u=u, sigma=sigma, E=E, X_0=X_0, Y_0=Y_0, **settings_BT2246
486 ... )
487 ... * 2
488 ... * (1 / 1.27)
489 ... )
490 ... # doctest: +ELLIPSIS
491 array([ 0.0218764..., 0.0141848..., 0.0095244..., 0.0066805..., \
4920.0049246...,
493 0.0038228..., 0.0031188..., 0.0026627..., 0.0023674..., \
4940.0021814...])
495 """
497 u = as_float_array(u)
498 k = as_float_array(k)
499 T = as_float_array(T)
500 X_0 = as_float_array(X_0)
501 Y_0 = X_0 if Y_0 is None else as_float_array(Y_0)
502 X_max = as_float_array(X_max)
503 Y_max = X_max if Y_max is None else as_float_array(Y_max)
504 N_max = as_float_array(N_max)
505 n = as_float_array(n)
506 p = as_float_array(p)
507 E = as_float_array(E)
508 phi_0 = as_float_array(phi_0)
509 u_0 = as_float_array(u_0)
511 M_opt = optical_MTF_Barten1999(u, sigma)
513 M_as = 1 / (
514 maximum_angular_size_Barten1999(u, X_0, X_max, N_max)
515 * maximum_angular_size_Barten1999(u, Y_0, Y_max, N_max)
516 )
518 S = (M_opt / k) / np.sqrt(
519 2 / T * M_as * (1 / (n * p * E) + phi_0 / (1 - np.exp(-((u / u_0) ** 2))))
520 )
522 return as_float(S)