diff --git a/packages/open_vp_cal/src/open_vp_cal/core/constants.py b/packages/open_vp_cal/src/open_vp_cal/core/constants.py index f6012bb2..4d32d7ed 100644 --- a/packages/open_vp_cal/src/open_vp_cal/core/constants.py +++ b/packages/open_vp_cal/src/open_vp_cal/core/constants.py @@ -174,6 +174,33 @@ def all() -> list[str]: """ Returns the list of all Enum values""" return [member.value for member in LedWallSettingsKeys] + +# Properties that are linked between a wall and its verification wall. +# When set on the parent wall, these propagate to the verification wall. +# When accessed on a verification wall, these read from the parent wall. +LINKED_LED_WALL_PROPERTIES: frozenset[str] = frozenset({ + LedWallSettingsKeys.AVOID_CLIPPING, + LedWallSettingsKeys.ENABLE_EOTF_CORRECTION, + LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION, + LedWallSettingsKeys.AUTO_WB_SOURCE, + LedWallSettingsKeys.CALCULATION_ORDER, + LedWallSettingsKeys.PRIMARIES_SATURATION, + LedWallSettingsKeys.INPUT_PLATE_GAMUT, + LedWallSettingsKeys.NATIVE_CAMERA_GAMUT, + LedWallSettingsKeys.NUM_GREY_PATCHES, + LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT, + LedWallSettingsKeys.SHADOW_ROLLOFF, + LedWallSettingsKeys.TARGET_GAMUT, + LedWallSettingsKeys.TARGET_EOTF, + LedWallSettingsKeys.TARGET_MAX_LUM_NITS, + LedWallSettingsKeys.TARGET_TO_SCREEN_CAT, + LedWallSettingsKeys.MATCH_REFERENCE_WALL, + LedWallSettingsKeys.REFERENCE_WALL, + LedWallSettingsKeys.USE_WHITE_POINT_OFFSET, + LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE, +}) + + class PATCHES(StrEnum): """ Constants to define the names of the patches we use for the calibration, and a small helper function to get the order of the patches diff --git a/packages/open_vp_cal/src/open_vp_cal/framework/frame.py b/packages/open_vp_cal/src/open_vp_cal/framework/frame.py index ed762175..9afc5bfa 100644 --- a/packages/open_vp_cal/src/open_vp_cal/framework/frame.py +++ b/packages/open_vp_cal/src/open_vp_cal/framework/frame.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from OpenImageIO import ImageBuf - from open_vp_cal.project_settings import ProjectSettings + from open_vp_cal.led_wall_settings import LedWallSettings class Frame: @@ -29,14 +29,14 @@ class Frame: A class to represent a single frame of an image sequence. """ - def __init__(self, project_settings: "ProjectSettings"): + def __init__(self, led_wall_settings: "LedWallSettings"): """ Initializes a Frame instance with frame number, file name, and image buffer set to None. """ self._frame_num = None self._file_name = None self._image_buf = None - self._project_settings = project_settings + self._led_wall_settings = led_wall_settings @property def frame_num(self) -> int: diff --git a/packages/open_vp_cal/src/open_vp_cal/framework/sequence_loader.py b/packages/open_vp_cal/src/open_vp_cal/framework/sequence_loader.py index 7626bbe5..4efb99fd 100644 --- a/packages/open_vp_cal/src/open_vp_cal/framework/sequence_loader.py +++ b/packages/open_vp_cal/src/open_vp_cal/framework/sequence_loader.py @@ -266,7 +266,7 @@ def _load_frame(self, frame_num: int) -> Frame: if not os.path.exists(full_file_path): raise IOError(f"File {full_file_path} does not exist.") - frame = self.frame_class(self.led_wall_settings.project_settings) + frame = self.frame_class(self.led_wall_settings) frame.frame_num = frame_num frame.file_name = full_file_name frame.image_buf = imaging_utils.load_image(full_file_path) diff --git a/packages/open_vp_cal/src/open_vp_cal/imaging/imaging_utils.py b/packages/open_vp_cal/src/open_vp_cal/imaging/imaging_utils.py index 1f724837..d9bf0d40 100644 --- a/packages/open_vp_cal/src/open_vp_cal/imaging/imaging_utils.py +++ b/packages/open_vp_cal/src/open_vp_cal/imaging/imaging_utils.py @@ -659,18 +659,18 @@ def get_scaled_cie_spectrum_bg_image(max_scale: int) -> Oiio.ImageBuf: def load_image_buffer_to_qimage(buffer: Oiio.ImageBuf, - project_settings: "ProjectSettings") -> QImage: + input_plate_gamut: str) -> QImage: """ Load an image buffer into a QImage Args: buffer: The image buffer to load - project_settings: The project settings we want to use to access the correct ocio config + input_plate_gamut: The input plate gamut colour space name for color conversion Returns: The QImage loaded from the buffer """ buf = apply_color_conversion( - buffer, project_settings.current_wall.input_plate_gamut, "sRGB - Display") + buffer, input_plate_gamut, "sRGB - Display") with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp: res = buf.write(temp.name, Oiio.UINT8) if not res: @@ -684,17 +684,17 @@ def load_image_buffer_to_qimage(buffer: Oiio.ImageBuf, def load_image_buffer_to_qpixmap(buffer: Oiio.ImageBuf, - project_settings: "ProjectSettings") -> QPixmap: + input_plate_gamut: str) -> QPixmap: """ Load an Oiio.ImageBuf into a QPixmap so we can display it Args: buffer: The image buffer to load - project_settings: The project settings we want to use to access the correct ocio config + input_plate_gamut: The input plate gamut colour space name for color conversion Returns: The QPixmap loaded from the buffer """ - image = load_image_buffer_to_qimage(buffer, project_settings) + image = load_image_buffer_to_qimage(buffer, input_plate_gamut) pixmap = QPixmap.fromImage( image ) diff --git a/packages/open_vp_cal/src/open_vp_cal/led_wall_settings.py b/packages/open_vp_cal/src/open_vp_cal/led_wall_settings.py index 5b1f4824..d46402f2 100644 --- a/packages/open_vp_cal/src/open_vp_cal/led_wall_settings.py +++ b/packages/open_vp_cal/src/open_vp_cal/led_wall_settings.py @@ -18,9 +18,9 @@ """ from __future__ import annotations import json -from typing import List, Union, Any +from typing import List, Union, Any, Optional from typing import TYPE_CHECKING -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, PrivateAttr, ConfigDict from open_vp_cal.core import constants from open_vp_cal.core.structures import ProcessingResults @@ -31,8 +31,15 @@ from open_vp_cal.project_settings import ProjectSettings -class LedWallSettingsBaseModel(BaseModel): - """Base model for LedWallSettings with typing.""" +class LedWallSettings(BaseModel): + """A pydantic model class to handle led wall settings with serialization and business logic.""" + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + + # ===== Serialized Fields (from former LedWallSettingsBaseModel) ===== name: str = Field(default="Wall1") avoid_clipping: bool = Field(default=False) enable_eotf_correction: bool = Field(default=True) @@ -41,15 +48,30 @@ class LedWallSettingsBaseModel(BaseModel): input_sequence_folder: str = Field(default="") num_grey_patches: int = Field(default=30, ge=0, le=100) primaries_saturation: float = Field(default=0.7, ge=0, le=1) - calculation_order: constants.CalculationOrder = Field(default=constants.CalculationOrder(constants.CalculationOrder.default())) - input_plate_gamut: constants.ColourSpace|str = Field(default=constants.ColourSpace(constants.ColourSpace.default_ref())) - native_camera_gamut: constants.CameraColourSpace|str = Field(default=constants.CameraColourSpace(constants.CameraColourSpace.default())) - reference_to_target_cat: constants.CAT = Field(default=constants.CAT(constants.CAT.CAT_BRADFORD)) - roi: List[List[int]] = Field(default=[], description="roi is consist of 4 points [[tl.x,tl.y],[tr.x,tr.y],[br.x,br.y],[bl.x,bl.y]]") + calculation_order: constants.CalculationOrder = Field( + default=constants.CalculationOrder(constants.CalculationOrder.default()) + ) + input_plate_gamut: constants.ColourSpace | str = Field( + default=constants.ColourSpace(constants.ColourSpace.default_ref()) + ) + native_camera_gamut: constants.CameraColourSpace | str = Field( + default=constants.CameraColourSpace(constants.CameraColourSpace.default()) + ) + reference_to_target_cat: constants.CAT = Field( + default=constants.CAT(constants.CAT.CAT_BRADFORD) + ) + roi: List[List[int]] = Field( + default=[], + description="roi is consist of 4 points [[tl.x,tl.y],[tr.x,tr.y],[br.x,br.y],[bl.x,bl.y]]" + ) shadow_rolloff: float = Field(default=0.008) target_max_lum_nits: int = Field(default=1000, ge=0, le=constants.PQ.PQ_MAX_NITS) - target_gamut: constants.LedColourSpace|str = Field(default=constants.LedColourSpace(constants.LedColourSpace.default_target())) - target_eotf: constants.EOTF = Field(default=constants.EOTF(constants.EOTF.default())) + target_gamut: constants.LedColourSpace | str = Field( + default=constants.LedColourSpace(constants.LedColourSpace.default_target()) + ) + target_eotf: constants.EOTF = Field( + default=constants.EOTF(constants.EOTF.default()) + ) target_to_screen_cat: constants.CAT = Field(default=constants.CAT.CAT_NONE) match_reference_wall: bool = Field(default=False) reference_wall: str = Field(default="") @@ -58,6 +80,21 @@ class LedWallSettingsBaseModel(BaseModel): is_verification_wall: bool = Field(default=False) verification_wall: str = Field(default="") + # ===== Runtime Fields (excluded from serialization) ===== + processing_results: ProcessingResults = Field( + default_factory=ProcessingResults, + exclude=True + ) + separation_results: Optional[SeparationResults] = Field( + default=None, + exclude=True + ) + + # ===== Private Attributes (not in schema at all) ===== + _sequence_loader: Optional[SequenceLoader] = PrivateAttr(default=None) + _sequence_loader_class: type = PrivateAttr(default=SequenceLoader) + _project_settings: Optional["ProjectSettings"] = PrivateAttr(default=None) + @field_validator( "roi", mode="before", @@ -77,594 +114,228 @@ def upgrade_roi(cls, value: Any) -> List[List[int]]: """ if isinstance(value, list) and len(value) == 4 and all(isinstance(e, int) for e in value): left, right, top, bottom = value - top_left:List[int] = [left, top] - top_right:List[int] = [right, top] - bottom_right:List[int] = [right, bottom] - bottom_left:List[int] = [left, bottom] + top_left: List[int] = [left, top] + top_right: List[int] = [right, top] + bottom_right: List[int] = [right, bottom] + bottom_left: List[int] = [left, bottom] return [top_left, top_right, bottom_right, bottom_left] return value - -class LedWallSettings: - """A class to handle led wall settings.""" - def __init__(self, project_settings: ProjectSettings, name="Wall1"): - """Initialize an empty LedWallSettings object.""" - self.processing_results:ProcessingResults = ProcessingResults() - self.separation_results:SeparationResults|None = None - self.project_settings = project_settings - - self._sequence_loader = None - self._sequence_loader_class = SequenceLoader - self._led_settings = LedWallSettingsBaseModel(name=name) - - def reset_defaults(self): - """Reset the LedWallSettings object to its default values.""" - self._led_settings = LedWallSettingsBaseModel(name=self._led_settings.name) - - def clear(self): - """Clears the roi, processing and separation results. So that we can start fresh with - a new sequence being loaded""" - self.processing_results = ProcessingResults() - self.separation_results = None - self.roi = [] - - def clear_led_settings(self): - """ - Clear the LED settings and restore them to the defaults - """ - self._led_settings = LedWallSettingsBaseModel(name=self.name) - - def _set_property(self, field_name: constants.LedWallSettingsKeys, value: Any) -> None: - """ Sets the internal property data stores for the given field name, and given value. - If the led wall is a verification wall, it will not set the verification wall's settings - If the led wall has a verification wall, it will also set the value on the verification wall + def __init__(self, project_settings: Optional["ProjectSettings"] = None, **data): + """Initialize a LedWallSettings object. Args: - field_name: The name of the property to set in the data store - value: The value we want to set the property to + project_settings: The project this LED wall belongs to + **data: Field values to initialize """ - if self.is_verification_wall: - return - - setattr(self._led_settings, field_name, value) + super().__init__(**data) + self._project_settings = project_settings + self._sequence_loader = None + self._sequence_loader_class = SequenceLoader - if not self.verification_wall_as_wall: + def __setattr__(self, name: str, value: Any) -> None: + """Custom setattr to handle verification wall linking and validation. + + For linked properties: + - Verification walls cannot modify these (changes are ignored) + - Parent walls propagate changes to their verification wall + + For reference_wall and verification_wall: + - Validates the wall exists and isn't itself + + For target_eotf: + - Sets target_max_lum_nits appropriately for non-PQ EOTFs + """ + # Handle reference_wall validation + if name == 'reference_wall': + value = self._validate_reference_wall(value) + + # Handle verification_wall validation + if name == 'verification_wall': + value = self._validate_verification_wall(value) + + # Handle target_eotf - set target_max_lum_nits for non-PQ EOTFs + if name == 'target_eotf' and value != constants.EOTF.EOTF_ST2084: + # For non-PQ EOTFs, set the appropriate max lum + if value == constants.EOTF.EOTF_HLG: + max_lum = constants.TARGET_MAX_LUM_NITS_HLG + else: + max_lum = constants.TARGET_MAX_LUM_NITS_NONE_PQ + # Set target_max_lum_nits after setting target_eotf + super().__setattr__(name, value) + self.target_max_lum_nits = max_lum return - setattr(self.verification_wall_as_wall._led_settings, field_name, value) - - def _get_property(self, field_name: constants.LedWallSettingsKeys) -> Any: - """ Gets the internal property data stores for the given field name, and given value. - This is used when its important for verification walls to refer to their parent wall for settings which should be joined - - Fields which are not hard linked such as name, should directly access internally via their own property - - Args: - field_name: The name of the property to set in the data store - """ - if not self.is_verification_wall: - return getattr(self._led_settings, field_name) - - wall = self.verification_wall_as_wall - if wall is not None: - return getattr(wall._led_settings, field_name) - - raise ValueError("The Wall is a verification wall, but the parent wall was removed") - - @property - def name(self) -> str: - """The name of the LED wall - - Returns: - str: A list of custom primaries and a custom name for led wall we are calibrating - """ - return self._led_settings.name - - @name.setter - def name(self, value: str): - """ Sets the name of the LED wall - - Args: - value (str): The name of the LED wall - """ - self._led_settings.name = value - - @property - def avoid_clipping(self) -> bool: - """ Whether we want to avoid clipping by the LED wall. - Ensures that we scale the results of the calibrations down to ensure that any values pushed above the actual - peak are scaled back - - Returns: - bool: Whether we want to avoid clipping or not - """ - return self._get_property(constants.LedWallSettingsKeys.AVOID_CLIPPING) - - @avoid_clipping.setter - def avoid_clipping(self, value: bool): - """ Set whether we want to avoid clipping on the LED wall or not - - Args: - value (bool): Whether we want to avoid clipping on the LED wall or not - """ - self._set_property(constants.LedWallSettingsKeys.AVOID_CLIPPING, value) - - @property - def enable_eotf_correction(self) -> bool: - """Whether enable eotf correction is enabled or disabled - - Returns: - bool: Whether eotf correction is enabled or disabled - """ - return self._get_property(constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION) - - @enable_eotf_correction.setter - def enable_eotf_correction(self, value: bool): - """Set the eotf correction to be enabled or disabled - - Args: - value (bool): Whether eotf correction is enabled or disabled - """ - self._set_property(constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION, value) - - @property - def enable_gamut_compression(self) -> bool: - """Whether enable gamut compression is enabled or disabled - - Returns: - bool: Whether enable gamut compression is enabled or disabled - """ - return self._get_property(constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION) - - @enable_gamut_compression.setter - def enable_gamut_compression(self, value: bool): - """Set the gamut compression to be enabled or disabled - - Args: - value (bool): Whether enable gamut compression is enabled or disabled - """ - self._set_property(constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION, value) - - @property - def auto_wb_source(self) -> bool: - """Whether auto-white-balance is enabled or disabled - - Returns: - bool: Whether auto white balance is enabled or disabled - """ - return self._get_property(constants.LedWallSettingsKeys.AUTO_WB_SOURCE) - - @auto_wb_source.setter - def auto_wb_source(self, value: bool): - """Set the auto white balance to be enabled or disabled - - Args: - value (bool): Whether auto white balance is enabled or disabled - """ - self._set_property(constants.LedWallSettingsKeys.AUTO_WB_SOURCE, value) - - @property - def input_sequence_folder(self) -> str: - """ Return the input sequence folder. - - Verification walls have to have unique input sequence folders, so we access the led_settings directly - - Returns: - str: The input sequence folder. - """ - return self._led_settings.input_sequence_folder - - @input_sequence_folder.setter - def input_sequence_folder(self, value: str): - """Set the input sequence folder. We do not set this on the verification wall as this needs to be unique - - Args: - value (str): The input sequence folder. - """ - self._led_settings.input_sequence_folder = value - - @property - def calculation_order(self) -> constants.CalculationOrder: - """Return the Calculation Order - - Returns: - constants.CalculationOrder: The calculation order of the calculations - """ - return self._get_property(constants.LedWallSettingsKeys.CALCULATION_ORDER) - - @calculation_order.setter - def calculation_order(self, value: str): - """Set the Calculation Order - - Args: - value (constants.CalculationOrder): The calculation order of the calculations - """ - self._set_property(constants.LedWallSettingsKeys.CALCULATION_ORDER, value) - - @property - def primaries_saturation(self) -> float: - """Return the primaries' saturation. - - Returns: - float: The primaries saturation. - """ - return self._get_property(constants.LedWallSettingsKeys.PRIMARIES_SATURATION) - - @primaries_saturation.setter - def primaries_saturation(self, value: float): - """Set the primaries' saturation. - - Args: - value (float): The primaries saturation. - """ - self._set_property(constants.LedWallSettingsKeys.PRIMARIES_SATURATION, value) - - @property - def input_plate_gamut(self) -> constants.ColourSpace: - """Returns the input colorspace of the plate - - Returns: - constants.ColourSpace: The input colorspace of the plate - """ - return self._get_property(constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT) - - @input_plate_gamut.setter - def input_plate_gamut(self, value: constants.ColourSpace): - """Set the reference colorspace of the plate - - Args: - value (constants.ColourSpace): The colour space we want to set the input too for the plate - """ - self._set_property(constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT, value) - - @property - def native_camera_gamut(self) -> constants.CameraColourSpace: - """Returns the native colorspace of the camera the plate was shot with originally - - Returns: - constants.ColourSpace: The native colorspace of the camera the plate was shot with originally - """ - return self._get_property(constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT) - - @native_camera_gamut.setter - def native_camera_gamut(self, value: constants.CameraColourSpace): - """Set the native colorspace of the camera the plate was shot with originally - - Args: - value (constants.CameraColourSpace): The native colorspace of the camera the plate was shot with originally - """ - self._set_property(constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT, value) - - @property - def num_grey_patches(self) -> int: - """Return the num_grey_patches. - Returns: - int: The number of grey patches used to ramp the number of nits - """ - return self._get_property(constants.LedWallSettingsKeys.NUM_GREY_PATCHES) - - @num_grey_patches.setter - def num_grey_patches(self, value: int): - """Set the num_grey_patches. - - Args: - value (int): The number of grey patches used to ramp the number of nits - """ - self._set_property(constants.LedWallSettingsKeys.NUM_GREY_PATCHES, value) - - @property - def reference_to_target_cat(self) -> constants.CAT: - """Returns the reference to target cat - - Returns: - constants.ColourSpace: The reference to target cat - """ - return self._get_property(constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT) - - @reference_to_target_cat.setter - def reference_to_target_cat(self, value: constants.CAT): - """Set the reference to target cat - - Args: - value (constants.CAT): The reference to a target cat - """ - self._set_property(constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT, value) - - @property - def roi(self) -> List[List[int]]: - """Return the region of interest (ROI). - - Verification walls have to have unique ROI, so we access the led_settings directly - - Returns: - Any: The region of interest (ROI). - """ - return self._led_settings.roi - - @roi.setter - def roi(self, value: List[List[int]]): - """ Set the region of interest (ROI). We do not set this on the verification wall as this needs to be unique - - Args: - value (Any): The region of interest (ROI). - """ - self._led_settings.roi = value - - @property - def shadow_rolloff(self) -> float: - """Returns the shadow rolloff - - Returns: - float: The shadow rolloff - """ - return self._get_property(constants.LedWallSettingsKeys.SHADOW_ROLLOFF) - - @shadow_rolloff.setter - def shadow_rolloff(self, value: float): - """Set the shadow rolloff - - Args: - value (float): the shadow rolloff - """ - self._set_property(constants.LedWallSettingsKeys.SHADOW_ROLLOFF, value) - - @property - def target_gamut(self) -> constants.ColourSpace|str: - """Returns the target colorspace - - Returns: - constants.ColourSpace: The target colorspace - """ - return self._get_property(constants.LedWallSettingsKeys.TARGET_GAMUT) - - @target_gamut.setter - def target_gamut(self, value: constants.ColourSpace|str): - """Set the target colorspace + # Handle target_max_lum_nits - enforce limits based on current EOTF + if name == 'target_max_lum_nits': + current_eotf = getattr(self, 'target_eotf', constants.EOTF.EOTF_ST2084) + if current_eotf != constants.EOTF.EOTF_ST2084: + # For non-PQ EOTFs, override the value + if current_eotf == constants.EOTF.EOTF_HLG: + value = constants.TARGET_MAX_LUM_NITS_HLG + else: + value = constants.TARGET_MAX_LUM_NITS_NONE_PQ + + # Check if this is a linked property and we're a verification wall + if name in constants.LINKED_LED_WALL_PROPERTIES: + # Verification walls can't modify linked properties directly + if getattr(self, 'is_verification_wall', False): + return + # Set the value on self + super().__setattr__(name, value) + # Propagate to verification wall if it exists + verification_wall = self.verification_wall_as_wall + if verification_wall is not None: + # Use object.__setattr__ to bypass the verification wall's blocking logic + object.__setattr__(verification_wall, name, value) + else: + super().__setattr__(name, value) + + def _validate_reference_wall(self, value: Any) -> str: + """Validate and normalize reference_wall value.""" + if not value: + return "" - Args: - value (constants.ColourSpace): the colour space we want to set the target as - """ - self._set_property(constants.LedWallSettingsKeys.TARGET_GAMUT, value) + ref_wall_name = value.name if isinstance(value, LedWallSettings) else str(value) - @property - def target_eotf(self) -> constants.EOTF: - """Returns the target eotf + # Can't set reference wall to itself + if ref_wall_name == getattr(self, 'name', ''): + raise ValueError("Cannot set the reference wall to be the same as the current wall") - Returns: - constants.EOTF: The target eotf - """ - return self._get_property(constants.LedWallSettingsKeys.TARGET_EOTF) + # Verify the wall exists in the project (if we have a project_settings reference) + project = getattr(self, '_project_settings', None) + if project is not None: + # This will raise ValueError if the wall doesn't exist + led_wall = project.get_led_wall(ref_wall_name) + return led_wall.name - @target_eotf.setter - def target_eotf(self, value: constants.EOTF): - """Set the target eotf, which also forces the target max lum to be 100, if not using PQ. + return ref_wall_name - Args: - value (constants.EOTF): the eotf for the target - """ - target_max_lum_nits = ( - constants.TARGET_MAX_LUM_NITS_HLG if value == constants.EOTF.EOTF_HLG - else constants.TARGET_MAX_LUM_NITS_NONE_PQ if value != constants.EOTF.EOTF_ST2084 - else None - ) + def _validate_verification_wall(self, value: Any) -> str: + """Validate and normalize verification_wall value.""" + if not value: + return "" - self._set_property(constants.LedWallSettingsKeys.TARGET_EOTF, value) - if target_max_lum_nits is not None: - self._set_property(constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS, - target_max_lum_nits) + wall_name = value.name if isinstance(value, LedWallSettings) else str(value) - @property - def target_max_lum_nits(self) -> int: - """Return the target max luminance in nits. + # Can't set verification wall to itself + if wall_name == getattr(self, 'name', ''): + raise ValueError("Cannot set the verification wall to be the same as the current wall") - Returns: - int: target max luminance in nits. - """ - return self._get_property(constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS) + return wall_name - @target_max_lum_nits.setter - def target_max_lum_nits(self, value: int): - """ Set the target max luminance in nits, unless you are using an eotf which is not PQ, in which case - we force 100 nits, aka 1.0 + def __getattribute__(self, name: str) -> Any: + """Custom getattribute to handle verification wall linking. - Args: - value (int): target max luminance in nits. + For linked properties, verification walls read from their parent wall. + If the parent wall was removed (verification_wall=""), fall back to local values. """ - if self.target_eotf != constants.EOTF.EOTF_ST2084: - value = constants.TARGET_MAX_LUM_NITS_NONE_PQ + # For non-linked properties, just use normal attribute access + if name not in constants.LINKED_LED_WALL_PROPERTIES: + return super().__getattribute__(name) - if self.target_eotf == constants.EOTF.EOTF_HLG: - value = constants.TARGET_MAX_LUM_NITS_HLG + # For linked properties, check if this is a verification wall + try: + is_verification = object.__getattribute__(self, '__dict__').get('is_verification_wall', False) + except (AttributeError, KeyError): + # Model might not be fully initialized yet + return super().__getattribute__(name) - self._set_property(constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS, value) + if not is_verification: + return super().__getattribute__(name) - @property - def target_to_screen_cat(self) -> constants.CAT: - """Returns the target screen cat + # This is a verification wall - get value from parent + try: + project_settings = object.__getattribute__(self, '__pydantic_private__').get('_project_settings') + verification_wall_name = object.__getattribute__(self, '__dict__').get('verification_wall', '') + except (AttributeError, KeyError, TypeError): + return super().__getattribute__(name) - Returns:z - constants.CAT: The target screen cat - """ - return self._get_property(constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT) + # If parent wall was removed (verification_wall=""), fall back to local value + if not verification_wall_name: + return super().__getattribute__(name) - @target_to_screen_cat.setter - def target_to_screen_cat(self, value: constants.CAT): - """Set the target screen cat + if project_settings is not None: + try: + parent_wall = project_settings.get_led_wall(verification_wall_name) + if parent_wall is not None: + return getattr(parent_wall, name) + except ValueError: + # Parent wall was removed, fall back to local value + return super().__getattribute__(name) - Args: - value (constants.CAT): the target screen cat - """ - self._set_property(constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT, value) + raise ValueError("The Wall is a verification wall, but the parent wall was removed") @property - def match_reference_wall(self) -> bool: - """ Whether we are using an external white point from a reference LED wall or not + def project_settings(self) -> Optional["ProjectSettings"]: + """The project settings this wall belongs to.""" + return self._project_settings - Returns: - bool: Gets whether we want to use an external white point from a reference LED or not - """ - return self._get_property(constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL) - - @match_reference_wall.setter - def match_reference_wall(self, value: bool): - """ Set whether we are using an external white point from a reference LED wall or not - - Args: - value (bool): Whether to use the external white point from a reference LED or not - """ - self._set_property(constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL, value) + @project_settings.setter + def project_settings(self, value: "ProjectSettings") -> None: + """Set the project settings reference.""" + self._project_settings = value @property - def reference_wall(self) -> str: - """ Get the reference wall we want to use as the external white point + def verification_wall_as_wall(self) -> Union["LedWallSettings", None]: + """Get the led wall which this wall is linked to for verifying the calibration. Returns: - str: The name of the led wall we want to use as the reference wall - """ - return self._led_settings.reference_wall - - @reference_wall.setter - def reference_wall(self, value: Union[LedWallSettings, str]): - """ Set the reference wall we want to use as the external white point - - Args: - value: The LED wall we want to set as the reference wall + LedWallSettings: The LED wall this wall is linked to for verifying the calibration """ - if not value: - # Changed behaviour so we fall back to the default value - self._led_settings.reference_wall = "" - return - - ref_wall_name:str = value.name if isinstance(value, LedWallSettings) else value - if ref_wall_name == self.name: - raise ValueError("Cannot set the reference wall to be the same as the current wall") - - # We get the led wall to make sure it exists and is added to the project - # raise if the wall does not exist - led_wall = self.project_settings.get_led_wall(ref_wall_name) - self._set_property(constants.LedWallSettingsKeys.REFERENCE_WALL, led_wall.name) + wall_name = object.__getattribute__(self, 'verification_wall') + if wall_name and self._project_settings is not None: + try: + return self._project_settings.get_led_wall(wall_name) + except ValueError: + return None + return None @property - def reference_wall_as_wall(self) -> Union[LedWallSettings, None]: - """ Get the reference wall we want to use as the external white point + def reference_wall_as_wall(self) -> Union["LedWallSettings", None]: + """Get the reference wall we want to use as the external white point. Returns: LedWallSettings: The LED wall we want to use as the reference wall """ wall_name = self.reference_wall - if wall_name: - return self.project_settings.get_led_wall(wall_name) - return None - - @property - def use_white_point_offset(self) -> bool: - """ Whether we are using a white point offset for the LED wall or not - - Returns: - bool: Gets whether we want to use a white point offset or not - """ - return self._get_property(constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET) - - @use_white_point_offset.setter - def use_white_point_offset(self, value: bool): - """ Set whether we are using a white point offset or not - - Args: - value (bool): Whether to use the external white point or not - """ - self._set_property(constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET, value) - - @property - def white_point_offset_source(self) -> str: - """ The source which contains an image sample from which we want to calculate the white point offset from - - Returns: - str: The filepath which contains the image we want to sample to calculate the white point offset from - """ - return self._get_property(constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE) - - @white_point_offset_source.setter - def white_point_offset_source(self, value: str): - """ Set the source which contains an image sample from which we want to calculate the white point offset from - - Args: - value (str): The filepath which contains the image we want to sample to calculate the white point offset from - """ - self._set_property(constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE, value) - - @property - def verification_wall(self) -> str: - """ Get the name of the led wall which this wall is linked to for verifying the calibration - - Returns: - str: The name of the led wall which this wall linked for verification - """ - return self._led_settings.verification_wall - - @property - def verification_wall_as_wall(self) -> Union[LedWallSettings, None]: - """ Get the led wall which this wall is linked to for verifying the calibration - - Returns: - LedWallSettings: The LED wall this wall is linked to for verifying the calibration - """ - wall_name = self.verification_wall - if wall_name: - return self.project_settings.get_led_wall(wall_name) + if wall_name and self._project_settings is not None: + try: + return self._project_settings.get_led_wall(wall_name) + except ValueError: + return None return None - @verification_wall.setter - def verification_wall(self, value: Union[LedWallSettings, str]): - """ Set the led wall which this wall is linked to verify the calibration. - We do not directly set this on the verification wall as this needs to be a bidirectional link - We leave the setting of this value to the api call within project settings to establish this link - - Args: - value: The LED wall which this instance is intended to verify - """ - if not value: - # Changed behaviour so we fall back to the default value - self._led_settings.verification_wall = "" - return - - ver_wall_name = value.name if isinstance(value, LedWallSettings) else value - if ver_wall_name == self.name: - raise ValueError("Cannot set the verification wall to be the same as the current wall") - - # We get the led wall to make sure it exists and is added to the project - # raise if the wall does not exist - led_wall = self.project_settings.get_led_wall(ver_wall_name) - self._led_settings.verification_wall = led_wall.name - - @property - def is_verification_wall(self) -> bool: - """ Whether this wall is a verification wall which should take settings from the linked wall, - or if this is the original wall which should be dictating the settings to the linked wall + def reset_defaults(self): + """Reset the LedWallSettings object to its default values.""" + name = self.name + defaults = LedWallSettings(project_settings=self._project_settings, name=name) - Returns: - bool: Whether this wall is a verification wall or not - """ - return self._led_settings.is_verification_wall + # Set target_eotf first to ensure correct target_max_lum_nits behavior + if 'target_eotf' in LedWallSettings.model_fields: + setattr(self, 'target_eotf', getattr(defaults, 'target_eotf')) - @is_verification_wall.setter - def is_verification_wall(self, value: bool) -> None: - """ Set whether this wall is a verification wall which should take settings from the linked wall, - or if this is the original wall which should be dictating the settings to the linked wall + # Copy all field values except name and target_eotf (already set) + for field_name in LedWallSettings.model_fields: + if field_name not in ('name', 'target_eotf'): + setattr(self, field_name, getattr(defaults, field_name)) - We do not set this on the verification wall directly, as this needs to be unique we leave this - to the project settings api to establish the correct values + def clear(self): + """Clears the roi, processing and separation results. So that we can start fresh with + a new sequence being loaded""" + self.processing_results = ProcessingResults() + self.separation_results = None + self.roi = [] - Args: - value: Whether this wall is to be set as a Verification wall or not - """ - self._led_settings.is_verification_wall = value + def clear_led_settings(self): + """Clear the LED settings and restore them to the defaults.""" + self.reset_defaults() def has_valid_white_balance_options(self) -> bool: - """ Checks whether the white balance options are valid or not, we can only have one of these options + """Checks whether the white balance options are valid or not, we can only have one of these options set at anyone time Returns: True or False depending on whether the white balance options are valid or not - """ values = [self.auto_wb_source, self.match_reference_wall, self.use_white_point_offset].count(True) if values > 1: @@ -672,12 +343,12 @@ def has_valid_white_balance_options(self) -> bool: return True @classmethod - def from_json_file(cls, project_settings: ProjectSettings, json_file: str): + def from_json_file(cls, project_settings: "ProjectSettings", json_file: str) -> "LedWallSettings": """Create a LedWallSettings object from a JSON file. Args: - project_settings (ProjectSettings): The project we want the LED wall to belong to - json_file (str): The path to the JSON file. + project_settings: The project we want the LED wall to belong to + json_file: The path to the JSON file. Returns: LedWallSettings: A LedWallSettings object. @@ -686,8 +357,8 @@ def from_json_file(cls, project_settings: ProjectSettings, json_file: str): return cls._from_json_data(project_settings, json_data) @classmethod - def from_json_string(cls, project_settings: ProjectSettings, json_string: str): - """ Creates a LedWallSettings object from a JSON string. + def from_json_string(cls, project_settings: "ProjectSettings", json_string: str) -> "LedWallSettings": + """Creates a LedWallSettings object from a JSON string. Args: project_settings: The project we want the LED wall to belong to @@ -695,48 +366,47 @@ def from_json_string(cls, project_settings: ProjectSettings, json_string: str): Returns: A LedWallSettings object. """ - instance = cls(project_settings) - instance._led_settings = LedWallSettingsBaseModel.model_validate_json(json_string) + instance = cls.model_validate_json(json_string) + instance._project_settings = project_settings return instance @classmethod - def _from_json_data(cls, project_settings, json_data): - instance = cls(project_settings) - instance._led_settings = LedWallSettingsBaseModel.model_validate(json_data) + def _from_json_data(cls, project_settings: "ProjectSettings", json_data: dict) -> "LedWallSettings": + """Create a LedWallSettings from a dictionary.""" + instance = cls.model_validate(json_data) + instance._project_settings = project_settings return instance @classmethod - def from_dict(cls, project_settings: ProjectSettings, input_dict: dict) -> LedWallSettings: - """ Creates a LedWallSettings object from a dictionary. + def from_dict(cls, project_settings: "ProjectSettings", input_dict: dict) -> "LedWallSettings": + """Creates a LedWallSettings object from a dictionary. Args: - project_settings: - input_dict: + project_settings: The project we want the LED wall to belong to + input_dict: The dictionary with settings Returns: LedWallSettings """ - instance = cls(project_settings) - instance._led_settings = LedWallSettingsBaseModel.model_validate(input_dict) + instance = cls.model_validate(input_dict) + instance._project_settings = project_settings return instance def to_dict(self) -> dict: - """ Returns a dictionary representation of the LedWallSettings object. + """Returns a dictionary representation of the LedWallSettings object. Returns: A dictionary representation of the LedWallSettings object. - """ - return self._led_settings.model_dump() + return self.model_dump() @classmethod - def _settings_from_json_file(cls, json_file) -> dict: - """ Returns the project settings from a JSON file. + def _settings_from_json_file(cls, json_file: str) -> dict: + """Returns the project settings from a JSON file. Args: json_file: The path to the JSON file. Returns: The project settings from a JSON file - """ with open(json_file, 'r', encoding='utf-8') as file: data = json.load(file) @@ -746,14 +416,14 @@ def to_json(self, json_file: str): """Save the LedWallSettings object to a JSON file. Args: - json_file (str): The path to the JSON file. + json_file: The path to the JSON file. """ with open(json_file, 'w', encoding='utf-8') as file: - file.write(self._led_settings.model_dump_json(indent=4)) + file.write(self.model_dump_json(indent=4)) @property - def sequence_loader(self): - """Returns the sequence loader for the LED wall""" + def sequence_loader(self) -> SequenceLoader: + """Returns the sequence loader for the LED wall.""" if not self._sequence_loader: self._sequence_loader = self._sequence_loader_class(self) return self._sequence_loader diff --git a/packages/open_vp_cal/src/open_vp_cal/project_settings.py b/packages/open_vp_cal/src/open_vp_cal/project_settings.py index cb44faed..8f3c7c4e 100644 --- a/packages/open_vp_cal/src/open_vp_cal/project_settings.py +++ b/packages/open_vp_cal/src/open_vp_cal/project_settings.py @@ -21,17 +21,34 @@ import json import math from pathlib import Path -from typing import Dict, List, Union, Any, Type -from pydantic import BaseModel, Field, field_validator, field_serializer +from typing import Dict, List, Union, Any, Type, Optional +from pydantic import ( + BaseModel, + Field, + field_validator, + field_serializer, + model_validator, + model_serializer, + PrivateAttr, + ConfigDict, +) import open_vp_cal from open_vp_cal.core import constants, ocio_utils, utils -from open_vp_cal.led_wall_settings import LedWallSettings, LedWallSettingsBaseModel +from open_vp_cal.led_wall_settings import LedWallSettings from open_vp_cal.core.resource_loader import ResourceLoader -class ProjectSettingsBaseModel(BaseModel): - """Base model for LedWallSettings with typing.""" +class ProjectSettings(BaseModel): + """A pydantic model class to handle project settings with serialization and business logic.""" + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + + # ===== Serialized Fields (from former ProjectSettingsBaseModel) ===== + openvp_cal_version: str = Field(default=open_vp_cal.__version__) content_max_lum: float = Field(default=constants.PQ.PQ_MAX_NITS) file_format: constants.FileFormats = Field(default=constants.FileFormats(constants.FileFormats.default())) resolution_width: int = Field(default=constants.DEFAULT_RESOLUTION_WIDTH) @@ -40,15 +57,18 @@ class ProjectSettingsBaseModel(BaseModel): ocio_config_path: str = Field(default="") custom_logo_path: str = Field(default="") frames_per_patch: int = Field(default=1) - reference_gamut: constants.ColourSpace|str = Field(default=constants.ColourSpace(constants.ColourSpace.CS_ACES)) - led_walls: List[LedWallSettingsBaseModel] = Field(default=[]) - project_custom_primaries: Dict[str, List[List[float]]] = Field(default={}) - frame_rate: constants.FrameRates|float = Field(default=constants.FrameRates(constants.FrameRates.default())) + reference_gamut: constants.ColourSpace | str = Field(default=constants.ColourSpace(constants.ColourSpace.CS_ACES)) + led_walls: List[LedWallSettings] = Field(default_factory=list) + project_custom_primaries: Dict[str, List[List[float]]] = Field(default_factory=dict) + frame_rate: constants.FrameRates | float = Field(default=constants.FrameRates(constants.FrameRates.default())) export_lut_for_aces_cct: bool = Field(default=False) export_lut_for_aces_cct_in_target_out: bool = Field(default=False) - project_id: str = Field(default=utils.generate_truncated_hash()) + project_id: str = Field(default_factory=utils.generate_truncated_hash) lut_size: int = Field(default=constants.DEFAULT_LUZ_SIZE) + # ===== Private Attributes (not in schema at all) ===== + _led_wall_class: Type[LedWallSettings] = PrivateAttr(default=LedWallSettings) + @field_validator( "ocio_config_path", mode="before", @@ -76,7 +96,7 @@ def upgrade_ocio_config_path(cls, value: Any) -> str: json_schema_input_type=Union[constants.FrameRates, float] ) @classmethod - def try_convert_to_enum_frame_rates(cls, value:Any) -> constants.FrameRates|float: + def try_convert_to_enum_frame_rates(cls, value: Any) -> constants.FrameRates | float: """ Try to convert a float frame rate to an enum frame rate if possible. Precision is 1e-3, for example: @@ -85,348 +105,83 @@ def try_convert_to_enum_frame_rates(cls, value:Any) -> constants.FrameRates|floa 23.999 == constants.FrameRates.FPS_24 """ if isinstance(value, float): - for frame_rate in constants.FrameRates: + for frame_rate in constants.FrameRates: if math.isclose(value, frame_rate.value, abs_tol=11e-4): return frame_rate return value -class OpenVPCalSettingsModel(BaseModel): - """Base model for OpenVPCalSettings with typing.""" - openvp_cal_version: str = Field(default=open_vp_cal.__version__) - project_settings: ProjectSettingsBaseModel = Field(default=ProjectSettingsBaseModel()) - - -class ProjectSettings: - """A class to handle project settings.""" - def __init__(self): - """Initialize an empty ProjectSettings object.""" - from open_vp_cal.led_wall_settings import LedWallSettings - self._project_settings: ProjectSettingsBaseModel = ProjectSettingsBaseModel() - self._led_walls:List[LedWallSettings] = [] - self._led_wall_class = LedWallSettings - - def clear_project_settings(self): - """ - Clear the project settings and restore them to the defaults - """ - self._project_settings = ProjectSettingsBaseModel() - self._led_walls:List[LedWallSettings] = [] - - @property - def custom_logo_path(self) -> str: - """ The filepath to the custom logo for the pattern generation + @model_validator(mode='before') + @classmethod + def handle_nested_format(cls, data: Any) -> Any: + """Handle loading old nested JSON format. + + Old format: {"openvp_cal_version": "...", "project_settings": {...}} + New format: direct fields + """ + if isinstance(data, dict) and constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS in data: + # Old nested format - extract inner project_settings and version + inner = data[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS].copy() + inner['openvp_cal_version'] = data.get( + constants.OpenVPCalSettingsKeys.VERSION, + open_vp_cal.__version__ + ) + return inner + return data - Returns: - str: The filepath to the custom logo for the pattern generation - """ - return self._project_settings.custom_logo_path + @model_serializer(mode='wrap') + def serialize_nested(self, handler) -> dict: + """Output backwards-compatible nested JSON format.""" + data = handler(self) + version = data.pop('openvp_cal_version', open_vp_cal.__version__) + return { + constants.OpenVPCalSettingsKeys.VERSION: version, + constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS: data + } - @custom_logo_path.setter - def custom_logo_path(self: ProjectSettings, value: str): - """Set the filepath to the custom logo for the pattern generation + def __init__(self, led_wall_class: Optional[Type[LedWallSettings]] = None, **data): + """Initialize a ProjectSettings object. Args: - value (bool): File path to a custom logo + led_wall_class: Optional custom LedWallSettings class to use + **data: Field values to initialize """ - self._project_settings.custom_logo_path = value - - @property - def project_custom_primaries(self) -> Dict[str, List[List[float]]]: - """ Gets all the custom primaries for the project + super().__init__(**data) + if led_wall_class is not None: + self._led_wall_class = led_wall_class + else: + self._led_wall_class = LedWallSettings - Returns: - dict: The dictionary of the custom primaries - """ - return self._project_settings.project_custom_primaries + # Ensure all led walls have reference to this project + for wall in self.led_walls: + wall._project_settings = self - @project_custom_primaries.setter - def project_custom_primaries(self: ProjectSettings, value: Dict[str, List[List[float]]]): - """ Sets the custom primaries to use for the project + def clear_project_settings(self): + """Clear the project settings and restore them to the defaults.""" + defaults = ProjectSettings() - Args: - value: The dictionary for the custom primaries - """ - self._project_settings.project_custom_primaries = value + # Copy all field values from defaults + for field_name in ProjectSettings.model_fields: + if field_name != 'openvp_cal_version': + setattr(self, field_name, getattr(defaults, field_name)) - def add_custom_primary(self: ProjectSettings, name: str, primaries: List[List[float]]): + def add_custom_primary(self, name: str, primaries: List[List[float]]): """ Adds a custom primary to the project Args: name (str): The name of the custom primary primaries (List[float]): The list of primaries to add """ - if name in self._project_settings.project_custom_primaries: + if name in self.project_custom_primaries: raise ValueError(f'Custom primary {name} already exists') - self._project_settings.project_custom_primaries[name] = primaries - - @property - def project_id(self) -> str: - """Get the project id for the current project - - Returns: - str: The project id for the current project - """ - # project_id will be generated by default if not exists - return self._project_settings.project_id - - @project_id.setter - def project_id(self: ProjectSettings, value: str): - """Set the project id to the given value - - Args: - value (str): The project id we want to set - """ - self._project_settings.project_id = value - - @property - def file_format(self) -> constants.FileFormats: - """Get the file format we want to generate patterns to - - Returns: - constants.FileFormats: The file format we want to generate patterns to - """ - return self._project_settings.file_format - - @file_format.setter - def file_format(self: ProjectSettings, value: constants.FileFormats): - """Set the file format we want to generate patterns to - - Args: - value (constants.FileFormats): The file format we want to generate patterns to - """ - self._project_settings.file_format = value - - @property - def frames_per_patch(self) -> int: - """Get the number of frames per patch we want to generate - - Returns: - int: The number of frames per patch to generate - """ - return self._project_settings.frames_per_patch - - @frames_per_patch.setter - def frames_per_patch(self: ProjectSettings, value: int): - """ The number of frames per patch we want to generate - - Args: - value (int): The number of frames per patch we want to generate - """ - self._project_settings.frames_per_patch = value - - @property - def led_walls(self) -> List[LedWallSettings]: - """Return the LED walls in the project - - Returns: - list: The list of led walls stored in the project - """ - return self._led_walls - - @led_walls.setter - def led_walls(self: ProjectSettings, value: List[LedWallSettings]): - """Set the LED walls config path. - - Args: - value (list): A list of led walls we want to store in the project - """ - walls = [] - for wall in value: - if isinstance(wall, self._led_wall_class): - walls.append(wall) - else: - raise ValueError(f'Wall {wall} is not an instance of {self._led_wall_class.__name__}') - self._led_walls = walls - - @property - def ocio_config_path(self) -> str: - """ Return the OCIO config path we want to use as a base for the exported ocio config. - - Returns: - str: The OCIO config path. - """ - return self._project_settings.ocio_config_path - - @ocio_config_path.setter - def ocio_config_path(self: ProjectSettings, value: str): - """Set the OCIO config path used as the base for writing out the resulting ocio config. This is not used for - any internal computation - - - Args: - value (str): The OCIO config path. - """ - self._project_settings.ocio_config_path = value - - @property - def output_folder(self) -> str: - """Return the output folder we want to write our patches, luts, and configs too. - - Returns: - str: The folder path - """ - return self._project_settings.output_folder - - @output_folder.setter - def output_folder(self: ProjectSettings, value: str): - """Set the folder path for the output folder - - Args: - value (str): The folder path for the outputs - """ - self._project_settings.output_folder = value - - @property - def resolution_width(self) -> int: - """Returns the resolution width of the patterns we are going to generate - - Returns: - int: The resolution width of the patterns we are going to generate - """ - return self._project_settings.resolution_width - - @resolution_width.setter - def resolution_width(self: ProjectSettings, value: int): - """Sets the resolution width of the patterns we want to generate - - Args: - value (int): The resolution width - """ - self._project_settings.resolution_width = value - - @property - def resolution_height(self) -> int: - """Returns the resolution height of the patterns we are going to generate - - Returns: - int: The resolution height of the patterns we are going to generate - """ - return self._project_settings.resolution_height - - @resolution_height.setter - def resolution_height(self: ProjectSettings, value: int): - """Sets the resolution height of the patterns we want to generate - - Args: - value (int): The resolution height - """ - self._project_settings.resolution_height = value - - @property - def reference_gamut(self) -> constants.ColourSpace|str: - """ Returns the reference colorspace of the working space - - Returns: - constants.ColourSpace: Returns the reference colorspace of the working space - """ - return self._project_settings.reference_gamut - - @reference_gamut.setter - def reference_gamut(self: ProjectSettings, value: constants.ColourSpace|str): - """ Set the reference colorspace of the working space, defaults to ACES2065-1 - should only be set with extreme care, as other working spaces - not fully supported - - Args: - value (constants.ColourSpace): The colour space we want to set the input too for the plate - """ - self._project_settings.reference_gamut = value - - @property - def frame_rate(self) -> constants.FrameRates|float: - """ The frame rate for the shooting frame rate for the camera, used in certain SPG patterns - - Returns: - float: The shooting frame rate of the camera - """ - return self._project_settings.frame_rate - - @frame_rate.setter - def frame_rate(self: ProjectSettings, value: constants.FrameRates|float): - """ Sets the frame rate for the shooting frame rate - - Args: - value (float): The frame rate we want to set - """ - self._project_settings.frame_rate = value - - @property - def export_lut_for_aces_cct(self) -> bool: - """ Get whether we want to export out lut for aces cct - - Returns: - bool: Whether we want out luts to be exported for aces cct - """ - return self._project_settings.export_lut_for_aces_cct - - @export_lut_for_aces_cct.setter - def export_lut_for_aces_cct(self: ProjectSettings, value: bool): - """ Set whether we want to export out lut for aces cct - - Args: - value (bool): Set whether we want to export out lut for aces cct - """ - self._project_settings.export_lut_for_aces_cct = value - - @property - def export_lut_for_aces_cct_in_target_out(self) -> bool: - """ Get whether we want to export out lut for aces cct in and target out - - Returns: - bool: Whether we want our luts to be exported for aces cct, with cct in and target out - """ - return self._project_settings.export_lut_for_aces_cct_in_target_out - - @export_lut_for_aces_cct_in_target_out.setter - def export_lut_for_aces_cct_in_target_out(self: ProjectSettings, value: bool): - """ Set whether we want to export out lut for aces cct with aces cct in and target out - - Args: - value (bool): Set whether we want to export out lut for aces cct, with cct in and target out - """ - self._project_settings.export_lut_for_aces_cct_in_target_out = value - - @property - def content_max_lum(self) -> float: - """Get the content max luminance for the project - - Returns: - int: The content max luminance for the project - """ - # content_max_lum will be set to PQ_MAX_NITS by default when v1.x is loaded - return self._project_settings.content_max_lum - - @content_max_lum.setter - def content_max_lum(self: ProjectSettings, value: float): - """Set the content max luminance for the project - - Args: - value (int): The content max luminance for the project - """ - self._project_settings.content_max_lum = value - - @property - def lut_size(self): - """Get the size of the lut for the project - - Returns: - int: The size of the luz for the project - """ - # lut_size will be set to DEFAULT_LUZ_SIZE by default when v1.x is loaded - return self._project_settings.lut_size - - @lut_size.setter - def lut_size(self: ProjectSettings, value: int): - """Set the size of the lut for the project - - Args: - value (int): The size of the lut for the project - """ - self._project_settings.lut_size = value + self.project_custom_primaries[name] = primaries @classmethod - def from_json(cls: Type[ProjectSettings], json_file: str, led_wall_class: Type[LedWallSettings] = LedWallSettings) -> ProjectSettings: + def from_json( + cls, + json_file: str, + led_wall_class: Type[LedWallSettings] = LedWallSettings + ) -> ProjectSettings: """Create a ProjectSettings object from a JSON file. Args: @@ -438,12 +193,10 @@ def from_json(cls: Type[ProjectSettings], json_file: str, led_wall_class: Type[L ProjectSettings: A ProjectSettings object. """ data = cls._settings_from_json_file(json_file) - if not data[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS]: - raise ValueError(f'No project settings found in {json_file}') return cls.from_dict(data, led_wall_class=led_wall_class) @classmethod - def _settings_from_json_file(cls, json_file) -> dict: + def _settings_from_json_file(cls, json_file: str) -> dict: """ Load the project settings from a JSON file. Args: @@ -456,37 +209,40 @@ def _settings_from_json_file(cls, json_file) -> dict: data = json.load(file) return data - def to_json(self: ProjectSettings, json_file: str): + def to_json(self, json_file: str): """Save the ProjectSettings object to a JSON file. Args: json_file (str): The path to the JSON file. """ with open(json_file, 'w', encoding='utf-8') as file: - file.write(self.get_open_vp_cal_model().model_dump_json(indent=4)) + file.write(self.model_dump_json(indent=4)) @classmethod - def from_dict(cls, data: dict, led_wall_class: Type[LedWallSettings] = LedWallSettings) -> ProjectSettings: + def from_dict( + cls, + data: dict, + led_wall_class: Type[LedWallSettings] = LedWallSettings + ) -> ProjectSettings: """ Creates a ProjectSettings object from a dictionary. - Note that input will be modified. - Args: - data (dict):from_dict The dictionary to create the ProjectSettings object from + data (dict): The dictionary to create the ProjectSettings object from led_wall_class (Type): The class type of the LedWallSettings to use for the project settings Returns: ProjectSettings """ - instance: ProjectSettings = cls() + # The model_validator handles nested format conversion + instance = cls.model_validate(data) instance._led_wall_class = led_wall_class - instance._project_settings = ProjectSettingsBaseModel.model_validate( - data[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS]) + # Recreate led walls with proper class and project reference walls = [] - for wall in instance._project_settings.led_walls: - wall_inst = led_wall_class.from_dict(instance, wall.model_dump()) + for wall in instance.led_walls: + wall_dict = wall.model_dump() if isinstance(wall, BaseModel) else wall + wall_inst = led_wall_class.from_dict(instance, wall_dict) walls.append(wall_inst) instance.led_walls = walls @@ -498,16 +254,9 @@ def to_dict(self) -> Dict: Returns: Dict """ - return self.get_open_vp_cal_model().model_dump() - - def get_open_vp_cal_model(self) -> OpenVPCalSettingsModel: - openVpCalSettings = OpenVPCalSettingsModel() - openVpCalSettings.openvp_cal_version = open_vp_cal.__version__ - openVpCalSettings.project_settings = self._project_settings - openVpCalSettings.project_settings.led_walls = [led_wall._led_settings for led_wall in self.led_walls] - return openVpCalSettings + return self.model_dump() - def add_led_wall(self: ProjectSettings, name: str) -> LedWallSettings: + def add_led_wall(self, name: str) -> LedWallSettings: """ Adds a new LED wall to the project settings Args: @@ -520,11 +269,11 @@ def add_led_wall(self: ProjectSettings, name: str) -> LedWallSettings: if name in existing_names: raise ValueError(f'Led wall {name} already exists') - led_wall = self._led_wall_class(self, name) + led_wall = self._led_wall_class(self, name=name) self.led_walls.append(led_wall) return led_wall - def copy_led_wall(self: ProjectSettings, existing_wall_name, new_name: str) -> LedWallSettings: + def copy_led_wall(self, existing_wall_name: str, new_name: str) -> LedWallSettings: """ Adds a new LED wall to the project settings based on a copy of an existing wall with a new name Args: @@ -549,7 +298,7 @@ def copy_led_wall(self: ProjectSettings, existing_wall_name, new_name: str) -> L self.led_walls.append(new_led_wall) return new_led_wall - def add_verification_wall(self: ProjectSettings, existing_wall_name: str) -> LedWallSettings: + def add_verification_wall(self, existing_wall_name: str) -> LedWallSettings: """ Adds a new LED wall to the project settings which mirrors all the settings from the existing wall, and whose settings cannot be changed directly, only via the parent @@ -588,7 +337,7 @@ def add_verification_wall(self: ProjectSettings, existing_wall_name: str) -> Led existing_led_wall.verification_wall = new_led_wall.name return new_led_wall - def remove_led_wall(self: ProjectSettings, name: str): + def remove_led_wall(self, name: str): """ Removes a LED wall from the project Args: @@ -609,7 +358,7 @@ def remove_led_wall(self: ProjectSettings, name: str): led_wall.reference_wall = "" led_wall.match_reference_wall = False - def get_led_wall(self: ProjectSettings, name: str) -> LedWallSettings: + def get_led_wall(self, name: str) -> LedWallSettings: """ Returns a LED wall from the project Args: @@ -621,7 +370,7 @@ def get_led_wall(self: ProjectSettings, name: str) -> LedWallSettings: raise ValueError(f'Led wall {name} not found') @property - def export_folder(self: ProjectSettings) -> str: + def export_folder(self) -> str: """ Returns the folder to export the calibration results to Returns: @@ -629,7 +378,7 @@ def export_folder(self: ProjectSettings) -> str: """ return os.path.join(self.output_folder, constants.ProjectFolders.EXPORT) - def reset_led_wall(self: ProjectSettings, name: str) -> None: + def reset_led_wall(self, name: str) -> None: """ Resets a LED wall to the default settings but preserves the link to the verification wall Args: @@ -641,7 +390,7 @@ def reset_led_wall(self: ProjectSettings, name: str) -> None: led_wall.reset_defaults() led_wall.verification_wall = verification_wall - def get_ocio_colorspace_names(self: ProjectSettings)-> list[str]: + def get_ocio_colorspace_names(self) -> list[str]: """ Gets the colour space names from either the project ocio config, or the default config diff --git a/packages/open_vp_cal/src/open_vp_cal/widgets/project_settings_widget.py b/packages/open_vp_cal/src/open_vp_cal/widgets/project_settings_widget.py index 2010708f..bafe893e 100644 --- a/packages/open_vp_cal/src/open_vp_cal/widgets/project_settings_widget.py +++ b/packages/open_vp_cal/src/open_vp_cal/widgets/project_settings_widget.py @@ -38,10 +38,10 @@ from open_vp_cal.widgets.utils import LockableWidget -class ProjectSettingsModel(ProjectSettings, QObject): +class ProjectSettingsModel(QObject): """ A class used to represent the Model in MVC structure. - ... + Wraps ProjectSettings with Qt signals for UI integration. Attributes ---------- @@ -53,7 +53,7 @@ class ProjectSettingsModel(ProjectSettings, QObject): set_data(key: str, value: object) Updates the stored data with the new value and emits the data_changed signal. - Get_data(key: str) + get_data(key: str) Retrieves the value of a given key from the stored data. """ @@ -64,6 +64,9 @@ class ProjectSettingsModel(ProjectSettings, QObject): register_custom_gamut_from_load = Signal(str) input_plate_gamut_changed = Signal() + # Attributes that belong to this class, not delegated to _settings + _own_attrs = frozenset({'_settings', 'parent', '_led_wall_class', 'default_data', 'current_wall'}) + def __init__(self, parent=None, led_wall_class=None): """ Constructs all the necessary attributes for the Model object. @@ -74,13 +77,65 @@ def __init__(self, parent=None, led_wall_class=None): parent widget (default is None) """ QObject.__init__(self) - ProjectSettings.__init__(self) - self.parent = parent - self._led_wall_class = led_wall_class - self.default_data = {} - self.current_wall = None + # Store attributes in __dict__ directly to bypass our custom __setattr__ + self.__dict__['_settings'] = ProjectSettings(led_wall_class=led_wall_class) + self.__dict__['parent'] = parent + self.__dict__['_led_wall_class'] = led_wall_class + self.__dict__['default_data'] = {} + self.__dict__['current_wall'] = None self.refresh_default_data() + def __getattr__(self, name: str): + """Delegate attribute access to the wrapped ProjectSettings.""" + # Check if in our own dict first + if name in self.__dict__: + return self.__dict__[name] + # Delegate to settings + settings = self.__dict__.get('_settings') + if settings is not None: + return getattr(settings, name) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + def __setattr__(self, name: str, value): + """Delegate attribute setting to the wrapped ProjectSettings for known fields.""" + # Handle our own attributes directly + if name in self._own_attrs: + self.__dict__[name] = value + return + + # If _settings exists and has this attribute, delegate to it + settings = self.__dict__.get('_settings') + if settings is not None and name in ProjectSettings.model_fields: + setattr(settings, name, value) + return + + # Fall back to normal attribute setting + self.__dict__[name] = value + + # Explicit delegation for commonly used methods + def to_json(self, json_file: str): + """Save the ProjectSettings object to a JSON file.""" + return self._settings.to_json(json_file) + + @classmethod + def from_json(cls, json_file: str, led_wall_class=None): + """Create a ProjectSettingsModel from a JSON file.""" + instance = cls(led_wall_class=led_wall_class) + instance._settings = ProjectSettings.from_json(json_file, led_wall_class=led_wall_class) + return instance + + def to_dict(self): + """Returns the settings as a dictionary.""" + return self._settings.to_dict() + + def get_led_wall(self, name: str) -> LedWallSettings: + """Returns a LED wall from the project.""" + return self._settings.get_led_wall(name) + + def add_custom_primary(self, name: str, primaries): + """Adds a custom primary to the project.""" + return self._settings.add_custom_primary(name, primaries) + def refresh_default_data(self): """ Refreshes the default data dictionary with the current default values from the LedWallSettings class @@ -88,7 +143,7 @@ def refresh_default_data(self): target_gamut_options = constants.ColourSpace.all().copy() target_gamut_options.pop(target_gamut_options.index(constants.ColourSpace.CS_ACES)) target_gamut_options.extend(self.project_custom_primaries.keys()) - default_led_wall = LedWallSettings(self, constants.DEFAULT) + default_led_wall = LedWallSettings(self._settings, name=constants.DEFAULT) self.default_data = { constants.ProjectSettingsKeys.OUTPUT_FOLDER: {constants.DEFAULT: self.output_folder}, @@ -234,25 +289,33 @@ def load_from_json(self, json_file: str): json_file (str): The path to the JSON file. """ - project_settings = self.from_json(json_file, led_wall_class=self._led_wall_class) - for key, _ in project_settings._project_settings: + project_settings = ProjectSettings.from_json(json_file, led_wall_class=self._led_wall_class) + # Iterate over project settings model fields (excluding version and walls) + for key in ProjectSettings.model_fields: + if key in ('openvp_cal_version', 'led_walls'): + continue value = getattr(project_settings, key) self.set_data(key, value) - for led_wall in self.led_walls: - led_wall.project_settings = self + # Copy the led walls + self._settings.led_walls = project_settings.led_walls + for led_wall in self._settings.led_walls: + led_wall._project_settings = self._settings self.led_wall_added.emit(led_wall) self.refresh_default_data() - for gamut_name in self.project_custom_primaries: + for gamut_name in self._settings.project_custom_primaries: self.register_custom_gamut_from_load.emit(gamut_name) def clear_project_settings(self): """ Clears the project settings back to the default settings and emits a signal to inform the views """ - super().clear_project_settings() - for key, value in self._project_settings: - self.data_changed.emit(key, value) + self._settings.clear_project_settings() + # Emit data changes for all project settings fields (excluding version and walls) + for key in ProjectSettings.model_fields: + if key in ('openvp_cal_version', 'led_walls'): + continue + self.data_changed.emit(key, getattr(self._settings, key)) def add_led_wall(self, name: str) -> Union[LedWallSettings, None]: """ @@ -265,7 +328,7 @@ def add_led_wall(self, name: str) -> Union[LedWallSettings, None]: """ try: - led_wall = super().add_led_wall(name) + led_wall = self._settings.add_led_wall(name) except ValueError as handled_exception: self.error_occurred.emit(str(handled_exception)) return None @@ -283,10 +346,14 @@ def _add_led_wall(self, led_wall: LedWallSettings) -> LedWallSettings: """ self.current_wall = led_wall self.led_wall_added.emit(led_wall) - for key, _ in self._project_settings: + # Emit data changes for all project settings fields (excluding version and walls) + for key in ProjectSettings.model_fields: + if key in ('openvp_cal_version', 'led_walls'): + continue self.data_changed.emit(key, self.get_data(key)) - for key, _ in self.current_wall._led_settings: + # Emit data changes for all led wall settings fields + for key in LedWallSettings.model_fields: self.data_changed.emit(key, self.get_data(key)) return led_wall @@ -301,7 +368,7 @@ def copy_led_wall(self, existing_wall_name: str, new_name: str) -> LedWallSettin """ try: - led_wall = super().copy_led_wall(existing_wall_name, new_name) + led_wall = self._settings.copy_led_wall(existing_wall_name, new_name) except ValueError as handled_exception: self.error_occurred.emit(str(handled_exception)) return None @@ -318,7 +385,7 @@ def add_verification_wall(self, existing_wall_name: str) -> LedWallSettings: """ try: - led_wall = super().add_verification_wall(existing_wall_name) + led_wall = self._settings.add_verification_wall(existing_wall_name) except ValueError as handled_exception: self.error_occurred.emit(str(handled_exception)) return None @@ -331,8 +398,8 @@ def remove_led_wall(self, name: str) -> None: Args: name: The name of the wall to remove """ - super().remove_led_wall(name) - if not self.led_walls: + self._settings.remove_led_wall(name) + if not self._settings.led_walls: self.current_wall = None self.led_wall_removed.emit(name) @@ -343,20 +410,24 @@ def set_current_wall(self, led_wall: Union[str, LedWallSettings]) -> None: led_wall: The wall to set as the current wall """ if isinstance(led_wall, str): - for wall in self.led_walls: + for wall in self._settings.led_walls: if wall.name == led_wall: self.current_wall = wall else: self.current_wall = led_wall - for key, _ in self._project_settings: + # Emit data changes for all project settings fields (excluding version and walls) + for key in ProjectSettings.model_fields: + if key in ('openvp_cal_version', 'led_walls'): + continue self.data_changed.emit(key, self.get_data(key)) - for key, _ in self.current_wall._led_settings: + # Emit data changes for all led wall settings fields + for key in LedWallSettings.model_fields: self.data_changed.emit(key, self.get_data(key)) def reset_led_wall(self, name: str) -> None: - super().reset_led_wall(name) + self._settings.reset_led_wall(name) self.set_current_wall(name) diff --git a/packages/open_vp_cal/src/open_vp_cal/widgets/stage_widget.py b/packages/open_vp_cal/src/open_vp_cal/widgets/stage_widget.py index 13b7886d..ef8e83a5 100644 --- a/packages/open_vp_cal/src/open_vp_cal/widgets/stage_widget.py +++ b/packages/open_vp_cal/src/open_vp_cal/widgets/stage_widget.py @@ -35,8 +35,8 @@ class LedWallTimelineLoader(LedWallSettings): A specialization of the LedWallSettings which allows us to override the sequence loader class """ - def __init__(self, project_settings: ProjectSettings, name="Wall1"): - super().__init__(project_settings, name) + def __init__(self, project_settings: ProjectSettings = None, name: str = "Wall1", **data): + super().__init__(project_settings, name=name, **data) self._sequence_loader_class = TimelineLoader diff --git a/packages/open_vp_cal/src/open_vp_cal/widgets/timeline_widget.py b/packages/open_vp_cal/src/open_vp_cal/widgets/timeline_widget.py index c0259197..e3e14636 100644 --- a/packages/open_vp_cal/src/open_vp_cal/widgets/timeline_widget.py +++ b/packages/open_vp_cal/src/open_vp_cal/widgets/timeline_widget.py @@ -39,8 +39,8 @@ class PixMapFrame(Frame): """ A Frame which holds a QPixmap instead of an ImageBuf """ - def __init__(self, project_settings: ProjectSettingsModel): - super().__init__(project_settings) + def __init__(self, led_wall_settings: LedWallSettings): + super().__init__(led_wall_settings) self._pixmap = None @property @@ -59,7 +59,8 @@ def load_pixmap(self) -> None: """ Load the pixmap from the image buffer if it does not exist """ if not self._pixmap: - self._pixmap = load_image_buffer_to_qpixmap(self._image_buf, self._project_settings) + self._pixmap = load_image_buffer_to_qpixmap( + self._image_buf, self._led_wall_settings.input_plate_gamut) def clear_pixmap(self) -> None: """ Clears the pixmap diff --git a/tests/test_open_vp_cal/test_led_wall_settings.py b/tests/test_open_vp_cal/test_led_wall_settings.py index 5e1cf961..e4a5f203 100644 --- a/tests/test_open_vp_cal/test_led_wall_settings.py +++ b/tests/test_open_vp_cal/test_led_wall_settings.py @@ -18,7 +18,8 @@ import json from typing import List from open_vp_cal.framework.identify_separation import SeparationResults -from open_vp_cal.led_wall_settings import LedWallSettings, LedWallSettingsBaseModel, ProcessingResults +from open_vp_cal.led_wall_settings import LedWallSettings +from open_vp_cal.core.structures import ProcessingResults from open_vp_cal.core import constants from test_utils import TestBase @@ -112,49 +113,55 @@ def tearDown(self): def test_default_values(self): # Test for refactoring - # Check the number of legacy fields is the same as the number of fields in the new model + # Check the number of legacy fields is the same as the number of serialized fields in the new model # We can remove this test once we add more fields to the new model in future. - self.assertEqual(len(self.legacy_default), len(LedWallSettingsBaseModel.model_fields)) + # Note: model_fields includes runtime fields (processing_results, separation_results) which are excluded + serialized_fields = {k: v for k, v in LedWallSettings.model_fields.items() + if not v.json_schema_extra or not v.json_schema_extra.get('exclude', False)} + serialized_fields = {k: v for k, v in LedWallSettings.model_fields.items() + if k not in ('processing_results', 'separation_results')} + self.assertEqual(len(self.legacy_default), len(serialized_fields)) # Test for refactoring # Check the default values are the same as the legacy default values # We can remove this test once we update the default values in future. newWall: LedWallSettings = LedWallSettings(self.project_settings) - new_default_values = newWall._led_settings - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NAME], new_default_values.name) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.AVOID_CLIPPING], new_default_values.avoid_clipping) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION], new_default_values.enable_eotf_correction) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION], new_default_values.enable_gamut_compression) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.AUTO_WB_SOURCE], new_default_values.auto_wb_source) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.INPUT_SEQUENCE_FOLDER], new_default_values.input_sequence_folder) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NUM_GREY_PATCHES], new_default_values.num_grey_patches) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.PRIMARIES_SATURATION], new_default_values.primaries_saturation) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.CALCULATION_ORDER], new_default_values.calculation_order) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT], new_default_values.input_plate_gamut) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT], new_default_values.native_camera_gamut) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT], new_default_values.reference_to_target_cat) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ROI], new_default_values.roi) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.SHADOW_ROLLOFF], new_default_values.shadow_rolloff) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS], new_default_values.target_max_lum_nits) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_GAMUT], new_default_values.target_gamut) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_EOTF], new_default_values.target_eotf) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT], new_default_values.target_to_screen_cat) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL], new_default_values.match_reference_wall) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.REFERENCE_WALL], new_default_values.reference_wall) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET], new_default_values.use_white_point_offset) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE], new_default_values.white_point_offset_source) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.IS_VERIFICATION_WALL], new_default_values.is_verification_wall) - self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.VERIFICATION_WALL], new_default_values.verification_wall) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NAME], newWall.name) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.AVOID_CLIPPING], newWall.avoid_clipping) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION], newWall.enable_eotf_correction) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION], newWall.enable_gamut_compression) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.AUTO_WB_SOURCE], newWall.auto_wb_source) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.INPUT_SEQUENCE_FOLDER], newWall.input_sequence_folder) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NUM_GREY_PATCHES], newWall.num_grey_patches) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.PRIMARIES_SATURATION], newWall.primaries_saturation) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.CALCULATION_ORDER], newWall.calculation_order) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT], newWall.input_plate_gamut) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT], newWall.native_camera_gamut) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT], newWall.reference_to_target_cat) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.ROI], newWall.roi) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.SHADOW_ROLLOFF], newWall.shadow_rolloff) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS], newWall.target_max_lum_nits) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_GAMUT], newWall.target_gamut) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_EOTF], newWall.target_eotf) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT], newWall.target_to_screen_cat) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL], newWall.match_reference_wall) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.REFERENCE_WALL], newWall.reference_wall) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET], newWall.use_white_point_offset) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE], newWall.white_point_offset_source) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.IS_VERIFICATION_WALL], newWall.is_verification_wall) + self.assertEqual(self.legacy_default[constants.LedWallSettingsKeys.VERIFICATION_WALL], newWall.verification_wall) def test_reset_defaults(self): self.wall.use_white_point_offset = True self.assertEqual(self.wall.use_white_point_offset, True) - self.assertEqual(self.wall._led_settings.use_white_point_offset, True) self.wall.reset_defaults() self.assertEqual(self.wall.use_white_point_offset, False) - self.assertEqual(self.wall._led_settings.use_white_point_offset, False) - self.assertEqual(self.wall._led_settings, LedWallSettingsBaseModel(name=self.wall.name)) + # Check that all fields match defaults except name + defaults = LedWallSettings(self.project_settings, name=self.wall.name) + for field in LedWallSettings.model_fields: + if field not in ('processing_results', 'separation_results'): + self.assertEqual(getattr(self.wall, field), getattr(defaults, field)) def test_clear(self): self.wall.roi = upgrade_legacy_roi([1, 2, 3, 4]) @@ -162,14 +169,16 @@ def test_clear(self): self.wall.separation_results = SeparationResults() self.wall.clear() self.assertEqual(self.wall.roi, []) - self.assertEqual(self.wall._led_settings.roi, []) self.assertEqual(self.wall.processing_results.__dict__, ProcessingResults().__dict__) self.assertIsNone(self.wall.separation_results) def test_clear_led_settings(self): self.wall.target_eotf = constants.EOTF.EOTF_SRGB self.wall.clear_led_settings() - self.assertEqual(self.wall._led_settings, LedWallSettingsBaseModel(name=self.wall.name)) + defaults = LedWallSettings(self.project_settings, name=self.wall.name) + for field in LedWallSettings.model_fields: + if field not in ('processing_results', 'separation_results'): + self.assertEqual(getattr(self.wall, field), getattr(defaults, field)) def test_fields_all_included_in_test(self): sample_keys = list(self.sample.keys()) @@ -179,117 +188,121 @@ def test_fields_all_included_in_test(self): constants_all.sort() self.assertEqual(sample_keys, constants_all) - default_keys = list(LedWallSettingsBaseModel.model_fields.keys()) - default_keys.sort() - self.assertEqual(sample_keys, default_keys) + # Only check serialized fields (exclude runtime fields) + serialized_fields = [k for k in LedWallSettings.model_fields.keys() + if k not in ('processing_results', 'separation_results')] + serialized_fields.sort() + self.assertEqual(sample_keys, serialized_fields) def test_led_wall_settings_keys(self): constants_all = constants.LedWallSettingsKeys.all().copy() constants_all.sort() - led_settings_keys = list(LedWallSettingsBaseModel.model_fields.keys()) + # Only check serialized fields (exclude runtime fields) + led_settings_keys = [k for k in LedWallSettings.model_fields.keys() + if k not in ('processing_results', 'separation_results')] led_settings_keys.sort() self.assertEqual(constants_all, led_settings_keys, "LedWallSettingsKeys should reflect all fields in the model. Add new keys to LedWallSettingsKeys.") def test_initialization(self): self.assertEqual(self.wall.name, "TestWall") - self.assertEqual(self.wall._led_settings.name, "TestWall") - self.assertEqual(self.wall._led_settings.avoid_clipping, False) - self.assertEqual(self.wall._led_settings.enable_eotf_correction, True) - self.assertEqual(self.wall._led_settings.enable_gamut_compression, True) - self.assertEqual(self.wall._led_settings.auto_wb_source, False) - self.assertEqual(self.wall._led_settings.input_sequence_folder, "") - self.assertEqual(self.wall._led_settings.num_grey_patches, 30) - self.assertEqual(self.wall._led_settings.primaries_saturation, 0.7) - self.assertEqual(self.wall._led_settings.calculation_order, constants.CalculationOrder(constants.CalculationOrder.default())) - self.assertEqual(self.wall._led_settings.input_plate_gamut, constants.ColourSpace(constants.ColourSpace.default_ref())) - self.assertEqual(self.wall._led_settings.native_camera_gamut, constants.CameraColourSpace(constants.CameraColourSpace.default())) - self.assertEqual(self.wall._led_settings.reference_to_target_cat, constants.CAT(constants.CAT.CAT_BRADFORD)) - self.assertEqual(self.wall._led_settings.roi, []) - self.assertEqual(self.wall._led_settings.shadow_rolloff, 0.008) - self.assertEqual(self.wall._led_settings.target_max_lum_nits, 1000) - self.assertEqual(self.wall._led_settings.target_gamut, constants.LedColourSpace(constants.LedColourSpace.default_target())) - self.assertEqual(self.wall._led_settings.target_eotf, constants.EOTF(constants.EOTF.default())) - self.assertEqual(self.wall._led_settings.target_to_screen_cat, constants.CAT.CAT_NONE) - self.assertEqual(self.wall._led_settings.match_reference_wall, False) - self.assertEqual(self.wall._led_settings.reference_wall, "") - self.assertEqual(self.wall._led_settings.white_point_offset_source, "") - self.assertEqual(self.wall._led_settings.use_white_point_offset, False) - self.assertEqual(self.wall._led_settings.is_verification_wall, False) - self.assertEqual(self.wall._led_settings.verification_wall, "") + self.assertEqual(self.wall.name, "TestWall") + self.assertEqual(self.wall.avoid_clipping, False) + self.assertEqual(self.wall.enable_eotf_correction, True) + self.assertEqual(self.wall.enable_gamut_compression, True) + self.assertEqual(self.wall.auto_wb_source, False) + self.assertEqual(self.wall.input_sequence_folder, "") + self.assertEqual(self.wall.num_grey_patches, 30) + self.assertEqual(self.wall.primaries_saturation, 0.7) + self.assertEqual(self.wall.calculation_order, constants.CalculationOrder(constants.CalculationOrder.default())) + self.assertEqual(self.wall.input_plate_gamut, constants.ColourSpace(constants.ColourSpace.default_ref())) + self.assertEqual(self.wall.native_camera_gamut, constants.CameraColourSpace(constants.CameraColourSpace.default())) + self.assertEqual(self.wall.reference_to_target_cat, constants.CAT(constants.CAT.CAT_BRADFORD)) + self.assertEqual(self.wall.roi, []) + self.assertEqual(self.wall.shadow_rolloff, 0.008) + self.assertEqual(self.wall.target_max_lum_nits, 1000) + self.assertEqual(self.wall.target_gamut, constants.LedColourSpace(constants.LedColourSpace.default_target())) + self.assertEqual(self.wall.target_eotf, constants.EOTF(constants.EOTF.default())) + self.assertEqual(self.wall.target_to_screen_cat, constants.CAT.CAT_NONE) + self.assertEqual(self.wall.match_reference_wall, False) + self.assertEqual(self.wall.reference_wall, "") + self.assertEqual(self.wall.white_point_offset_source, "") + self.assertEqual(self.wall.use_white_point_offset, False) + self.assertEqual(self.wall.is_verification_wall, False) + self.assertEqual(self.wall.verification_wall, "") def test_name(self): self.wall.name = "NewName" self.assertEqual(self.wall.name, "NewName") - self.assertEqual(self.wall._led_settings.name, "NewName") + self.assertEqual(self.wall.name, "NewName") def test_avoid_clipping(self): self.wall.avoid_clipping = True self.assertEqual(self.wall.avoid_clipping, True) - self.assertEqual(self.wall._led_settings.avoid_clipping, True) + self.assertEqual(self.wall.avoid_clipping, True) # is removed def test_enable_eotf_correction(self): self.wall.enable_eotf_correction = False self.assertEqual(self.wall.enable_eotf_correction, False) - self.assertEqual(self.wall._led_settings.enable_eotf_correction, False) + self.assertEqual(self.wall.enable_eotf_correction, False) def test_enable_gamut_compression(self): self.wall.enable_gamut_compression = False self.assertEqual(self.wall.enable_gamut_compression, False) - self.assertEqual(self.wall._led_settings.enable_gamut_compression, False) + self.assertEqual(self.wall.enable_gamut_compression, False) def test_auto_wb_source(self): self.wall.auto_wb_source = False self.assertEqual(self.wall.auto_wb_source, False) - self.assertEqual(self.wall._led_settings.auto_wb_source, False) + self.assertEqual(self.wall.auto_wb_source, False) def test_input_sequence_folder(self): self.wall.input_sequence_folder = "/new/path" self.assertEqual(self.wall.input_sequence_folder, "/new/path") - self.assertEqual(self.wall._led_settings.input_sequence_folder, "/new/path") + self.assertEqual(self.wall.input_sequence_folder, "/new/path") def test_calculation_order(self): self.wall.calculation_order = constants.CalculationOrder.CO_EOTF_CS self.assertEqual(self.wall.calculation_order, constants.CalculationOrder.CO_EOTF_CS) - self.assertEqual(self.wall._led_settings.calculation_order, constants.CalculationOrder.CO_EOTF_CS) + self.assertEqual(self.wall.calculation_order, constants.CalculationOrder.CO_EOTF_CS) def test_primaries_saturation(self): self.wall.primaries_saturation = 0.1 self.assertAlmostEqual(self.wall.primaries_saturation, 0.1) - self.assertAlmostEqual(self.wall._led_settings.primaries_saturation, 0.1) + self.assertAlmostEqual(self.wall.primaries_saturation, 0.1) def test_input_plate_gamut(self): self.wall.input_plate_gamut = constants.ColourSpace.CS_SRGB self.assertEqual(self.wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) - self.assertEqual(self.wall._led_settings.input_plate_gamut, constants.ColourSpace.CS_SRGB) + self.assertEqual(self.wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) def test_native_camera_gamut(self): self.wall.native_camera_gamut = constants.CameraColourSpace.ARRI_WIDE_GAMUT_3 self.assertEqual(self.wall.native_camera_gamut, constants.CameraColourSpace.ARRI_WIDE_GAMUT_3) - self.assertEqual(self.wall._led_settings.native_camera_gamut, constants.CameraColourSpace.ARRI_WIDE_GAMUT_3) + self.assertEqual(self.wall.native_camera_gamut, constants.CameraColourSpace.ARRI_WIDE_GAMUT_3) def test_num_grey_patches(self): self.wall.num_grey_patches = 25 self.assertEqual(self.wall.num_grey_patches, 25) - self.assertEqual(self.wall._led_settings.num_grey_patches, 25) + self.assertEqual(self.wall.num_grey_patches, 25) def test_reference_to_target_cat(self): self.wall.reference_to_target_cat = constants.CAT.CAT_BRADFORD self.assertEqual(self.wall.reference_to_target_cat, constants.CAT.CAT_BRADFORD) - self.assertEqual(self.wall._led_settings.reference_to_target_cat, constants.CAT.CAT_BRADFORD) + self.assertEqual(self.wall.reference_to_target_cat, constants.CAT.CAT_BRADFORD) # is removed def test_roi(self): legacy_roi = [1, 2, 3, 4] self.assertEqual(upgrade_legacy_roi(legacy_roi), - LedWallSettingsBaseModel.upgrade_roi(legacy_roi)) + LedWallSettings.upgrade_roi(legacy_roi)) roi = upgrade_legacy_roi(legacy_roi) self.wall.roi = roi self.assertEqual(self.wall.roi, roi) - self.assertEqual(self.wall._led_settings.roi, roi) + self.assertEqual(self.wall.roi, roi) def test_empty_roi(self): self.wall.roi = [] @@ -298,51 +311,51 @@ def test_empty_roi(self): def test_shadow_rolloff(self): self.wall.shadow_rolloff = 0.1 self.assertEqual(self.wall.shadow_rolloff, 0.1) - self.assertEqual(self.wall._led_settings.shadow_rolloff, 0.1) + self.assertEqual(self.wall.shadow_rolloff, 0.1) def test_target_gamut(self): self.wall.target_gamut = constants.ColourSpace.CS_BT2020 self.assertEqual(self.wall.target_gamut, constants.ColourSpace.CS_BT2020) - self.assertEqual(self.wall._led_settings.target_gamut, constants.ColourSpace.CS_BT2020) + self.assertEqual(self.wall.target_gamut, constants.ColourSpace.CS_BT2020) def test_target_eotf(self): self.wall.target_eotf = constants.EOTF.EOTF_ST2084 self.wall.target_max_lum_nits = 30 self.assertEqual(self.wall.target_eotf, constants.EOTF.EOTF_ST2084) - self.assertEqual(self.wall._led_settings.target_eotf, constants.EOTF.EOTF_ST2084) + self.assertEqual(self.wall.target_eotf, constants.EOTF.EOTF_ST2084) + self.assertEqual(self.wall.target_max_lum_nits, 30) self.assertEqual(self.wall.target_max_lum_nits, 30) - self.assertEqual(self.wall._led_settings.target_max_lum_nits, 30) # target_max_lum_nits should be set to when target_eotf is not ST2084 self.wall.target_eotf = constants.EOTF.EOTF_BT1886 self.assertEqual(self.wall.target_eotf, constants.EOTF.EOTF_BT1886) - self.assertEqual(self.wall._led_settings.target_eotf, constants.EOTF.EOTF_BT1886) + self.assertEqual(self.wall.target_eotf, constants.EOTF.EOTF_BT1886) + self.assertEqual(self.wall.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) self.assertEqual(self.wall.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) - self.assertEqual(self.wall._led_settings.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) def test_target_max_lum_nits(self): self.wall.target_eotf = constants.EOTF.EOTF_ST2084 self.wall.target_max_lum_nits = 2000 self.assertEqual(self.wall.target_max_lum_nits, 2000) - self.assertEqual(self.wall._led_settings.target_max_lum_nits, 2000) + self.assertEqual(self.wall.target_max_lum_nits, 2000) # target_max_lum_nits should be set to when target_eotf is not ST2084 self.wall.target_eotf = constants.EOTF.EOTF_BT1886 self.wall.target_max_lum_nits = 2000 self.assertEqual(self.wall.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) - self.assertEqual(self.wall._led_settings.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) + self.assertEqual(self.wall.target_max_lum_nits, constants.TARGET_MAX_LUM_NITS_NONE_PQ) def test_target_to_screen_cat(self): self.wall.target_to_screen_cat = constants.CAT.CAT_BIANCO2010 self.assertEqual(self.wall.target_to_screen_cat, constants.CAT.CAT_BIANCO2010) - self.assertEqual(self.wall._led_settings.target_to_screen_cat, constants.CAT.CAT_BIANCO2010) + self.assertEqual(self.wall.target_to_screen_cat, constants.CAT.CAT_BIANCO2010) # is removed def test_match_reference_wall(self): self.wall.match_reference_wall = True self.assertEqual(self.wall.match_reference_wall, True) - self.assertEqual(self.wall._led_settings.match_reference_wall, True) + self.assertEqual(self.wall.match_reference_wall, True) def test_reference_wall(self): # Check we can set a new wall by instance @@ -350,14 +363,14 @@ def test_reference_wall(self): self.wall.reference_wall = new_wall self.assertEqual(self.wall.reference_wall, new_wall.name) self.assertEqual(self.wall.reference_wall_as_wall.name, new_wall.name) # type: ignore - self.assertEqual(self.wall._led_settings.reference_wall, new_wall.name) + self.assertEqual(self.wall.reference_wall, new_wall.name) # Check we can set a new wall by name another_wall = self.project_settings.add_led_wall("AnotherWall") self.wall.reference_wall = another_wall.name self.assertEqual(self.wall.reference_wall, another_wall.name) self.assertEqual(self.wall.reference_wall_as_wall.name, another_wall.name) # type: ignore - self.assertEqual(self.wall._led_settings.reference_wall, another_wall.name) + self.assertEqual(self.wall.reference_wall, another_wall.name) # Setting reference_wall to itself should raise ValueError (by instance) with self.assertRaises(ValueError): @@ -368,7 +381,7 @@ def test_reference_wall(self): # Setting reference_wall to a non-existent wall should raise ValueError (by instance) with self.assertRaises(ValueError): - self.wall.reference_wall = LedWallSettings(self.project_settings, "NonExistentWall") + self.wall.reference_wall = LedWallSettings(self.project_settings, name="NonExistentWall") # Setting reference_wall to a non-existent wall should raise ValueError (by name) with self.assertRaises(ValueError): @@ -395,25 +408,25 @@ def test_remove_reference_wall(self): def test_use_white_point_offset(self): self.wall.use_white_point_offset = True self.assertEqual(self.wall.use_white_point_offset, True) - self.assertEqual(self.wall._led_settings.use_white_point_offset, True) + self.assertEqual(self.wall.use_white_point_offset, True) def test_white_point_offset_source(self): self.wall.white_point_offset_source = "new_file.exr" self.assertEqual(self.wall.white_point_offset_source, "new_file.exr") - self.assertEqual(self.wall._led_settings.white_point_offset_source, "new_file.exr") + self.assertEqual(self.wall.white_point_offset_source, "new_file.exr") def test_verification_wall(self): new_wall = self.project_settings.add_led_wall("NewWall") verification_wall = self.project_settings.add_verification_wall(new_wall.name) self.assertEqual(verification_wall.is_verification_wall, True) - self.assertEqual(verification_wall._led_settings.is_verification_wall, True) + self.assertEqual(verification_wall.is_verification_wall, True) + self.assertEqual(new_wall.is_verification_wall, False) self.assertEqual(new_wall.is_verification_wall, False) - self.assertEqual(new_wall._led_settings.is_verification_wall, False) self.assertEqual(new_wall.verification_wall, verification_wall.name) - self.assertEqual(new_wall._led_settings.verification_wall, verification_wall.name) + self.assertEqual(new_wall.verification_wall, verification_wall.name) + self.assertEqual(verification_wall.verification_wall, new_wall.name) self.assertEqual(verification_wall.verification_wall, new_wall.name) - self.assertEqual(verification_wall._led_settings.verification_wall, new_wall.name) # Test That Changing A Param On The Main Wall Changes On The Verification Wall new_wall.input_plate_gamut = constants.ColourSpace.CS_SRGB @@ -423,7 +436,7 @@ def test_verification_wall(self): verification_wall.input_plate_gamut = constants.ColourSpace.CS_P3 self.assertEqual(verification_wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) self.assertEqual(new_wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) - self.assertEqual(new_wall._led_settings.input_plate_gamut, constants.ColourSpace.CS_SRGB) + self.assertEqual(new_wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) new_wall.target_eotf = constants.EOTF.EOTF_GAMMA_1_8 self.assertEqual(verification_wall.target_eotf, constants.EOTF.EOTF_GAMMA_1_8) @@ -433,28 +446,30 @@ def test_verification_wall(self): # Ensure we set back to 2084 so that we avoid the fixing of the peak lum being set to 100 new_wall.target_eotf = constants.EOTF.EOTF_ST2084 - other_linked_properties = [ - constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION, - constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION, - constants.LedWallSettingsKeys.AUTO_WB_SOURCE, - constants.LedWallSettingsKeys.CALCULATION_ORDER, - constants.LedWallSettingsKeys.PRIMARIES_SATURATION, - constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT, - constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT, - constants.LedWallSettingsKeys.NUM_GREY_PATCHES, - constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT, - constants.LedWallSettingsKeys.SHADOW_ROLLOFF, - constants.LedWallSettingsKeys.TARGET_GAMUT, - constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS, - constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT, - constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL, - constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET, - constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE] - - # We test all the other linked params with false data - for count, linked_prop in enumerate(other_linked_properties): - setattr(new_wall, linked_prop, count) - self.assertEqual(getattr(verification_wall, linked_prop), count) + + # Test additional linked properties with valid test values + linked_properties_with_values = { + constants.LedWallSettingsKeys.ENABLE_EOTF_CORRECTION: False, + constants.LedWallSettingsKeys.ENABLE_GAMUT_COMPRESSION: False, + constants.LedWallSettingsKeys.AUTO_WB_SOURCE: True, + constants.LedWallSettingsKeys.CALCULATION_ORDER: constants.CalculationOrder.CO_EOTF_CS, + constants.LedWallSettingsKeys.PRIMARIES_SATURATION: 0.5, + constants.LedWallSettingsKeys.NATIVE_CAMERA_GAMUT: constants.CameraColourSpace.ARRI_WIDE_GAMUT_3, + constants.LedWallSettingsKeys.NUM_GREY_PATCHES: 25, + constants.LedWallSettingsKeys.REFERENCE_TO_TARGET_CAT: constants.CAT.CAT_CAT02, + constants.LedWallSettingsKeys.SHADOW_ROLLOFF: 0.01, + constants.LedWallSettingsKeys.TARGET_GAMUT: constants.LedColourSpace.CS_P3, + constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS: 500, + constants.LedWallSettingsKeys.TARGET_TO_SCREEN_CAT: constants.CAT.CAT_BRADFORD, + constants.LedWallSettingsKeys.MATCH_REFERENCE_WALL: True, + constants.LedWallSettingsKeys.USE_WHITE_POINT_OFFSET: True, + constants.LedWallSettingsKeys.WHITE_POINT_OFFSET_SOURCE: "test.exr", + } + + # Test that linked properties propagate from main wall to verification wall + for linked_prop, test_value in linked_properties_with_values.items(): + setattr(new_wall, linked_prop, test_value) + self.assertEqual(getattr(verification_wall, linked_prop), test_value) def test_add_verification_wall_to_verification_wall(self): new_wall = self.project_settings.add_led_wall("NewWall") @@ -482,12 +497,23 @@ def test_set_property_on_verification_wall_does_not_propagate(self): verification_wall.input_plate_gamut = constants.ColourSpace.CS_SRGB self.assertNotEqual(new_wall.input_plate_gamut, None) - def test_get_property_on_verification_wall_raises_if_parent_removed(self): + def test_get_property_on_verification_wall_falls_back_if_parent_removed(self): + """When parent wall is removed, verification walls fall back to local values.""" new_wall = self.project_settings.add_led_wall("NewWall") verification_wall = self.project_settings.add_verification_wall(new_wall.name) + + # Set a value on the parent wall, which should propagate to verification wall + new_wall.input_plate_gamut = constants.ColourSpace.CS_SRGB + self.assertEqual(verification_wall.input_plate_gamut, constants.ColourSpace.CS_SRGB) + + # Remove the parent wall - verification_wall should fall back to its local value self.project_settings.remove_led_wall(new_wall.name) - with self.assertRaises(ValueError): - _ = verification_wall._get_property(constants.LedWallSettingsKeys.INPUT_PLATE_GAMUT) + + # After removal, accessing linked property returns local value (the last synced value) + # The verification_wall link is cleared, so it uses its own stored value + self.assertEqual(verification_wall.verification_wall, "") + # Local value should still be accessible + _ = verification_wall.input_plate_gamut # Should not raise def test_verification_wall_edge_cases(self): @@ -515,13 +541,12 @@ def test_verification_wall_as_wall_none(self): """Test that verification_wall_as_wall returns None when no verification wall is set.""" self.assertIsNone(self.wall.verification_wall_as_wall) - # Test with a non-existent verification wall name - self.wall._led_settings.verification_wall = "NonExistentWall" - with self.assertRaises(ValueError): - _ = self.wall.verification_wall_as_wall + # Test with a non-existent verification wall name - returns None + self.wall.verification_wall = "NonExistentWall" + self.assertIsNone(self.wall.verification_wall_as_wall) # Test with empty string - self.wall._led_settings.verification_wall = "" + self.wall.verification_wall = "" self.assertIsNone(self.wall.verification_wall_as_wall) def test_has_valid_white_balance_options(self): @@ -542,19 +567,19 @@ def test_json_conversion(self): new_wall2 = LedWallSettings.from_json_file(self.project_settings, self.json_path) for key in self.sample: - self.assertEqual(getattr(new_wall2._led_settings, key), self.sample_expected[key]) + self.assertEqual(getattr(new_wall2, key), self.sample_expected[key]) def test_from_json_string(self): json_str = json.dumps(self.sample) new_wall = LedWallSettings.from_json_string(self.project_settings, json_str) for key in self.sample: - self.assertEqual(getattr(new_wall._led_settings, key), self.sample_expected[key]) + self.assertEqual(getattr(new_wall, key), self.sample_expected[key]) def test_from_dict(self): new_wall = LedWallSettings.from_dict(self.project_settings, self.sample) for key in self.sample: - self.assertEqual(getattr(new_wall._led_settings, key), self.sample_expected[key]) + self.assertEqual(getattr(new_wall, key), self.sample_expected[key]) def test_to_dict(self): json_str = json.dumps(self.sample) @@ -643,7 +668,7 @@ def test_sequence_loader_edge_cases(self): def test_roi_upgrade(self): led_wall = LedWallSettings.from_dict(self.project_settings, self.sample) - self.assertEqual(led_wall._led_settings.roi, self.sample_expected[constants.LedWallSettingsKeys.ROI]) + self.assertEqual(led_wall.roi, self.sample_expected[constants.LedWallSettingsKeys.ROI]) def test_all_sample_project_json(self): for project_settings_path in self.get_all_test_project_settings_path(): diff --git a/tests/test_open_vp_cal/test_project_settings.py b/tests/test_open_vp_cal/test_project_settings.py index 3f485813..5aeb2e56 100644 --- a/tests/test_open_vp_cal/test_project_settings.py +++ b/tests/test_open_vp_cal/test_project_settings.py @@ -22,7 +22,7 @@ import open_vp_cal from open_vp_cal.led_wall_settings import LedWallSettings -from open_vp_cal.project_settings import ProjectSettings, ProjectSettingsBaseModel +from open_vp_cal.project_settings import ProjectSettings from open_vp_cal.core import constants, utils from test_utils import TestBase from test_led_wall_settings import upgrade_legacy_roi @@ -141,7 +141,8 @@ def test_fields_all_included_in_test(self): """Test that all fields in the model are covered by our test sample.""" test_settings_keys = list(self.test_settings[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS].keys()) test_settings_keys.sort() - model_keys = list(ProjectSettingsBaseModel.model_fields.keys()) + # Get model fields but exclude openvp_cal_version (handled at outer level) + model_keys = [k for k in ProjectSettings.model_fields.keys() if k != 'openvp_cal_version'] model_keys.sort() self.assertEqual(test_settings_keys, model_keys) @@ -149,18 +150,21 @@ def test_project_settings_keys(self): """Test that ProjectSettingsKeys should reflect all fields in the model.""" constants_all = constants.ProjectSettingsKeys.all().copy() constants_all.sort() - model_keys = list(ProjectSettingsBaseModel.model_fields.keys()) + # Get model fields but exclude openvp_cal_version (handled separately) + model_keys = [k for k in ProjectSettings.model_fields.keys() if k != 'openvp_cal_version'] model_keys.sort() self.assertEqual(constants_all, model_keys, "ProjectSettingsKeys should reflect all fields in the model. Add new keys to ProjectSettingsKeys.") def test_default_values(self): """Test for refactoring - check the number of legacy fields is the same as the number of fields in the new model.""" # We can remove this test once we add more fields to the new model in future. - self.assertEqual(len(self.legacy_default), len(ProjectSettingsBaseModel.model_fields)) + # Note: ProjectSettings now includes openvp_cal_version, so we exclude it from count + model_fields_count = len([k for k in ProjectSettings.model_fields.keys() if k != 'openvp_cal_version']) + self.assertEqual(len(self.legacy_default), model_fields_count) # Test for refactoring - check the default values are the same as the legacy default values # We can remove this test once we update the default values in future. - new_settings = ProjectSettingsBaseModel() + new_settings = ProjectSettings() self.assertEqual(self.legacy_default[constants.ProjectSettingsKeys.CONTENT_MAX_LUM], new_settings.content_max_lum) self.assertEqual(self.legacy_default[constants.ProjectSettingsKeys.FILE_FORMAT], new_settings.file_format) self.assertEqual(self.legacy_default[constants.ProjectSettingsKeys.RESOLUTION_WIDTH], new_settings.resolution_width) @@ -290,12 +294,12 @@ def test_frame_rate(self): def test_try_convert_to_enum_frame_rates(self): """Test try_convert_to_enum_frame_rates function.""" - self.assertNotEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(23.99), constants.FrameRates.FPS_24) - self.assertNotEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(23.998), constants.FrameRates.FPS_24) - self.assertEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(23.999), constants.FrameRates.FPS_24) - self.assertEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(24.000), constants.FrameRates.FPS_24) - self.assertEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(24.001), constants.FrameRates.FPS_24) - self.assertNotEqual(ProjectSettingsBaseModel.try_convert_to_enum_frame_rates(24.002), constants.FrameRates.FPS_24) + self.assertNotEqual(ProjectSettings.try_convert_to_enum_frame_rates(23.99), constants.FrameRates.FPS_24) + self.assertNotEqual(ProjectSettings.try_convert_to_enum_frame_rates(23.998), constants.FrameRates.FPS_24) + self.assertEqual(ProjectSettings.try_convert_to_enum_frame_rates(23.999), constants.FrameRates.FPS_24) + self.assertEqual(ProjectSettings.try_convert_to_enum_frame_rates(24.000), constants.FrameRates.FPS_24) + self.assertEqual(ProjectSettings.try_convert_to_enum_frame_rates(24.001), constants.FrameRates.FPS_24) + self.assertNotEqual(ProjectSettings.try_convert_to_enum_frame_rates(24.002), constants.FrameRates.FPS_24) def test_export_lut_for_aces_cct(self): """Test export_lut_for_aces_cct field.""" @@ -323,23 +327,25 @@ def test_lut_size(self): def test_clear_project_settings(self): self.project_settings.clear_project_settings() - settings_default = ProjectSettingsBaseModel().model_dump() + settings_default = ProjectSettings().model_dump()[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS] for key in constants.ProjectSettingsKeys: if key == constants.ProjectSettingsKeys.PROJECT_ID: continue self.assertEqual(getattr(self.project_settings, key), settings_default[key]) def test_set_get_input_sequence_folder(self): - """Test setting and getting input sequence folder.""" + """Test setting and getting input sequence folder on a wall.""" test_value = "/new/path/" - self.settings.input_sequence_folder = test_value - self.assertEqual(self.settings.input_sequence_folder, test_value) + wall = self.settings.add_led_wall("TestWall") + wall.input_sequence_folder = test_value + self.assertEqual(wall.input_sequence_folder, test_value) - def test_set_get_input_sequence_input_transform(self): - """Test setting and getting input sequence input transform.""" - test_value = "ACES2065-1" - self.settings.input_sequence_input_transform = test_value - self.assertEqual(self.settings.input_sequence_input_transform, test_value) + def test_set_get_input_plate_gamut(self): + """Test setting and getting input plate gamut on a wall.""" + test_value = constants.ColourSpace.CS_ACES + wall = self.settings.add_led_wall("TestWall") + wall.input_plate_gamut = test_value + self.assertEqual(wall.input_plate_gamut, test_value) def test_set_get_ocio_config_path(self): """Test setting and getting OCIO config path.""" @@ -348,16 +354,18 @@ def test_set_get_ocio_config_path(self): self.assertEqual(self.settings.ocio_config_path, test_value) def test_set_get_primaries_saturation(self): - """Test setting and getting primaries saturation.""" + """Test setting and getting primaries saturation on a wall.""" test_value = 0.8 - self.settings.primaries_saturation = test_value - self.assertEqual(self.settings.primaries_saturation, test_value) + wall = self.settings.add_led_wall("TestWall") + wall.primaries_saturation = test_value + self.assertEqual(wall.primaries_saturation, test_value) def test_set_get_num_grey_patches(self): - """Test setting and getting the num_grey_patches""" + """Test setting and getting the num_grey_patches on a wall.""" test_value = 10 - self.settings.num_grey_patches = test_value - self.assertEqual(self.settings.num_grey_patches, test_value) + wall = self.settings.add_led_wall("TestWall") + wall.num_grey_patches = test_value + self.assertEqual(wall.num_grey_patches, test_value) def test_from_json(self): """Test creating a ProjectSettings object from a JSON file and saving it back.""" @@ -377,7 +385,8 @@ def test_from_json(self): self.assertEqual(len(test_led_walls), len(loaded_led_walls)) for test_led_wall, loaded_led_wall in zip(test_led_walls, loaded_led_walls): for test_key in constants.LedWallSettingsKeys: - self.assertEqual(test_led_wall[test_key], getattr(loaded_led_wall._led_settings, test_key)) + # Access fields directly on the led wall (it's now a pydantic model) + self.assertEqual(test_led_wall[test_key], getattr(loaded_led_wall, test_key)) for loaded_led_wall in loaded_led_walls: self.assertIsInstance(loaded_led_wall, LedWallSettings) diff --git a/tests/test_open_vp_cal/test_project_settings_model.py b/tests/test_open_vp_cal/test_project_settings_model.py new file mode 100644 index 00000000..ffbecec5 --- /dev/null +++ b/tests/test_open_vp_cal/test_project_settings_model.py @@ -0,0 +1,284 @@ +""" +Copyright 2024 Netflix Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Unit tests for ProjectSettingsModel UI interactions. +""" +import json +import os +import tempfile +import unittest + +from PySide6.QtCore import QCoreApplication + +from open_vp_cal.widgets.project_settings_widget import ProjectSettingsModel +from open_vp_cal.widgets.stage_widget import LedWallTimelineLoader +from open_vp_cal.led_wall_settings import LedWallSettings +from open_vp_cal.core import constants + +# QCoreApplication instance needed for Qt signal tests (no display required) +app = QCoreApplication.instance() or QCoreApplication([]) + + +class TestProjectSettingsModel(unittest.TestCase): + """Tests for ProjectSettingsModel UI interactions.""" + + def setUp(self): + """Set up test fixtures.""" + self.model = ProjectSettingsModel(led_wall_class=LedWallTimelineLoader) + + def tearDown(self): + """Clean up any walls after each test.""" + for wall in list(self.model.led_walls): + self.model.remove_led_wall(wall.name) + + def test_add_led_wall(self): + """Test adding an LED wall emits the correct signal.""" + signal_received = [] + self.model.led_wall_added.connect(lambda wall: signal_received.append(wall)) + + wall = self.model.add_led_wall("TestWall") + + self.assertIsNotNone(wall) + self.assertEqual(wall.name, "TestWall") + self.assertEqual(len(self.model.led_walls), 1) + self.assertEqual(len(signal_received), 1) + self.assertEqual(signal_received[0].name, "TestWall") + self.assertIsInstance(wall, LedWallTimelineLoader) + + def test_add_led_wall_duplicate_name(self): + """Test adding a duplicate wall name emits error signal.""" + error_received = [] + self.model.error_occurred.connect(lambda msg: error_received.append(msg)) + + self.model.add_led_wall("TestWall") + result = self.model.add_led_wall("TestWall") + + self.assertIsNone(result) + self.assertEqual(len(error_received), 1) + self.assertIn("already exists", error_received[0]) + + def test_remove_led_wall(self): + """Test removing an LED wall emits the correct signal.""" + signal_received = [] + self.model.led_wall_removed.connect(lambda name: signal_received.append(name)) + + self.model.add_led_wall("TestWall") + self.assertEqual(len(self.model.led_walls), 1) + + self.model.remove_led_wall("TestWall") + + self.assertEqual(len(self.model.led_walls), 0) + self.assertEqual(len(signal_received), 1) + self.assertEqual(signal_received[0], "TestWall") + + def test_copy_led_wall(self): + """Test copying an LED wall creates a new wall with different name.""" + self.model.add_led_wall("OriginalWall") + original = self.model.get_led_wall("OriginalWall") + original.target_max_lum_nits = 500 + + copied = self.model.copy_led_wall("OriginalWall", "CopiedWall") + + self.assertIsNotNone(copied) + self.assertEqual(copied.name, "CopiedWall") + self.assertEqual(len(self.model.led_walls), 2) + self.assertEqual(copied.target_max_lum_nits, 500) + + def test_copy_led_wall_duplicate_name(self): + """Test copying a wall with an existing name emits error signal.""" + error_received = [] + self.model.error_occurred.connect(lambda msg: error_received.append(msg)) + + self.model.add_led_wall("Wall1") + self.model.add_led_wall("Wall2") + + result = self.model.copy_led_wall("Wall1", "Wall2") + + self.assertIsNone(result) + self.assertEqual(len(error_received), 1) + self.assertIn("already exists", error_received[0]) + + def test_add_verification_wall(self): + """Test adding a verification wall links it to the parent wall.""" + self.model.add_led_wall("MainWall") + + verify_wall = self.model.add_verification_wall("MainWall") + + self.assertIsNotNone(verify_wall) + self.assertEqual(verify_wall.name, "Verify_MainWall") + self.assertTrue(verify_wall.is_verification_wall) + self.assertEqual(verify_wall.verification_wall, "MainWall") + + main_wall = self.model.get_led_wall("MainWall") + self.assertEqual(main_wall.verification_wall, "Verify_MainWall") + + def test_add_verification_wall_to_verification_wall(self): + """Test that adding a verification wall to a verification wall fails.""" + error_received = [] + self.model.error_occurred.connect(lambda msg: error_received.append(msg)) + + self.model.add_led_wall("MainWall") + self.model.add_verification_wall("MainWall") + + result = self.model.add_verification_wall("Verify_MainWall") + + self.assertIsNone(result) + self.assertEqual(len(error_received), 1) + + def test_set_current_wall(self): + """Test setting current wall emits data_changed signals.""" + data_changed_received = [] + self.model.data_changed.connect(lambda key, val: data_changed_received.append((key, val))) + + self.model.add_led_wall("Wall1") + self.model.add_led_wall("Wall2") + data_changed_received.clear() + + self.model.set_current_wall("Wall2") + + self.assertEqual(self.model.current_wall.name, "Wall2") + self.assertGreater(len(data_changed_received), 0) + + def test_set_current_wall_by_object(self): + """Test setting current wall by LedWallSettings object.""" + self.model.add_led_wall("Wall1") + wall2 = self.model.add_led_wall("Wall2") + + self.model.set_current_wall(wall2) + + self.assertEqual(self.model.current_wall.name, "Wall2") + + def test_set_data_project_property(self): + """Test set_data updates project property and emits signal.""" + data_changed_received = [] + self.model.data_changed.connect(lambda key, val: data_changed_received.append((key, val))) + + self.model.set_data(constants.ProjectSettingsKeys.OUTPUT_FOLDER, "/new/output/path") + + self.assertEqual(self.model.output_folder, "/new/output/path") + self.assertIn((constants.ProjectSettingsKeys.OUTPUT_FOLDER, "/new/output/path"), data_changed_received) + + def test_set_data_wall_property(self): + """Test set_data updates wall property and emits signal.""" + self.model.add_led_wall("TestWall") + data_changed_received = [] + self.model.data_changed.connect(lambda key, val: data_changed_received.append((key, val))) + + self.model.set_data(constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS, 800) + + self.assertEqual(self.model.current_wall.target_max_lum_nits, 800) + self.assertIn((constants.LedWallSettingsKeys.TARGET_MAX_LUM_NITS, 800), data_changed_received) + + def test_clear_project_settings(self): + """Test clearing project settings restores defaults.""" + self.model.output_folder = "/custom/path" + self.model.frames_per_patch = 5 + + self.model.clear_project_settings() + + self.assertNotEqual(self.model.output_folder, "/custom/path") + self.assertEqual(self.model.frames_per_patch, 1) + + def test_reset_led_wall(self): + """Test resetting an LED wall restores defaults but preserves verification link.""" + self.model.add_led_wall("MainWall") + self.model.add_verification_wall("MainWall") + + main_wall = self.model.get_led_wall("MainWall") + main_wall.target_max_lum_nits = 500 + main_wall.primaries_saturation = 0.5 + + self.model.reset_led_wall("MainWall") + + main_wall = self.model.get_led_wall("MainWall") + self.assertEqual(main_wall.target_max_lum_nits, 1000) + self.assertEqual(main_wall.primaries_saturation, 0.7) + self.assertEqual(main_wall.verification_wall, "Verify_MainWall") + + def test_save_and_load_json(self): + """Test saving and loading project settings preserves data.""" + self.model.add_led_wall("Wall1") + self.model.add_led_wall("Wall2") + self.model.output_folder = "/test/output" + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json_path = f.name + + try: + self.model.to_json(json_path) + + with open(json_path, 'r') as f: + data = json.load(f) + + self.assertIn(constants.OpenVPCalSettingsKeys.VERSION, data) + self.assertIn(constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS, data) + self.assertEqual(len(data[constants.OpenVPCalSettingsKeys.PROJECT_SETTINGS]['led_walls']), 2) + finally: + os.unlink(json_path) + + def test_get_led_wall(self): + """Test getting a wall by name.""" + self.model.add_led_wall("TestWall") + + wall = self.model.get_led_wall("TestWall") + + self.assertIsNotNone(wall) + self.assertEqual(wall.name, "TestWall") + + def test_get_led_wall_not_found(self): + """Test getting a non-existent wall raises ValueError.""" + with self.assertRaises(ValueError): + self.model.get_led_wall("NonExistentWall") + + def test_add_custom_primary(self): + """Test adding a custom primary.""" + primaries = [[0.7, 0.3], [0.2, 0.7], [0.1, 0.1], [0.3127, 0.329]] + + self.model.add_custom_primary("CustomGamut", primaries) + + self.assertIn("CustomGamut", self.model.project_custom_primaries) + self.assertEqual(self.model.project_custom_primaries["CustomGamut"], primaries) + + def test_add_custom_primary_duplicate(self): + """Test adding a duplicate custom primary raises ValueError.""" + primaries = [[0.7, 0.3], [0.2, 0.7], [0.1, 0.1], [0.3127, 0.329]] + self.model.add_custom_primary("CustomGamut", primaries) + + with self.assertRaises(ValueError): + self.model.add_custom_primary("CustomGamut", primaries) + + +class TestPixMapFrameGamut(unittest.TestCase): + """Tests for PixMapFrame using led_wall_settings for gamut access.""" + + def test_pixmap_frame_uses_led_wall_input_plate_gamut(self): + """Test that PixMapFrame accesses input_plate_gamut from led_wall_settings.""" + from open_vp_cal.widgets.timeline_widget import PixMapFrame + + # Create a wall with specific input_plate_gamut + model = ProjectSettingsModel(led_wall_class=LedWallTimelineLoader) + wall = model.add_led_wall("TestWall") + wall.input_plate_gamut = constants.ColourSpace.CS_ACES + + # Create a PixMapFrame with the wall + frame = PixMapFrame(wall) + + # Verify the frame has access to the led_wall_settings + self.assertEqual(frame._led_wall_settings, wall) + self.assertEqual(frame._led_wall_settings.input_plate_gamut, constants.ColourSpace.CS_ACES) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_open_vp_cal/test_utils.py b/tests/test_open_vp_cal/test_utils.py index b5b58c50..6a7d1b68 100644 --- a/tests/test_open_vp_cal/test_utils.py +++ b/tests/test_open_vp_cal/test_utils.py @@ -28,7 +28,7 @@ from open_vp_cal.core import constants from open_vp_cal.imaging import imaging_utils from open_vp_cal.project_settings import ProjectSettings -from open_vp_cal.led_wall_settings import LedWallSettingsBaseModel +from open_vp_cal.led_wall_settings import LedWallSettings from open_vp_cal.main import run_cli import OpenImageIO as Oiio @@ -144,7 +144,7 @@ def recalc_old_roi(self, roi:List[int]) -> List[List[int]]: list: A list of four tuples representing the corners in the following order: top left, top right, bottom right, bottom left. """ - return LedWallSettingsBaseModel.upgrade_roi(roi) + return LedWallSettings.upgrade_roi(roi) def pre_process_vp_cal_1x(self, project_settings, output_folder, input_colour_space="ACES2065-1"): """ For all unit tests which where created using v1.x we need to double the