Coverage for colour/temperature/ohno2013.py: 100%

89 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:01 +1300

1""" 

2Ohno (2013) Correlated Colour Temperature 

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

4 

5Define the *Ohno (2013)* correlated colour temperature :math:`T_{cp}` 

6computation objects. 

7 

8- :func:`colour.temperature.uv_to_CCT_Ohno2013`: Compute correlated colour 

9 temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` from specified 

10 *CIE UCS* colourspace *uv* chromaticity coordinates using the 

11 *Ohno (2013)* method. 

12- :func:`colour.temperature.CCT_to_uv_Ohno2013`: Compute *CIE UCS* 

13 colourspace *uv* chromaticity coordinates from specified correlated 

14 colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` using the 

15 *Ohno (2013)* method. 

16 

17References 

18---------- 

19- :cite:`Ohno2014a` : Ohno, Yoshiro. (2014). Practical Use and Calculation of 

20 CCT and Duv. LEUKOS, 10(1), 47-55. doi:10.1080/15502724.2014.839020 

21""" 

22 

23from __future__ import annotations 

24 

25import numpy as np 

26 

27from colour.algebra import euclidean_distance, sdiv, sdiv_mode 

28from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments 

29from colour.hints import ( # noqa: TC001 

30 ArrayLike, 

31 Domain1, 

32 NDArrayFloat, 

33 Range1, 

34) 

35from colour.models import UCS_to_uv, UCS_to_XYZ, XYZ_to_UCS, uv_to_UCS 

36from colour.temperature import CCT_to_uv_Planck1900 

37from colour.utilities import ( 

38 CACHE_REGISTRY, 

39 as_float_array, 

40 attest, 

41 is_caching_enabled, 

42 optional, 

43 runtime_warning, 

44 tsplit, 

45 tstack, 

46) 

47 

48__author__ = "Colour Developers" 

49__copyright__ = "Copyright 2013 Colour Developers" 

50__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

51__maintainer__ = "Colour Developers" 

52__email__ = "colour-developers@colour-science.org" 

53__status__ = "Production" 

54 

55__all__ = [ 

56 "CCT_MINIMAL_OHNO2013", 

57 "CCT_MAXIMAL_OHNO2013", 

58 "CCT_DEFAULT_SPACING_OHNO2013", 

59 "planckian_table", 

60 "uv_to_CCT_Ohno2013", 

61 "CCT_to_uv_Ohno2013", 

62 "XYZ_to_CCT_Ohno2013", 

63 "CCT_to_XYZ_Ohno2013", 

64] 

65 

66CCT_MINIMAL_OHNO2013: float = 1000 

67CCT_MAXIMAL_OHNO2013: float = 100000 

68CCT_DEFAULT_SPACING_OHNO2013: float = 1.001 

69 

70_CACHE_PLANCKIAN_TABLE: dict = CACHE_REGISTRY.register_cache( 

71 f"{__name__}._CACHE_PLANCKIAN_TABLE" 

72) 

73 

74 

75def planckian_table( 

76 cmfs: MultiSpectralDistributions, 

77 start: float, 

78 end: float, 

79 spacing: float, 

80) -> NDArrayFloat: 

81 """ 

82 Generate a planckian table from the specified *CIE UCS* colourspace 

83 *uv* chromaticity coordinates, colour matching functions, and 

84 temperature range using the *Ohno (2013)* method. 

85 

86 Parameters 

87 ---------- 

88 cmfs 

89 Standard observer colour matching functions. 

90 start 

91 Temperature range start in kelvin degrees. 

92 end 

93 Temperature range end in kelvin degrees. 

94 spacing 

95 Spacing between values of the underlying Planckian table expressed 

96 as a multiplier. Default to 1.001. The closer to 1.0, the higher 

97 the precision of the returned colour temperature :math:`T_{cp}` and 

98 :math:`\\Delta_{uv}`. A value of 1.01 provides a good balance 

99 between performance and accuracy. The ``spacing`` value must be 

100 greater than 1. 

101 

102 Returns 

103 ------- 

104 :class:`list` 

105 Planckian table. 

106 

107 Examples 

108 -------- 

109 >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT 

110 >>> cmfs = ( 

111 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] 

112 ... .copy() 

113 ... .align(SPECTRAL_SHAPE_DEFAULT) 

114 ... ) 

115 >>> uv = np.array([0.1978, 0.3122]) 

116 >>> planckian_table(cmfs, 1000, 1010, 1.005) 

117 ... # doctest: +ELLIPSIS 

118 array([[ 1.00000000e+03, 4.4796...e-01, 3.5462...e-01], 

119 [ 1.00100000e+03, 4.4772...e-01, 3.5464...e-01], 

120 [ 1.00600500e+03, 4.4656...e-01, 3.5475...e-01], 

121 [ 1.00900000e+03, 4.4586...e-01, 3.5481...e-01], 

122 [ 1.01000000e+03, 4.4563...e-01, 3.5483...e-01]]) 

123 """ 

