Skip to content

Commit 0849832

Browse files
authored
Merge pull request #88 from ampas/feature/prelinearized
Pre Linearised Camera & Fix For Formatting Of CLF
2 parents 36544fc + 12df746 commit 0849832

File tree

8 files changed

+222
-24
lines changed

8 files changed

+222
-24
lines changed

aces/idt/core/common.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ def clf_processing_elements(
420420
multipliers: ArrayLike,
421421
k_factor: float,
422422
use_range: bool = True,
423+
include_white_balance_in_clf: bool = False,
423424
) -> Et.Element:
424425
"""
425426
Add the *Common LUT Format* (CLF) elements for given *IDT* matrix,
@@ -437,6 +438,8 @@ def clf_processing_elements(
437438
use_range
438439
Whether to use the range node to clamp the graph before the exposure
439440
factor :math:`k`.
441+
include_white_balance_in_clf
442+
Whether to include the white balance multipliers in the *CLF*.
440443
441444
Returns
442445
-------
@@ -445,15 +448,24 @@ def clf_processing_elements(
445448
"""
446449

447450
def format_array(a: NDArrayFloat) -> str:
448-
"""Format given array :math:`a`."""
451+
"""Format given array :math:`a` into 3 lines of 3 numbers."""
452+
# Reshape the array into a 3x3 matrix
453+
reshaped_array = a.reshape(3, 3)
449454

450-
return re.sub(r"\[|\]|,", "", "\n".join(map(str, a.tolist())))
455+
# Convert each row to a string and join them with newlines
456+
formatted_lines = [" ".join(map(str, row)) for row in reshaped_array]
451457

452-
et_RGB_w = Et.SubElement(root, "Matrix", inBitDepth="32f", outBitDepth="32f")
453-
et_description = Et.SubElement(et_RGB_w, "Description")
454-
et_description.text = "White balance multipliers *b*."
455-
et_array = Et.SubElement(et_RGB_w, "Array", dim="3 3")
456-
et_array.text = f"\n{format_array(np.diag(multipliers))}"
458+
# Join all the lines with newline characters
459+
formatted_string = "\n\t\t".join(formatted_lines)
460+
461+
return formatted_string
462+
463+
if include_white_balance_in_clf:
464+
et_RGB_w = Et.SubElement(root, "Matrix", inBitDepth="32f", outBitDepth="32f")
465+
et_description = Et.SubElement(et_RGB_w, "Description")
466+
et_description.text = "White balance multipliers *b*."
467+
et_array = Et.SubElement(et_RGB_w, "Array", dim="3 3")
468+
et_array.text = f"\n\t\t{format_array(np.diag(multipliers))}"
457469

458470
if use_range:
459471
et_range = Et.SubElement(
@@ -474,13 +486,13 @@ def format_array(a: NDArrayFloat) -> str:
474486
"the scene producing ACES values [0.18, 0.18, 0.18]."
475487
)
476488
et_array = Et.SubElement(et_k, "Array", dim="3 3")
477-
et_array.text = f"\n{format_array(np.ravel(np.diag([k_factor] * 3)))}"
489+
et_array.text = f"\n\t\t{format_array(np.ravel(np.diag([k_factor] * 3)))}"
478490

479491
et_M = Et.SubElement(root, "Matrix", inBitDepth="32f", outBitDepth="32f")
480492
et_description = Et.SubElement(et_M, "Description")
481493
et_description.text = "*Input Device Transform* (IDT) matrix *B*."
482494
et_array = Et.SubElement(et_M, "Array", dim="3 3")
483-
et_array.text = f"\n{format_array(matrix)}"
495+
et_array.text = f"\n\t\t{format_array(matrix)}"
484496

485497
return root
486498

aces/idt/core/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,14 @@ class ProjectSettingsMetadataConstants:
397397
ui_category=UICategories.HIDDEN,
398398
)
399399

