Coverage for colorimetry/dominant.py: 63%
57 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"""
2Dominant Wavelength and Purity
3==============================
5Define the objects to compute the *dominant wavelength* and *purity* of a
6colour and related quantities:
8- :func:`colour.dominant_wavelength`
9- :func:`colour.complementary_wavelength`
10- :func:`colour.excitation_purity`
11- :func:`colour.colorimetric_purity`
13References
14----------
15- :cite:`CIETC1-482004o` : CIE TC 1-48. (2004). 9.1 Dominant wavelength and
16 purity. In CIE 015:2004 Colorimetry, 3rd Edition (pp. 32-33).
17 ISBN:978-3-901906-33-6
18- :cite:`Erdogana` : Erdogan, T. (n.d.). How to Calculate Luminosity,
19 Dominant Wavelength, and Excitation Purity (p. 7).
20 http://www.semrock.com/Data/Sites/1/semrockpdfs/\
21whitepaper_howtocalculateluminositywavelengthandpurity.pdf
22"""
24from __future__ import annotations
26import typing
28import numpy as np
30from colour.algebra import euclidean_distance, sdiv, sdiv_mode
31from colour.colorimetry import MultiSpectralDistributions, handle_spectral_arguments
32from colour.geometry import extend_line_segment, intersect_line_segments
34if typing.TYPE_CHECKING:
35 from colour.hints import ArrayLike, NDArrayFloat, NDArrayInt, Tuple
37from colour.models import XYZ_to_xy
38from colour.utilities import as_float_array, required
40__author__ = "Colour Developers"
41__copyright__ = "Copyright 2013 Colour Developers"
42__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
43__maintainer__ = "Colour Developers"
44__email__ = "colour-developers@colour-science.org"
45__status__ = "Production"
47__all__ = [
48 "closest_spectral_locus_wavelength",
49 "dominant_wavelength",
50 "complementary_wavelength",
51 "excitation_purity",
52 "colorimetric_purity",
53]
56@required("SciPy")
57def closest_spectral_locus_wavelength(
58 xy: ArrayLike, xy_n: ArrayLike, xy_s: ArrayLike, inverse: bool = False
59) -> Tuple[NDArrayInt, NDArrayFloat]:
60 """
61 Compute the coordinates and closest spectral locus wavelength index to the
62 point where the line defined by the achromatic stimulus :math:`xy_n` to
63 colour stimulus :math:`xy` *CIE xy* chromaticity coordinates intersects
64 the spectral locus.
66 Parameters
67 ----------
68 xy
69 Colour stimulus *CIE xy* chromaticity coordinates.
70 xy_n
71 Achromatic stimulus *CIE xy* chromaticity coordinates.
72 xy_s
73 Spectral locus *CIE xy* chromaticity coordinates.
74 inverse
75 The intersection will be computed using the colour stimulus :math:`xy`
76 to achromatic stimulus :math:`xy_n` inverse direction.
78 Returns
79 -------
80 :class:`tuple`
81 Closest wavelength index, intersection point *CIE xy* chromaticity
82 coordinates.
84 Raises
85 ------
86 ValueError
87 If no closest spectral locus wavelength index and coordinates found.
89 Examples
90 --------
91 >>> from colour.colorimetry import MSDS_CMFS
92 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
93 >>> xy = np.array([0.54369557, 0.32107944])
94 >>> xy_n = np.array([0.31270000, 0.32900000])
95 >>> xy_s = XYZ_to_xy(cmfs.values)
96 >>> ix, intersect = closest_spectral_locus_wavelength(xy, xy_n, xy_s)
97 >>> print(ix) #
98 256
99 >>> print(intersect) # doctest: +ELLIPSIS
100 [ 0.6835474... 0.3162840...]
101 """
103 import scipy.spatial.distance # noqa: PLC0415
105 xy = as_float_array(xy)
106 xy_n = np.resize(xy_n, xy.shape)
107 xy_s = as_float_array(xy_s)
109 xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy)
111 # Closing horse-shoe shape to handle line of purples intersections.
112 xy_s = np.vstack([xy_s, xy_s[0, :]])
114 xy_wl = intersect_line_segments(
115 np.concatenate((xy_n, xy_e), -1),
116 np.hstack([xy_s, np.roll(xy_s, 1, axis=0)]),
117 ).xy
118 # Extracting the first intersection per-wavelength.
119 xy_wl = np.sort(xy_wl, 1)[:, 0, :]
121 i_wl = np.argmin(scipy.spatial.distance.cdist(xy_wl, xy_s), axis=-1)
123 i_wl = np.reshape(i_wl, xy.shape[0:-1])
124 xy_wl = np.reshape(xy_wl, xy.shape)
126 return i_wl, xy_wl
129def dominant_wavelength(
130 xy: ArrayLike,
131 xy_n: ArrayLike,
132 cmfs: MultiSpectralDistributions | None = None,
133 inverse: bool = False,
134) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:
135 """
136 Compute the *dominant wavelength* :math:`\\lambda_d` for colour stimulus
137 :math:`xy` and the related :math:`xy_wl` first and :math:`xy_{cw}` second
138 intersection coordinates with the spectral locus.
140 In the eventuality where the :math:`xy_wl` first intersection coordinates
141 are on the line of purples, the *complementary wavelength* will be
142 computed in lieu.
144 The *complementary wavelength* is indicated by a negative sign and the
145 :math:`xy_{cw}` second intersection coordinates which are set by default
146 to the same value as :math:`xy_wl` first intersection coordinates will be
147 set to the *complementary dominant wavelength* intersection coordinates
148 with the spectral locus.
150 Parameters
151 ----------
152 xy
153 Colour stimulus *CIE xy* chromaticity coordinates.
154 xy_n
155 Achromatic stimulus *CIE xy* chromaticity coordinates.
156 cmfs
157 Standard observer colour matching functions, default to the
158 *CIE 1931 2 Degree Standard Observer*.
159 inverse
160 Inverse the computation direction to retrieve the
161 *complementary wavelength*.
163 Returns
164 -------
165 :class:`tuple`
166 *Dominant wavelength*, first intersection point *CIE xy* chromaticity
167 coordinates, second intersection point *CIE xy* chromaticity
168 coordinates.
170 References
171 ----------
172 :cite:`CIETC1-482004o`, :cite:`Erdogana`
174 Examples
175 --------
176 *Dominant wavelength* computation:
178 >>> from colour.colorimetry import MSDS_CMFS
179 >>> from pprint import pprint
180 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
181 >>> xy = np.array([0.54369557, 0.32107944])
182 >>> xy_n = np.array([0.31270000, 0.32900000])
183 >>> pprint(dominant_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS
184 (array(616...),
185 array([ 0.6835474..., 0.3162840...]),
186 array([ 0.6835474..., 0.3162840...]))
188 *Complementary dominant wavelength* is returned if the first intersection
189 is located on the line of purples:
191 >>> xy = np.array([0.37605506, 0.24452225])
192 >>> pprint(dominant_wavelength(xy, xy_n)) # doctest: +ELLIPSIS
193 (array(-509.0),
194 array([ 0.4572314..., 0.1362814...]),
195 array([ 0.0104096..., 0.7320745...]))
196 """
198 cmfs, _illuminant = handle_spectral_arguments(cmfs)
200 xy = as_float_array(xy)
201 xy_n = np.resize(xy_n, xy.shape)
203 xy_s = XYZ_to_xy(cmfs.values)
205 i_wl, xy_wl = closest_spectral_locus_wavelength(xy, xy_n, xy_s, inverse)
206 xy_cwl = xy_wl
207 wl = cmfs.wavelengths[i_wl]
209 xy_e = extend_line_segment(xy, xy_n) if inverse else extend_line_segment(xy_n, xy)
210 intersect = intersect_line_segments(
211 np.concatenate((xy_n, xy_e), -1), np.hstack([xy_s[0], xy_s[-1]])
212 ).intersect
213 intersect = np.reshape(intersect, wl.shape)
215 i_wl_r, xy_cwl_r = closest_spectral_locus_wavelength(xy, xy_n, xy_s, not inverse)
216 wl_r = -cmfs.wavelengths[i_wl_r]
218 wl = np.where(intersect, wl_r, wl)
219 xy_cwl = np.where(intersect[..., None], xy_cwl_r, xy_cwl)
221 return wl, np.squeeze(xy_wl), np.squeeze(xy_cwl)
224def complementary_wavelength(
225 xy: ArrayLike,
226 xy_n: ArrayLike,
227 cmfs: MultiSpectralDistributions | None = None,
228) -> Tuple[NDArrayFloat, NDArrayFloat, NDArrayFloat]:
229 """
230 Compute the *complementary wavelength* :math:`\\lambda_c` for the
231 specified colour stimulus :math:`xy` and the related :math:`xy_wl` first
232 and :math:`xy_{cw}` second intersection coordinates with the spectral
233 locus.
235 In the eventuality where the :math:`xy_wl` first intersection coordinates
236 are on the line of purples, the *dominant wavelength* will be computed in
237 lieu.
239 The *dominant wavelength* is indicated by a negative sign and the
240 :math:`xy_{cw}` second intersection coordinates which are set by default
241 to the same value as :math:`xy_wl` first intersection coordinates will be
242 set to the *dominant wavelength* intersection coordinates with the
243 spectral locus.
245 Parameters
246 ----------
247 xy
248 Colour stimulus *CIE xy* chromaticity coordinates.
249 xy_n
250 Achromatic stimulus *CIE xy* chromaticity coordinates.
251 cmfs
252 Standard observer colour matching functions, default to the
253 *CIE 1931 2 Degree Standard Observer*.
255 Returns
256 -------
257 :class:`tuple`
258 *Complementary wavelength*, first intersection point *CIE xy*
259 chromaticity coordinates, second intersection point *CIE xy*
260 chromaticity coordinates.
262 References
263 ----------
264 :cite:`CIETC1-482004o`, :cite:`Erdogana`
266 Examples
267 --------
268 *Complementary wavelength* computation:
270 >>> from colour.colorimetry import MSDS_CMFS
271 >>> from pprint import pprint
272 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
273 >>> xy = np.array([0.37605506, 0.24452225])
274 >>> xy_n = np.array([0.31270000, 0.32900000])
275 >>> pprint(complementary_wavelength(xy, xy_n, cmfs)) # doctest: +ELLIPSIS
276 (array(509.0),
277 array([ 0.0104096..., 0.7320745...]),
278 array([ 0.0104096..., 0.7320745...]))
280 *Dominant wavelength* is returned if the first intersection is located on
281 the line of purples:
283 >>> xy = np.array([0.54369557, 0.32107944])
284 >>> pprint(complementary_wavelength(xy, xy_n)) # doctest: +ELLIPSIS
285 (array(492.0),
286 array([ 0.0364795 , 0.3384712...]),
287 array([ 0.0364795 , 0.3384712...]))
288 """
290 return dominant_wavelength(xy, xy_n, cmfs, True)
293def excitation_purity(
294 xy: ArrayLike,
295 xy_n: ArrayLike,
296 cmfs: MultiSpectralDistributions | None = None,
297) -> NDArrayFloat:
298 """
299 Compute the *excitation purity* :math:`P_e` for the specified colour
300 stimulus :math:`xy`.
302 Parameters
303 ----------
304 xy
305 Colour stimulus *CIE xy* chromaticity coordinates.
306 xy_n
307 Achromatic stimulus *CIE xy* chromaticity coordinates.
308 cmfs
309 Standard observer colour matching functions, default to the
310 *CIE 1931 2 Degree Standard Observer*.
312 Returns
313 -------
314 :class:`np.float` or :class:`numpy.ndarray`
315 *Excitation purity* :math:`P_e`.
317 References
318 ----------
319 :cite:`CIETC1-482004o`, :cite:`Erdogana`
321 Examples
322 --------
323 >>> from colour.colorimetry import MSDS_CMFS
324 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
325 >>> xy = np.array([0.54369557, 0.32107944])
326 >>> xy_n = np.array([0.31270000, 0.32900000])
327 >>> excitation_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS
328 0.6228856...
329 """
331 _wl, xy_wl, _xy_cwl = dominant_wavelength(xy, xy_n, cmfs)
333 with sdiv_mode():
334 return sdiv(
335 euclidean_distance(xy_n, xy),
336 euclidean_distance(xy_n, xy_wl),
337 )
340def colorimetric_purity(
341 xy: ArrayLike,
342 xy_n: ArrayLike,
343 cmfs: MultiSpectralDistributions | None = None,
344) -> NDArrayFloat:
345 """
346 Compute the *colorimetric purity* :math:`P_c` for the specified
347 colour stimulus :math:`xy`.
349 Parameters
350 ----------
351 xy
352 Colour stimulus *CIE xy* chromaticity coordinates.
353 xy_n
354 Achromatic stimulus *CIE xy* chromaticity coordinates.
355 cmfs
356 Standard observer colour matching functions, default to the
357 *CIE 1931 2 Degree Standard Observer*.
359 Returns
360 -------
361 :class:`np.float` or :class:`numpy.ndarray`
362 *Colorimetric purity* :math:`P_c`.
364 References
365 ----------
366 :cite:`CIETC1-482004o`, :cite:`Erdogana`
368 Examples
369 --------
370 >>> from colour.colorimetry import MSDS_CMFS
371 >>> cmfs = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
372 >>> xy = np.array([0.54369557, 0.32107944])
373 >>> xy_n = np.array([0.31270000, 0.32900000])
374 >>> colorimetric_purity(xy, xy_n, cmfs) # doctest: +ELLIPSIS
375 0.6135828...
376 """
378 xy = as_float_array(xy)
380 _wl, xy_wl, _xy_cwl = dominant_wavelength(xy, xy_n, cmfs)
381 P_e = excitation_purity(xy, xy_n, cmfs)
383 with sdiv_mode():
384 return P_e * sdiv(xy_wl[..., 1], xy[..., 1])