Source code for colour.contrast.barten1999

"""
Barten (1999) Contrast Sensitivity Function
===========================================

Defines the *Barten (1999)* contrast sensitivity function:

-   :func:`colour.contrast.contrast_sensitivity_function_Barten1999`

References
----------
-   :cite:`Barten1999` : Barten, P. G. (1999). Contrast Sensitivity of the
    Human Eye and Its Effects on Image Quality. SPIE. doi:10.1117/3.353254
-   :cite:`Barten2003` : Barten, P. G. J. (2003). Formula for the contrast
    sensitivity of the human eye. In Y. Miyake & D. R. Rasmussen (Eds.),
    Proceedings of SPIE (Vol. 5294, pp. 231-238). doi:10.1117/12.537476
-   :cite:`Cowan2004` : Cowan, M., Kennel, G., Maier, T., & Walker, B. (2004).
    Contrast Sensitivity Experiment to Determine the Bit Depth for Digital
    Cinema. SMPTE Motion Imaging Journal, 113(9), 281-292. doi:10.5594/j11549
-   :cite:`InternationalTelecommunicationUnion2015` : International
    Telecommunication Union. (2015). Report ITU-R BT.2246-4 - The present
    state of ultra-high definition television BT Series Broadcasting service
    (Vol. 5, pp. 1-92).
    https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2246-4-2015-PDF-E.pdf
-   :cite:`Watson2012` : Watson, A. B., & Yellott, J. I. (2012). A unified
    formula for light-adapted pupil size. Journal of Vision, 12(10), 12.
    doi:10.1167/12.10.12
"""

from __future__ import annotations

import numpy as np

from colour.hints import ArrayLike, NDArrayFloat
from colour.utilities import as_float, as_float_array

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "optical_MTF_Barten1999",
    "pupil_diameter_Barten1999",
    "sigma_Barten1999",
    "retinal_illuminance_Barten1999",
    "maximum_angular_size_Barten1999",
    "contrast_sensitivity_function_Barten1999",
]