400+
INCLUDE_WHITE_BALANCE_IN_CLF = Metadata(
401+
default_value=False,
402+
description="Whether to include the White Balance Matrix in the CLF",
403+
display_name="Include White Balance in CLF",
404+
ui_type=UITypes.BOOLEAN_FIELD,
405+
ui_category=UICategories.STANDARD,
406+
)
407+
400408
ALL: ClassVar[tuple[Metadata, ...]] = (
401409
SCHEMA_VERSION,
402410
CAMERA_MAKE,
@@ -427,4 +435,5 @@ class ProjectSettingsMetadataConstants:
427435
FILE_TYPE,
428436
EV_WEIGHTS,
429437
OPTIMIZATION_KWARGS,
438+
INCLUDE_WHITE_BALANCE_IN_CLF,
430439
)

aces/idt/framework/project_settings.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ def __init__(self, **kwargs: Dict):
166166
"optimization_kwargs",
167167
IDTProjectSettings.optimization_kwargs.metadata.default_value,
168168
)
169+
self._include_white_balance_in_clf = kwargs.get(
170+
"include_white_balance_in_clf",
171+
IDTProjectSettings.include_white_balance_in_clf.metadata.default_value,
172+
)
169173

170174
@metadata_property(metadata=MetadataConstants.SCHEMA_VERSION)
171175
def schema_version(self) -> str:
@@ -544,6 +548,18 @@ def optimization_kwargs(self) -> Dict:
544548

545549
return self._optimization_kwargs
546550

