Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add RGB_Colorspace comparisons #1274

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions colour/models/rgb/rgb_colourspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from __future__ import annotations

from copy import deepcopy
from functools import partial

import numpy as np

Expand Down Expand Up @@ -277,6 +278,63 @@ def __init__(
self._use_derived_matrix_XYZ_to_RGB: bool = False
self.use_derived_matrix_XYZ_to_RGB = use_derived_matrix_XYZ_to_RGB

def __eq__(self, other: object) -> bool:
"""Return weather or not two RGB spaces are equivalent and would produce
the same results with XYZ_to_RGB and visa-versa. Can detect and compare
instances of `partial` for the cctf properties.

Parameters
----------
other : object

Returns
-------
bool
"""
if not isinstance(other, RGB_Colourspace):
return False

cctf_decoding_eq: bool
if isinstance(self.cctf_decoding, partial) and isinstance(
other.cctf_decoding, partial
):
cctf_decoding_eq = np.all(
(
self.cctf_decoding.func == other.cctf_decoding.func,
np.all(self.cctf_decoding.args == other.cctf_decoding.args),
np.all(self.cctf_decoding.keywords == other.cctf_decoding.keywords),
)
)
else:
cctf_decoding_eq = self.cctf_decoding == other.cctf_decoding

cctf_encoding_eq: bool
if isinstance(self.cctf_encoding, partial) and isinstance(
other.cctf_encoding, partial
):
cctf_encoding_eq = np.all(
(
self.cctf_encoding.func == other.cctf_encoding.func,
np.all(self.cctf_encoding.args == other.cctf_encoding.args),
np.all(self.cctf_encoding.keywords == other.cctf_encoding.keywords),
)
)
else:
cctf_encoding_eq = self.cctf_encoding == other.cctf_encoding

return np.all(
(
self.name == other.name,
np.all(self.primaries == other.primaries),
np.all(self.whitepoint == self.whitepoint),
self.whitepoint_name == self.whitepoint_name,
np.all(self.matrix_RGB_to_XYZ == other.matrix_RGB_to_XYZ),
np.all(self.matrix_XYZ_to_RGB == other.matrix_XYZ_to_RGB),
cctf_decoding_eq,
cctf_encoding_eq,
)
)

@property
def name(self) -> str:
"""
Expand Down
74 changes: 74 additions & 0 deletions colour/models/rgb/tests/test_rgb_colourspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import re
import textwrap
from functools import partial
from itertools import product

import numpy as np
Expand All @@ -24,6 +25,7 @@
matrix_RGB_to_RGB,
normalised_primary_matrix,
)
from colour.models.rgb.transfer_functions.gamma import gamma_function
from colour.utilities import domain_range_scale, ignore_numpy_errors

__author__ = "Colour Developers"
Expand Down Expand Up @@ -66,6 +68,77 @@ def setup_method(self):
linear_function,
)

class TestRGBSpace__eq__:
def setup_method(self):
pass
# Some pytest possible issue requires this since the encapsulating
# class has a setup_method.

@staticmethod
def get_two_rgb_spaces():
p = np.array([0.73470, 0.26530, 0.00000, 1.00000, 0.00010, -0.07700])
whitepoint = np.array([0.32168, 0.33767])
matrix_RGB_to_XYZ = np.identity(3)
matrix_XYZ_to_RGB = np.identity(3)

s1 = RGB_Colourspace(
"RGB Colourspace",
p,
whitepoint,
"ACES",
matrix_RGB_to_XYZ,
matrix_XYZ_to_RGB,
linear_function,
linear_function,
)

s2 = RGB_Colourspace(
"RGB Colourspace",
p,
whitepoint,
"ACES",
matrix_RGB_to_XYZ,
matrix_XYZ_to_RGB,
linear_function,
linear_function,
)

return s1, s2

def test_simple_eq(self):
s1, s2 = self.get_two_rgb_spaces()

assert s1 is not s2
assert s1 == s2

# Even if one space uses derived, if they return the same matrix
# they are equivalent
s2.use_derived_matrix_RGB_to_XYZ = True
s1.matrix_RGB_to_XYZ = s2.matrix_RGB_to_XYZ
s1.matrix_XYZ_to_RGB = s2.matrix_XYZ_to_RGB

assert s1 == s2

def test_partial_cctf(self):
s1, s2 = self.get_two_rgb_spaces()

s1.cctf_decoding = partial(gamma_function, exponent=2)
s1.cctf_encoding = partial(gamma_function, exponent=1 / 2)
assert s1 != s2

s2.cctf_encoding = partial(gamma_function, exponent=1 / 2)
assert s1 != s2

s2.cctf_decoding = partial(gamma_function, exponent=1.5)
assert s1 != s2

s2.cctf_decoding = partial(gamma_function, 2)
# Should be equal, but because one partial uses kwargs it is an error
assert s1 != s2

s2.cctf_decoding = partial(gamma_function, exponent=2)
assert s1 == s2

def test_required_attributes(self):
"""Test the presence of required attributes."""

Expand All @@ -92,6 +165,7 @@ def test_required_methods(self):
"__init__",
"__str__",
"__repr__",
"__eq__",
"use_derived_transformation_matrices",
"chromatically_adapt",
"copy",
Expand Down
98 changes: 95 additions & 3 deletions colour/models/rgb/transfer_functions/gamma.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,107 @@

__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 __eq__(self, other: object) -> bool:
"""Return if two gamma functions have the same parameters and therefore
produce the same results

Parameters
----------
other : object

Returns
-------
bool
"""
if not isinstance(other, GammaFunction):
return False

return (
self.exponent == other.exponent
and self.negative_number_handling == other.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.
Expand Down
Loading
Loading