Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions aces/idt/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
SDS_COLOURCHECKERS,
SDS_ILLUMINANTS,
SpectralDistribution,
sd_blackbody,
sd_to_aces_relative_exposure_values,
)
from colour.algebra import euclidean_distance, vecmul
Expand All @@ -41,6 +42,8 @@
optimisation_factory_rawtoaces_v1,
whitepoint_preserving_matrix,
)
from colour.colorimetry import sd_CIE_illuminant_D_series
from colour.temperature import CCT_to_xy_CIE_D

if typing.TYPE_CHECKING:
from colour.hints import (
Expand Down Expand Up @@ -120,21 +123,68 @@ def get_sds_colour_checker(colour_checker_name: str) -> Tuple[SpectralDistributi
return tuple(SDS_COLOURCHECKERS[colour_checker_name].values())


def get_sds_illuminant(illuminant_name: str) -> SpectralDistribution:
def get_sds_illuminant(
illuminant_name: str, temperature: float | None = None
) -> SpectralDistribution:
"""
Return the *ACES* reference illuminant spectral distribution, for the given
illuminant name.

Parameters
----------
illuminant_name
Name of the illuminant.
Name of the illuminant. Can be a standard illuminant name from
SDS_ILLUMINANTS or a special type like "Blackbody", "Custom", or "Daylight".
temperature
Temperature in Kelvin for Blackbody or Daylight illuminants. Required when
illuminant_name is "Blackbody" or "Daylight". Default is *None*.

Returns
-------
:class:`SpectralDistribution`
Illuminant spectral distribution.

Raises
------
ValueError
If illuminant_name is "Blackbody" or "Daylight" and temperature is not provided.
If illuminant_name is "Custom" (not yet implemented).

Notes
-----
- For "Blackbody" illuminants, the `sd_blackbody` function is used to
generate a spectral distribution at the specified temperature using
Planck's law.
- For "Daylight" illuminants, the CIE D Series illuminant is generated
using the specified correlated colour temperature (CCT). A correction
factor of 1.4388/1.4380 is applied to the CCT as per CIE 015:2004
recommendation to account for the difference between historical and
modern Planck's constant values. This ensures generated illuminants
match internationally agreed CIE D Series xy chromaticity coordinates.
- "Custom" illuminants are not yet implemented and will raise a ValueError.

References
----------
:cite:`CIETC1-482004` - CIE 015:2004: Colorimetry, 3rd Edition
"""

if illuminant_name == "Blackbody":
if temperature is None:
msg = 'Temperature parameter is required for "Blackbody" illuminant.'
raise ValueError(msg)
return sd_blackbody(temperature)

if illuminant_name == "Daylight":
if temperature is None:
msg = 'Temperature parameter is required for "Daylight" illuminant.'
raise ValueError(msg)
# Apply the correction factor as per CIE 015:2004 recommendation
CCT_corrected = temperature * 1.4388 / 1.4380
xy = CCT_to_xy_CIE_D(CCT_corrected)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KelSolaar thomas sir would be great if you can double check for me this is the right way to handle black body and daylight options.

return sd_CIE_illuminant_D_series(xy)
if illuminant_name == "Custom":
msg = '"Custom" illuminant type is not yet implemented.'
raise ValueError(msg)
return SDS_ILLUMINANTS[illuminant_name]


Expand Down
10 changes: 10 additions & 0 deletions aces/idt/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,15 @@ class ProjectSettingsMetadataConstants:
ui_category=UICategories.STANDARD,
)

ILLUMINANT_CUSTOM_TEMPERATURE = Metadata(
name="illuminant_custom_temperature",
default_value=6500,
description="The temperature in Kelvin for Blackbody or Daylight illuminants",
display_name="Illuminant Temperature (K)",
ui_type=UITypes.INT_FIELD,
ui_category=UICategories.ADVANCED,
)

ADDITIONAL_CAMERA_SETTINGS = Metadata(
name="additional_camera_settings",
default_value="",
Expand Down Expand Up @@ -488,6 +497,7 @@ class ProjectSettingsMetadataConstants:
ACES_USER_NAME,
ISO,
TEMPERATURE,
ILLUMINANT_CUSTOM_TEMPERATURE,
ADDITIONAL_CAMERA_SETTINGS,
LIGHTING_SETUP_DESCRIPTION,
DEBAYERING_PLATFORM,
Expand Down
37 changes: 36 additions & 1 deletion aces/idt/framework/project_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def __init__(self, **kwargs: Dict) -> None:
IDTProjectSettings.temperature.metadata.name,
IDTProjectSettings.temperature.metadata.default_value,
)
self._illuminant_custom_temperature = kwargs.get(
IDTProjectSettings.illuminant_custom_temperature.metadata.name,
IDTProjectSettings.illuminant_custom_temperature.metadata.default_value,
)
self._additional_camera_settings = kwargs.get(
IDTProjectSettings.additional_camera_settings.metadata.name,
IDTProjectSettings.additional_camera_settings.metadata.default_value,
Expand Down Expand Up @@ -291,6 +295,30 @@ def temperature(self) -> int:

return self._temperature

@metadata_property(metadata=MetadataConstants.ILLUMINANT_CUSTOM_TEMPERATURE)
def illuminant_custom_temperature(self) -> int:
"""
Getter property for the illuminant custom temperature in Kelvin degrees.

This property is used when the illuminant is set to "Blackbody" or
"Daylight". It specifies the correlated colour temperature (CCT) in
Kelvin for generating the spectral distribution.

Returns
-------
:class:`int`
Illuminant custom temperature in Kelvin degrees.

Notes
-----
- For Blackbody illuminants, this temperature is used directly with
Planck's law to generate the spectral power distribution.
- For Daylight illuminants, this temperature is used to generate a
CIE D Series illuminant at the specified CCT.
"""

return self._illuminant_custom_temperature

@metadata_property(metadata=MetadataConstants.ADDITIONAL_CAMERA_SETTINGS)
def additional_camera_settings(self) -> str:
"""
Expand Down Expand Up @@ -638,9 +666,16 @@ def get_reference_colour_checker_samples(self) -> NDArrayFloat:
Reference colour checker samples.
"""

# Pass illuminant_custom_temperature if illuminant is "Blackbody" or "Daylight"
temperature = (
self.illuminant_custom_temperature
if self.illuminant in ("Blackbody", "Daylight")
else None
)

return generate_reference_colour_checker(
get_sds_colour_checker(self.reference_colour_checker),
get_sds_illuminant(self.illuminant),
get_sds_illuminant(self.illuminant, temperature=temperature),
)

def get_optimization_factory(self) -> callable:
Expand Down
18 changes: 15 additions & 3 deletions apps/idt_calculator_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ def toggle_modal(n_clicks, is_open):
State(_uid("encoding-transfer-function-field"), "value"),
State(_uid("rgb-display-colourspace-select"), "value"),
State(_uid("illuminant-select"), "value"),
State(_uid("cct-field"), "value"),
State(_uid("illuminant-datatable"), "data"),
State(_uid("chromatic-adaptation-transform-select"), "value"),
State(_uid("optimisation-space-select"), "value"),
Expand Down Expand Up @@ -1170,6 +1171,7 @@ def compute_idt_camera(
encoding_transfer_function,
RGB_display_colourspace,
illuminant_name,
illuminant_custom_temperature,
illuminant_data,
chromatic_adaptation_transform,
optimisation_space,
Expand Down Expand Up @@ -1217,6 +1219,9 @@ def compute_idt_camera(
*RGB* display colourspace.
illuminant_name : str
Name of the illuminant.
illuminant_custom_temperature : float
Custom correlated colour temperature (CCT) in Kelvin for Blackbody
or Daylight illuminants.
illuminant_data : list
List of wavelength dicts of illuminant data.
chromatic_adaptation_transform : str
Expand Down Expand Up @@ -1249,6 +1254,7 @@ def compute_idt_camera(
'Computing "IDT" with "%s" using parameters:\n'
'\tRGB Display Colourspace : "%s"\n'
'\tIlluminant Name : "%s"\n'
'\tIlluminant Custom Temperature : "%s"\n'
'\tIlluminant Data : "%s"\n'
'\tChromatic Adaptation Transform : "%s"\n'
'\tOptimisation Space : "%s"\n'
Expand All @@ -1261,6 +1267,7 @@ def compute_idt_camera(
generator_name,
RGB_display_colourspace,
illuminant_name,
illuminant_custom_temperature,
illuminant_data,
chromatic_adaptation_transform,
optimisation_space,
Expand Down Expand Up @@ -1359,6 +1366,7 @@ def compute_idt_camera(
encoding_colourspace=encoding_colourspace,
encoding_transfer_function=encoding_transfer_function,
illuminant=illuminant_name,
illuminant_custom_temperature=float(illuminant_custom_temperature),
)
_IDT_GENERATOR_APPLICATION = IDTGeneratorApplication(
generator_name, project_settings
Expand All @@ -1368,12 +1376,16 @@ def compute_idt_camera(
_HASH_IDT_ARCHIVE = hash_file(_PATH_UPLOADED_IDT_ARCHIVE)
LOGGER.debug('"Archive hash: "%s"', _HASH_IDT_ARCHIVE)

if _CACHE_DATA_ARCHIVE_TO_SAMPLES.get(_HASH_IDT_ARCHIVE) is None:
# Include illuminant and temperature in cache key since they affect
# reference samples
cache_key = f"{_HASH_IDT_ARCHIVE}_{illuminant_name}_{illuminant_custom_temperature}"

if _CACHE_DATA_ARCHIVE_TO_SAMPLES.get(cache_key) is None:
_IDT_GENERATOR_APPLICATION.extract(_PATH_UPLOADED_IDT_ARCHIVE)
os.remove(_PATH_UPLOADED_IDT_ARCHIVE)
_IDT_GENERATOR_APPLICATION.validate_project_settings()
_IDT_GENERATOR_APPLICATION.generator.sample()
_CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] = (
_CACHE_DATA_ARCHIVE_TO_SAMPLES[cache_key] = (
_IDT_GENERATOR_APPLICATION.project_settings.data,
_IDT_GENERATOR_APPLICATION.generator.samples_analysis,
_IDT_GENERATOR_APPLICATION.generator.baseline_exposure,
Expand All @@ -1383,7 +1395,7 @@ def compute_idt_camera(
_IDT_GENERATOR_APPLICATION.project_settings.data,
_IDT_GENERATOR_APPLICATION.generator._samples_analysis, # noqa: SLF001
_IDT_GENERATOR_APPLICATION.generator._baseline_exposure, # noqa: SLF001
) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE]
) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[cache_key]

generator = _IDT_GENERATOR_APPLICATION.generator
project_settings = _IDT_GENERATOR_APPLICATION.project_settings
Expand Down
1 change: 1 addition & 0 deletions tests/resources/example_from_folder.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"cleanup": true,
"reference_colour_checker": "ISO 17321-1",
"illuminant": "D60",
"illuminant_custom_temperature": 6500,
"file_type": "",
"ev_weights": [],
"optimization_kwargs": {}
Expand Down
1 change: 1 addition & 0 deletions tests/resources/synthetic_001/test_project.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"camera_model": "",
"iso": 800,
"temperature": 6000,
"illuminant_custom_temperature": 6500,
"additional_camera_settings": "",
"lighting_setup_description": "",
"debayering_platform": "",
Expand Down
1 change: 1 addition & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def test__str__(self) -> None:
Flatten_Clf : False
Grey_Card_Reference : [ 0.18 0.18 0.18]
Illuminant : D60
Illuminant_Custom_Temperature : 6500
Illuminant_Interpolator : Linear
Include_Exposure_Factor_In_Clf : False
Include_White_Balance_In_Clf : False
Expand Down
86 changes: 86 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
find_clipped_exposures,
find_similar_rows,
generate_reference_colour_checker,
get_sds_illuminant,
)
from tests.test_utils import TestIDTBase

Expand All @@ -28,6 +29,7 @@
"TestCalculateCameraNpmAndPrimariesWp",
"TestFindSimilarRows",
"TestFindClippedExposures",
"TestGetSdsIlluminant",
]


Expand Down Expand Up @@ -360,3 +362,87 @@ def test_scenario_6(self) -> None:
)
expected_ev_keys = [-6.0, -5.0, -4.0, 4.0, 5.0, 6.0]
self.assertEqual(removed_evs, expected_ev_keys)


class TestGetSdsIlluminant:
"""
Define :func:`aces.idt.core.common.get_sds_illuminant` definition unit tests
methods.
"""

def test_get_sds_illuminant_valid_name(self) -> None:
"""
Test :func:`aces.idt.core.common.get_sds_illuminant` with a valid
illuminant name.
"""

# Test with D60, the default ACES illuminant
illuminant = get_sds_illuminant("D60")
assert illuminant is not None
assert hasattr(illuminant, "wavelengths")
assert hasattr(illuminant, "values")

def test_get_sds_illuminant_blackbody_without_temperature(self) -> None:
"""
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Blackbody'
illuminant name without providing a temperature parameter.

This should raise a ValueError since temperature is required for
Blackbody illuminants.
"""

import pytest

with pytest.raises(ValueError, match="Temperature parameter is required"):
get_sds_illuminant("Blackbody")

def test_get_sds_illuminant_blackbody_with_temperature(self) -> None:
"""
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Blackbody'
illuminant name and a valid temperature parameter.

This test verifies that the Blackbody illuminant functionality works
correctly when a temperature is provided.
"""

# Test with common blackbody temperatures
for temperature in [5500, 6500, 3200]:
illuminant = get_sds_illuminant("Blackbody", temperature=temperature)
assert illuminant is not None
assert hasattr(illuminant, "wavelengths")
assert hasattr(illuminant, "values")
# Verify the illuminant name includes the temperature
assert str(temperature) in illuminant.name

def test_get_sds_illuminant_daylight_without_temperature(self) -> None:
"""
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Daylight'
illuminant name without providing a temperature parameter.

This should raise a ValueError since temperature is required for
Daylight illuminants.
"""

import pytest

with pytest.raises(ValueError, match="Temperature parameter is required"):
get_sds_illuminant("Daylight")

def test_get_sds_illuminant_daylight_with_temperature(self) -> None:
"""
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Daylight'
illuminant name and a valid temperature parameter.

This test verifies that the Daylight (CIE D Series) illuminant
functionality works correctly when a temperature is provided.
"""

# Test with common daylight temperatures
for temperature in [5500, 6500, 7500]:
illuminant = get_sds_illuminant("Daylight", temperature=temperature)
assert illuminant is not None
assert hasattr(illuminant, "wavelengths")
assert hasattr(illuminant, "values")
# Daylight illuminants should have spectral data
assert len(illuminant.wavelengths) > 0
assert len(illuminant.values) > 0