diff --git a/colour/appearance/hellwig2022.py b/colour/appearance/hellwig2022.py index 8620bee1e2..903d1bdcbf 100644 --- a/colour/appearance/hellwig2022.py +++ b/colour/appearance/hellwig2022.py @@ -854,7 +854,7 @@ def colourfulness_correlate( a = as_float_array(a) b = as_float_array(b) - M = 43.0 * N_c * e_t * np.sqrt(a**2 + b**2) + M = 43.0 * N_c * e_t * np.hypot(a, b) return M diff --git a/colour/appearance/kim2009.py b/colour/appearance/kim2009.py index a4e6277ad8..5736e292de 100644 --- a/colour/appearance/kim2009.py +++ b/colour/appearance/kim2009.py @@ -352,7 +352,7 @@ def XYZ_to_Kim2009( # Computing the correlate of *chroma* :math:`C`. a_k, n_k = 456.5, 0.62 - C = a_k * spow(np.sqrt(a**2 + b**2), n_k) + C = a_k * spow(np.hypot(a, b), n_k) # Computing the correlate of *colourfulness* :math:`M`. a_m, b_m = 0.11, 0.61 diff --git a/colour/contrast/barten1999.py b/colour/contrast/barten1999.py index 6112e37eb3..393ac83f92 100644 --- a/colour/contrast/barten1999.py +++ b/colour/contrast/barten1999.py @@ -189,7 +189,7 @@ def sigma_Barten1999( C_ab = as_float_array(C_ab) d = as_float_array(d) - return as_float(np.sqrt(sigma_0**2 + (C_ab * d) ** 2)) + return as_float(np.hypot(sigma_0, C_ab * d)) def retinal_illuminance_Barten1999( diff --git a/colour/models/din99.py b/colour/models/din99.py index 7d35a60f41..72f041d246 100644 --- a/colour/models/din99.py +++ b/colour/models/din99.py @@ -243,7 +243,7 @@ def DIN99_to_Lab( h_99 = np.arctan2(b_99, a_99) - np.radians(c_7) - C_99 = np.sqrt(a_99**2 + b_99**2) + C_99 = np.hypot(a_99, b_99) G = np.expm1((c_8 / c_5) * C_99 * k_CH * k_E) / c_6 e = G * np.cos(h_99) diff --git a/colour/models/rgb/hanbury2003.py b/colour/models/rgb/hanbury2003.py index e24aa29db4..8d07ad6570 100644 --- a/colour/models/rgb/hanbury2003.py +++ b/colour/models/rgb/hanbury2003.py @@ -98,7 +98,7 @@ def RGB_to_IHLS(RGB: ArrayLike) -> NDArrayFloat: Y, C_1, C_2 = tsplit(vector_dot(MATRIX_RGB_TO_YC_1_C_2, RGB)) - C = np.sqrt(C_1**2 + C_2**2) + C = np.hypot(C_1, C_2) with sdiv_mode(): C_1_C = sdiv(C_1, C) diff --git a/colour/plotting/temperature.py b/colour/plotting/temperature.py index c0b8d6287f..3eb59d2cc4 100644 --- a/colour/plotting/temperature.py +++ b/colour/plotting/temperature.py @@ -36,9 +36,10 @@ UCS_uv_to_xy, XYZ_to_UCS, xy_to_Luv_uv, + xy_to_UCS_uv, xy_to_XYZ, ) -from colour.temperature import mired_to_CCT, CCT_to_uv +from colour.temperature import mired_to_CCT, CCT_to_uv, CCT_to_xy_CIE_D from colour.plotting import ( CONSTANTS_COLOUR_STYLE, CONSTANTS_ARROW_STYLE, @@ -70,6 +71,7 @@ __status__ = "Production" __all__ = [ + "plot_daylight_locus", "plot_planckian_locus", "plot_planckian_locus_in_chromaticity_diagram", "plot_planckian_locus_in_chromaticity_diagram_CIE1931", @@ -77,6 +79,140 @@ ] +@override_style() +def plot_daylight_locus( + daylight_locus_colours: ArrayLike | str | None = None, + daylight_locus_opacity: float = 1, + daylight_locus_use_mireds: bool = False, + method: Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + | str = "CIE 1931", + **kwargs: Any, +) -> Tuple[plt.Figure, plt.Axes]: + """ + Plot the *Daylight Locus* according to given method. + + Parameters + ---------- + daylight_locus_colours + Colours of the *Daylight Locus*, if ``daylight_locus_colours`` is set + to *RGB*, the colours will be computed according to the corresponding + chromaticity coordinates. + daylight_locus_opacity + Opacity of the *Daylight Locus*. + daylight_locus_use_mireds + Whether to use micro reciprocal degrees for the iso-temperature lines. + method + *Chromaticity Diagram* method. + + Other Parameters + ---------------- + kwargs + {:func:`colour.plotting.artist`, :func:`colour.plotting.render`}, + See the documentation of the previously listed definitions. + + Returns + ------- + :class:`tuple` + Current figure and axes. + + Examples + -------- + >>> plot_daylight_locus( + ... daylight_locus_colours="RGB", + ... method="CIE 1960 UCS", + ... ) + ... # doctest: +ELLIPSIS + (
, <...Axes...>) + + .. image:: ../_static/Plotting_Plot_Daylight_Locus.png + :align: center + :alt: plot_daylight_locus + """ + + method = validate_method( + method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"] + ) + + daylight_locus_colours = optional( + daylight_locus_colours, CONSTANTS_COLOUR_STYLE.colour.dark + ) + + settings: Dict[str, Any] = {"uniform": True} + settings.update(kwargs) + + _figure, axes = artist(**settings) + + if method == "cie 1931": + + def xy_to_ij(xy: NDArrayFloat) -> NDArrayFloat: + """ + Convert given *CIE xy* chromaticity coordinates to *ij* + chromaticity coordinates. + """ + + return xy + + elif method == "cie 1960 ucs": + + def xy_to_ij(xy: NDArrayFloat) -> NDArrayFloat: + """ + Convert given *CIE xy* chromaticity coordinates to *ij* + chromaticity coordinates. + """ + + return xy_to_UCS_uv(xy) + + elif method == "cie 1976 ucs": + + def xy_to_ij(xy: NDArrayFloat) -> NDArrayFloat: + """ + Convert given *CIE xy* chromaticity coordinates to *ij* + chromaticity coordinates. + """ + + return xy_to_Luv_uv(xy) + + def CCT_to_plotting_colourspace(CCT): + """ + Convert given correlated colour temperature :math:`T_{cp}` to the + default plotting colourspace. + """ + + return normalise_maximum( + XYZ_to_plotting_colourspace(xy_to_XYZ(CCT_to_xy_CIE_D(CCT))), + axis=-1, + ) + + start, end = ( + (0, 1000) if daylight_locus_use_mireds else (1e6 / 600, 1e6 / 10) + ) + + CCT = np.arange(start, end + 100, 10) * 1.4388 / 1.4380 + CCT = mired_to_CCT(CCT) if daylight_locus_use_mireds else CCT + ij = xy_to_ij(CCT_to_xy_CIE_D(CCT)).reshape(-1, 1, 2) + + use_RGB_daylight_locus_colours = ( + str(daylight_locus_colours).upper() == "RGB" + ) + if use_RGB_daylight_locus_colours: + pl_colours = CCT_to_plotting_colourspace(CCT) + else: + pl_colours = daylight_locus_colours + + line_collection = LineCollection( + np.concatenate([ij[:-1], ij[1:]], axis=1), + colors=pl_colours, + alpha=daylight_locus_opacity, + zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line, + ) + axes.add_collection(line_collection) + + settings = {"axes": axes} + settings.update(kwargs) + + return render(**settings) + + @override_style() def plot_planckian_locus( planckian_locus_colours: ArrayLike | str | None = None, @@ -128,7 +264,6 @@ def plot_planckian_locus( >>> plot_planckian_locus( ... planckian_locus_colours="RGB", ... method="CIE 1960 UCS", - ... use_mireds=True, ... ) ... # doctest: +ELLIPSIS (
, <...Axes...>) @@ -194,8 +329,8 @@ def uv_to_ij(uv: NDArrayFloat) -> NDArrayFloat: def CCT_D_uv_to_plotting_colourspace(CCT_D_uv): """ - Convert given *uv* chromaticity coordinates to the default plotting - colourspace. + Convert given correlated colour temperature :math:`T_{cp}` and + :math:`\\Delta_{uv}` to the default plotting colourspace. """ return normalise_maximum( diff --git a/colour/plotting/tests/test_temperature.py b/colour/plotting/tests/test_temperature.py index 20f5feac75..367fdc9ea5 100644 --- a/colour/plotting/tests/test_temperature.py +++ b/colour/plotting/tests/test_temperature.py @@ -9,6 +9,7 @@ plot_planckian_locus_in_chromaticity_diagram_CIE1960UCS, ) from colour.plotting.temperature import ( + plot_daylight_locus, plot_planckian_locus, plot_planckian_locus_in_chromaticity_diagram, ) @@ -21,6 +22,7 @@ __status__ = "Production" __all__ = [ + "TestPlotDaylightLocus", "TestPlotPlanckianLocus", "TestPlotPlanckianLocusInChromaticityDiagram", "TestPlotPlanckianLocusInChromaticityDiagramCIE1931", @@ -28,6 +30,38 @@ ] +class TestPlotDaylightLocus(unittest.TestCase): + """ + Define :func:`colour.plotting.temperature.plot_daylight_locus` definition + unit tests methods. + """ + + def test_plot_daylight_locus(self): + """ + Test :func:`colour.plotting.temperature.plot_daylight_locus` + definition. + """ + + figure, axes = plot_daylight_locus() + + self.assertIsInstance(figure, Figure) + self.assertIsInstance(axes, Axes) + + self.assertRaises( + ValueError, lambda: plot_daylight_locus(method="Undefined") + ) + + figure, axes = plot_daylight_locus(method="CIE 1976 UCS") + + self.assertIsInstance(figure, Figure) + self.assertIsInstance(axes, Axes) + + figure, axes = plot_daylight_locus(planckian_locus_colours="RGB") + + self.assertIsInstance(figure, Figure) + self.assertIsInstance(axes, Axes) + + class TestPlotPlanckianLocus(unittest.TestCase): """ Define :func:`colour.plotting.temperature.plot_planckian_locus` definition diff --git a/docs/colour.plotting.rst b/docs/colour.plotting.rst index 1e131fab32..8c068c9c75 100644 --- a/docs/colour.plotting.rst +++ b/docs/colour.plotting.rst @@ -272,6 +272,7 @@ Colour Temperature & Correlated Colour Temperature .. autosummary:: :toctree: generated/ + plot_daylight_locus plot_planckian_locus plot_planckian_locus_in_chromaticity_diagram diff --git a/utilities/generate_plots.py b/utilities/generate_plots.py index efac2bad8a..2cd4eb36fb 100755 --- a/utilities/generate_plots.py +++ b/utilities/generate_plots.py @@ -117,6 +117,7 @@ plot_hull_section_contour, ) from colour.plotting.temperature import ( # noqa: E402 + plot_daylight_locus, plot_planckian_locus, plot_planckian_locus_in_chromaticity_diagram, ) @@ -909,6 +910,13 @@ def generate_documentation_plots(output_directory: str): )[0] ) + arguments["filename"] = os.path.join( + output_directory, "Plotting_Plot_Daylight_Locus.png" + ) + plt.close( + plot_daylight_locus(daylight_locus_colours="RGB", **arguments)[0] + ) + arguments["filename"] = os.path.join( output_directory, "Plotting_Plot_Planckian_Locus.png" )