124 

125 hash_key = hash((cmfs, start, end, spacing)) 

126 if is_caching_enabled() and hash_key in _CACHE_PLANCKIAN_TABLE: 

127 table = _CACHE_PLANCKIAN_TABLE[hash_key].copy() 

128 else: 

129 attest(spacing > 1, "Spacing value must be greater than 1!") 

130 

131 Ti = [start, start + 1] 

132 next_ti = start + 1 

133 next_spacing = spacing 

134 while (next_ti := next_ti * next_spacing) < end: 

135 Ti.append(next_ti) 

136 

137 # Slightly decrease step-size for higher CCT. 

138 D = (next_ti - CCT_MINIMAL_OHNO2013) / ( 

139 CCT_MAXIMAL_OHNO2013 - CCT_MINIMAL_OHNO2013 

140 ) 

141 D = min(max(D, 0), 1) 

142 next_spacing = spacing * (1 - D) + (1 + (spacing - 1) / 10) * D 

143 Ti = np.concatenate([Ti, [end - 1, end]]) 

144 

145 table = np.concatenate( 

146 [np.reshape(Ti, (-1, 1)), CCT_to_uv_Planck1900(Ti, cmfs)], axis=1 

147 ) 

148 _CACHE_PLANCKIAN_TABLE[hash_key] = table.copy() 

149 

150 return table 

151 

152 

153def uv_to_CCT_Ohno2013( 

154 uv: ArrayLike, 

155 cmfs: MultiSpectralDistributions | None = None, 

156 start: float | None = None, 

157 end: float | None = None, 

158 spacing: float | None = None, 

159) -> NDArrayFloat: 

160 """ 

161 Compute the correlated colour temperature :math:`T_{cp}` and 

162 :math:`\\Delta_{uv}` from the specified *CIE UCS* colourspace *uv* 

163 chromaticity coordinates using the *Ohno (2013)* method. 

164 

165 Parameters 

166 ---------- 

167 uv 

168 *CIE UCS* colourspace *uv* chromaticity coordinates. 

169 cmfs 

170 Standard observer colour matching functions, default to the 

171 *CIE 1931 2 Degree Standard Observer*. 

172 start 

173 Temperature range start in kelvin degrees, default to 1000. 

174 end 

175 Temperature range end in kelvin degrees, default to 100000. 

176 spacing 

177 Spacing between values of the underlying Planckian table expressed 

178 as a multiplier. Default to 1.001. The closer to 1.0, the higher 

179 the precision of the returned colour temperature :math:`T_{cp}` and 

180 :math:`\\Delta_{uv}`. A value of 1.01 provides a good balance 

181 between performance and accuracy. The ``spacing`` value must be 

182 greater than 1. 

183 

184 Returns 

185 ------- 

186 :class:`numpy.ndarray` 

187 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. 

188 

189 References 

190 ---------- 

191 :cite:`Ohno2014a` 

192 

193 Examples 

194 -------- 

195 >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT 

196 >>> cmfs = ( 

197 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] 

198 ... .copy() 

199 ... .align(SPECTRAL_SHAPE_DEFAULT) 

200 ... ) 

201 >>> uv = np.array([0.1978, 0.3122]) 

202 >>> uv_to_CCT_Ohno2013(uv, cmfs) # doctest: +ELLIPSIS 

203 array([ 6.50747...e+03, 3.22334...e-03]) 

204 """ 

205 

206 uv = as_float_array(uv) 

207 cmfs, _illuminant = handle_spectral_arguments(cmfs) 

208 start = optional(start, CCT_MINIMAL_OHNO2013) 

209 end = optional(end, CCT_MAXIMAL_OHNO2013) 

210 spacing = optional(spacing, CCT_DEFAULT_SPACING_OHNO2013) 

211 

212 shape = uv.shape 

213 uv = np.reshape(uv, (-1, 2)) 

214 

215 # Planckian tables creation through cascade expansion. 

216 tables_data = [] 

217 for uv_i in uv: 

218 table = planckian_table(cmfs, start, end, spacing) 

219 dists = euclidean_distance(table[:, 1:], uv_i) 

220 index = np.argmin(dists) 

221 if index == 0: 

222 runtime_warning( 

223 "Minimal distance index is on lowest planckian table bound, " 

224 "unpredictable results may occur!" 

225 ) 

