diff --git a/colour/algebra/interpolation.py b/colour/algebra/interpolation.py index ac57f30e2..3b23ea847 100644 --- a/colour/algebra/interpolation.py +++ b/colour/algebra/interpolation.py @@ -1127,42 +1127,14 @@ def y(self, value: ArrayLike): self._y = value - yp1 = np.ravel( - ( - np.dot( - self.SPRAGUE_C_COEFFICIENTS[0], - np.reshape(np.array(value[0:6]), (6, 1)), - ) - ) - / 209 - )[0] - yp2 = np.ravel( - ( - np.dot( - self.SPRAGUE_C_COEFFICIENTS[1], - np.reshape(np.array(value[0:6]), (6, 1)), - ) + yp1, yp2, yp3, yp4 = ( + np.sum( + self.SPRAGUE_C_COEFFICIENTS + * np.asarray((value[0:6], value[0:6], value[-6:], value[-6:])), + axis=1, ) / 209 - )[0] - yp3 = np.ravel( - ( - np.dot( - self.SPRAGUE_C_COEFFICIENTS[2], - np.reshape(np.array(value[-6:]), (6, 1)), - ) - ) - / 209 - )[0] - yp4 = np.ravel( - ( - np.dot( - self.SPRAGUE_C_COEFFICIENTS[3], - np.reshape(np.array(value[-6:]), (6, 1)), - ) - ) - / 209 - )[0] + ) self._yp = np.concatenate( [ @@ -1217,36 +1189,24 @@ def _evaluate(self, x: NDArrayFloat) -> NDArrayFloat: r = self._yp - a0p = r[i] - a1p = (2 * r[i - 2] - 16 * r[i - 1] + 16 * r[i + 1] - 2 * r[i + 2]) / 24 - a2p = (-r[i - 2] + 16 * r[i - 1] - 30 * r[i] + 16 * r[i + 1] - r[i + 2]) / 24 - a3p = ( - -9 * r[i - 2] - + 39 * r[i - 1] - - 70 * r[i] - + 66 * r[i + 1] - - 33 * r[i + 2] - + 7 * r[i + 3] - ) / 24 - a4p = ( - 13 * r[i - 2] - - 64 * r[i - 1] - + 126 * r[i] - - 124 * r[i + 1] - + 61 * r[i + 2] - - 12 * r[i + 3] - ) / 24 - a5p = ( - -5 * r[i - 2] - + 25 * r[i - 1] - - 50 * r[i] - + 50 * r[i + 1] - - 25 * r[i + 2] - + 5 * r[i + 3] - ) / 24 - - y = a0p + a1p * X + a2p * X**2 + a3p * X**3 + a4p * X**4 + a5p * X**5 + r_s = np.asarray((r[i - 2], r[i - 1], r[i], r[i + 1], r[i + 2], r[i + 3])) + w_s = np.asarray( + ( + (2, -16, 0, 16, -2, 0), + (-1, 16, -30, 16, -1, 0), + (-9, 39, -70, 66, -33, 7), + (13, -64, 126, -124, 61, -12), + (-5, 25, -50, 50, -25, 5), + ) + ) + a = np.dot(w_s, r_s) / 24 + + # Fancy vector code here... use underlying numpy structures to accelerate + # parts of the linear algebra. + y = r[i] + (a.reshape(5, -1) * X ** np.arange(1, 6).reshape(-1, 1)).sum(axis=0) + if y.size == 1: + return y[0] return y def _validate_dimensions(self): diff --git a/colour/colorimetry/generation.py b/colour/colorimetry/generation.py index f55033953..2dbda755e 100644 --- a/colour/colorimetry/generation.py +++ b/colour/colorimetry/generation.py @@ -465,8 +465,8 @@ def sd_gaussian_fwhm( SpectralShape(360.0, 780.0, 1.0) >>> sd[555] # doctest: +SKIP 1.0 - >>> sd[530] - 0.0625 + >>> sd[530] # doctest: +ELLIPSIS + 0.062... """ settings = {"name": f"{peak_wavelength}nm - {fwhm} FWHM - Gaussian"} @@ -543,8 +543,8 @@ def sd_gaussian( SpectralShape(360.0, 780.0, 1.0) >>> sd[555] # doctest: +SKIP 1.0 - >>> sd[530] - 0.0625 + >>> sd[530] # doctest: +ELLIPSIS + 0.062... """ method = validate_method(method, tuple(SD_GAUSSIAN_METHODS)) diff --git a/colour/colorimetry/lefs.py b/colour/colorimetry/lefs.py index b111c025e..322759c50 100644 --- a/colour/colorimetry/lefs.py +++ b/colour/colorimetry/lefs.py @@ -78,7 +78,7 @@ def mesopic_weighting_function( Examples -------- >>> mesopic_weighting_function(500, 0.2) # doctest: +ELLIPSIS - 0.7052200... + 0.7052... """ photopic_lef = optional( diff --git a/colour/models/rgb/transfer_functions/gamma.py b/colour/models/rgb/transfer_functions/gamma.py index 49eb6d7fc..153ddf632 100644 --- a/colour/models/rgb/transfer_functions/gamma.py +++ b/colour/models/rgb/transfer_functions/gamma.py @@ -25,15 +25,87 @@ __all__ = [ "gamma_function", + "GammaFunction", ] +NegativeNumberHandlingType = ( + Literal["Clamp", "Indeterminate", "Mirror", "Preserve"] | str +) + + +class GammaFunction: + """Provides an object oriented interface to contain optional parameters for + an underlying :func:gamma_function call. Useful for providing both a simpler + and constructed api for gamma_function as well as allowing for control flow. + """ + + def __init__( + self, + exponent: float = 1, + negative_number_handling: NegativeNumberHandlingType = "Indeterminate", + ): + """ + Construct an object oriented interface to contain optional parameters for + an underlying :func:gamma_function call. Useful for providing both a simpler + and constructed api for gamma_function as well as allowing for control flow. + + Parameters + ---------- + exponent : float, optional + The exponent value in a^b, by default 1 + negative_number_handling : NegativeNumberHandlingType, optional + Defines the behavior for negative number handling, by default + "Indeterminate" + + See Also + -------- + :func:gamma_function + """ + self._exponent = exponent + self._negative_number_handling = negative_number_handling + + @property + def exponent(self) -> float: + """The exponent, b, in the function a^b + + Returns + ------- + float + """ + return self._exponent + + @property + def negative_number_handling(self) -> NegativeNumberHandlingType: + """How to treat negative numbers. See also :func:gamma_function + + Returns + ------- + NegativeNumberHandlingType + See also :func:gamma_function + """ + return self._negative_number_handling + + def __call__(self, a: ArrayLike): + """Calculate a typical encoding / decoding function on `a`. Representative + of the function a ^ b where b is determined by the instance value of + `exponent` and negative handling behavior is defined by the instance + value `negative_number_handling`. See also :func:gamma_function + + Parameters + ---------- + a : ArrayLike + """ + return gamma_function( + a, + exponent=self.exponent, + negative_number_handling=self.negative_number_handling, + ) + def gamma_function( a: ArrayLike, exponent: ArrayLike = 1, - negative_number_handling: ( - Literal["Clamp", "Indeterminate", "Mirror", "Preserve"] | str - ) = "Indeterminate", + negative_number_handling: NegativeNumberHandlingType = "Indeterminate", ) -> NDArrayFloat: """ Define a typical gamma encoding / decoding function. diff --git a/colour/models/rgb/transfer_functions/tests/test_gamma.py b/colour/models/rgb/transfer_functions/tests/test_gamma.py index 05a7e1cb9..41c4d2245 100644 --- a/colour/models/rgb/transfer_functions/tests/test_gamma.py +++ b/colour/models/rgb/transfer_functions/tests/test_gamma.py @@ -3,11 +3,11 @@ :mod:`colour.models.rgb.transfer_functions.gamma` module. """ - import numpy as np from colour.constants import TOLERANCE_ABSOLUTE_TESTS from colour.models.rgb.transfer_functions import gamma_function +from colour.models.rgb.transfer_functions.gamma import GammaFunction from colour.utilities import ignore_numpy_errors __author__ = "Colour Developers" @@ -22,6 +22,199 @@ ] +class TestGammaFunctionClass: + def test_gamma_function_class(self): + """ + Test :func:`colour.models.rgb.transfer_functions.gamma.\ + gamma_function` definition. + """ + + np.testing.assert_allclose( + GammaFunction(2.2)(0.0), 0.0, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + np.testing.assert_allclose( + GammaFunction(2.2)(0.18), + 0.022993204992707, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(1.0 / 2.2)(0.022993204992707), + 0.18, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(2.0)(-0.18), + 0.0323999999999998, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_array_equal(GammaFunction(2.2)(-0.18), np.nan) + + np.testing.assert_allclose( + GammaFunction(2.2, "Mirror")(-0.18), + -0.022993204992707, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(-0.18), + -0.18, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(-0.18), + 0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_array_equal(GammaFunction(-2.2)(-0.18), np.nan) + + np.testing.assert_allclose( + GammaFunction(-2.2, "Mirror")(0.0), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(0.0), + 0.0, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(0.0), 0, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + def test_n_dimensional_gamma_function(self): + """ + Test :func:`colour.models.rgb.transfer_functions.gamma.\ +gamma_function` definition n-dimensional arrays support. + """ + + a = 0.18 + a_p = GammaFunction(2.2)(a) + + a = np.tile(a, 6) + a_p = np.tile(a_p, 6) + np.testing.assert_allclose( + GammaFunction(2.2)(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = np.reshape(a, (2, 3)) + a_p = np.reshape(a_p, (2, 3)) + np.testing.assert_allclose( + GammaFunction(2.2)(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = np.reshape(a, (2, 3, 1)) + a_p = np.reshape(a_p, (2, 3, 1)) + np.testing.assert_allclose( + GammaFunction(2.2)(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = -0.18 + a_p = -0.022993204992707 + np.testing.assert_allclose( + GammaFunction(2.2, "Mirror")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.tile(a, 6) + a_p = np.tile(a_p, 6) + np.testing.assert_allclose( + GammaFunction(2.2, "Mirror")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.reshape(a, (2, 3)) + a_p = np.reshape(a_p, (2, 3)) + np.testing.assert_allclose( + GammaFunction(2.2, "Mirror")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.reshape(a, (2, 3, 1)) + a_p = np.reshape(a_p, (2, 3, 1)) + np.testing.assert_allclose( + GammaFunction(2.2, "Mirror")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = -0.18 + a_p = -0.18 + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.tile(a, 6) + a_p = np.tile(a_p, 6) + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.reshape(a, (2, 3)) + a_p = np.reshape(a_p, (2, 3)) + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = np.reshape(a, (2, 3, 1)) + a_p = np.reshape(a_p, (2, 3, 1)) + np.testing.assert_allclose( + GammaFunction(2.2, "Preserve")(a), + a_p, + atol=TOLERANCE_ABSOLUTE_TESTS, + ) + + a = -0.18 + a_p = 0.0 + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = np.tile(a, 6) + a_p = np.tile(a_p, 6) + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = np.reshape(a, (2, 3)) + a_p = np.reshape(a_p, (2, 3)) + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + a = np.reshape(a, (2, 3, 1)) + a_p = np.reshape(a_p, (2, 3, 1)) + np.testing.assert_allclose( + GammaFunction(2.2, "Clamp")(a), a_p, atol=TOLERANCE_ABSOLUTE_TESTS + ) + + @ignore_numpy_errors + def test_nan_gamma_function(self): + """ + Test :func:`colour.models.rgb.transfer_functions.gamma.\ +gamma_function` definition nan support. + """ + + cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan] + GammaFunction(cases)(cases) + + class TestGammaFunction: """ Define :func:`colour.models.rgb.transfer_functions.gamma.gamma_function`