Coverage for temperature/ohno2013.py: 73%

1"""

2 Ohno (2013) Correlated Colour Temperature

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

4

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

6 computation 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

17 References

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

23 from __future__ import annotations

24

25 import numpy as np

26

27 from colour.algebra import euclidean_distance, sdiv, sdiv_mode

28 from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments

29 from colour.hints import ( # noqa: TC001

30 ArrayLike,

31 Domain1,

32 NDArrayFloat,

33 Range1,

34)

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

36 from colour.temperature import CCT_to_uv_Planck1900

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

66 CCT_MINIMAL_OHNO2013: float = 1000

67 CCT_MAXIMAL_OHNO2013: float = 100000

68 CCT_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

75 def 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

153 def 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

283 def 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

346 def 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

415 def 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)))