226 index += 1 

227 elif index == len(table) - 1: 

228 runtime_warning( 

229 "Minimal distance index is on highest planckian table bound, " 

230 "unpredictable results may occur!" 

231 ) 

232 index -= 1 

233 

234 tables_data.append( 

235 np.vstack( 

236 [ 

237 [*table[index - 1, ...], dists[index - 1]], 

238 [*table[index, ...], dists[index]], 

239 [*table[index + 1, ...], dists[index + 1]], 

240 ] 

241 ) 

242 ) 

243 tables = as_float_array(tables_data) 

244 

245 Tip, uip, vip, dip = tsplit(tables[:, 0, :]) 

246 Ti, _ui, _vi, di = tsplit(tables[:, 1, :]) 

247 Tin, uin, vin, din = tsplit(tables[:, 2, :]) 

248 

249 # Triangular solution. 

250 l = np.hypot(uin - uip, vin - vip) # noqa: E741 

251 x = (dip**2 - din**2 + l**2) / (2 * l) 

252 T_t = Tip + (Tin - Tip) * (x / l) 

253 

254 vtx = vip + (vin - vip) * (x / l) 

255 sign = np.sign(uv[..., 1] - vtx) 

256 D_uv_t = (dip**2 - x**2) ** (1 / 2) * sign 

257 

258 # Parabolic solution. 

259 X = (Tin - Ti) * (Tip - Tin) * (Ti - Tip) 

260 a = (Tip * (din - di) + Ti * (dip - din) + Tin * (di - dip)) * X**-1 

261 b = -(Tip**2 * (din - di) + Ti**2 * (dip - din) + Tin**2 * (di - dip)) * X**-1 

262 c = ( 

263 -( 

264 dip * (Tin - Ti) * Ti * Tin 

265 + di * (Tip - Tin) * Tip * Tin 

266 + din * (Ti - Tip) * Tip * Ti 

267 ) 

268 * X**-1 

269 ) 

270 

271 T_p = -b / (2 * a) 

272 D_uv_p = (a * T_p**2 + b * T_p + c) * sign 

273 

274 CCT_D_uv = np.where( 

275 (np.abs(D_uv_t) >= 0.002)[..., None], 

276 tstack([T_p, D_uv_p]), 

277 tstack([T_t, D_uv_t]), 

278 ) 

279 

280 return np.reshape(CCT_D_uv, shape) 

281 

282 

283def CCT_to_uv_Ohno2013( 

284 CCT_D_uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None 

285) -> NDArrayFloat: 

286 """ 

287 Compute the *CIE UCS* colourspace *uv* chromaticity coordinates from 

288 the specified correlated colour temperature :math:`T_{cp}`, 

289 :math:`\\Delta_{uv}` and colour matching functions using 

290 *Ohno (2013)* method. 

291 

292 Parameters 

293 ---------- 

294 CCT_D_uv 

295 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. 

296 cmfs 

297 Standard observer colour matching functions, default to the 

298 *CIE 1931 2 Degree Standard Observer*. 

299 

300 Returns 

301 ------- 

302 :class:`numpy.ndarray` 

303 *CIE UCS* colourspace *uv* chromaticity coordinates. 

304 

305 References 

306 ---------- 

307 :cite:`Ohno2014a` 

308 

309 Examples 

310 -------- 

311 >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT 

312 >>> cmfs = ( 

313 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] 

314 ... .copy() 

315 ... .align(SPECTRAL_SHAPE_DEFAULT) 

316 ... ) 

317 >>> CCT_D_uv = np.array([6507.4342201047066, 0.003223690901513]) 

318 >>> CCT_to_uv_Ohno2013(CCT_D_uv, cmfs) # doctest: +ELLIPSIS 

319 array([ 0.1977999..., 0.3122004...]) 

320 """ 

321 

322 CCT, D_uv = tsplit(CCT_D_uv) 

323 

324 cmfs, _illuminant = handle_spectral_arguments(cmfs) 

325 

326 uv_0 = CCT_to_uv_Planck1900(CCT, cmfs) 

327 uv_1 = CCT_to_uv_Planck1900(CCT + 0.01, cmfs) 

328 

329 du, dv = tsplit(uv_0 - uv_1) 

330 

331 h = np.hypot(du, dv) 

332 

333 with sdiv_mode(): 

334 uv = tstack( 

335 [ 

336 uv_0[..., 0] - D_uv * sdiv(dv, h), 

337 uv_0[..., 1] + D_uv * sdiv(du, h), 

338 ] 

339 ) 

340 