[docs] def optical_MTF_Barten1999( u: ArrayLike, sigma: ArrayLike = 0.01 ) -> NDArrayFloat: """ Return the optical modulation transfer function (MTF) :math:`M_{opt}` of the eye using *Barten (1999)* method. Parameters ---------- u Spatial frequency :math:`u`, the cycles per degree. sigma Standard deviation :math:`\\sigma` of the line-spread function resulting from the convolution of the different elements of the convolution process. Returns ------- :class:`numpy.ndarray` Optical modulation transfer function (MTF) :math:`M_{opt}` of the eye. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, Examples -------- >>> optical_MTF_Barten1999(4, 0.01) # doctest: +ELLIPSIS 0.9689107... """ u = as_float_array(u) sigma = as_float_array(sigma) return as_float(np.exp(-2 * np.pi**2 * sigma**2 * u**2))
[docs] def pupil_diameter_Barten1999( L: ArrayLike, X_0: ArrayLike = 60, Y_0: ArrayLike | None = None, ) -> NDArrayFloat: """ Return the pupil diameter for given luminance and object or stimulus angular size using *Barten (1999)* method. Parameters ---------- L Average luminance :math:`L` in :math:`cd/m^2`. X_0 Angular size of the object :math:`X_0` in degrees in the x direction. Y_0 Angular size of the object :math:`X_0` in degrees in the y direction. Returns ------- :class:`numpy.ndarray` Pupil diameter. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, :cite:`Watson2012` Notes ----- - The *Log* function is using base 10 as indicated by :cite:`Watson2012`. Examples -------- >>> pupil_diameter_Barten1999(100, 60, 60) # doctest: +ELLIPSIS 2.7931307... """ L = as_float_array(L) X_0 = as_float_array(X_0) Y_0 = X_0 if Y_0 is None else as_float_array(Y_0) return as_float(5 - 3 * np.tanh(0.4 * np.log10(L * X_0 * Y_0 / 40**2)))
[docs] def sigma_Barten1999( sigma_0: ArrayLike = 0.5 / 60, C_ab: ArrayLike = 0.08 / 60, d: ArrayLike = 2.1, ) -> NDArrayFloat: """ Return the standard deviation :math:`\\sigma` of the line-spread function resulting from the convolution of the different elements of the convolution process using *Barten (1999)* method. The :math:`\\sigma` quantity depends on the pupil diameter :math:`d` of the eye lens. For very small pupil diameters, :math:`\\sigma` increases inversely proportionally with pupil size because of diffraction, and for large pupil diameters, :math:`\\sigma` increases about linearly with pupil size because of chromatic aberration and others aberrations. Parameters ---------- sigma_0 Constant :math:`\\sigma_{0}` in degrees. C_ab Spherical aberration of the eye :math:`C_{ab}` in :math:`degrees\\div mm`. d Pupil diameter :math:`d` in millimeters. Returns ------- :class:`numpy.ndarray` Standard deviation :math:`\\sigma` of the line-spread function resulting from the convolution of the different elements of the convolution process. Warnings -------- This definition expects :math:`\\sigma_{0}` and :math:`C_{ab}` to be given in degrees and :math:`degrees\\div mm` respectively. However, in the literature, the values for :math:`\\sigma_{0}` and :math:`C_{ab}` are usually given in :math:`arc min` and :math:`arc min\\div mm` respectively, thus they need to be divided by 60. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, Examples -------- >>> sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1) # doctest: +ELLIPSIS 0.0087911... """ sigma_0 = as_float_array(sigma_0) C_ab = as_float_array(C_ab) d = as_float_array(d) return as_float(np.hypot(sigma_0, C_ab * d))
[docs] def retinal_illuminance_Barten1999( L: ArrayLike, d: ArrayLike = 2.1, apply_stiles_crawford_effect_correction: bool = True, ) -> NDArrayFloat: """ Return the retinal illuminance :math:`E` in Trolands for given average luminance :math:`L` and pupil diameter :math:`d` using *Barten (1999)* method. Parameters ---------- L Average luminance :math:`L` in :math:`cd/m^2`. d Pupil diameter :math:`d` in millimeters. apply_stiles_crawford_effect_correction Whether to apply the correction for *Stiles-Crawford* effect. Returns ------- :class:`numpy.ndarray` Retinal illuminance :math:`E` in Trolands. Notes ----- - This definition is for use with photopic viewing conditions and thus corrects for the Stiles-Crawford effect by default, i.e. directional sensitivity of the cone cells with lower response of cone cells receiving light from the edge of the pupil. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, Examples -------- >>> retinal_illuminance_Barten1999(100, 2.1) # doctest: +ELLIPSIS 330.4115803... >>> retinal_illuminance_Barten1999(100, 2.1, False) # doctest: +ELLIPSIS 346.3605900... """ d = as_float_array(d) L = as_float_array(L) E = (np.pi * d**2) / 4 * L if apply_stiles_crawford_effect_correction: E *= 1 - (d / 9.7) ** 2 + (d / 12.4) ** 4 return as_float(E)
[docs] def maximum_angular_size_Barten1999( u: ArrayLike, X_0: ArrayLike = 60, X_max: ArrayLike = 12, N_max: ArrayLike = 15, ) -> NDArrayFloat: """ Return the maximum angular size :math:`X` of the object considered using *Barten (1999)* method. Parameters ---------- u Spatial frequency :math:`u`, the cycles per degree. X_0 Angular size :math:`X_0` in degrees of the object in the x direction. X_max Maximum angular size :math:`X_{max}` in degrees of the integration area in the x direction. N_max Maximum number of cycles :math:`N_{max}` over which the eye can integrate the information. Returns ------- :class:`numpy.ndarray` Maximum angular size :math:`X` of the object considered. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, Examples -------- >>> maximum_angular_size_Barten1999(4) # doctest: +ELLIPSIS 3.5729480... """ u = as_float_array(u) X_0 = as_float_array(X_0) X_max = as_float_array(X_max) N_max = as_float_array(N_max) return as_float( (1 / X_0**2 + 1 / X_max**2 + u**2 / N_max**2) ** -0.5 )
[docs] def contrast_sensitivity_function_Barten1999( u: ArrayLike, sigma: ArrayLike = sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1), k: ArrayLike = 3.0, T: ArrayLike = 0.1, X_0: ArrayLike = 60, Y_0: ArrayLike | None = None, X_max: ArrayLike = 12, Y_max: ArrayLike | None = None, N_max: ArrayLike = 15, n: ArrayLike = 0.03, p: ArrayLike = 1.2274 * 10**6, E: ArrayLike = retinal_illuminance_Barten1999(20, 2.1), phi_0: ArrayLike = 3 * 10**-8, u_0: ArrayLike = 7, ) -> NDArrayFloat: """ Return the contrast sensitivity :math:`S` of the human eye according to the contrast sensitivity function (CSF) described by *Barten (1999)*. Contrast sensitivity is defined as the inverse of the modulation threshold of a sinusoidal luminance pattern. The modulation threshold of this pattern is generally defined by 50% probability of detection. The contrast sensitivity function or CSF gives the contrast sensitivity as a function of spatial frequency. In the CSF, the spatial frequency is expressed in angular units with respect to the eye. It reaches a maximum between 1 and 10 cycles per degree with a fall off at higher and lower spatial frequencies. Parameters ---------- u Spatial frequency :math:`u`, the cycles per degree. sigma Standard deviation :math:`\\sigma` of the line-spread function resulting from the convolution of the different elements of the convolution process. k Signal-to-noise (SNR) ratio :math:`k`. T Integration time :math:`T` in seconds of the eye. X_0 Angular size :math:`X_0` in degrees of the object in the x direction. Y_0 Angular size :math:`Y_0` in degrees of the object in the y direction. X_max Maximum angular size :math:`X_{max}` in degrees of the integration area in the x direction. Y_max Maximum angular size :math:`Y_{max}` in degrees of the integration area in the y direction. N_max Maximum number of cycles :math:`N_{max}` over which the eye can integrate the information. n Quantum efficiency of the eye :math:`n`. p Photon conversion factor :math:`p` in :math:`photons\\div seconds\\div degrees^2\\div Trolands` that depends on the light source. E Retinal illuminance :math:`E` in Trolands. phi_0 Spectral density :math:`\\phi_0` in :math:`seconds degrees^2` of the neural noise. u_0 Spatial frequency :math:`u_0` in :math:`cycles\\div degrees` above which the lateral inhibition ceases. Returns ------- :class:`numpy.ndarray` Contrast sensitivity :math:`S`. Warnings -------- This definition expects :math:`\\sigma_{0}` and :math:`C_{ab}` used in the computation of :math:`\\sigma` to be given in degrees and :math:`degrees\\div mm` respectively. However, in the literature, the values for :math:`\\sigma_{0}` and :math:`C_{ab}` are usually given in :math:`arc min` and :math:`arc min\\div mm` respectively, thus they need to be divided by 60. Notes ----- - The formula holds for bilateral viewing and for equal dimensions of the object in x and y direction. For monocular vision, the contrast sensitivity is a factor :math:`\\sqrt{2}` smaller. - *Barten (1999)* CSF default values for the :math:`k`, :math:`\\sigma_{0}`, :math:`C_{ab}`, :math:`T`, :math:`X_{max}`, :math:`N_{max}`, :math:`n`, :math:`\\phi_{0}` and :math:`u_0` constants are valid for a standard observer with good vision and with an age between 20 and 30 years. - The other constants have been filled using reference data from *Figure 31* in :cite:`InternationalTelecommunicationUnion2015` but must be adapted to the current use case. - The product of :math:`u`, the cycles per degree, and :math:`X_0`, the number of degrees, gives the number of cycles :math:`P_c` in a pattern. Therefore, :math:`X_0` can be made a variable dependent on :math:`u` such as :math:`X_0 = P_c / u`. References ---------- :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`, :cite:`InternationalTelecommunicationUnion2015`, Examples -------- >>> contrast_sensitivity_function_Barten1999(4) # doctest: +ELLIPSIS 360.8691122... Reproducing *Figure 31* in \ :cite:`InternationalTelecommunicationUnion2015` illustrating the minimum detectable contrast according to *Barten (1999)* model with the assumed conditions for UHDTV applications. The minimum detectable contrast :math:`MDC` is then defined as follows:: :math:`MDC = 1 / CSF * 2 * (1 / 1.27)` where :math:`2` is used for the conversion from modulation to contrast and :math:`1 / 1.27` is used for the conversion from sinusoidal to rectangular waves. >>> from scipy.optimize import fmin >>> settings_BT2246 = { ... "k": 3.0, ... "T": 0.1, ... "X_max": 12, ... "N_max": 15, ... "n": 0.03, ... "p": 1.2274 * 10**6, ... "phi_0": 3 * 10**-8, ... "u_0": 7, ... } >>> >>> def maximise_spatial_frequency(L): ... maximised_spatial_frequency = [] ... for L_v in L: ... X_0 = 60 ... d = pupil_diameter_Barten1999(L_v, X_0) ... sigma = sigma_Barten1999(0.5 / 60, 0.08 / 60, d) ... E = retinal_illuminance_Barten1999(L_v, d, True) ... maximised_spatial_frequency.append( ... fmin( ... lambda x: ( ... -contrast_sensitivity_function_Barten1999( ... u=x, ... sigma=sigma, ... X_0=X_0, ... E=E, ... **settings_BT2246 ... ) ... ), ... 0, ... disp=False, ... )[0] ... ) ... return as_float(np.array(maximised_spatial_frequency)) ... >>> >>> L = np.logspace(np.log10(0.01), np.log10(100), 10) >>> X_0 = Y_0 = 60 >>> d = pupil_diameter_Barten1999(L, X_0, Y_0) >>> sigma = sigma_Barten1999(0.5 / 60, 0.08 / 60, d) >>> E = retinal_illuminance_Barten1999(L, d) >>> u = maximise_spatial_frequency(L) >>> ( ... 1 ... / contrast_sensitivity_function_Barten1999( ... u=u, sigma=sigma, E=E, X_0=X_0, Y_0=Y_0, **settings_BT2246 ... ) ... * 2 ... * (1 / 1.27) ... ) ... # doctest: +ELLIPSIS array([ 0.0218764..., 0.0141848..., 0.0095244..., 0.0066805..., \ 0.0049246..., 0.0038228..., 0.0031188..., 0.0026627..., 0.0023674..., \ 0.0021814...]) """ u = as_float_array(u) k = as_float_array(k) T = as_float_array(T) X_0 = as_float_array(X_0) Y_0 = X_0 if Y_0 is None else as_float_array(Y_0) X_max = as_float_array(X_max) Y_max = X_max if Y_max is None else as_float_array(Y_max) N_max = as_float_array(N_max) n = as_float_array(n) p = as_float_array(p) E = as_float_array(E) phi_0 = as_float_array(phi_0) u_0 = as_float_array(u_0) M_opt = optical_MTF_Barten1999(u, sigma) M_as = 1 / ( maximum_angular_size_Barten1999(u, X_0, X_max, N_max) * maximum_angular_size_Barten1999(u, Y_0, Y_max, N_max) ) S = (M_opt / k) / np.sqrt( 2 / T * M_as * (1 / (n * p * E) + phi_0 / (1 - np.exp(-((u / u_0) ** 2)))) ) return as_float(S)