From 2560c2bafe907f3eb1b1be96deb667e488305025 Mon Sep 17 00:00:00 2001 From: Thomas Mansencal Date: Sun, 16 Jun 2024 17:08:13 +1200 Subject: [PATCH] Resurrect UI functionality. --- .pre-commit-config.yaml | 2 +- aces/idt/__init__.py | 42 ++-- aces/idt/application.py | 283 ++++++++++++----------- aces/idt/core/__init__.py | 16 +- aces/idt/core/common.py | 11 +- aces/idt/core/constants.py | 70 +++--- aces/idt/core/structures.py | 71 +++--- aces/idt/framework/project_settings.py | 297 +++++++++++++++---------- aces/idt/generators/__init__.py | 2 +- aces/idt/generators/base_generator.py | 153 ++++++++----- aces/idt/generators/prosumer_camera.py | 119 ++++------ apps/common.py | 24 +- apps/idt_calculator_p_2013_001.py | 10 +- apps/idt_calculator_prosumer_camera.py | 209 +++++++---------- tests/test_application.py | 16 +- 15 files changed, 691 insertions(+), 634 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d46fa7e..19914b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: rev: 1.16.0 hooks: - id: blacken-docs - language_version: python3.9 + language_version: python3.10 - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.1.0" hooks: diff --git a/aces/idt/__init__.py b/aces/idt/__init__.py index d30496e..0c98ca0 100644 --- a/aces/idt/__init__.py +++ b/aces/idt/__init__.py @@ -6,13 +6,13 @@ SD_ILLUMINANT_ACES, SDS_COLORCHECKER_CLASSIC, SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC, - BaseSerializable, DecodingMethods, DirectoryStructure, - IDTMetaData, - IDTMetadataProperty, Interpolators, LUTSize, + Metadata, + MetadataProperty, + MixinSerializableProperties, OptimizationSpace, PathEncoder, ProjectSettingsMetadataConstants, @@ -28,9 +28,9 @@ get_sds_colour_checker, get_sds_illuminant, hash_file, - idt_metadata_property, list_sub_directories, mask_outliers, + metadata_property, optimisation_factory_IPT, optimisation_factory_Oklab, png_compare_colour_checkers, @@ -44,12 +44,27 @@ from .application import IDTGeneratorApplication # isort: skip __all__ = [ + "CAT", "OPTIMISATION_FACTORIES", "RGB_COLORCHECKER_CLASSIC_ACES", "SAMPLES_COUNT_DEFAULT", "SD_ILLUMINANT_ACES", "SDS_COLORCHECKER_CLASSIC", "SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC", + "DecodingMethods", + "DirectoryStructure", + "Interpolators", + "LUTSize", + "Metadata", + "MetadataProperty", + "MixinSerializableProperties", + "OptimizationSpace", + "PathEncoder", + "ProjectSettingsMetadataConstants", + "RGBDisplayColourspace", + "SerializableConstants", + "UICategories", + "UITypes", "clf_processing_elements", "error_delta_E", "extract_archive", @@ -60,34 +75,19 @@ "hash_file", "list_sub_directories", "mask_outliers", + "metadata_property", "optimisation_factory_IPT", "optimisation_factory_Oklab", "png_compare_colour_checkers", "slugify", "sort_exposure_keys", "working_directory", - "CAT", - "DirectoryStructure", - "DecodingMethods", - "Interpolators", - "LUTSize", - "OptimizationSpace", - "ProjectSettingsMetadataConstants", - "RGBDisplayColourspace", - "UICategories", - "UITypes", - "BaseSerializable", - "IDTMetaData", - "IDTMetadataProperty", - "PathEncoder", - "SerializableConstants", - "idt_metadata_property", ] __all__ += ["IDTProjectSettings"] __all__ += [ + "GENERATORS", "IDTBaseGenerator", "IDTGeneratorProsumerCamera", - "GENERATORS", ] __all__ += ["IDTGeneratorApplication"] diff --git a/aces/idt/application.py b/aces/idt/application.py index af878c5..aa4e6e8 100644 --- a/aces/idt/application.py +++ b/aces/idt/application.py @@ -1,12 +1,16 @@ -"""Module holds the main application class that controls all idt generation +""" +IDT Generator Application +========================= +Define the *IDT* generator application class. """ + import logging import re from pathlib import Path -from typing import Optional -from colour.utilities import attest +from colour.hints import List +from colour.utilities import attest, optional import aces.idt.core.common from aces.idt.core.constants import DirectoryStructure @@ -24,115 +28,102 @@ __all__ = [ "IDTGeneratorApplication", ] + LOGGER = logging.getLogger(__name__) class IDTGeneratorApplication: - """The main application class which handles project loading and saving, the - generator selection and execution, as well as any other analytical computation not - related to the generators, such as calculating data needed for display - + """ + Define the *IDT* generator application that handles project loading and + saving, generator selection and execution, as well as any other analytical + computations not related to the generators, such as calculating the data + required for display. + + Parameters + ---------- + generator + Name of the *IDT* generator to use. + project_settings + *IDT* project settings. """ - def __init__(self): - self._project_settings = IDTProjectSettings() + def __init__( + self, + generator: str = "IDTGeneratorProsumerCamera", + project_settings: IDTProjectSettings | None = None, + ) -> None: + self._project_settings = optional(project_settings, IDTProjectSettings()) self._generator = None + self.generator = generator @property - def generator_names(self): - """Return the names of the generators available + def generator_names(self) -> List: + """ + Getter property for the available *IDT* generator names. Returns ------- - list - A list of names for the available generators - + :class:`list` + Available *IDT* generator names. """ - return list(GENERATORS.keys()) + + return list(GENERATORS) @property - def generator(self): - """Return the current generator + def generator(self) -> IDTBaseGenerator: + """ + Getter and setter property for the selected *IDT* generator type. Returns ------- - IDTBaseGenerator - The current generator - + :class`IDTBaseGenerator`: + Selected *IDT* generator type. """ + return self._generator @generator.setter - def generator(self, value: type[IDTBaseGenerator]): + def generator(self, value: str): + """Setter for the **self.generator** property.""" + if value not in self.generator_names: - raise ValueError(f"Invalid generator name: {value}") + raise ValueError( + f'"{value}" generator is invalid, must be one of ' + f'"{self.generator_names}"!' + ) - generator_class = GENERATORS[value] - self._generator = generator_class(self.project_settings) + self._generator = GENERATORS[value](self.project_settings) @property - def project_settings(self): - """Return the current project settings + def project_settings(self) -> IDTProjectSettings: + """ + Getter and setter property for the *IDT* project settings. + + A single instance of the *IDT* project settings exists within the class + and its values are updated rather than replaced with the new passed + instance. Returns ------- - IDTProjectSettings - The current project settings - + :class:`IDTProjectSettings` + *IDT* project settings. """ + return self._project_settings @project_settings.setter def project_settings(self, value: IDTProjectSettings): - """ - Set the project settings, maintains a single instance of the project settings, - and updates the values - - Parameters - ---------- - value: IDTProjectSettings - The project settings to update with + """Setter for the **self.project_settings** property.""" - """ self._project_settings.update(value) - def extract_archive(self, archive: str, directory: Optional[str] = None): + def _update_project_settings_from_implicit_directory_structure( + self, root_directory: Path + ) -> None: """ - Extract the specification from the *IDT* archive. - - Parameters - ---------- - archive : str - Archive to extract. - directory : str, optional - Known directory we want to extract the archive to - """ - directory = aces.idt.core.common.extract_archive(archive, directory) - extracted_directories = aces.idt.core.common.list_sub_directories(directory) - root_directory = extracted_directories[0] - - json_files = list(root_directory.glob("*.json")) - if len(json_files) > 1: - raise ValueError("Multiple JSON files found in the root directory.") - - elif len(json_files) == 1: - LOGGER.info( - 'Found explicit "%s" "IDT" project settings file.', json_files[0] - ) - self.project_settings = IDTProjectSettings.from_file(json_files[0]) - else: - LOGGER.info('Assuming implicit "IDT" specification...') - self.project_settings.camera_model = Path(archive).stem - self._update_data_from_file_structure(root_directory) - - self._update_data_with_posix_paths(root_directory) - self._validate_images() - return directory - - def _update_data_from_file_structure(self, root_directory: Path): - """For the root directory of the extracted archive, update the project - settings 'data' with the file structure found on disk, when no project_settings - file is stored on disk. Assuming the following structure: + Update the *IDT* project settings using the sub-directory structure under + given root directory. The sub-directory structure should be defined as + follows:: data colour_checker @@ -145,9 +136,10 @@ def _update_data_from_file_structure(self, root_directory: Path): Parameters ---------- - root_directory: The folder we want to parse - + root_directory: + Root directory holding the sub-directory structure. """ + colour_checker_directory = ( root_directory / DirectoryStructure.DATA / DirectoryStructure.COLOUR_CHECKER ) @@ -166,6 +158,7 @@ def _update_data_from_file_structure(self, root_directory: Path): self.project_settings.data[DirectoryStructure.FLATFIELD] = list( flatfield_directory.glob("*.*") ) + grey_card_directory = ( root_directory / DirectoryStructure.DATA / DirectoryStructure.GREY_CARD ) @@ -174,13 +167,17 @@ def _update_data_from_file_structure(self, root_directory: Path): flatfield_directory.glob("*.*") ) - def _update_data_with_posix_paths(self, root_directory: Path): - """Update the project settings 'data' with the POSIX paths vs string paths. + def _verify_archive(self, root_directory: Path | str) -> None: + """ + Verify the *IDT* archive at given root directory. Parameters ---------- - root_directory : Path + root_directory + Root directory holding the *IDT* archive and that needs to be + verified. """ + for exposure in list( self.project_settings.data[DirectoryStructure.COLOUR_CHECKER].keys() ): @@ -226,43 +223,95 @@ def _update_data_with_posix_paths(self, root_directory: Path): else: self.project_settings.data[DirectoryStructure.GREY_CARD] = [] - def process_from_archive(self, archive: str): - """Process the IDT based on the zip archive provided + def _verify_file_type(self) -> None: + """ + Verify that the *IDT* archive contains a unique file type and set the + used file type accordingly. + """ + + file_types = set() + for _, value in self.project_settings.data[ + DirectoryStructure.COLOUR_CHECKER + ].items(): + for item in value: + file_types.add(item.suffix) + + for item in self.project_settings.data[DirectoryStructure.GREY_CARD]: + file_types.add(item.suffix) + + if len(file_types) > 1: + raise ValueError( + f'Multiple file types found in the project settings: "{file_types}"' + ) + + self.project_settings.file_type = next(iter(file_types)) + + def extract(self, archive: str, directory: str | None = None) -> str: + """ + Extract the *IDT* archive. Parameters ---------- - archive: str - The filepath to the archive + archive + Archive to extract. + directory + Directory to extract the archive to. Returns ------- - IDTBaseGenerator - returns the generator after it is finished + :class:`str` + Extracted directory. """ - if not self.generator: - raise ValueError("No Idt Generator Set") - self.project_settings.working_directory = self.extract_archive(archive) - self.generator.sample() - self.generator.sort() - self.generator.generate_LUT() - self.generator.filter_LUT() - self.generator.decode() - self.generator.optimise() - return self.generator + directory = aces.idt.core.common.extract_archive(archive, directory) + extracted_directories = aces.idt.core.common.list_sub_directories(directory) + root_directory = next(iter(extracted_directories)) + + json_files = list(root_directory.glob("*.json")) + + if len(json_files) > 1: + raise ValueError('Multiple "JSON" files found in the root directory!') + elif len(json_files) == 1: + json_file = next(iter(json_files)) + LOGGER.info('Found explicit "%s" "IDT" project settings file.', json_file) + self.project_settings = IDTProjectSettings.from_file(json_file) + else: + LOGGER.info('Assuming implicit "IDT" specification...') + self.project_settings.camera_model = Path(archive).stem + self._update_project_settings_from_implicit_directory_structure( + root_directory + ) + + self.project_settings.working_directory = root_directory - def process_from_project_settings(self): - """Process an IDT based on the project settings stored within the application + self._verify_archive(root_directory) + self._verify_file_type() + + return directory + + def process(self, archive: str | None) -> IDTBaseGenerator: + """ + Compute the *IDT* either using given archive *zip* file path or the + current *IDT* project settings if not given. + + Parameters + ---------- + archive + Archive *zip* file path. Returns ------- - IDTBaseGenerator - returns the generator after it is finished + :class:`IDTBaseGenerator` + Instantiated *IDT* generator. """ - if not self.generator: - raise ValueError("No Idt Generator Set") - # Ensuring that exposure values in the specification are floating point numbers. + if self.generator is None: + raise ValueError('No "IDT" generator was selected!') + + if archive is not None: + self.project_settings.working_directory = self.extract(archive) + + # Enforcing exposure values as floating point numbers. for exposure in list( self.project_settings.data[DirectoryStructure.COLOUR_CHECKER].keys() ): @@ -283,33 +332,14 @@ def process_from_project_settings(self): self.generator.filter_LUT() self.generator.decode() self.generator.optimise() + return self.generator - def _validate_images(self): - """Validate that the images provided are all the same file extension, and store - this in the file type property + def zip( + self, output_directory: Path | str, archive_serialised_generator: bool = False + ) -> str: """ - file_types = [] - for _, value in self.project_settings.data[ - DirectoryStructure.COLOUR_CHECKER - ].items(): - for item in value: - if not item.exists(): - raise ValueError(f"File does not exist: {item}") - file_types.append(item.suffix) - - for item in self.project_settings.data[DirectoryStructure.GREY_CARD]: - if not item.exists(): - raise ValueError(f"File does not exist: {item}") - file_types.append(item.suffix) - - if len(set(file_types)) > 1: - raise ValueError("Multiple file types found in the project settings") - - self.project_settings.file_type = file_types[0] - - def zip(self, output_directory: str, archive_serialised_generator: bool = False): - """Create a zip of the results from the idt creation + Create a *zip* file with the output of the *IDT* application process. Parameters ---------- @@ -323,6 +353,7 @@ def zip(self, output_directory: str, archive_serialised_generator: bool = False) :class:`str` *Zip* file path. """ + if not self.generator: raise ValueError("No Idt Generator Set") diff --git a/aces/idt/core/__init__.py b/aces/idt/core/__init__.py index 5a8145c..a85cc03 100644 --- a/aces/idt/core/__init__.py +++ b/aces/idt/core/__init__.py @@ -35,12 +35,12 @@ UITypes, ) from .structures import ( - BaseSerializable, - IDTMetaData, - IDTMetadataProperty, + Metadata, + MetadataProperty, + MixinSerializableProperties, PathEncoder, SerializableConstants, - idt_metadata_property, + metadata_property, ) __all__ = [ @@ -82,10 +82,10 @@ ] __all__ += [ - "BaseSerializable", - "IDTMetaData", - "IDTMetadataProperty", + "Metadata", + "MetadataProperty", + "MixinSerializableProperties", "PathEncoder", "SerializableConstants", - "idt_metadata_property", + "metadata_property", ] diff --git a/aces/idt/core/common.py b/aces/idt/core/common.py index eaff4fa..4813e98 100644 --- a/aces/idt/core/common.py +++ b/aces/idt/core/common.py @@ -2,7 +2,7 @@ Common IDT Utilities ==================== -Define common IDT utilities objects. +Define the common *IDT* utilities objects. """ from __future__ import annotations @@ -651,13 +651,15 @@ def hash_file(path: str) -> str: def extract_archive(archive: str, directory: None | str = None) -> str: """ - Extract the archive to the given directory or a temporary directory. + Extract the archive to the given directory or a temporary directory if + not given. Parameters ---------- - archive : str + archive Archive to extract. - directory : str, optional known directory + directory + Directory to extract the archive to. Returns ------- @@ -677,6 +679,7 @@ def extract_archive(archive: str, directory: None | str = None) -> str: ) shutil.unpack_archive(archive, directory) + return directory diff --git a/aces/idt/core/constants.py b/aces/idt/core/constants.py index 0e4c85f..31d9f00 100644 --- a/aces/idt/core/constants.py +++ b/aces/idt/core/constants.py @@ -2,7 +2,7 @@ IDT Constants ============= -Define the constants for the project. +Define the constants for the package. """ from __future__ import annotations @@ -13,7 +13,7 @@ import numpy as np from colour.hints import Tuple -from aces.idt.core.structures import IDTMetaData +from aces.idt.core.structures import Metadata __author__ = "Alex Forsythe, Joshua Pines, Thomas Mansencal, Nick Shaw, Adam Davis" __copyright__ = "Copyright 2022 Academy of Motion Picture Arts and Sciences" @@ -37,7 +37,7 @@ class DirectoryStructure: - """Constants for the directory names which make up the data structure.""" + """Constants for the directory names which compose the data structure.""" DATA: ClassVar[str] = "data" COLOUR_CHECKER: ClassVar[str] = "colour_checker" @@ -114,7 +114,7 @@ def ALL(cls) -> Tuple[str]: Available chromatic adaptation transforms. """ - return tuple(sorted(colour.CHROMATIC_ADAPTATION_TRANSFORMS)) + return *sorted(colour.CHROMATIC_ADAPTATION_TRANSFORMS), "None" class OptimizationSpace: @@ -153,7 +153,7 @@ class DecodingMethods: class ProjectSettingsMetadataConstants: """Constants for the project settings.""" - SCHEMA_VERSION = IDTMetaData( + SCHEMA_VERSION = Metadata( default_value="0.1.0", description="The project settings schema version", display_name="Schema Version", @@ -161,7 +161,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - CAMERA_MAKE = IDTMetaData( + CAMERA_MAKE = Metadata( default_value="", description="The make of the camera used to capture the images", display_name="Camera Make", @@ -169,7 +169,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - CAMERA_MODEL = IDTMetaData( + CAMERA_MODEL = Metadata( default_value="", description="The model of the camera used to capture the images", display_name="Camera Model", @@ -177,7 +177,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - RGB_DISPLAY_COLOURSPACE = IDTMetaData( + RGB_DISPLAY_COLOURSPACE = Metadata( default_value=RGBDisplayColourspace.DEFAULT, description="The RGB display colourspace", display_name="RGB Display Colourspace", @@ -186,7 +186,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - CAT = IDTMetaData( + CAT = Metadata( default_value=CAT.DEFAULT, description="The CAT", display_name="CAT", @@ -195,7 +195,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - OPTIMISATION_SPACE = IDTMetaData( + OPTIMISATION_SPACE = Metadata( default_value=OptimizationSpace.DEFAULT, description="The optimisation space", display_name="Optimisation Space", @@ -204,7 +204,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - ILLUMINANT_INTERPOLATOR = IDTMetaData( + ILLUMINANT_INTERPOLATOR = Metadata( default_value=Interpolators.DEFAULT, description="The illuminant interpolator", display_name="Illuminant Interpolator", @@ -213,7 +213,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - DECODING_METHOD = IDTMetaData( + DECODING_METHOD = Metadata( default_value=DecodingMethods.DEFAULT, description="The decoding method", display_name="Decoding Method", @@ -222,7 +222,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - EV_RANGE = IDTMetaData( + EV_RANGE = Metadata( default_value=[-1.0, 0.0, 1.0], description="The EV range", display_name="EV Range", @@ -230,7 +230,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - GREY_CARD_REFERENCE = IDTMetaData( + GREY_CARD_REFERENCE = Metadata( default_value=[0.18, 0.18, 0.18], description="The grey card reference", display_name="Grey Card Reference", @@ -238,7 +238,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - LUT_SIZE = IDTMetaData( + LUT_SIZE = Metadata( default_value=LUTSize.DEFAULT, description="The LUT size", display_name="LUT Size", @@ -247,7 +247,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - LUT_SMOOTHING = IDTMetaData( + LUT_SMOOTHING = Metadata( default_value=16, description="The LUT smoothing", display_name="LUT Smoothing", @@ -255,7 +255,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - ACES_TRANSFORM_ID = IDTMetaData( + ACES_TRANSFORM_ID = Metadata( default_value="", description="The ACES transform ID", display_name="ACES Transform ID", @@ -263,7 +263,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - ACES_USER_NAME = IDTMetaData( + ACES_USER_NAME = Metadata( default_value="", description="The ACES username", display_name="ACES Username", @@ -271,7 +271,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - ISO = IDTMetaData( + ISO = Metadata( default_value=800, description="The ISO", display_name="ISO", @@ -279,7 +279,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - TEMPERATURE = IDTMetaData( + TEMPERATURE = Metadata( default_value=6000, description="The temperature", display_name="Temperature", @@ -287,7 +287,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - ADDITIONAL_CAMERA_SETTINGS = IDTMetaData( + ADDITIONAL_CAMERA_SETTINGS = Metadata( default_value="", description="The additional camera settings", display_name="Additional Camera Settings", @@ -295,7 +295,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - LIGHTING_SETUP_DESCRIPTION = IDTMetaData( + LIGHTING_SETUP_DESCRIPTION = Metadata( default_value="", description="The lighting setup description", display_name="Lighting Setup Description", @@ -303,7 +303,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - DEBAYERING_PLATFORM = IDTMetaData( + DEBAYERING_PLATFORM = Metadata( default_value="", description="The debayering platform", display_name="Debayering Platform", @@ -311,7 +311,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - DEBAYERING_SETTINGS = IDTMetaData( + DEBAYERING_SETTINGS = Metadata( default_value="", description="The debayering settings", display_name="Debayering Settings", @@ -319,7 +319,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - ENCODING_COLOUR_SPACE = IDTMetaData( + ENCODING_COLOUR_SPACE = Metadata( default_value="", description="The encoding colour space", display_name="Encoding Colour Space", @@ -327,7 +327,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - DATA = IDTMetaData( + DATA = Metadata( default_value={ DirectoryStructure.COLOUR_CHECKER: {}, DirectoryStructure.GREY_CARD: {}, @@ -339,7 +339,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - WORKING_DIR = IDTMetaData( + WORKING_DIR = Metadata( default_value="", description="The file path to the working directory", display_name="Working Directory", @@ -347,7 +347,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - CLEAN_UP = IDTMetaData( + CLEAN_UP = Metadata( default_value=False, description="Do we want to cleanup the directory after we finish", display_name="Cleanup", @@ -355,7 +355,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - REFERENCE_COLOUR_CHECKER = IDTMetaData( + REFERENCE_COLOUR_CHECKER = Metadata( default_value="ISO 17321-1", description="The reference colour checker we want to use", ui_type=UITypes.OPTIONS_FIELD, @@ -363,15 +363,15 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.ADVANCED, ) - ILLUMINANT = IDTMetaData( + ILLUMINANT = Metadata( default_value="D60", description="The illuminant we want to use for the reference colour checker", ui_type=UITypes.OPTIONS_FIELD, - options=sorted(colour.SDS_ILLUMINANTS), + options=["Custom", "Daylight", "Blackbody", *sorted(colour.SDS_ILLUMINANTS)], ui_category=UICategories.ADVANCED, ) - FILE_TYPE = IDTMetaData( + FILE_TYPE = Metadata( default_value="", description="The file type of the recorded footage is detected from " "the archive", @@ -380,7 +380,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.STANDARD, ) - EV_WEIGHTS = IDTMetaData( + EV_WEIGHTS = Metadata( default_value=np.array([]), description="Normalised weights used to sum the exposure values. If not given," "the median of the exposure values is used.", @@ -389,7 +389,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - OPTIMIZATION_KWARGS = IDTMetaData( + OPTIMIZATION_KWARGS = Metadata( default_value={}, description="Parameters for the optimization function scipy.optimize.minimize", display_name="Optimization Kwargs", @@ -397,7 +397,7 @@ class ProjectSettingsMetadataConstants: ui_category=UICategories.HIDDEN, ) - ALL: ClassVar[tuple[IDTMetaData, ...]] = ( + ALL: ClassVar[tuple[Metadata, ...]] = ( SCHEMA_VERSION, CAMERA_MAKE, CAMERA_MODEL, diff --git a/aces/idt/core/structures.py b/aces/idt/core/structures.py index baa42fb..d8e8bc7 100644 --- a/aces/idt/core/structures.py +++ b/aces/idt/core/structures.py @@ -1,8 +1,9 @@ """ -IDT Structures -============== +Structures +========== -Define various helper classes used throughout the IDT generator. +Define various helper classes, especially to serialize properties to and from +*JSON*. """ from __future__ import annotations @@ -24,10 +25,10 @@ __all__ = [ "PathEncoder", "SerializableConstants", - "IDTMetaData", - "IDTMetadataProperty", - "idt_metadata_property", - "BaseSerializable", + "Metadata", + "MetadataProperty", + "metadata_property", + "MixinSerializableProperties", ] @@ -70,13 +71,12 @@ class SerializableConstants: @dataclass -class IDTMetaData: +class Metadata: """ - Store the metadata information for a property within an IDT project. + Store the metadata information for a serializable property. - This data is not used in any calculations or computation and is primarily - designed to store data which would be associated with how the property - should be displayed in a UI. + This metadata is primarily designed to store data associated with how the + property might be displayed in a UI. """ default_value: Any = field(default=None) @@ -88,17 +88,17 @@ class IDTMetaData: options: Any = field(default=None) -class IDTMetadataProperty: +class MetadataProperty: """ - Define a property descriptor storing a :class:`IDTMetaData` class instance - and supporting getter and setter functionality. + Define a property storing a :class:`Metadata` class instance with support + for getter and setter functionality. """ def __init__( self, getter: Callable, setter: Callable | None = None, - metadata: IDTMetaData | None = None, + metadata: Metadata | None = None, ): self.getter = getter self.setter = setter @@ -106,44 +106,46 @@ def __init__( def __get__(self, instance: Any, owner: Any) -> Any: """Get the value of the property.""" + if instance is None: - # Return the descriptor itself when accessed from the class + # Return the object itself when accessed from the class. return self + return self.getter(instance) def __set__(self, instance: Any, value: Any) -> Any: """Set the value of the property.""" + if self.setter: self.setter(instance, value) else: raise AttributeError("No setter defined for this property") -def idt_metadata_property(metadata: IDTMetaData | None = None) -> Any: +def metadata_property(metadata: Metadata | None = None) -> Any: """ - Create a property from given :class`IDTMetaData` class instance, supporting - both getter and setter functionality. + Decorate a class attribute to create a property using given :class`Metadata` + class instance, supporting both getter and setter functionality. """ def wrapper(getter): - # Define a setter function to handle setting the value def setter(instance, value): setattr(instance, f"_{getter.__name__}", value) - # Create a IDTMetadataProperty descriptor with both getter and setter - return IDTMetadataProperty(getter, setter, metadata) + return MetadataProperty(getter, setter, metadata) return wrapper -class BaseSerializable: +class MixinSerializableProperties: """ - Define a base class for serializable objects with IDTMetadataProperties - that can be converted to and from *JSON*. + Define a mixin class for serializable objects containing + :class:`IDTMetadataProperty` class instances that can be converted to and + from *JSON*. """ @property - def properties(self) -> Tuple[str, IDTMetadataProperty]: + def properties(self) -> Tuple[str, MetadataProperty]: """ Generator for the properties of the object getting the name and property as a tuple. @@ -153,9 +155,9 @@ def properties(self) -> Tuple[str, IDTMetadataProperty]: :class:`tuple` """ - for name, descriptor in self.__class__.__dict__.items(): - if isinstance(descriptor, IDTMetadataProperty): - yield name, descriptor + for name, object_ in self.__class__.__dict__.items(): + if isinstance(object_, MetadataProperty): + yield name, object_ def to_json(self) -> str: """ @@ -176,7 +178,7 @@ def to_json(self) -> str: return json.dumps(output, indent=4, cls=PathEncoder) @classmethod - def from_json(cls, data: Any) -> BaseSerializable: + def from_json(cls, data: Any) -> MixinSerializableProperties: """ Create a new instance of the class from the given object or *JSON* string. @@ -188,7 +190,7 @@ def from_json(cls, data: Any) -> BaseSerializable: Returns ------- - :class:`BaseSerializable` + :class:`MixinSerializableProperties` Loaded object. """ @@ -202,6 +204,7 @@ def from_json(cls, data: Any) -> BaseSerializable: prop.setter(item, data[SerializableConstants.HEADER][name]) else: prop.setter(item, data[SerializableConstants.DATA]) + return item def to_file(self, filepath: str): @@ -218,7 +221,7 @@ def to_file(self, filepath: str): f.write(self.to_json()) @classmethod - def from_file(cls, filepath: str) -> BaseSerializable: + def from_file(cls, filepath: str) -> MixinSerializableProperties: """ Load the object from a *JSON* file. @@ -229,7 +232,7 @@ def from_file(cls, filepath: str) -> BaseSerializable: Returns ------- - :class:`BaseSerializable` + :class:`MixinSerializableProperties` Loaded object. """ diff --git a/aces/idt/framework/project_settings.py b/aces/idt/framework/project_settings.py index 0a67e84..bb1e8c8 100644 --- a/aces/idt/framework/project_settings.py +++ b/aces/idt/framework/project_settings.py @@ -11,8 +11,8 @@ from aces.idt.core import ( OPTIMISATION_FACTORIES, - BaseSerializable, DirectoryStructure, + MixinSerializableProperties, ) from aces.idt.core import ProjectSettingsMetadataConstants as MetadataConstants from aces.idt.core import ( @@ -20,7 +20,7 @@ generate_reference_colour_checker, get_sds_colour_checker, get_sds_illuminant, - idt_metadata_property, + metadata_property, sort_exposure_keys, ) @@ -36,77 +36,141 @@ ] -class IDTProjectSettings(BaseSerializable): +class IDTProjectSettings(MixinSerializableProperties): """ - Hold the project settings for the *IDT* application. The order the - properties are defined is the order they are serialized and deserialized. + Define the project settings for an *IDT* generator. + + The properties are serialized and deserialized in the order they are + defined in this class. + + Other Parameters + ---------------- + kwargs + Optional keyword arguments used to initialise the project settings. """ - def __init__(self): + def __init__(self, **kwargs: Dict): super().__init__() self._schema_version = IDTProjectSettings.schema_version.metadata.default_value - self._aces_transform_id = ( - IDTProjectSettings.aces_transform_id.metadata.default_value + + self._aces_transform_id = kwargs.get( + "aces_transform_id", + IDTProjectSettings.aces_transform_id.metadata.default_value, + ) + self._aces_user_name = kwargs.get( + "aces_user_name", + IDTProjectSettings.aces_user_name.metadata.default_value, + ) + self._camera_make = kwargs.get( + "camera_make", + IDTProjectSettings.camera_make.metadata.default_value, + ) + self._camera_model = kwargs.get( + "camera_model", + IDTProjectSettings.camera_model.metadata.default_value, + ) + self._iso = kwargs.get( + "iso", + IDTProjectSettings.iso.metadata.default_value, ) - self._aces_user_name = IDTProjectSettings.aces_user_name.metadata.default_value - self._camera_make = IDTProjectSettings.camera_make.metadata.default_value - self._camera_model = IDTProjectSettings.camera_model.metadata.default_value - self._iso = IDTProjectSettings.iso.metadata.default_value - self._temperature = IDTProjectSettings.temperature.metadata.default_value - self._additional_camera_settings = ( - IDTProjectSettings.additional_camera_settings.metadata.default_value + self._temperature = kwargs.get( + "temperature", + IDTProjectSettings.temperature.metadata.default_value, ) - self._lighting_setup_description = ( - IDTProjectSettings.lighting_setup_description.metadata.default_value + self._additional_camera_settings = kwargs.get( + "additional_camera_settings", + IDTProjectSettings.additional_camera_settings.metadata.default_value, ) - self._debayering_platform = ( - IDTProjectSettings.debayering_platform.metadata.default_value + self._lighting_setup_description = kwargs.get( + "lighting_setup_description", + IDTProjectSettings.lighting_setup_description.metadata.default_value, ) - self._debayering_settings = ( - IDTProjectSettings.debayering_settings.metadata.default_value + self._debayering_platform = kwargs.get( + "debayering_platform", + IDTProjectSettings.debayering_platform.metadata.default_value, ) - self._encoding_colourspace = ( - IDTProjectSettings.encoding_colourspace.metadata.default_value + self._debayering_settings = kwargs.get( + "debayering_settings", + IDTProjectSettings.debayering_settings.metadata.default_value, ) - self._rgb_display_colourspace = ( - IDTProjectSettings.rgb_display_colourspace.metadata.default_value + self._encoding_colourspace = kwargs.get( + "encoding_colourspace", + IDTProjectSettings.encoding_colourspace.metadata.default_value, ) - self._cat = IDTProjectSettings.cat.metadata.default_value - self._optimisation_space = ( - IDTProjectSettings.optimisation_space.metadata.default_value + self._rgb_display_colourspace = kwargs.get( + "rgb_display_colourspace", + IDTProjectSettings.rgb_display_colourspace.metadata.default_value, ) - self._illuminant_interpolator = ( - IDTProjectSettings.illuminant_interpolator.metadata.default_value + self._cat = kwargs.get( + "cat", + IDTProjectSettings.cat.metadata.default_value, ) - self._decoding_method = ( - IDTProjectSettings.decoding_method.metadata.default_value + self._optimisation_space = kwargs.get( + "optimisation_space", + IDTProjectSettings.optimisation_space.metadata.default_value, ) - self._ev_range = IDTProjectSettings.ev_range.metadata.default_value - self._grey_card_reference = ( - IDTProjectSettings.grey_card_reference.metadata.default_value + self._illuminant_interpolator = kwargs.get( + "illuminant_interpolator", + IDTProjectSettings.illuminant_interpolator.metadata.default_value, ) - self._lut_size = IDTProjectSettings.lut_size.metadata.default_value - self._lut_smoothing = IDTProjectSettings.lut_smoothing.metadata.default_value + self._decoding_method = kwargs.get( + "decoding_method", + IDTProjectSettings.decoding_method.metadata.default_value, + ) + self._ev_range = kwargs.get( + "ev_range", + IDTProjectSettings.ev_range.metadata.default_value, + ) + self._grey_card_reference = kwargs.get( + "grey_card_reference", + IDTProjectSettings.grey_card_reference.metadata.default_value, + ) + self._lut_size = kwargs.get( + "lut_size", + IDTProjectSettings.lut_size.metadata.default_value, + ) + self._lut_smoothing = kwargs.get( + "lut_smoothing", + IDTProjectSettings.lut_smoothing.metadata.default_value, + ) + self._data = IDTProjectSettings.data.metadata.default_value - self._working_directory = ( - IDTProjectSettings.working_directory.metadata.default_value + + self._working_directory = kwargs.get( + "working_directory", + IDTProjectSettings.working_directory.metadata.default_value, ) - self._cleanup = IDTProjectSettings.cleanup.metadata.default_value - self._reference_colour_checker = ( - IDTProjectSettings.reference_colour_checker.metadata.default_value + self._cleanup = kwargs.get( + "cleanup", + IDTProjectSettings.cleanup.metadata.default_value, ) - self._illuminant = IDTProjectSettings.illuminant.metadata.default_value - self._file_type = IDTProjectSettings.file_type.metadata.default_value - self._ev_weights = IDTProjectSettings.ev_weights.metadata.default_value - self._optimization_kwargs = ( - IDTProjectSettings.optimization_kwargs.metadata.default_value + + self._reference_colour_checker = kwargs.get( + "reference_colour_checker", + IDTProjectSettings.reference_colour_checker.metadata.default_value, + ) + self._illuminant = kwargs.get( + "illuminant", + IDTProjectSettings.illuminant.metadata.default_value, + ) + self._file_type = kwargs.get( + "file_type", + IDTProjectSettings.file_type.metadata.default_value, + ) + self._ev_weights = kwargs.get( + "ev_weights", + IDTProjectSettings.ev_weights.metadata.default_value, + ) + self._optimization_kwargs = kwargs.get( + "optimization_kwargs", + IDTProjectSettings.optimization_kwargs.metadata.default_value, ) - @idt_metadata_property(metadata=MetadataConstants.SCHEMA_VERSION) + @metadata_property(metadata=MetadataConstants.SCHEMA_VERSION) def schema_version(self) -> str: """ - Return the schema version. + Getter property for the schema version. Returns ------- @@ -116,10 +180,10 @@ def schema_version(self) -> str: return self._schema_version - @idt_metadata_property(metadata=MetadataConstants.ACES_TRANSFORM_ID) + @metadata_property(metadata=MetadataConstants.ACES_TRANSFORM_ID) def aces_transform_id(self) -> str: """ - Return the *ACEStransformID*. + Getter property for the *ACEStransformID*. Returns ------- @@ -129,9 +193,10 @@ def aces_transform_id(self) -> str: return self._aces_transform_id - @idt_metadata_property(metadata=MetadataConstants.ACES_USER_NAME) + @metadata_property(metadata=MetadataConstants.ACES_USER_NAME) def aces_user_name(self) -> str: - """Return the *ACESuserName*. + """ + Getter property for the *ACESuserName*. Returns ------- @@ -141,10 +206,10 @@ def aces_user_name(self) -> str: return self._aces_user_name - @idt_metadata_property(metadata=MetadataConstants.CAMERA_MAKE) + @metadata_property(metadata=MetadataConstants.CAMERA_MAKE) def camera_make(self) -> str: """ - Return the camera make. + Getter property for the camera make. Returns ------- @@ -154,10 +219,10 @@ def camera_make(self) -> str: return self._camera_make - @idt_metadata_property(metadata=MetadataConstants.CAMERA_MODEL) + @metadata_property(metadata=MetadataConstants.CAMERA_MODEL) def camera_model(self) -> str: """ - Return the camera model. + Getter property for the camera model. Returns ------- @@ -167,10 +232,10 @@ def camera_model(self) -> str: return self._camera_model - @idt_metadata_property(metadata=MetadataConstants.ISO) + @metadata_property(metadata=MetadataConstants.ISO) def iso(self) -> int: """ - Return the camera ISO value. + Getter property for the camera ISO value. Returns ------- @@ -180,9 +245,11 @@ def iso(self) -> int: return self._iso - @idt_metadata_property(metadata=MetadataConstants.TEMPERATURE) + @metadata_property(metadata=MetadataConstants.TEMPERATURE) def temperature(self) -> int: - """Return the camera white-balance colour temperature in Kelvin degrees. + """ + Getter property for the camera white-balance colour temperature in + Kelvin degrees. Returns ------- @@ -191,9 +258,10 @@ def temperature(self) -> int: """ return self._temperature - @idt_metadata_property(metadata=MetadataConstants.ADDITIONAL_CAMERA_SETTINGS) + @metadata_property(metadata=MetadataConstants.ADDITIONAL_CAMERA_SETTINGS) def additional_camera_settings(self) -> str: - """Return the additional camera settings. + """ + Getter property for the additional camera settings. Returns ------- @@ -203,10 +271,10 @@ def additional_camera_settings(self) -> str: return self._additional_camera_settings - @idt_metadata_property(metadata=MetadataConstants.LIGHTING_SETUP_DESCRIPTION) + @metadata_property(metadata=MetadataConstants.LIGHTING_SETUP_DESCRIPTION) def lighting_setup_description(self) -> str: """ - Return the lighting setup description. + Getter property for the lighting setup description. Returns ------- @@ -216,10 +284,10 @@ def lighting_setup_description(self) -> str: return self._lighting_setup_description - @idt_metadata_property(metadata=MetadataConstants.DEBAYERING_PLATFORM) + @metadata_property(metadata=MetadataConstants.DEBAYERING_PLATFORM) def debayering_platform(self) -> str: """ - Return the debayering platform name. + Getter property for the debayering platform name. Returns ------- @@ -229,10 +297,10 @@ def debayering_platform(self) -> str: return self._debayering_platform - @idt_metadata_property(metadata=MetadataConstants.DEBAYERING_SETTINGS) + @metadata_property(metadata=MetadataConstants.DEBAYERING_SETTINGS) def debayering_settings(self) -> str: """ - Return the debayering platform settings. + Getter property for the debayering platform settings. Returns ------- @@ -242,10 +310,10 @@ def debayering_settings(self) -> str: return self._debayering_settings - @idt_metadata_property(metadata=MetadataConstants.ENCODING_COLOUR_SPACE) + @metadata_property(metadata=MetadataConstants.ENCODING_COLOUR_SPACE) def encoding_colourspace(self) -> str: """ - Return the encoding colour space. + Getter property for the encoding colour space. Returns ------- @@ -255,10 +323,10 @@ def encoding_colourspace(self) -> str: return self._encoding_colourspace - @idt_metadata_property(metadata=MetadataConstants.RGB_DISPLAY_COLOURSPACE) + @metadata_property(metadata=MetadataConstants.RGB_DISPLAY_COLOURSPACE) def rgb_display_colourspace(self) -> str: """ - Return the *RGB* display colour space. + Getter property for the *RGB* display colour space. Returns ------- @@ -268,9 +336,10 @@ def rgb_display_colourspace(self) -> str: return self._rgb_display_colourspace - @idt_metadata_property(metadata=MetadataConstants.CAT) + @metadata_property(metadata=MetadataConstants.CAT) def cat(self) -> str: - """Return the chromatic adaptation transform name. + """ + Getter property for the chromatic adaptation transform name. Returns ------- @@ -280,10 +349,10 @@ def cat(self) -> str: return self._cat - @idt_metadata_property(metadata=MetadataConstants.OPTIMISATION_SPACE) + @metadata_property(metadata=MetadataConstants.OPTIMISATION_SPACE) def optimisation_space(self) -> str: """ - Return the optimisation space name. + Getter property for the optimisation space name. Returns ------- @@ -293,10 +362,10 @@ def optimisation_space(self) -> str: return self._optimisation_space - @idt_metadata_property(metadata=MetadataConstants.ILLUMINANT_INTERPOLATOR) + @metadata_property(metadata=MetadataConstants.ILLUMINANT_INTERPOLATOR) def illuminant_interpolator(self) -> str: """ - Return the illuminant interpolator name. + Getter property for the illuminant interpolator name. Returns ------- @@ -306,10 +375,10 @@ def illuminant_interpolator(self) -> str: return self._illuminant_interpolator - @idt_metadata_property(metadata=MetadataConstants.DECODING_METHOD) + @metadata_property(metadata=MetadataConstants.DECODING_METHOD) def decoding_method(self) -> str: """ - Return the decoding method name. + Getter property for the decoding method name. Returns ------- @@ -319,10 +388,10 @@ def decoding_method(self) -> str: return self._decoding_method - @idt_metadata_property(metadata=MetadataConstants.EV_RANGE) + @metadata_property(metadata=MetadataConstants.EV_RANGE) def ev_range(self) -> List: """ - Return the EV range. + Getter property for the EV range. Returns ------- @@ -332,10 +401,10 @@ def ev_range(self) -> List: return self._ev_range - @idt_metadata_property(metadata=MetadataConstants.GREY_CARD_REFERENCE) + @metadata_property(metadata=MetadataConstants.GREY_CARD_REFERENCE) def grey_card_reference(self) -> List: """ - Return the grey card reference values. + Getter property for the grey card reference values. Returns ------- @@ -345,10 +414,10 @@ def grey_card_reference(self) -> List: return self._grey_card_reference - @idt_metadata_property(metadata=MetadataConstants.LUT_SIZE) + @metadata_property(metadata=MetadataConstants.LUT_SIZE) def lut_size(self) -> int: """ - Return the *LUT* size. + Getter property for the *LUT* size. Returns ------- @@ -358,9 +427,10 @@ def lut_size(self) -> int: return self._lut_size - @idt_metadata_property(metadata=MetadataConstants.LUT_SMOOTHING) + @metadata_property(metadata=MetadataConstants.LUT_SMOOTHING) def lut_smoothing(self): - """Return the *LUT* smoothing. + """ + Getter property for the *LUT* smoothing. Returns ------- @@ -370,10 +440,10 @@ def lut_smoothing(self): return self._lut_smoothing - @idt_metadata_property(metadata=MetadataConstants.DATA) + @metadata_property(metadata=MetadataConstants.DATA) def data(self) -> Dict: """ - Return the data structure holding the image sequences. + Getter property for the data structure holding the image sequences. Returns ------- @@ -383,10 +453,10 @@ def data(self) -> Dict: return self._data - @idt_metadata_property(metadata=MetadataConstants.WORKING_DIR) + @metadata_property(metadata=MetadataConstants.WORKING_DIR) def working_directory(self) -> str: """ - Return the working directory for the project. + Getter property for the working directory for the project. Returns ------- @@ -396,10 +466,10 @@ def working_directory(self) -> str: return self._working_directory - @idt_metadata_property(metadata=MetadataConstants.CLEAN_UP) + @metadata_property(metadata=MetadataConstants.CLEAN_UP) def cleanup(self) -> bool: """ - Return whether the working directory should be cleaned up. + Getter property for whether the working directory should be cleaned up. Returns ------- @@ -409,10 +479,10 @@ def cleanup(self) -> bool: return self._cleanup - @idt_metadata_property(metadata=MetadataConstants.REFERENCE_COLOUR_CHECKER) + @metadata_property(metadata=MetadataConstants.REFERENCE_COLOUR_CHECKER) def reference_colour_checker(self) -> str: """ - Return the reference colour checker name. + Getter property for the reference colour checker name. Returns ------- @@ -422,10 +492,10 @@ def reference_colour_checker(self) -> str: return self._reference_colour_checker - @idt_metadata_property(metadata=MetadataConstants.ILLUMINANT) + @metadata_property(metadata=MetadataConstants.ILLUMINANT) def illuminant(self) -> str: """ - Return the illuminant name. + Getter property for the illuminant name. Returns ------- @@ -435,10 +505,10 @@ def illuminant(self) -> str: return self._illuminant - @idt_metadata_property(metadata=MetadataConstants.FILE_TYPE) + @metadata_property(metadata=MetadataConstants.FILE_TYPE) def file_type(self) -> str: """ - Return the file type, i.e., file extension. + Getter property for the file type, i.e., file extension. Returns ------- @@ -448,10 +518,10 @@ def file_type(self) -> str: return self._file_type - @idt_metadata_property(metadata=MetadataConstants.EV_WEIGHTS) + @metadata_property(metadata=MetadataConstants.EV_WEIGHTS) def ev_weights(self) -> NDArrayFloat: """ - Return the *EV* weights. + Getter property for the *EV* weights. Returns ------- @@ -461,10 +531,10 @@ def ev_weights(self) -> NDArrayFloat: return self._ev_weights - @idt_metadata_property(metadata=MetadataConstants.OPTIMIZATION_KWARGS) + @metadata_property(metadata=MetadataConstants.OPTIMIZATION_KWARGS) def optimization_kwargs(self) -> Dict: """ - Return the optimization keyword arguments. + Getter property for the optimization keyword arguments. Returns ------- @@ -530,13 +600,14 @@ def update(self, value: IDTProjectSettings): @classmethod def from_directory(cls, directory: str) -> IDTProjectSettings: """ - Create a new project settings for a given directory on disk and build the - data structure based on the files on disk. + Create a new project settings for a given directory on disk and build + the data structure based on the files on disk. Parameters ---------- directory : str - The directory to the project root containing the image sequence folders. + The directory to the project root containing the image sequence + directories. """ instance = cls() @@ -557,7 +628,7 @@ def from_directory(cls, directory: str) -> IDTProjectSettings: grey_card_path ): raise ValueError( - "Required 'colour_checker' or 'grey_card' folder does not exist." + 'Required "colour_checker" or "grey_card" folder does not exist.' ) # Populate colour_checker data @@ -618,10 +689,10 @@ def __str__(self) -> str: }, {"line_break": True}, ] - for name, _ in self.properties: + + for name, _descriptor in self.properties: attributes.append({"name": f"{name}", "label": f"{name}".title()}) - attributes.append( - {"line_break": True}, - ) + attributes.append({"line_break": True}) + return multiline_str(self, attributes) diff --git a/aces/idt/generators/__init__.py b/aces/idt/generators/__init__.py index f0e2f06..4e14362 100644 --- a/aces/idt/generators/__init__.py +++ b/aces/idt/generators/__init__.py @@ -1,7 +1,7 @@ from .base_generator import IDTBaseGenerator from .prosumer_camera import IDTGeneratorProsumerCamera -GENERATORS = {IDTGeneratorProsumerCamera.generator_name: IDTGeneratorProsumerCamera} +GENERATORS = {IDTGeneratorProsumerCamera.GENERATOR_NAME: IDTGeneratorProsumerCamera} __all__ = ["IDTBaseGenerator"] __all__ += ["IDTGeneratorProsumerCamera"] diff --git a/aces/idt/generators/base_generator.py b/aces/idt/generators/base_generator.py index 1b5f514..3a31bb5 100644 --- a/aces/idt/generators/base_generator.py +++ b/aces/idt/generators/base_generator.py @@ -1,6 +1,8 @@ """ -Module holds a common base generator class for which all other IDT generators inherit -from +IDT Base Generator +================== + +Define the *IDT* base generator class. """ import base64 @@ -19,7 +21,8 @@ import cv2 import jsonpickle import numpy as np -from colour import LUT1D, read_image +from colour import LUT1D, LUT3x1D, read_image +from colour.hints import NDArrayFloat from colour.utilities import Structure, as_float_array, zeros from colour_checker_detection.detection import ( as_int32_array, @@ -29,6 +32,7 @@ ) from matplotlib import pyplot as plt +from aces.idt import IDTProjectSettings from aces.idt.core import ( SAMPLES_COUNT_DEFAULT, SETTINGS_SEGMENTATION_COLORCHECKER_CLASSIC, @@ -54,9 +58,47 @@ class IDTBaseGenerator(ABC): """ - A Class which holds the core logic for any generator, and should be inherited from + Define the base class that any *IDT* generator must be inherit from. + + Parameters + ---------- + project_settings : IDTProjectSettings, optional + *IDT* generator settings. + + Attributes + ---------- + - :attr:`~aces.idt.IDTBaseGenerator.project_settings` + - :attr:`~aces.idt.IDTBaseGenerator.image_colour_checker_segmentation` + - :attr:`~aces.idt.IDTBaseGenerator.baseline_exposure` + - :attr:`~aces.idt.IDTBaseGenerator.image_grey_card_sampling` + - :attr:`~aces.idt.IDTBaseGenerator.samples_camera` + - :attr:`~aces.idt.IDTBaseGenerator.samples_reference` + - :attr:`~aces.idt.IDTBaseGenerator.LUT_unfiltered` + - :attr:`~aces.idt.IDTBaseGenerator.LUT_filtered` + - :attr:`~aces.idt.IDTBaseGenerator.LUT_decoding` + - :attr:`~aces.idt.IDTBaseGenerator.M` + - :attr:`~aces.idt.IDTBaseGenerator.RGB_w` + - :attr:`~aces.idt.IDTBaseGenerator.k` + - :attr:`~aces.idt.IDTBaseGenerator.samples_analysis` + + Methods + ------- + - :meth:`~aces.idt.IDTBaseGenerator.sample` + - :meth:`~aces.idt.IDTBaseGenerator.sort` + - :meth:`~aces.idt.IDTBaseGenerator.generate_LUT` + - :meth:`~aces.idt.IDTBaseGenerator.filter_LUT` + - :meth:`~aces.idt.IDTBaseGenerator.decode` + - :meth:`~aces.idt.IDTBaseGenerator.optimise` + - :meth:`~aces.idt.IDTBaseGenerator.zip` + - :meth:`~aces.idt.IDTBaseGenerator.to_clf` + - :meth:`~aces.idt.IDTBaseGenerator.png_colour_checker_segmentation` + - :meth:`~aces.idt.IDTBaseGenerator.png_grey_card_sampling` + - :meth:`~aces.idt.IDTBaseGenerator.png_extrapolated_camera_samples` """ + GENERATOR_NAME = "IDTBaseGenerator" + """*IDT* generator name.""" + def __init__(self, project_settings): self._project_settings = project_settings self._samples_analysis = None @@ -77,18 +119,20 @@ def __init__(self, project_settings): self._image_colour_checker_segmentation = None @property - def project_settings(self): - """Returns the project settings passed into the generator from the application + def project_settings(self) -> IDTProjectSettings: + """ + Getter property for the project settings used by the generator. Returns ------- - IDTProjectSettings - The ProjectSettings passed into the generator + :class:`IDTProjectSettings` + Project settings used by the generator. """ + return self._project_settings @property - def image_colour_checker_segmentation(self): + def image_colour_checker_segmentation(self) -> NDArrayFloat | None: """ Getter property for the image of the colour checker with segmentation contours. @@ -102,7 +146,7 @@ def image_colour_checker_segmentation(self): return self._image_colour_checker_segmentation @property - def baseline_exposure(self): + def baseline_exposure(self) -> float: """ Getter property for the baseline exposure. @@ -115,7 +159,7 @@ def baseline_exposure(self): return self._baseline_exposure @property - def image_grey_card_sampling(self): + def image_grey_card_sampling(self) -> NDArrayFloat | None: """ Getter property for the image the grey card with sampling contours. contours. @@ -129,7 +173,7 @@ def image_grey_card_sampling(self): return self._image_grey_card_sampling @property - def samples_camera(self): + def samples_camera(self) -> NDArrayFloat | None: """ Getter property for the samples of the camera produced by the sorting process. @@ -143,7 +187,7 @@ def samples_camera(self): return self._samples_camera @property - def samples_reference(self): + def samples_reference(self) -> NDArrayFloat | None: """ Getter property for the reference samples produced by the sorting process. @@ -157,7 +201,7 @@ def samples_reference(self): return self._samples_reference @property - def LUT_unfiltered(self): + def LUT_unfiltered(self) -> LUT3x1D | None: """ Getter property for the unfiltered *LUT*. @@ -170,7 +214,7 @@ def LUT_unfiltered(self): return self._LUT_unfiltered @property - def LUT_filtered(self): + def LUT_filtered(self) -> LUT3x1D | None: """ Getter property for the filtered *LUT*. @@ -183,7 +227,7 @@ def LUT_filtered(self): return self._LUT_filtered @property - def LUT_decoding(self): + def LUT_decoding(self) -> LUT1D | LUT3x1D | None: """ Getter property for the (final) decoding *LUT*. @@ -196,9 +240,9 @@ def LUT_decoding(self): return self._LUT_decoding @property - def M(self): + def M(self) -> NDArrayFloat | None: """ - Getter property for the *IDT* matrix :math:`M`, + Getter property for the *IDT* matrix :math:`M`. Returns ------- @@ -209,7 +253,7 @@ def M(self): return self._M @property - def RGB_w(self): + def RGB_w(self) -> NDArrayFloat | None: """ Getter property for the white balance multipliers :math:`RGB_w`. @@ -222,7 +266,7 @@ def RGB_w(self): return self._RGB_w @property - def k(self): + def k(self) -> NDArrayFloat | None: """ Getter property for the exposure factor :math:`k` that results in a nominally "18% gray" object in the scene producing ACES values @@ -237,7 +281,7 @@ def k(self): return self._k @property - def samples_analysis(self): + def samples_analysis(self) -> NDArrayFloat | None: """ Getter property for the samples produced by the colour checker sampling process. @@ -250,7 +294,7 @@ def samples_analysis(self): return self._samples_analysis - def sample(self): + def sample(self) -> None: """ Sample the images from the *IDT* specification. """ @@ -504,18 +548,13 @@ def _reformat_image(image): if self.project_settings.cleanup: shutil.rmtree(self.project_settings.working_directory) - def sort(self): + def sort(self) -> None: """ Sort the samples produced by the image sampling process. The *ACES* reference samples are sorted and indexed as a function of the camera samples ordering. This ensures that the camera samples are monotonically increasing. - - Parameters - ---------- - reference_colour_checker : NDArray - Reference *ACES* *RGB* values for the *ColorChecker Classic*. """ LOGGER.info("Sorting camera and reference samples...") @@ -538,34 +577,34 @@ def sort(self): self._samples_reference = self._samples_reference[indices] @abstractmethod - def generate_LUT(self): - """Implement the generation of the list and must populate the unfiltered lut""" + def generate_LUT(self) -> None: + """Generate the unfiltered *LUT*.""" @abstractmethod - def filter_LUT(self): - """Implement the filtering of the lut and must populate the filtered lut""" + def filter_LUT(self) -> None: + """Filter/smooth the unfiltered *LUT* to produce the decoding *LUT*.""" @abstractmethod - def decode(self): - """Implement the decoding of the lut""" + def decode(self) -> None: + """Decode the samples with the decoding *LUT*.""" @abstractmethod - def optimise(self): + def optimise(self) -> None: """Implement any optimisation of the lut that is required""" def zip( - self, output_directory, information=None, archive_serialised_generator=False - ): + self, + output_directory: Path | str, + archive_serialised_generator: bool = False, + ) -> str: """ Zip the *Common LUT Format* (CLF) resulting from the *IDT* generation process. Parameters ---------- - output_directory : str + output_directory Output directory for the *zip* file. - information : dict - Information pertaining to the *IDT* and the computation parameters. archive_serialised_generator : bool Whether to serialise and archive the *IDT* generator. @@ -574,26 +613,27 @@ def zip( :class:`str` *Zip* file path. """ + # TODO There is a whole bunch of computation which happens within the ui to # calculate things like the delta_e. All of that logic should be moved to the # application or generator so we do not need to go out to the UI, to do # calculations which then come back into application / generator in order # for us to write it out - information = information or {} + + output_directory = Path(output_directory) LOGGER.info( 'Zipping the "CLF" resulting from the "IDT" generation ' - 'process in "%s" output directory using given information: "%s".', + 'process in "%s" output directory.', output_directory, - information, ) - if not os.path.exists(output_directory): - os.makedirs(output_directory) + + output_directory.mkdir(parents=True, exist_ok=True) camera_make = self.project_settings.camera_make camera_model = self.project_settings.camera_model - clf_path = self.to_clf(output_directory, information) + clf_path = self.to_clf(output_directory) json_path = f"{output_directory}/{camera_make}.{camera_model}.json" with open(json_path, "w") as json_file: @@ -601,6 +641,8 @@ def zip( zip_file = Path(output_directory) / f"IDT_{camera_make}_{camera_model}.zip" current_working_directory = os.getcwd() + + output_directory = str(output_directory) try: os.chdir(output_directory) with ZipFile(zip_file, "w") as zip_archive: @@ -612,7 +654,7 @@ def zip( return zip_file - def to_clf(self, output_directory, information): + def to_clf(self, output_directory: Path | str) -> str: """ Convert the *IDT* generation process data to *Common LUT Format* (CLF). @@ -620,8 +662,6 @@ def to_clf(self, output_directory, information): ---------- output_directory : str Output directory for the zip file. - information : dict - Information pertaining to the *IDT* and the computation parameters. Returns ------- @@ -631,9 +671,8 @@ def to_clf(self, output_directory, information): LOGGER.info( 'Converting "IDT" generation process data to "CLF" in "%s"' - 'output directory using given information: "%s".', + "output directory.", output_directory, - information, ) project_settings = self.project_settings @@ -662,7 +701,7 @@ def format_array(a): et_output_descriptor.text = "ACES2065-1" et_info = Et.SubElement(root, "Info") - et_metadata = Et.SubElement(et_info, "Archive") + et_metadata = Et.SubElement(et_info, "AcademyIDTCalculator") for key, prop in project_settings.properties: value = prop.getter(project_settings) if key == "schema_version": @@ -672,10 +711,6 @@ def format_array(a): et_metadata, key.replace("_", " ").title().replace(" ", "") ) sub_element.text = str(value) - et_academy_idt_calculator = Et.SubElement(et_info, "AcademyIDTCalculator") - for key, value in information.items(): - sub_element = Et.SubElement(et_academy_idt_calculator, key) - sub_element.text = str(value) et_lut = Et.SubElement( root, @@ -705,7 +740,7 @@ def format_array(a): return clf_path - def png_colour_checker_segmentation(self): + def png_colour_checker_segmentation(self) -> str | None: """ Return the colour checker segmentation image as *PNG* data. @@ -730,7 +765,7 @@ def png_colour_checker_segmentation(self): return data_png - def png_grey_card_sampling(self): + def png_grey_card_sampling(self) -> str | None: """ Return the grey card image sampling as *PNG* data. @@ -754,7 +789,7 @@ def png_grey_card_sampling(self): return data_png - def png_extrapolated_camera_samples(self): + def png_extrapolated_camera_samples(self) -> str | None: """ Return the extrapolated camera samples as *PNG* data. diff --git a/aces/idt/generators/prosumer_camera.py b/aces/idt/generators/prosumer_camera.py index 6d6697e..4289b11 100644 --- a/aces/idt/generators/prosumer_camera.py +++ b/aces/idt/generators/prosumer_camera.py @@ -1,13 +1,13 @@ """ -Input Device Transform (IDT) Prosumer Camera Utilities -====================================================== +IDT Prosumer Camera Generator +============================= + +Define the *IDT* generator class for a *Prosumer Camera*. """ import base64 import io -import json import logging -from pathlib import Path import colour import matplotlib as mpl @@ -23,6 +23,7 @@ LUT3x1D, ) from colour.algebra import smoothstep_function, vector_dot +from colour.hints import NDArrayFloat, Tuple from colour.io import LUT_to_LUT from colour.models import RGB_COLOURSPACE_ACES2065_1, RGB_luminance from colour.utilities import ( @@ -59,56 +60,31 @@ class IDTGeneratorProsumerCamera(IDTBaseGenerator): Parameters ---------- - archive : str, optional - *IDT* archive path, i.e. a zip file path. - image_format : str, optional - Image format to filter. - directory : str, optional - Working directory. - specification : dict, optional - *IDT* archive specification. - cleanup : bool, optional - Whether to cleanup, i.e. remove, the working directory. + project_settings : IDTProjectSettings, optional + *IDT* generator settings. Attributes ---------- - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.image_colour_checker_segmentation` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.image_grey_card_sampling` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.baseline_exposure` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.samples_analysis` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.samples_camera` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.samples_reference` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.samples_decoded` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.samples_weighted` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.LUT_unfiltered` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.LUT_filtered` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.LUT_decoding` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.M` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.RGB_w` - - :attr:`~aces.idt.IDTGeneratorProsumerCamera.k` + - :attr:`~aces.idt.IDTBaseGenerator.GENERATOR_NAME` + - :attr:`~aces.idt.IDTBaseGenerator.samples_decoded` + - :attr:`~aces.idt.IDTBaseGenerator.samples_weighted` Methods ------- - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.__str__` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.from_specification` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.sample` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.sort` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.generate_LUT` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.filter_LUT` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.decode` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.optimise` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.to_clf` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.zip` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.png_colour_checker_segmentation` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.png_grey_card_sampling` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.png_measured_camera_samples` - - :meth:`~aces.idt.IDTGeneratorProsumerCamera.png_extrapolated_camera_samples` + - :meth:`~aces.idt.IDTBaseGenerator.__str__` + - :meth:`~aces.idt.IDTBaseGenerator.generate_LUT` + - :meth:`~aces.idt.IDTBaseGenerator.filter_LUT` + - :meth:`~aces.idt.IDTBaseGenerator.decode` + - :meth:`~aces.idt.IDTBaseGenerator.optimise` + - :meth:`~aces.idt.IDTBaseGenerator.png_measured_camera_samples` + - :meth:`~aces.idt.IDTBaseGenerator.png_extrapolated_camera_samples` """ - generator_name = "IDTGeneratorProsumerCamera" + GENERATOR_NAME = "IDTGeneratorProsumerCamera" + """*IDT* generator name.""" - def __init__(self, application): - super().__init__(application) + def __init__(self, project_settings): + super().__init__(project_settings) self._lut_blending_edge_left = None self._lut_blending_edge_right = None @@ -117,7 +93,7 @@ def __init__(self, application): self._samples_weighted = None @property - def samples_decoded(self): + def samples_decoded(self) -> NDArrayFloat | None: """ Getter property for the samples of the camera decoded by applying the filtered *LUT*. @@ -131,7 +107,7 @@ def samples_decoded(self): return self._samples_decoded @property - def samples_weighted(self): + def samples_weighted(self) -> NDArrayFloat | None: """ Getter property for the decoded samples of the camera weighted across multiple exposures. @@ -145,7 +121,7 @@ def samples_weighted(self): return self._samples_weighted - def __str__(self): + def __str__(self) -> str: """ Return a formatted string representation of the *IDT* generator. @@ -154,8 +130,12 @@ def __str__(self): :class:`str` Formatted string representation. """ + samples_analysis = None - if self._samples_analysis is not None: + if ( + self._samples_analysis is not None + and self._samples_analysis.get("data") is not None + ): samples_analysis = self._samples_analysis["data"]["colour_checker"].get( self._baseline_exposure ) @@ -185,7 +165,7 @@ def __str__(self): ], ) - def generate_LUT(self): + def generate_LUT(self) -> LUT3x1D: """ Generate an unfiltered linearisation *LUT* for the camera samples. @@ -270,7 +250,7 @@ def generate_LUT(self): return self._LUT_unfiltered - def filter_LUT(self): + def filter_LUT(self) -> LUT3x1D: """ Filter/smooth the linearisation *LUT* for the camera samples. @@ -324,7 +304,7 @@ def filter_LUT(self): return self._LUT_filtered - def decode(self): + def decode(self) -> None: """ Decode the samples produced by the image sampling process. """ @@ -396,7 +376,7 @@ def decode(self): ) ) - def optimise(self): + def optimise(self) -> Tuple[NDArrayFloat]: """ Compute the *IDT* matrix. @@ -412,8 +392,8 @@ def optimise(self): # Exposure values to use when computing the *IDT* matrix. EV_range = tuple(self.project_settings.ev_range) - # Normalised weights used to sum the exposure values. If not given, the median - # of the exposure values is used. + # Normalised weights used to sum the exposure values. If not given, the + # median of the exposure values is used. EV_weights = self.project_settings.ev_weights # Training data multi-spectral distributions, defaults to using the *RAW to @@ -498,7 +478,7 @@ def optimise(self): return self._M, self._RGB_w, self._k - def png_measured_camera_samples(self): + def png_measured_camera_samples(self) -> str | None: """ Return the measured camera samples as *PNG* data. @@ -527,7 +507,7 @@ def png_measured_camera_samples(self): return data_png - def png_extrapolated_camera_samples(self): + def png_extrapolated_camera_samples(self) -> str | None: """ Return the extrapolated camera samples as *PNG* data. @@ -536,6 +516,7 @@ def png_extrapolated_camera_samples(self): :class:`str` or None *PNG* data. """ + # TODO This looks specific to the prosumer camera as its using the blending # edges, so will keep this as an override of the base class if ( @@ -571,27 +552,3 @@ def png_extrapolated_camera_samples(self): plt.close() return data_png - - -if __name__ == "__main__": - logging.basicConfig() - logging.getLogger().setLevel(logging.INFO) - - resources_directory = Path(__file__).parent / "tests" / "resources" - - idt_generator = IDTGeneratorProsumerCamera.from_archive( - resources_directory / "synthetic_001.zip", cleanup=True - ) - - LOGGER.info(idt_generator) - - with open( - resources_directory / "synthetic_001" / "synthetic_001.json" - ) as json_file: - specification = json.load(json_file) - - idt_generator = IDTGeneratorProsumerCamera.from_specification( - specification, resources_directory / "synthetic_001" - ) - - LOGGER.info(idt_generator) diff --git a/apps/common.py b/apps/common.py index b7ba9c9..079537e 100644 --- a/apps/common.py +++ b/apps/common.py @@ -27,7 +27,7 @@ Tooltip, ) -from aces.idt import OPTIMISATION_FACTORIES, clf_processing_elements +from aces.idt import IDTProjectSettings, clf_processing_elements __author__ = "Alex Forsythe, Gayle McAdams, Thomas Mansencal, Nick Shaw" __copyright__ = "Copyright 2020 Academy of Motion Picture Arts and Sciences" @@ -240,18 +240,17 @@ def metadata_card_default(_uid, *args): """ OPTIONS_CAMERA_SENSITIVITIES = [ - {"label": key, "value": key} for key in sorted(MSDS_CAMERA_SENSITIVITIES) + {"label": key, "value": key} + for key in ["Custom", *sorted(MSDS_CAMERA_SENSITIVITIES)] ] """ Camera sensitivities options for a :class:`Dropdown` class instance. OPTIONS_CAMERA_SENSITIVITIES : list """ -OPTIONS_CAMERA_SENSITIVITIES.insert(0, {"label": "Custom", "value": "Custom"}) OPTIONS_CAT = [ - {"label": key, "value": key} - for key in sorted(colour.CHROMATIC_ADAPTATION_TRANSFORMS.keys()) + {"label": key, "value": key} for key in IDTProjectSettings.cat.metadata.options ] """ *Chromatic adaptation transform* options for a :class:`Dropdown` class @@ -259,23 +258,20 @@ def metadata_card_default(_uid, *args): OPTIONS_CAT : list """ -OPTIONS_CAT.append({"label": "None", "value": None}) OPTIONS_ILLUMINANT = [ - {"label": key, "value": key} for key in sorted(colour.SDS_ILLUMINANTS.keys()) + {"label": key, "value": key} + for key in IDTProjectSettings.illuminant.metadata.options ] """ Illuminant options for a :class:`Dropdown`class instance. ILLUMINANTS_OPTIONS : list """ -OPTIONS_ILLUMINANT.insert(0, {"label": "Custom", "value": "Custom"}) -OPTIONS_ILLUMINANT.insert(1, {"label": "Blackbody", "value": "Blackbody"}) -OPTIONS_ILLUMINANT.insert(1, {"label": "Daylight", "value": "Daylight"}) OPTIONS_INTERPOLATION = [ {"label": key, "value": key} - for key in ["Cubic Spline", "Linear", "PCHIP", "Sprague (1880)"] + for key in IDTProjectSettings.illuminant_interpolator.metadata.options ] INTERPOLATORS = { @@ -291,7 +287,8 @@ def metadata_card_default(_uid, *args): """ OPTIONS_OPTIMISATION_SPACES = [ - {"label": key, "value": key} for key in OPTIMISATION_FACTORIES + {"label": key, "value": key} + for key in IDTProjectSettings.optimisation_space.metadata.options ] """ Optimisation colourspaces. @@ -300,7 +297,8 @@ def metadata_card_default(_uid, *args): """ OPTIONS_DISPLAY_COLOURSPACES = [ - {"label": key, "value": key} for key in ["sRGB", "Display P3"] + {"label": key, "value": key} + for key in IDTProjectSettings.rgb_display_colourspace.metadata.options ] """ Display colourspaces. diff --git a/apps/idt_calculator_p_2013_001.py b/apps/idt_calculator_p_2013_001.py index 398758b..3c0ec8a 100644 --- a/apps/idt_calculator_p_2013_001.py +++ b/apps/idt_calculator_p_2013_001.py @@ -57,7 +57,12 @@ Tooltip, ) -from aces.idt import error_delta_E, png_compare_colour_checkers, slugify +from aces.idt import ( + OPTIMISATION_FACTORIES, + error_delta_E, + png_compare_colour_checkers, + slugify, +) from app import APP, SERVER_URL, __version__ from apps.common import ( COLOUR_ENVIRONMENT, @@ -66,7 +71,6 @@ DELAY_TOOLTIP_DEFAULT, INTERPOLATORS, MSDS_CAMERA_SENSITIVITIES, - OPTIMISATION_FACTORIES, OPTIONS_CAMERA_SENSITIVITIES, OPTIONS_CAT, OPTIONS_DISPLAY_COLOURSPACES, @@ -905,6 +909,8 @@ def compute_idt_p2013_001( clicked. camera_model : str Name of the camera. + sensitivities_name : str + Name of the camera sensitivities. sensitivities_data : list List of wavelength dicts of camera sensitivities data. illuminant_name : str diff --git a/apps/idt_calculator_prosumer_camera.py b/apps/idt_calculator_prosumer_camera.py index 723b18c..16b4236 100644 --- a/apps/idt_calculator_prosumer_camera.py +++ b/apps/idt_calculator_prosumer_camera.py @@ -3,7 +3,6 @@ ========================================================= """ -import datetime import logging import os import tempfile @@ -23,7 +22,7 @@ from colour.characterisation import camera_RGB_to_ACES2065_1 from colour.models import RGB_COLOURSPACE_ACES2065_1 from colour.temperature import CCT_to_xy_CIE_D -from colour.utilities import CACHE_REGISTRY, as_float, as_int_scalar, optional +from colour.utilities import CACHE_REGISTRY, as_float from dash.dash_table import DataTable from dash.dash_table.Format import Format, Scheme from dash.dcc import Download, Link, Location, Markdown, Tab, Tabs, send_file @@ -65,7 +64,9 @@ from dash_uploader import Upload, callback, configure_upload from aces.idt import ( - IDTGeneratorProsumerCamera, + DirectoryStructure, + IDTGeneratorApplication, + IDTProjectSettings, error_delta_E, generate_reference_colour_checker, hash_file, @@ -78,7 +79,6 @@ DATATABLE_DECIMALS, DELAY_TOOLTIP_DEFAULT, INTERPOLATORS, - OPTIMISATION_FACTORIES, OPTIONS_CAT, OPTIONS_DISPLAY_COLOURSPACES, OPTIONS_ILLUMINANT, @@ -157,7 +157,7 @@ _PATH_UPLOADED_IDT_ARCHIVE = None _HASH_IDT_ARCHIVE = None -_IDT_GENERATOR = None +_IDT_GENERATOR_APPLICATION = None _PATH_IDT_ZIP = None _CACHE_DATA_ARCHIVE_TO_SAMPLES = CACHE_REGISTRY.register_cache( @@ -165,7 +165,8 @@ ) _OPTIONS_DECODING_METHOD = [ - {"label": key, "value": key} for key in ["Median", "Average", "Per Channel", "ACES"] + {"label": key, "value": key} + for key in IDTProjectSettings.decoding_method.metadata.options ] @@ -714,12 +715,6 @@ def _uid(id_): """ -def _attribute_value(attribute, default): - """Return given attribute value from the IDT specification.""" - - return optional(_IDT_GENERATOR.specification["header"].get(attribute), default) - - @callback( output=Output(_uid("compute-idt-card"), "style"), id=_uid("idt-archive-upload"), @@ -953,6 +948,8 @@ def download_idt_zip( Name of the debayering platform, e.g. "Resolve". debayering_settings : str Debayering platform settings. + encoding_colourspace : str + Encoding colourspace, e.g. "ARRI LogC4". Returns ------- @@ -961,25 +958,27 @@ def download_idt_zip( Download component. """ - _IDT_GENERATOR.specification["header"]["aces_transform_id"] = str(aces_transform_id) - _IDT_GENERATOR.specification["header"]["aces_user_name"] = str(aces_user_name) - _IDT_GENERATOR.specification["header"]["camera_make"] = str(camera_make) - _IDT_GENERATOR.specification["header"]["camera_model"] = str(camera_model) - _IDT_GENERATOR.specification["header"]["iso"] = float(iso) - _IDT_GENERATOR.specification["header"]["temperature"] = float(temperature) - _IDT_GENERATOR.specification["header"]["additional_camera_settings"] = str( + _IDT_GENERATOR_APPLICATION.project_settings.aces_transform_id = str( + aces_transform_id + ) + _IDT_GENERATOR_APPLICATION.project_settings.aces_user_name = str(aces_user_name) + _IDT_GENERATOR_APPLICATION.project_settings.camera_make = str(camera_make) + _IDT_GENERATOR_APPLICATION.project_settings.camera_model = str(camera_model) + _IDT_GENERATOR_APPLICATION.project_settings.iso = float(iso) + _IDT_GENERATOR_APPLICATION.project_settings.temperature = float(temperature) + _IDT_GENERATOR_APPLICATION.project_settings.additional_camera_settings = str( additional_camera_settings ) - _IDT_GENERATOR.specification["header"]["lighting_setup_description"] = str( + _IDT_GENERATOR_APPLICATION.project_settings.lighting_setup_description = str( lighting_setup_description ) - _IDT_GENERATOR.specification["header"]["debayering_platform"] = str( + _IDT_GENERATOR_APPLICATION.project_settings.debayering_platform = str( debayering_platform ) - _IDT_GENERATOR.specification["header"]["debayering_settings"] = str( + _IDT_GENERATOR_APPLICATION.project_settings.debayering_settings = str( debayering_settings ) - _IDT_GENERATOR.specification["header"]["encoding_colourspace"] = str( + _IDT_GENERATOR_APPLICATION.project_settings.encoding_colourspace = str( encoding_colourspace ) @@ -987,9 +986,8 @@ def download_idt_zip( global _PATH_IDT_ZIP # noqa: PLW0603 - _PATH_IDT_ZIP = _IDT_GENERATOR.zip( + _PATH_IDT_ZIP = _IDT_GENERATOR_APPLICATION.zip( os.path.dirname(_PATH_UPLOADED_IDT_ARCHIVE), - _IDT_GENERATOR.information, ) return send_file(_PATH_IDT_ZIP) @@ -1038,7 +1036,6 @@ def download_idt_zip( State(_uid("grey-card-reflectance"), "value"), State(_uid("lut-size-select"), "value"), State(_uid("lut-smoothing-input-number"), "value"), - State(_uid("url"), "href"), ], prevent_initial_call=True, ) @@ -1066,7 +1063,6 @@ def compute_idt_prosumer_camera( grey_card_reflectance, LUT_size, LUT_smoothing, - href, ): """ Compute the *Input Device Transform* (IDT) for a prosumer camera. @@ -1124,8 +1120,6 @@ def compute_idt_prosumer_camera( LUT_smoothing : integer Standard deviation of the gaussian convolution kernel used for smoothing. - href - URL. Returns ------- @@ -1197,11 +1191,24 @@ def compute_idt_prosumer_camera( chromatic_adaptation_transform=chromatic_adaptation_transform, ) - global _IDT_GENERATOR # noqa: PLW0603 + global _IDT_GENERATOR_APPLICATION # noqa: PLW0603 global _HASH_IDT_ARCHIVE # noqa: PLW0603 - _IDT_GENERATOR = IDTGeneratorProsumerCamera( - _PATH_UPLOADED_IDT_ARCHIVE, cleanup=True + project_settings = IDTProjectSettings( + aces_transform_id=aces_transform_id, + aces_user_name=aces_user_name, + camera_make=camera_make, + camera_model=camera_model, + iso=iso, + temperature=temperature, + additional_camera_settings=additional_camera_settings, + lighting_setup_description=lighting_setup_description, + debayering_platform=debayering_platform, + debayering_settings=debayering_settings, + encoding_colourspace=encoding_colourspace, + ) + _IDT_GENERATOR_APPLICATION = IDTGeneratorApplication( + "IDTGeneratorProsumerCamera", project_settings ) if _HASH_IDT_ARCHIVE is None: @@ -1209,64 +1216,31 @@ def compute_idt_prosumer_camera( LOGGER.debug('"Archive hash: "%s"', _HASH_IDT_ARCHIVE) if _CACHE_DATA_ARCHIVE_TO_SAMPLES.get(_HASH_IDT_ARCHIVE) is None: - _IDT_GENERATOR.extract() + _IDT_GENERATOR_APPLICATION.extract(_PATH_UPLOADED_IDT_ARCHIVE) os.remove(_PATH_UPLOADED_IDT_ARCHIVE) - _IDT_GENERATOR.sample() + _IDT_GENERATOR_APPLICATION.generator.sample() _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] = ( - _IDT_GENERATOR.specification, - _IDT_GENERATOR.samples_analysis, - _IDT_GENERATOR.baseline_exposure, + _IDT_GENERATOR_APPLICATION.project_settings.data, + _IDT_GENERATOR_APPLICATION.generator.samples_analysis, + _IDT_GENERATOR_APPLICATION.generator.baseline_exposure, ) else: ( - _IDT_GENERATOR._specification, - _IDT_GENERATOR._samples_analysis, - _IDT_GENERATOR._baseline_exposure, + _IDT_GENERATOR_APPLICATION.project_settings.data, + _IDT_GENERATOR_APPLICATION.generator._samples_analysis, + _IDT_GENERATOR_APPLICATION.generator._baseline_exposure, ) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] - aces_transform_id = _IDT_GENERATOR.specification["header"][ - "aces_transform_id" - ] = _attribute_value("aces_transform_id", aces_transform_id) - aces_user_name = _IDT_GENERATOR.specification["header"][ - "aces_user_name" - ] = _attribute_value("aces_user_name", aces_user_name) - camera_make = _IDT_GENERATOR.specification["header"][ - "camera_make" - ] = _attribute_value("camera_make", camera_make) - camera_model = _IDT_GENERATOR.specification["header"][ - "camera_model" - ] = _attribute_value("camera_model", camera_model) - iso = _IDT_GENERATOR.specification["header"]["iso"] = _attribute_value("iso", iso) - temperature = _IDT_GENERATOR.specification["header"][ - "temperature" - ] = _attribute_value("temperature", temperature) - additional_camera_settings = _IDT_GENERATOR.specification["header"][ - "additional_camera_settings" - ] = _attribute_value("additional_camera_settings", additional_camera_settings) - lighting_setup_description = _IDT_GENERATOR.specification["header"][ - "lighting_setup_description" - ] = _attribute_value("lighting_setup_description", lighting_setup_description) - debayering_platform = _IDT_GENERATOR.specification["header"][ - "debayering_platform" - ] = _attribute_value("debayering_platform", debayering_platform) - debayering_settings = _IDT_GENERATOR.specification["header"][ - "debayering_settings" - ] = _attribute_value("debayering_settings", debayering_settings) - encoding_colourspace = _IDT_GENERATOR.specification["header"][ - "encoding_colourspace" - ] = _attribute_value("encoding_colourspace", encoding_colourspace) - - _IDT_GENERATOR.sort(reference_colour_checker) - _IDT_GENERATOR.generate_LUT(as_int_scalar(LUT_size)) - _IDT_GENERATOR.filter_LUT(as_int_scalar(LUT_smoothing)) - _IDT_GENERATOR.decode(decoding_method, np.loadtxt([grey_card_reflectance])) - _IDT_GENERATOR.optimise( - np.loadtxt([EV_range]), - training_data=reference_colour_checker, - optimisation_factory=OPTIMISATION_FACTORIES[optimisation_space], - ) + generator = _IDT_GENERATOR_APPLICATION.generator + project_settings = _IDT_GENERATOR_APPLICATION.project_settings + + generator.sort() + generator.generate_LUT() + generator.filter_LUT() + generator.decode() + generator.optimise() - logging.info(str(_IDT_GENERATOR)) + logging.info(str(generator)) def RGB_working_to_RGB_display(RGB): """ @@ -1281,35 +1255,34 @@ def RGB_working_to_RGB_display(RGB): apply_cctf_encoding=True, ) - samples_median = _IDT_GENERATOR.samples_analysis["data"]["colour_checker"][ - _IDT_GENERATOR.baseline_exposure - ]["samples_median"] + samples_median = _IDT_GENERATOR_APPLICATION.generator.samples_analysis[ + DirectoryStructure.COLOUR_CHECKER + ][generator.baseline_exposure]["samples_median"] samples_idt = camera_RGB_to_ACES2065_1( # "camera_RGB_to_ACES2065_1" divides RGB by "min(RGB_w)" for highlights # recovery, this is not required here as the images are expected to be # fully processed, thus we pre-emptively multiply by "min(RGB_w)". - _IDT_GENERATOR.LUT_decoding.apply(samples_median) - * np.min(_IDT_GENERATOR.RGB_w), - _IDT_GENERATOR.M, - _IDT_GENERATOR.RGB_w, - _IDT_GENERATOR.k, + generator.LUT_decoding.apply(samples_median) * np.min(generator.RGB_w), + generator.M, + generator.RGB_w, + generator.k, ) - if _IDT_GENERATOR.baseline_exposure != 0: + if generator.baseline_exposure != 0: LOGGER.warning( "Compensating display and metric computations for non-zero " "baseline exposure!" ) - samples_idt *= pow(2, -_IDT_GENERATOR.baseline_exposure) + samples_idt *= pow(2, -generator.baseline_exposure) compare_colour_checkers_idt_correction = png_compare_colour_checkers( RGB_working_to_RGB_display(samples_idt), RGB_working_to_RGB_display(reference_colour_checker), ) - samples_decoded = _IDT_GENERATOR.LUT_decoding.apply(samples_median) + samples_decoded = generator.LUT_decoding.apply(samples_median) compare_colour_checkers_LUT_correction = png_compare_colour_checkers( RGB_working_to_RGB_display(samples_decoded), RGB_working_to_RGB_display(reference_colour_checker), @@ -1351,7 +1324,7 @@ def RGB_working_to_RGB_display(RGB): ] # Segmentation - colour_checker_segmentation = _IDT_GENERATOR.png_colour_checker_segmentation() + colour_checker_segmentation = generator.png_colour_checker_segmentation() if colour_checker_segmentation is not None: components += [ H3("Segmentation", style={"textAlign": "center"}), @@ -1360,7 +1333,7 @@ def RGB_working_to_RGB_display(RGB): style={"width": "100%"}, ), ] - grey_card_sampling = _IDT_GENERATOR.png_grey_card_sampling() + grey_card_sampling = generator.png_grey_card_sampling() if grey_card_sampling is not None: components += [ Img( @@ -1370,8 +1343,8 @@ def RGB_working_to_RGB_display(RGB): ] # Camera Samples - measured_camera_samples = _IDT_GENERATOR.png_measured_camera_samples() - extrapolated_camera_samples = _IDT_GENERATOR.png_extrapolated_camera_samples() + measured_camera_samples = generator.png_measured_camera_samples() + extrapolated_camera_samples = generator.png_extrapolated_camera_samples() if None not in (measured_camera_samples, extrapolated_camera_samples): components += [ H3("Measured Camera Samples", style={"textAlign": "center"}), @@ -1386,39 +1359,19 @@ def RGB_working_to_RGB_display(RGB): ), ] - _IDT_GENERATOR.information = { - "Application": f"{APP_NAME_LONG} - {__version__}", - "Url": href, - "Date": datetime.datetime.now(datetime.timezone.utc).strftime( - "%b %d, %Y %H:%M:%S" - ), - "RGBDisplayColourspace": RGB_display_colourspace, - "IlluminantName": illuminant_name, - "IlluminantData": parsed_illuminant_data, - "ChromaticAdaptationTransform": chromatic_adaptation_transform, - "OptimisationSpace": optimisation_space, - "IlluminantInterpolator": illuminant_interpolator, - "DecodingMethod": decoding_method, - "EVRange": EV_range, - "GreyCardReflectance": grey_card_reflectance, - "LUTSize": LUT_size, - "LUTSmoothing": LUT_smoothing, - "DeltaE": delta_E_idt, - } - return ( "", components, {"display": "block"}, - aces_transform_id, - aces_user_name, - camera_make, - camera_model, - iso, - temperature, - additional_camera_settings, - lighting_setup_description, - debayering_platform, - debayering_settings, - encoding_colourspace, + project_settings.aces_transform_id, + project_settings.aces_user_name, + project_settings.camera_make, + project_settings.camera_model, + project_settings.iso, + project_settings.temperature, + project_settings.additional_camera_settings, + project_settings.lighting_setup_description, + project_settings.debayering_platform, + project_settings.debayering_settings, + project_settings.encoding_colourspace, ) diff --git a/tests/test_application.py b/tests/test_application.py index 40ac5cf..01973f4 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -37,8 +37,8 @@ def test_prosumer_generator_sample(self): idt_application = IDTGeneratorApplication() idt_application.generator = "IDTGeneratorProsumerCamera" archive = os.path.join(self.get_test_resources_folder(), "synthetic_001.zip") - working_dir = idt_application.extract_archive(archive) - idt_application.project_settings.working_directory = working_dir + working_directory = idt_application.extract(archive) + idt_application.project_settings.working_directory = working_directory generator = idt_application.generator generator.sample() @@ -66,7 +66,7 @@ def test_prosumer_generator_sort(self): idt_application = IDTGeneratorApplication() idt_application.generator = "IDTGeneratorProsumerCamera" archive = os.path.join(self.get_test_resources_folder(), "synthetic_001.zip") - working_dir = idt_application.extract_archive(archive) + working_dir = idt_application.extract(archive) idt_application.project_settings.working_directory = working_dir generator = idt_application.generator @@ -98,7 +98,7 @@ def test_prosumer_generator_from_archive(self): idt_application.generator = "IDTGeneratorProsumerCamera" archive = os.path.join(self.get_test_resources_folder(), "synthetic_001.zip") - idt_generator_1 = idt_application.process_from_archive(archive) + idt_generator_1 = idt_application.process(archive) np.testing.assert_allclose( idt_generator_1.LUT_decoding.table, np.array( @@ -1157,7 +1157,7 @@ def test_prosumer_generator_from_archive(self): idt_application2 = IDTGeneratorApplication() idt_application2.generator = "IDTGeneratorProsumerCamera" archive2 = os.path.join(self.get_test_resources_folder(), "synthetic_002.zip") - idt_generator_2 = idt_application2.process_from_archive(archive2) + idt_generator_2 = idt_application2.process(archive2) np.testing.assert_allclose( idt_generator_1.M, @@ -1181,7 +1181,7 @@ def test_prosumer_generator_from_archive(self): idt_application3.generator = "IDTGeneratorProsumerCamera" archive3 = os.path.join(self.get_test_resources_folder(), "synthetic_003.zip") - idt_generator_3 = idt_application3.process_from_archive(archive3) + idt_generator_3 = idt_application3.process(archive3) np.testing.assert_allclose( idt_generator_3.M, @@ -1213,7 +1213,7 @@ def test_prosumer_generator_from_archive_no_json(self): idt_application.generator = "IDTGeneratorProsumerCamera" archive = os.path.join(self.get_test_resources_folder(), "synthetic_004.zip") - idt_application.process_from_archive(archive) + idt_application.process(archive) def test_prosumer_generator_from_archive_zip(self): """Test the prosumer generator from archive with a json file""" @@ -1221,7 +1221,7 @@ def test_prosumer_generator_from_archive_zip(self): idt_application.generator = "IDTGeneratorProsumerCamera" archive = os.path.join(self.get_test_resources_folder(), "synthetic_001.zip") - idt_application.process_from_archive(archive) + idt_application.process(archive) zip_file = idt_application.zip( self.get_test_output_folder(), archive_serialised_generator=False )