551+
@metadata_property(metadata=MetadataConstants.INCLUDE_WHITE_BALANCE_IN_CLF)
552+
def include_white_balance_in_clf(self) -> bool:
553+
"""
554+
Getter property for whether to include the white balance in the *CLF*.
555+
556+
Returns
557+
-------
558+
:class:`bool`
559+
Whether to include the white balance in the *CLF*.
560+
"""
561+
return self._include_white_balance_in_clf
562+
547563
def get_reference_colour_checker_samples(self) -> NDArrayFloat:
548564
"""
549565
Return the reference colour checker samples.

aces/idt/generators/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from .base_generator import IDTBaseGenerator
2+
from .prelinearized_idt import IDTGeneratorPreLinearizedCamera
23
from .prosumer_camera import IDTGeneratorProsumerCamera
34

4-
GENERATORS = {IDTGeneratorProsumerCamera.GENERATOR_NAME: IDTGeneratorProsumerCamera}
5+
GENERATORS = {
6+
IDTGeneratorProsumerCamera.GENERATOR_NAME: IDTGeneratorProsumerCamera,
7+
IDTGeneratorPreLinearizedCamera.GENERATOR_NAME: IDTGeneratorPreLinearizedCamera,
8+
}
59

610
__all__ = ["IDTBaseGenerator"]
711
__all__ += ["IDTGeneratorProsumerCamera"]
12+
__all__ += ["IDTGeneratorPreLinearizedCamera"]
813
__all__ += ["GENERATORS"]

aces/idt/generators/base_generator.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ def to_clf(self, output_directory: Path | str) -> str:
692692
def format_array(a):
693693
"""Format given array :math:`a`."""
694694

695-
return re.sub(r"\[|\]|,", "", "\n".join(map(str, a.tolist())))
695+
return re.sub(r"\[|\]|,", "", "\n\t\t".join(map(str, a.tolist())))
696696

697697
et_input_descriptor = Et.SubElement(root, "InputDescriptor")
698698
et_input_descriptor.text = f"{camera_make} {camera_model}"
@@ -712,21 +712,32 @@ def format_array(a):
712712
)
713713
sub_element.text = str(value)
714714

715-
et_lut = Et.SubElement(
716-
root,
717-
"LUT1D",
718-
inBitDepth="32f",
719-
outBitDepth="32f",
720-
interpolation="linear",
721-
)
722715
LUT_decoding = self._LUT_decoding
723-
channels = 1 if isinstance(LUT_decoding, LUT1D) else 3
724-
et_description = Et.SubElement(et_lut, "Description")
725-
et_description.text = f"Linearisation *{LUT_decoding.__class__.__name__}*."
726-
et_array = Et.SubElement(et_lut, "Array", dim=f"{LUT_decoding.size} {channels}")
727-
et_array.text = f"\n{format_array(LUT_decoding.table)}"
716+
if LUT_decoding:
717+
et_lut = Et.SubElement(
718+
root,
719+
"LUT1D",
720+
inBitDepth="32f",
721+
outBitDepth="32f",
722+
interpolation="linear",
723+
)
728724

729-
root = clf_processing_elements(root, self._M, self._RGB_w, self._k, False)
725+
channels = 1 if isinstance(LUT_decoding, LUT1D) else 3
726+
et_description = Et.SubElement(et_lut, "Description")
727+
et_description.text = f"Linearisation *{LUT_decoding.__class__.__name__}*."
728+
et_array = Et.SubElement(
729+
et_lut, "Array", dim=f"{LUT_decoding.size} {channels}"
730+
)
731+
et_array.text = f"\n\t\t{format_array(LUT_decoding.table)}"
732+
733+
root = clf_processing_elements(
734+
root,
735+
self._M,
736+
self._RGB_w,
737+
self._k,
738+
False,
739+
self.project_settings.include_white_balance_in_clf,
740+
)
730741

731742
clf_path = (
732743
f"{output_directory}/"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
IDT Prosumer Camera Generator
3+
=============================
4+
5+
Define the *IDT* generator class for a *Prosumer Camera*.
6+
"""
7+
8+
import logging
9+
10+
import matplotlib as mpl
11+
from colour import LUT3x1D
12+
from colour.utilities import as_float_array
13+
14+
from aces.idt import DirectoryStructure
15+
from aces.idt.generators.prosumer_camera import IDTGeneratorProsumerCamera
16+
17+
# TODO are the mpl.use things needed in every file?
18+
mpl.use("Agg")
19+
20+
__author__ = "Alex Forsythe, Joshua Pines, Thomas Mansencal, Nick Shaw, Adam Davis"
21+
__copyright__ = "Copyright 2022 Academy of Motion Picture Arts and Sciences"
22+
__license__ = "Academy of Motion Picture Arts and Sciences License Terms"
23+
__maintainer__ = "Academy of Motion Picture Arts and Sciences"
24+
__email__ = "acessupport@oscars.org"
25+
__status__ = "Production"
26+
27+
__all__ = [
28+
"IDTGeneratorPreLinearizedCamera",
29+
]
30+
31+
LOGGER = logging.getLogger(__name__)
32+
33+
34+
class IDTGeneratorPreLinearizedCamera(IDTGeneratorProsumerCamera):
35+
"""
36+
Define an *IDT* generator for a *PreLinearized Camera*.
37+
38+
Parameters
39+
----------
40+
project_settings : IDTProjectSettings, optional
41+
*IDT* generator settings.
42+
43+
Attributes
44+
----------
45+
- :attr:`~aces.idt.IDTBaseGenerator.GENERATOR_NAME`
46+
47+
Methods
48+
-------
49+
- :meth:`~aces.idt.IDTBaseGenerator.generate_LUT`
50+
- :meth:`~aces.idt.IDTBaseGenerator.filter_LUT`
51+
"""
52+
53+
GENERATOR_NAME = "IDTGeneratorPreLinearizedCamera"
54+
"""*IDT* generator name."""
55+
56+
def __init__(self, project_settings):
57+
super().__init__(project_settings)
58+
59+
def generate_LUT(self) -> LUT3x1D:
60+
"""
61+
Generate an unfiltered linearisation *LUT* for the camera samples.
62+
63+
The *LUT* generation process is worth describing, the camera samples are
64+
unlikely to cover the [0, 1] domain and thus need to be extrapolated.
65+
66+
Two extrapolated datasets are generated:
67+
68+
- Linearly extrapolated for the left edge missing data whose
69+
clipping likelihood is low and thus can be extrapolated safely.
70+
- Constant extrapolated for the right edge missing data whose
71+
clipping likelihood is high and thus cannot be extrapolated
72+
safely
73+
74+
Because the camera encoded data response is logarithmic, the slope of
75+
the center portion of the data is computed and fitted. The fitted line
76+
is used to extrapolate the right edge missing data. It is blended
77+
through a smoothstep with the constant extrapolated samples. The blend
78+
is fully achieved at the right edge of the camera samples.
79+
80+
Returns
81+
-------
82+
:class:`LUT3x1D`
83+
Unfiltered linearisation *LUT* for the camera samples.
84+
"""
85+
# size = self.project_settings.lut_size
86+
# LOGGER.info('Generating unfiltered "LUT3x1D" with "%s" size...', size)
87+
# self._LUT_unfiltered = LUT3x1D(size=size, name="LUT - Unfiltered")
88+
# return self._LUT_unfiltered
89+
return None
90+
91+
def filter_LUT(self) -> LUT3x1D:
92+
"""
93+
Filter/smooth the linearisation *LUT* for the camera samples.
94+
95+
The *LUT* filtering is performed with a gaussian convolution, the sigma
96+
value represents the window size. To prevent that the edges of the
97+
*LUT* are affected by the convolution, the *LUT* is extended, i.e.
98+
extrapolated at a safe two sigmas in both directions. The left edge is
99+
linearly extrapolated while the right edge is logarithmically
100+
extrapolated.
101+
102+
Returns
103+
-------
104+
:class:`LUT3x1D`
105+
Filtered linearisation *LUT* for the camera samples.
106+
"""
107+
# LOGGER.info('No Filtering Simply Copying "LUT3x1D"')
108+
#
109+
# self._LUT_filtered = self._LUT_unfiltered.copy()
110+
# self._LUT_filtered.name = "LUT - Filtered"
111+
# return self._LUT_filtered
112+
return None
113+
114+
def decode(self) -> None:
115+
"""
116+
Decode the camera samples.
117+
118+
The camera samples are decoded using the camera's *IDT* and the
119+
*IDT* generator settings.
120+
121+
Returns
122+
-------
123+
None
124+
"""
125+
LOGGER.info("Decoding camera samples...")
126+
self._samples_decoded = {}
127+
for EV in sorted(self._samples_analysis[DirectoryStructure.COLOUR_CHECKER]):
128+
self._samples_decoded[EV] = as_float_array(
129+
self._samples_analysis[DirectoryStructure.COLOUR_CHECKER][EV][
130+
"samples_median"
131+
]
132+
)

tests/resources/example_from_folder.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"aces_user_name": "",
66
"camera_make": "",
77
"camera_model": "",
8+
"include_white_balance_in_clf": false,
89
"iso": 800,
910
"temperature": 6000,
1011
"additional_camera_settings": "",

tests/test_application.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,3 +1226,15 @@ def test_prosumer_generator_from_archive_zip(self):
12261226
self.get_test_output_folder(), archive_serialised_generator=False
12271227
)
12281228
self.assertEqual(os.path.exists(zip_file), True)
1229+
1230+
def test_prelinearized_idt_generator_from_archive_zip(self):
1231+
"""Test the prosumer generator from archive with a json file"""
1232+
idt_application = IDTGeneratorApplication()
1233+
idt_application.generator = "IDTGeneratorPreLinearizedCamera"
1234+
1235+
archive = os.path.join(self.get_test_resources_folder(), "FULL_STOPS_EXR.zip")
1236+
idt_application.process(archive)
1237+
zip_file = idt_application.zip(
1238+
self.get_test_output_folder(), archive_serialised_generator=False
1239+
)
1240+
self.assertEqual(os.path.exists(zip_file), True)

0 commit comments

Comments
 (0)