diff --git a/aces/idt/core/common.py b/aces/idt/core/common.py index c5ce5e2..e5a3242 100644 --- a/aces/idt/core/common.py +++ b/aces/idt/core/common.py @@ -471,6 +471,10 @@ def format_array(a: NDArrayFloat) -> str: return "\n\t\t".join(formatted_lines) + LOGGER.info(f"flatten_clf: {flatten_clf}") + LOGGER.info(f"include_white_balance: {include_white_balance}") + LOGGER.info(f"include_exposure_factor: {include_exposure_factor}") + if not flatten_clf: if include_white_balance: et_RGB_w = ET.SubElement( @@ -507,6 +511,8 @@ def format_array(a: NDArrayFloat) -> str: clf_matrix = matrix + LOGGER.debug(f"not_flatten_clf_matrix: {clf_matrix}") + else: if not include_exposure_factor: k_factor = 1.0 @@ -516,6 +522,8 @@ def format_array(a: NDArrayFloat) -> str: if include_white_balance: clf_matrix = np.matmul(np.diag(multipliers), clf_matrix) + LOGGER.debug(f"flatten_clf_matrix: {clf_matrix}") + et_M = ET.SubElement(root, "Matrix", inBitDepth="32f", outBitDepth="32f") et_description = ET.SubElement(et_M, "Description") et_description.text = "*Input Device Transform* (IDT) matrix *B*." @@ -956,6 +964,7 @@ def calculate_camera_npm_and_primaries_wp( target_white_point: str = "D65", chromatic_adaptation_transform: LiteralChromaticAdaptationTransform | str = "Bradford", + custom_illuminant_sd: SpectralDistribution | None = None, ) -> Tuple[np.array, np.array, np.array]: """ Calculate the camera's normalised primary (NPM) matrix, i.e., RGB to @@ -969,6 +978,9 @@ def calculate_camera_npm_and_primaries_wp( Target whitepoint to calculate the camera's NPM matrix for. chromatic_adaptation_transform *Chromatic adaptation* transform. + custom_illuminant_sd + Custom illuminant spectral distribution. This is required if + `target_white_point` is *Custom*. Returns ------- @@ -982,10 +994,27 @@ def calculate_camera_npm_and_primaries_wp( observer = "CIE 1931 2 Degree Standard Observer" source_whitepoint_xy = colour.CCS_ILLUMINANTS[observer]["D60"] - target_whitepoint_xy = colour.CCS_ILLUMINANTS[observer][target_white_point] - source_whitepoint_XYZ = colour.xy_to_XYZ(source_whitepoint_xy) - target_whitepoint_XYZ = colour.xy_to_XYZ(target_whitepoint_xy) + + LOGGER.debug(f"target_white_point: {target_white_point}") + if target_white_point == "Custom": + if custom_illuminant_sd is None: + raise ValueError( + "A custom illuminant spectral distribution must be provided " + "when 'target_white_point' is 'Custom'." + ) + # Convert from XYZ to xy and back to XYZ to normalize the Y component. Although directly dividing by Y would suffice, + # the native function from the colour library is used to minimize potential additional errors. + # Using sd_to_XYZ_integration instead of the default ASTM E308 method of sd_to_XYZ yields more scientifically accurate source XYZ values. + target_whitepoint_XYZ = colour.xy_to_XYZ( + colour.XYZ_to_xy(colour.colorimetry.sd_to_XYZ_integration(custom_illuminant_sd))) + target_whitepoint_xy_debug = colour.XYZ_to_xy(target_whitepoint_XYZ) + LOGGER.debug(f"Custom target_whitepoint_xy_debug: {target_whitepoint_xy_debug}") + LOGGER.debug(f"Custom target_whitepoint_XYZ: {target_whitepoint_XYZ}") + else: + target_whitepoint_xy = colour.CCS_ILLUMINANTS[observer][target_white_point] + LOGGER.debug(f"target_whitepoint_xy: {target_whitepoint_xy}") + target_whitepoint_XYZ = colour.xy_to_XYZ(target_whitepoint_xy) cat_matrix = colour.adaptation.matrix_chromatic_adaptation_VonKries( source_whitepoint_XYZ, diff --git a/aces/idt/framework/project_settings.py b/aces/idt/framework/project_settings.py index 7955455..4c9b080 100644 --- a/aces/idt/framework/project_settings.py +++ b/aces/idt/framework/project_settings.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from colour import SpectralDistribution from colour.hints import Dict, NDArrayFloat from colour.utilities import as_float_array, multiline_str @@ -149,6 +150,7 @@ def __init__(self, **kwargs: Dict) -> None: IDTProjectSettings.lut_smoothing.metadata.name, IDTProjectSettings.lut_smoothing.metadata.default_value, ) + self._custom_illuminant = None self._data = IDTProjectSettings.data.metadata.default_value @@ -628,6 +630,32 @@ def include_exposure_factor_in_clf(self) -> bool: return self._include_exposure_factor_in_clf + @property + def custom_illuminant(self) -> SpectralDistribution | None: + """ + Getter property for the custom illuminant spectral distribution. + + Returns + ------- + :class:`colour.SpectralDistribution` or :py:data:`None` + Custom illuminant spectral distribution. + """ + + return self._custom_illuminant + + @custom_illuminant.setter + def custom_illuminant(self, value: SpectralDistribution): + """ + Setter property for the custom illuminant spectral distribution. + + Parameters + ---------- + value : colour.SpectralDistribution + Custom illuminant spectral distribution. + """ + + self._custom_illuminant = value + def get_reference_colour_checker_samples(self) -> NDArrayFloat: """ Return the reference colour checker samples. @@ -638,9 +666,19 @@ def get_reference_colour_checker_samples(self) -> NDArrayFloat: Reference colour checker samples. """ + if self.illuminant == "Custom": + if self.custom_illuminant is None: + raise ValueError( + "Custom illuminant data not provided for 'Custom' " + "illuminant name." + ) + illuminant_sd = self.custom_illuminant + else: + illuminant_sd = get_sds_illuminant(self.illuminant) + return generate_reference_colour_checker( get_sds_colour_checker(self.reference_colour_checker), - get_sds_illuminant(self.illuminant), + illuminant_sd, ) def get_optimization_factory(self) -> callable: diff --git a/aces/idt/generators/log_camera.py b/aces/idt/generators/log_camera.py index 8932082..ad5e82b 100644 --- a/aces/idt/generators/log_camera.py +++ b/aces/idt/generators/log_camera.py @@ -510,6 +510,7 @@ def optimise(self) -> Tuple[NDArrayFloat]: self._M, target_white_point=self.project_settings.illuminant, chromatic_adaptation_transform=self.project_settings.cat, + **({"custom_illuminant_sd": self.project_settings.custom_illuminant} if self.project_settings.illuminant == "Custom" else {}), ) return self._M, self._RGB_w, self._k diff --git a/apps/idt_calculator_camera.py b/apps/idt_calculator_camera.py index fa542b5..c8d4f9f 100644 --- a/apps/idt_calculator_camera.py +++ b/apps/idt_calculator_camera.py @@ -418,6 +418,66 @@ def _uid(id_) -> str: delay=DELAY_TOOLTIP_DEFAULT, target=_uid("ev-range-input"), ), + InputGroup( + [ + InputGroupText("Flatten CLF"), + Select( + id=_uid("flatten-clf-select"), + options=[ + {"label": "Yes", "value": "True"}, + {"label": "No", "value": "False"}, + ], + value="False", + ), + ], + className="mb-1", + ), + Tooltip( + "Whether to flatten the CLF into a single " + "1D Lut & 1 3x3 Matrix.", + delay=DELAY_TOOLTIP_DEFAULT, + target=_uid("flatten-clf-select"), + ), + InputGroup( + [ + InputGroupText("Include White Balance in CLF"), + Select( + id=_uid("include-white-balance-in-clf-select"), + options=[ + {"label": "Yes", "value": "True"}, + {"label": "No", "value": "False"}, + ], + value="True", + ), + ], + className="mb-1", + ), + Tooltip( + "Whether to include the white balance " + "multipliers in the CLF.", + delay=DELAY_TOOLTIP_DEFAULT, + target=_uid("include-white-balance-in-clf-select"), + ), + InputGroup( + [ + InputGroupText("Include Exposure Factor in CLF"), + Select( + id=_uid("include-exposure-factor-in-clf-select"), + options=[ + {"label": "Yes", "value": "True"}, + {"label": "No", "value": "False"}, + ], + value="True", + ), + ], + className="mb-1", + ), + Tooltip( + 'Whether to include the exposure factor "k" ' + "in the CLF.", + delay=DELAY_TOOLTIP_DEFAULT, + target=_uid("include-exposure-factor-in-clf-select"), + ), ], id=_uid("advanced-options-collapse"), className="mb-1", @@ -1150,6 +1210,9 @@ def toggle_modal(n_clicks, is_open): State(_uid("grey-card-reflectance"), "value"), State(_uid("lut-size-select"), "value"), State(_uid("lut-smoothing-input-number"), "value"), + State(_uid("flatten-clf-select"), "value"), + State(_uid("include-white-balance-in-clf-select"), "value"), + State(_uid("include-exposure-factor-in-clf-select"), "value"), ], prevent_initial_call=True, ) @@ -1179,6 +1242,9 @@ def compute_idt_camera( grey_card_reflectance, LUT_size, LUT_smoothing, + flatten_clf, + include_white_balance_in_clf, + include_exposure_factor_in_clf, ): """ Compute the *Input Device Transform* (IDT) for a camera. @@ -1238,6 +1304,12 @@ def compute_idt_camera( LUT_smoothing : integer Standard deviation of the gaussian convolution kernel used for smoothing. + flatten_clf : str + Whether to flatten the CLF into a single 1D Lut & 1 3x3 Matrix. + include_white_balance_in_clf : str + Whether to include the white balance multipliers in the CLF. + include_exposure_factor_in_clf : str + Whether to include the exposure factor "k" in the CLF. Returns ------- @@ -1257,7 +1329,10 @@ def compute_idt_camera( '\tEV Range : "%s"\n' '\tGrey Card Reflectance : "%s"\n' '\tLUT Size : "%s"\n' - '\tLUT Smoothing : "%s"\n', + '\tLUT Smoothing : "%s"\n' + '\tFlatten CLF : "%s"\n' + '\tInclude White Balance in CLF : "%s"\n' + '\tInclude Exposure Factor in CLF : "%s"\n', generator_name, RGB_display_colourspace, illuminant_name, @@ -1270,6 +1345,9 @@ def compute_idt_camera( grey_card_reflectance, LUT_size, LUT_smoothing, + flatten_clf, + include_white_balance_in_clf, + include_exposure_factor_in_clf, ) aces_transform_id = str(aces_transform_id) @@ -1284,6 +1362,8 @@ def compute_idt_camera( debayering_settings = str(debayering_settings) encoding_colourspace = str(encoding_colourspace or "") encoding_transfer_function = str(encoding_transfer_function or "") + LUT_size = int(LUT_size) + LUT_smoothing = int(LUT_smoothing) # Validation: Check if the inputs are valid is_valid, errors = IDTProjectSettings.validate_core_requirements( @@ -1359,7 +1439,27 @@ def compute_idt_camera( encoding_colourspace=encoding_colourspace, encoding_transfer_function=encoding_transfer_function, illuminant=illuminant_name, + rgb_display_colourspace=RGB_display_colourspace, + cat=chromatic_adaptation_transform, + optimisation_space=optimisation_space, + illuminant_interpolator=illuminant_interpolator, + decoding_method=decoding_method, + ev_range=[float(value) for value in EV_range.split(" ") if value], + grey_card_reference=[ + float(value) for value in grey_card_reflectance.split(" ") if value + ], + lut_size=LUT_size, + lut_smoothing=LUT_smoothing, + flatten_clf=(lambda v: True if v == "True" else False)(flatten_clf), + include_white_balance_in_clf=(lambda v: True if v == "True" else False)( + include_white_balance_in_clf), + include_exposure_factor_in_clf=(lambda v: True if v == "True" else False)( + include_exposure_factor_in_clf), ) + + if illuminant_name == "Custom": + project_settings.custom_illuminant = illuminant + _IDT_GENERATOR_APPLICATION = IDTGeneratorApplication( generator_name, project_settings ) @@ -1372,16 +1472,27 @@ def compute_idt_camera( _IDT_GENERATOR_APPLICATION.extract(_PATH_UPLOADED_IDT_ARCHIVE) os.remove(_PATH_UPLOADED_IDT_ARCHIVE) _IDT_GENERATOR_APPLICATION.generator.sample() + colour_checker_segmentation = ( + _IDT_GENERATOR_APPLICATION.generator.png_colour_checker_segmentation() + ) + grey_card_sampling = ( + _IDT_GENERATOR_APPLICATION.generator.png_grey_card_sampling() + ) + _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] = ( _IDT_GENERATOR_APPLICATION.project_settings.data, _IDT_GENERATOR_APPLICATION.generator.samples_analysis, _IDT_GENERATOR_APPLICATION.generator.baseline_exposure, + colour_checker_segmentation, + grey_card_sampling, ) else: ( _IDT_GENERATOR_APPLICATION.project_settings.data, _IDT_GENERATOR_APPLICATION.generator._samples_analysis, # noqa: SLF001 _IDT_GENERATOR_APPLICATION.generator._baseline_exposure, # noqa: SLF001 + colour_checker_segmentation, + grey_card_sampling, ) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] generator = _IDT_GENERATOR_APPLICATION.generator @@ -1481,7 +1592,6 @@ def RGB_working_to_RGB_display(RGB): ] # Segmentation - colour_checker_segmentation = generator.png_colour_checker_segmentation() if colour_checker_segmentation is not None: components += [ H3("Segmentation", style={"textAlign": "center"}), @@ -1490,7 +1600,6 @@ def RGB_working_to_RGB_display(RGB): style={"width": "100%"}, ), ] - grey_card_sampling = generator.png_grey_card_sampling() if grey_card_sampling is not None: components += [ Img(