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

1""" 

2Barten (1999) Contrast Sensitivity Function 

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

4 

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

6human visual contrast perception thresholds. 

7 

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

9 

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

29 

30from __future__ import annotations 

31 

32import typing 

33 

34import numpy as np 

35 

36if typing.TYPE_CHECKING: 

37 from colour.hints import ArrayLike, NDArrayFloat 

38 

39from 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 

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. 

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 

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. 

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 

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. 

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 

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. 

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 

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. 

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 

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

304""" 

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

306""" 

307 

308E_DEFAULT: NDArrayFloat = retinal_illuminance_Barten1999(20, 2.1) 

309""" 

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

311""" 

312 

313 

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

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

4920.0049246..., 

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

4940.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)