Coverage for contrast/barten1999.py: 65%

1"""

2 Barten (1999) Contrast Sensitivity Function

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

4

5 Define the *Barten (1999)* contrast sensitivity function model for predicting

6 human visual contrast perception thresholds.

7

8- :func:`colour.contrast.contrast_sensitivity_function_Barten1999`

9

10 References

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"""

29

30 from __future__ import annotations

31

32 import typing

33

34 import numpy as np

35

36 if typing.TYPE_CHECKING:

37 from colour.hints import ArrayLike, NDArrayFloat

38

39 from colour.utilities import as_float, as_float_array

40

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"

47

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]

58

59

60 def 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.

64

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.

73

74 Returns

75 -------

76 :class:`numpy.ndarray`

77 Optical modulation transfer function (MTF) :math:`M_{opt}` of the eye.

78

79 References

80 ----------

81 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

82 :cite:`InternationalTelecommunicationUnion2015`,

83

84 Examples

85 --------

86 >>> optical_MTF_Barten1999(4, 0.01) # doctest: +ELLIPSIS

87 0.9689107...

88 """

89

90 u = as_float_array(u)

91 sigma = as_float_array(sigma)

92

93 return as_float(np.exp(-2 * np.pi**2 * sigma**2 * u**2))

94

95

96 def 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.

104

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.

113

114 Returns

115 -------

116 :class:`numpy.ndarray`

117 Pupil diameter.

118

119 References

120 ----------

121 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

122 :cite:`InternationalTelecommunicationUnion2015`, :cite:`Watson2012`

123

124 Notes

125 -----

126 - The *Log* function is using base 10 as indicated by :cite:`Watson2012`.

127

128 Examples

129 --------

130 >>> pupil_diameter_Barten1999(100, 60, 60) # doctest: +ELLIPSIS

131 2.7931307...

132 """

133

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)

137

138 return as_float(5 - 3 * np.tanh(0.4 * np.log10(L * X_0 * Y_0 / 40**2)))

139

140

141 def 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.

150

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.

156

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.

166

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.

173

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.

181

182 References

183 ----------

184 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

185 :cite:`InternationalTelecommunicationUnion2015`,

186

187 Examples

188 --------

189 >>> sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1) # doctest: +ELLIPSIS

190 0.0087911...

191 """

192

193 sigma_0 = as_float_array(sigma_0)

194 C_ab = as_float_array(C_ab)

195 d = as_float_array(d)

196

197 return as_float(np.hypot(sigma_0, C_ab * d))

198

199

200 def 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.

209

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.

218

219 Returns

220 -------

221 :class:`numpy.ndarray`

222 Retinal illuminance :math:`E` in Trolands.

223

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.

231

232 References

233 ----------

234 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

235 :cite:`InternationalTelecommunicationUnion2015`,

236

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 """

244

245 d = as_float_array(d)

246 L = as_float_array(L)

247

248 E = (np.pi * d**2) / 4 * L

249

250 if apply_stiles_crawford_effect_correction:

251 E *= 1 - (d / 9.7) ** 2 + (d / 12.4) ** 4

252

253 return as_float(E)

254

255

256 def 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.

265

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.

278

279 Returns

280 -------

281 :class:`numpy.ndarray`

282 Maximum angular size :math:`X` of the object considered.

283

284 References

285 ----------

286 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

287 :cite:`InternationalTelecommunicationUnion2015`,

288

289 Examples

290 --------

291 >>> maximum_angular_size_Barten1999(4) # doctest: +ELLIPSIS

292 3.5729480...

293 """

294

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)

299

300 return as_float((1 / X_0**2 + 1 / X_max**2 + u**2 / N_max**2) ** -0.5)

301

302

303 SIGMA_DEFAULT: NDArrayFloat = sigma_Barten1999(0.5 / 60, 0.08 / 60, 2.1)

304"""

305 Default standard deviation :math:`\\sigma`.

306"""

307

308 E_DEFAULT: NDArrayFloat = retinal_illuminance_Barten1999(20, 2.1)

309"""

310 Default retinal illuminance :math:`E` in Trolands.

311"""

312

313

314 def 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)*.

333

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.

342

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.

384

385 Returns

386 -------

387 :class:`numpy.ndarray`

388 Contrast sensitivity :math:`S`.

389

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.

398

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`.

416

417 References

418 ----------

419 :cite:`Barten1999`, :cite:`Barten2003`, :cite:`Cowan2004`,

420 :cite:`InternationalTelecommunicationUnion2015`,

421

422 Examples

423 --------

424 >>> contrast_sensitivity_function_Barten1999(4) # doctest: +ELLIPSIS

425 360.8691122...

426

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::

432

433 :math:`MDC = 1 / CSF * 2 * (1 / 1.27)`

434

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.

438

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

492 0.0049246...,

493 0.0038228..., 0.0031188..., 0.0026627..., 0.0023674..., \

494 0.0021814...])

495 """

496

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)

510

511 M_opt = optical_MTF_Barten1999(u, sigma)

512

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 )

517

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 )

521

522 return as_float(S)