341 uv[D_uv == 0] = uv_0[D_uv == 0] 

342 

343 return uv 

344 

345 

346def XYZ_to_CCT_Ohno2013( 

347 XYZ: Domain1, 

348 cmfs: MultiSpectralDistributions | None = None, 

349 start: float | None = None, 

350 end: float | None = None, 

351 spacing: float | None = None, 

352) -> NDArrayFloat: 

353 """ 

354 Compute the correlated colour temperature :math:`T_{cp}` and 

355 :math:`\\Delta_{uv}` from the specified *CIE XYZ* tristimulus values 

356 using the *Ohno (2013)* method. 

357 

358 The method computes the correlated colour temperature by finding the 

359 closest point on the Planckian locus to the specified chromaticity 

360 coordinates using an optimised search algorithm with configurable 

361 precision through the spacing parameter. 

362 

363 Parameters 

364 ---------- 

365 XYZ 

366 *CIE XYZ* tristimulus values. 

367 cmfs 

368 Standard observer colour matching functions, default to the 

369 *CIE 1931 2 Degree Standard Observer*. 

370 start 

371 Temperature range start in kelvins, default to 1000. 

372 end 

373 Temperature range end in kelvins, default to 100000. 

374 spacing 

375 Spacing between values of the underlying Planckian table expressed 

376 as a multiplier. Default to 1.001. The closer to 1.0, the higher 

377 the precision of the returned colour temperature :math:`T_{cp}` and 

378 :math:`\\Delta_{uv}`. A value of 1.01 provides a good balance 

379 between performance and accuracy. The ``spacing`` value must be 

380 greater than 1. 

381 

382 Returns 

383 ------- 

384 :class:`numpy.ndarray` 

385 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. 

386 

387 Notes 

388 ----- 

389 +------------+-----------------------+---------------+ 

390 | **Domain** | **Scale - Reference** | **Scale - 1** | 

391 +============+=======================+===============+ 

392 | ``XYZ`` | 1 | 1 | 

393 +------------+-----------------------+---------------+ 

394 

395 References 

396 ---------- 

397 :cite:`Ohno2014a` 

398 

399 Examples 

400 -------- 

401 >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT 

402 >>> cmfs = ( 

403 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] 

404 ... .copy() 

405 ... .align(SPECTRAL_SHAPE_DEFAULT) 

406 ... ) 

407 >>> XYZ = np.array([0.95035049, 1.0, 1.08935705]) 

408 >>> XYZ_to_CCT_Ohno2013(XYZ, cmfs) # doctest: +ELLIPSIS 

409 array([ 6.5074399...e+03, 3.2236914...e-03]) 

410 """ 

411 

412 return uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)), cmfs, start, end, spacing) 

413 

414 

415def CCT_to_XYZ_Ohno2013( 

416 CCT_D_uv: ArrayLike, cmfs: MultiSpectralDistributions | None = None 

417) -> Range1: 

418 """ 

419 Compute the *CIE XYZ* tristimulus values from the specified correlated 

420 colour temperature :math:`T_{cp}` and :math:`\\Delta_{uv}` using the 

421 *Ohno (2013)* method. 

422 

423 Parameters 

424 ---------- 

425 CCT_D_uv 

426 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`. 

427 cmfs 

428 Standard observer colour matching functions, default to the 

429 *CIE 1931 2 Degree Standard Observer*. 

430 

431 Returns 

432 ------- 

433 :class:`numpy.ndarray` 

434 *CIE XYZ* tristimulus values. 

435 

436 Notes 

437 ----- 

438 +-----------+-----------------------+---------------+ 

439 | **Range** | **Scale - Reference** | **Scale - 1** | 

440 +===========+=======================+===============+ 

441 | ``XYZ`` | 1 | 1 | 

442 +-----------+-----------------------+---------------+ 

443 

444 Examples 

445 -------- 

446 >>> from colour import MSDS_CMFS, SPECTRAL_SHAPE_DEFAULT 

447 >>> cmfs = ( 

448 ... MSDS_CMFS["CIE 1931 2 Degree Standard Observer"] 

449 ... .copy() 

450 ... .align(SPECTRAL_SHAPE_DEFAULT) 

451 ... ) 

452 >>> CCT_D_uv = np.array([6507.4342201047066, 0.003223690901513]) 

453 >>> CCT_to_XYZ_Ohno2013(CCT_D_uv, cmfs) # doctest: +ELLIPSIS 

454 array([ 0.9503504..., 1. , 1.0893570...]) 

455 """ 

456 

457 return UCS_to_XYZ(uv_to_UCS(CCT_to_uv_Ohno2013(CCT_D_uv, cmfs)))