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`