Skip to content

Commit 828b1e3

Browse files
Port code from PR in colour.
1 parent 1d8bb31 commit 828b1e3

File tree

14 files changed

+68811
-2
lines changed

14 files changed

+68811
-2
lines changed

colour_clf_io/processing.py

Lines changed: 880 additions & 0 deletions
Large diffs are not rendered by default.

colour_clf_io/tests/processing/__init__.py

Whitespace-only changes.

colour_clf_io/tests/processing/resources/LMT Kodak 2383 Print Emulation.xml

Lines changed: 1315 additions & 0 deletions
Large diffs are not rendered by default.

colour_clf_io/tests/processing/resources/lut1_with_half_domain_sample.xml

Lines changed: 65543 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# !/usr/bin/env python
2+
"""Define the unit tests for the :mod:`colour.io.clf` module."""
3+
4+
import unittest
5+
6+
__author__ = "Colour Developers"
7+
__copyright__ = "Copyright 2013 Colour Developers"
8+
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
9+
__maintainer__ = "Colour Developers"
10+
__email__ = "colour-developers@colour-science.org"
11+
__status__ = "Production"
12+
13+
import numpy as np
14+
15+
from colour_clf_io.tests.processing.test_common import (
16+
assert_ocio_consistency,
17+
rgb_sample_iter,
18+
)
19+
20+
21+
def assert_snippet_consistency(snippet: str) -> None:
22+
"""
23+
Evaluate the snippet with multiple values anc check that they are the same as the
24+
`ociochecklut` tools output.
25+
"""
26+
for rgb in rgb_sample_iter():
27+
value_rgb = np.array(rgb)
28+
assert_ocio_consistency(
29+
value_rgb, snippet, f"Failed to assert consistency for {rgb}"
30+
)
31+
32+
33+
class TestASC_CDL:
34+
"""
35+
Define test for applying Exponent nodes from a CLF file.
36+
"""
37+
38+
def test_ocio_consistency_fwd(self) -> None:
39+
"""
40+
Test that the execution is consistent with the OCIO reference.
41+
"""
42+
43+
example = """
44+
<ASC_CDL id="cc01234" inBitDepth="16f" outBitDepth="16f" style="Fwd">
45+
<SOPNode>
46+
<Slope>1.000000 1.000000 0.900000</Slope>
47+
<Offset>-0.030000 -0.020000 0.000000</Offset>
48+
<Power>1.2500000 1.000000 1.000000</Power>
49+
</SOPNode>
50+
<SatNode>
51+
<Saturation>1.700000</Saturation>
52+
</SatNode>
53+
</ASC_CDL>
54+
"""
55+
assert_snippet_consistency(example)
56+
57+
def test_ocio_consistency_rev(self) -> None:
58+
"""
59+
Test that the execution is consistent with the OCIO reference.
60+
"""
61+
62+
example = """
63+
<ASC_CDL id="cc01234" inBitDepth="16f" outBitDepth="16f" style="Rev">
64+
<SOPNode>
65+
<Slope>1.000000 1.000000 0.900000</Slope>
66+
<Offset>-0.030000 -0.020000 0.000000</Offset>
67+
<Power>1.2500000 1.000000 1.000000</Power>
68+
</SOPNode>
69+
<SatNode>
70+
<Saturation>1.700000</Saturation>
71+
</SatNode>
72+
</ASC_CDL>
73+
"""
74+
assert_snippet_consistency(example)
75+
76+
def test_ocio_consistency_fwd_no_clamp(self) -> None:
77+
"""
78+
Test that the execution is consistent with the OCIO reference.
79+
"""
80+
81+
example = """
82+
<ASC_CDL id="cc01234" inBitDepth="16f" outBitDepth="16f" style="FwdNoClamp">
83+
<SOPNode>
84+
<Slope>1.000000 1.000000 0.900000</Slope>
85+
<Offset>-0.030000 -0.020000 0.000000</Offset>
86+
<Power>1.2500000 1.000000 1.000000</Power>
87+
</SOPNode>
88+
<SatNode>
89+
<Saturation>1.700000</Saturation>
90+
</SatNode>
91+
</ASC_CDL>
92+
"""
93+
assert_snippet_consistency(example)
94+
95+
def test_ocio_consistency_rev_no_clamp(self) -> None:
96+
"""
97+
Test that the execution is consistent with the OCIO reference.
98+
"""
99+
100+
example = """
101+
<ASC_CDL id="cc01234" inBitDepth="16f" outBitDepth="16f" style="RevNoClamp">
102+
<SOPNode>
103+
<Slope>1.000000 1.000000 0.900000</Slope>
104+
<Offset>-0.030000 -0.020000 0.000000</Offset>
105+
<Power>1.2500000 1.000000 1.000000</Power>
106+
</SOPNode>
107+
<SatNode>
108+
<Saturation>1.700000</Saturation>
109+
</SatNode>
110+
</ASC_CDL>
111+
"""
112+
assert_snippet_consistency(example)
113+
114+
def test_ocio_consistency_default_args(self) -> None:
115+
"""
116+
Test that the execution is consistent with the OCIO reference.
117+
"""
118+
119+
example = """
120+
<ASC_CDL id="cc01234" inBitDepth="16f" outBitDepth="16f" style="Fwd">
121+
</ASC_CDL>
122+
"""
123+
assert_snippet_consistency(example)
124+
125+
126+
if __name__ == "__main__":
127+
unittest.main()
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Defines helper functionality for CLF tests.
3+
"""
4+
5+
import os
6+
import tempfile
7+
from collections.abc import Generator
8+
from typing import Any
9+
10+
import numpy as np
11+
12+
import colour_clf_io as clf
13+
from colour_clf_io.processing import apply
14+
15+
__all__ = [
16+
"assert_ocio_consistency",
17+
"assert_ocio_consistency_for_file",
18+
"snippet_to_process_list",
19+
]
20+
21+
from colour.hints import NDArrayFloat
22+
23+
EXAMPLE_WRAPPER = """<?xml version="1.0" ?>
24+
<ProcessList id="Example Wrapper" compCLFversion="3.0">
25+
{0}
26+
</ProcessList>
27+
"""
28+
29+
RESOURCES_ROOT: str = os.path.join(os.path.dirname(__file__), "resources")
30+
31+
32+
def wrap_snippet(snippet: str) -> str:
33+
"""# noqa: D401
34+
Takes a string that should contain the text representation of a CLF node, and
35+
returns valid CLF document. Essentially the given string is pasted into the
36+
`ProcessList` if a CLF document.
37+
38+
This is useful to quickly convert example snippets of Process Nodes into valid CLF
39+
documents for parsing.
40+
"""
41+
return EXAMPLE_WRAPPER.format(snippet)
42+
43+
44+
def snippet_to_process_list(snippet: str) -> clf.ProcessList | None:
45+
"""# noqa: D401
46+
Takes a string that should contain a valid body for a XML Process List and
47+
returns the parsed `ProcessList`.
48+
"""
49+
doc = wrap_snippet(snippet)
50+
return clf.read_clf(doc)
51+
52+
53+
def snippet_as_tmp_file(snippet: str) -> str:
54+
doc = wrap_snippet(snippet)
55+
tmp_folder = tempfile.gettempdir()
56+
tmp_file_name = tempfile.mktemp(suffix=".clf") # noqa: S306
57+
file_name = os.path.join(tmp_folder, tmp_file_name)
58+
with open(file_name, "w") as f:
59+
f.write(doc)
60+
return file_name
61+
62+
63+
def ocio_output_for_file(
64+
path: str, rgb: tuple[float, float, float] | NDArrayFloat | None = None
65+
) -> tuple[float, float, float]:
66+
"""Apply a color transform file to a flattened, one-dimensional list of
67+
R,G,B values.
68+
"""
69+
from PyOpenColorIO import FileTransform, GetCurrentConfig
70+
71+
xform = FileTransform(src=path)
72+
cpu = GetCurrentConfig().getProcessor(xform).getDefaultCPUProcessor()
73+
result = cpu.applyRGB(rgb)
74+
# Note: depending on the input, `applyRGB` will either return the result data, or
75+
# modify the data in place. If the return value was `None` the data was modified
76+
# in place and we use that instead.
77+
if result is None:
78+
result = rgb
79+
assert result is not None
80+
return (result[0], result[1], result[2])
81+
82+
83+
def ocio_output_for_snippet(
84+
snippet: str, rgb: tuple[float, float, float]
85+
) -> NDArrayFloat:
86+
f = snippet_as_tmp_file(snippet)
87+
try:
88+
r, g, b = ocio_output_for_file(f, rgb)
89+
return np.array([r, g, b])
90+
finally:
91+
os.remove(f)
92+
93+
94+
def result_as_array(result_text: str) -> NDArrayFloat:
95+
result_parts = result_text.strip().split()
96+
if len(result_parts) != 3:
97+
message = f"Invalid OCIO result: {result_text}"
98+
raise RuntimeError(message)
99+
result_values = list(map(float, result_parts))
100+
return np.array(result_values)
101+
102+
103+
def assert_ocio_consistency(
104+
value: NDArrayFloat, snippet: str, err_msg: str = "", decimals: int = 5
105+
) -> None:
106+
"""Assert that the colour library calculates the same output as the OCIO reference
107+
implementation for the given CLF snippet.
108+
"""
109+
process_list = snippet_to_process_list(snippet)
110+
if process_list is None:
111+
err = "Invalid CLF snippet."
112+
raise AssertionError(err)
113+
process_list_output = apply(process_list, value, normalised_values=True)
114+
value_tuple = value[0], value[1], value[2]
115+
ocio_output = ocio_output_for_snippet(snippet, value_tuple)
116+
# Note: OCIO only accepts 16-bit floats so the precision agreement is limited.
117+
np.testing.assert_array_almost_equal(
118+
process_list_output, ocio_output, err_msg=err_msg, decimal=decimals
119+
)
120+
121+
122+
def assert_ocio_consistency_for_file(value_rgb: NDArrayFloat, clf_path: str) -> None:
123+
"""Assert that the colour library calculates the same output as the OCIO reference
124+
implementation for the given file.
125+
"""
126+
127+
clf_data = clf.read_clf_from_file(clf_path)
128+
if clf_data is None:
129+
err = "Invalid CLF snippet."
130+
raise AssertionError(err)
131+
process_list_output = apply(clf_data, value_rgb, normalised_values=True)
132+
ocio_output = ocio_output_for_file(clf_path, value_rgb)
133+
np.testing.assert_array_almost_equal(process_list_output, ocio_output)
134+
135+
136+
def rgb_sample_iter(
137+
step: float = 0.2,
138+
) -> Generator[tuple[np.floating[Any], np.floating[Any], np.floating[Any]]]:
139+
for r in np.arange(0.0, 1.0, step):
140+
for g in np.arange(0.0, 1.0, step):
141+
for b in np.arange(0.0, 1.0, step):
142+
yield r, g, b

0 commit comments

Comments
 (0)