Coverage for temperature/ohno2013.py: 73%
89 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Ohno (2013) Correlated Colour Temperature
3=========================================
5Define the *Ohno (2013)* correlated colour temperature :math:`T_{cp}`
6computation objects.
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.
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"""
23from __future__ import annotations
25import numpy as np
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)
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"
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]
66CCT_MINIMAL_OHNO2013: float = 1000
67CCT_MAXIMAL_OHNO2013: float = 100000
68CCT_DEFAULT_SPACING_OHNO2013: float = 1.001
70_CACHE_PLANCKIAN_TABLE: dict = CACHE_REGISTRY.register_cache(
71 f"{__name__}._CACHE_PLANCKIAN_TABLE"
72)
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.
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.
102 Returns
103 -------
104 :class:`list`
105 Planckian table.
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 """
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!")
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)
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]])
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()
150 return table
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.
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.
184 Returns
185 -------
186 :class:`numpy.ndarray`
187 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.
189 References
190 ----------
191 :cite:`Ohno2014a`
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 """
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)
212 shape = uv.shape
213 uv = np.reshape(uv, (-1, 2))
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
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)
245 Tip, uip, vip, dip = tsplit(tables[:, 0, :])
246 Ti, _ui, _vi, di = tsplit(tables[:, 1, :])
247 Tin, uin, vin, din = tsplit(tables[:, 2, :])
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)
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
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 )
271 T_p = -b / (2 * a)
272 D_uv_p = (a * T_p**2 + b * T_p + c) * sign
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 )
280 return np.reshape(CCT_D_uv, shape)
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.
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*.
300 Returns
301 -------
302 :class:`numpy.ndarray`
303 *CIE UCS* colourspace *uv* chromaticity coordinates.
305 References
306 ----------
307 :cite:`Ohno2014a`
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 """
322 CCT, D_uv = tsplit(CCT_D_uv)
324 cmfs, _illuminant = handle_spectral_arguments(cmfs)
326 uv_0 = CCT_to_uv_Planck1900(CCT, cmfs)
327 uv_1 = CCT_to_uv_Planck1900(CCT + 0.01, cmfs)
329 du, dv = tsplit(uv_0 - uv_1)
331 h = np.hypot(du, dv)
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 )
341 uv[D_uv == 0] = uv_0[D_uv == 0]
343 return uv
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.
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.
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.
382 Returns
383 -------
384 :class:`numpy.ndarray`
385 Correlated colour temperature :math:`T_{cp}`, :math:`\\Delta_{uv}`.
387 Notes
388 -----
389 +------------+-----------------------+---------------+
390 | **Domain** | **Scale - Reference** | **Scale - 1** |
391 +============+=======================+===============+
392 | ``XYZ`` | 1 | 1 |
393 +------------+-----------------------+---------------+
395 References
396 ----------
397 :cite:`Ohno2014a`
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 """
412 return uv_to_CCT_Ohno2013(UCS_to_uv(XYZ_to_UCS(XYZ)), cmfs, start, end, spacing)
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.
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*.
431 Returns
432 -------
433 :class:`numpy.ndarray`
434 *CIE XYZ* tristimulus values.
436 Notes
437 -----
438 +-----------+-----------------------+---------------+
439 | **Range** | **Scale - Reference** | **Scale - 1** |
440 +===========+=======================+===============+
441 | ``XYZ`` | 1 | 1 |
442 +-----------+-----------------------+---------------+
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 """
457 return UCS_to_XYZ(uv_to_UCS(CCT_to_uv_Ohno2013(CCT_D_uv, cmfs)))