Skip to content

Commit 8c958ed

Browse files
committed
Fix a bug and expose functionality where the ui exposes blackbody and daylight with custom temperatures
These need to be passed through and handled in the backend to ensure we get the right illuminant We also have to update the cache_Key to invalidate the cache if the lluminant changes other wise we run with the wrong settings
1 parent 2425baf commit 8c958ed

File tree

8 files changed

+202
-6
lines changed

8 files changed

+202
-6
lines changed

aces/idt/core/common.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
SDS_COLOURCHECKERS,
3434
SDS_ILLUMINANTS,
3535
SpectralDistribution,
36+
sd_blackbody,
3637
sd_to_aces_relative_exposure_values,
3738
)
3839
from colour.algebra import euclidean_distance, vecmul
@@ -41,6 +42,8 @@
4142
optimisation_factory_rawtoaces_v1,
4243
whitepoint_preserving_matrix,
4344
)
45+
from colour.colorimetry import sd_CIE_illuminant_D_series
46+
from colour.temperature import CCT_to_xy_CIE_D
4447

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

122125

123-
def get_sds_illuminant(illuminant_name: str) -> SpectralDistribution:
126+
def get_sds_illuminant(
127+
illuminant_name: str, temperature: float | None = None
128+
) -> SpectralDistribution:
124129
"""
125130
Return the *ACES* reference illuminant spectral distribution, for the given
126131
illuminant name.
127132
128133
Parameters
129134
----------
130135
illuminant_name
131-
Name of the illuminant.
136+
Name of the illuminant. Can be a standard illuminant name from
137+
SDS_ILLUMINANTS or a special type like "Blackbody", "Custom", or "Daylight".
138+
temperature
139+
Temperature in Kelvin for Blackbody or Daylight illuminants. Required when
140+
illuminant_name is "Blackbody" or "Daylight". Default is *None*.
132141
133142
Returns
134143
-------
135144
:class:`SpectralDistribution`
145+
Illuminant spectral distribution.
146+
147+
Raises
148+
------
149+
ValueError
150+
If illuminant_name is "Blackbody" or "Daylight" and temperature is not provided.
151+
If illuminant_name is "Custom" (not yet implemented).
152+
153+
Notes
154+
-----
155+
- For "Blackbody" illuminants, the `sd_blackbody` function is used to
156+
generate a spectral distribution at the specified temperature using
157+
Planck's law.
158+
- For "Daylight" illuminants, the CIE D Series illuminant is generated
159+
using the specified correlated colour temperature (CCT). A correction
160+
factor of 1.4388/1.4380 is applied to the CCT as per CIE 015:2004
161+
recommendation to account for the difference between historical and
162+
modern Planck's constant values. This ensures generated illuminants
163+
match internationally agreed CIE D Series xy chromaticity coordinates.
164+
- "Custom" illuminants are not yet implemented and will raise a ValueError.
165+
166+
References
167+
----------
168+
:cite:`CIETC1-482004` - CIE 015:2004: Colorimetry, 3rd Edition
136169
"""
137170

171+
if illuminant_name == "Blackbody":
172+
if temperature is None:
173+
msg = 'Temperature parameter is required for "Blackbody" illuminant.'
174+
raise ValueError(msg)
175+
return sd_blackbody(temperature)
176+
177+
if illuminant_name == "Daylight":
178+
if temperature is None:
179+
msg = 'Temperature parameter is required for "Daylight" illuminant.'
180+
raise ValueError(msg)
181+
# Apply the correction factor as per CIE 015:2004 recommendation
182+
CCT_corrected = temperature * 1.4388 / 1.4380
183+
xy = CCT_to_xy_CIE_D(CCT_corrected)
184+
return sd_CIE_illuminant_D_series(xy)
185+
if illuminant_name == "Custom":
186+
msg = '"Custom" illuminant type is not yet implemented.'
187+
raise ValueError(msg)
138188
return SDS_ILLUMINANTS[illuminant_name]
139189

140190

aces/idt/core/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,15 @@ class ProjectSettingsMetadataConstants:
312312
ui_category=UICategories.STANDARD,
313313
)
314314

315+
ILLUMINANT_CUSTOM_TEMPERATURE = Metadata(
316+
name="illuminant_custom_temperature",
317+
default_value=6500,
318+
description="The temperature in Kelvin for Blackbody or Daylight illuminants",
319+
display_name="Illuminant Temperature (K)",
320+
ui_type=UITypes.INT_FIELD,
321+
ui_category=UICategories.ADVANCED,
322+
)
323+
315324
ADDITIONAL_CAMERA_SETTINGS = Metadata(
316325
name="additional_camera_settings",
317326
default_value="",
@@ -488,6 +497,7 @@ class ProjectSettingsMetadataConstants:
488497
ACES_USER_NAME,
489498
ISO,
490499
TEMPERATURE,
500+
ILLUMINANT_CUSTOM_TEMPERATURE,
491501
ADDITIONAL_CAMERA_SETTINGS,
492502
LIGHTING_SETUP_DESCRIPTION,
493503
DEBAYERING_PLATFORM,

aces/idt/framework/project_settings.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def __init__(self, **kwargs: Dict) -> None:
8585
IDTProjectSettings.temperature.metadata.name,
8686
IDTProjectSettings.temperature.metadata.default_value,
8787
)
88+
self._illuminant_custom_temperature = kwargs.get(
89+
IDTProjectSettings.illuminant_custom_temperature.metadata.name,
90+
IDTProjectSettings.illuminant_custom_temperature.metadata.default_value,
91+
)
8892
self._additional_camera_settings = kwargs.get(
8993
IDTProjectSettings.additional_camera_settings.metadata.name,
9094
IDTProjectSettings.additional_camera_settings.metadata.default_value,
@@ -291,6 +295,30 @@ def temperature(self) -> int:
291295

292296
return self._temperature
293297

298+
@metadata_property(metadata=MetadataConstants.ILLUMINANT_CUSTOM_TEMPERATURE)
299+
def illuminant_custom_temperature(self) -> int:
300+
"""
301+
Getter property for the illuminant custom temperature in Kelvin degrees.
302+
303+
This property is used when the illuminant is set to "Blackbody" or
304+
"Daylight". It specifies the correlated colour temperature (CCT) in
305+
Kelvin for generating the spectral distribution.
306+
307+
Returns
308+
-------
309+
:class:`int`
310+
Illuminant custom temperature in Kelvin degrees.
311+
312+
Notes
313+
-----
314+
- For Blackbody illuminants, this temperature is used directly with
315+
Planck's law to generate the spectral power distribution.
316+
- For Daylight illuminants, this temperature is used to generate a
317+
CIE D Series illuminant at the specified CCT.
318+
"""
319+
320+
return self._illuminant_custom_temperature
321+
294322
@metadata_property(metadata=MetadataConstants.ADDITIONAL_CAMERA_SETTINGS)
295323
def additional_camera_settings(self) -> str:
296324
"""
@@ -638,9 +666,16 @@ def get_reference_colour_checker_samples(self) -> NDArrayFloat:
638666
Reference colour checker samples.
639667
"""
640668

669+
# Pass illuminant_custom_temperature if illuminant is "Blackbody" or "Daylight"
670+
temperature = (
671+
self.illuminant_custom_temperature
672+
if self.illuminant in ("Blackbody", "Daylight")
673+
else None
674+
)
675+
641676
return generate_reference_colour_checker(
642677
get_sds_colour_checker(self.reference_colour_checker),
643-
get_sds_illuminant(self.illuminant),
678+
get_sds_illuminant(self.illuminant, temperature=temperature),
644679
)
645680

646681
def get_optimization_factory(self) -> callable:

apps/idt_calculator_camera.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,7 @@ def toggle_modal(n_clicks, is_open):
11411141
State(_uid("encoding-transfer-function-field"), "value"),
11421142
State(_uid("rgb-display-colourspace-select"), "value"),
11431143
State(_uid("illuminant-select"), "value"),
1144+
State(_uid("cct-field"), "value"),
11441145
State(_uid("illuminant-datatable"), "data"),
11451146
State(_uid("chromatic-adaptation-transform-select"), "value"),
11461147
State(_uid("optimisation-space-select"), "value"),
@@ -1170,6 +1171,7 @@ def compute_idt_camera(
11701171
encoding_transfer_function,
11711172
RGB_display_colourspace,
11721173
illuminant_name,
1174+
illuminant_custom_temperature,
11731175
illuminant_data,
11741176
chromatic_adaptation_transform,
11751177
optimisation_space,
@@ -1217,6 +1219,9 @@ def compute_idt_camera(
12171219
*RGB* display colourspace.
12181220
illuminant_name : str
12191221
Name of the illuminant.
1222+
illuminant_custom_temperature : float
1223+
Custom correlated colour temperature (CCT) in Kelvin for Blackbody
1224+
or Daylight illuminants.
12201225
illuminant_data : list
12211226
List of wavelength dicts of illuminant data.
12221227
chromatic_adaptation_transform : str
@@ -1249,6 +1254,7 @@ def compute_idt_camera(
12491254
'Computing "IDT" with "%s" using parameters:\n'
12501255
'\tRGB Display Colourspace : "%s"\n'
12511256
'\tIlluminant Name : "%s"\n'
1257+
'\tIlluminant Custom Temperature : "%s"\n'
12521258
'\tIlluminant Data : "%s"\n'
12531259
'\tChromatic Adaptation Transform : "%s"\n'
12541260
'\tOptimisation Space : "%s"\n'
@@ -1261,6 +1267,7 @@ def compute_idt_camera(
12611267
generator_name,
12621268
RGB_display_colourspace,
12631269
illuminant_name,
1270+
illuminant_custom_temperature,
12641271
illuminant_data,
12651272
chromatic_adaptation_transform,
12661273
optimisation_space,
@@ -1359,6 +1366,7 @@ def compute_idt_camera(
13591366
encoding_colourspace=encoding_colourspace,
13601367
encoding_transfer_function=encoding_transfer_function,
13611368
illuminant=illuminant_name,
1369+
illuminant_custom_temperature=float(illuminant_custom_temperature),
13621370
)
13631371
_IDT_GENERATOR_APPLICATION = IDTGeneratorApplication(
13641372
generator_name, project_settings
@@ -1368,12 +1376,16 @@ def compute_idt_camera(
13681376
_HASH_IDT_ARCHIVE = hash_file(_PATH_UPLOADED_IDT_ARCHIVE)
13691377
LOGGER.debug('"Archive hash: "%s"', _HASH_IDT_ARCHIVE)
13701378

1371-
if _CACHE_DATA_ARCHIVE_TO_SAMPLES.get(_HASH_IDT_ARCHIVE) is None:
1379+
# Include illuminant and temperature in cache key since they affect
1380+
# reference samples
1381+
cache_key = f"{_HASH_IDT_ARCHIVE}_{illuminant_name}_{illuminant_custom_temperature}"
1382+
1383+
if _CACHE_DATA_ARCHIVE_TO_SAMPLES.get(cache_key) is None:
13721384
_IDT_GENERATOR_APPLICATION.extract(_PATH_UPLOADED_IDT_ARCHIVE)
13731385
os.remove(_PATH_UPLOADED_IDT_ARCHIVE)
13741386
_IDT_GENERATOR_APPLICATION.validate_project_settings()
13751387
_IDT_GENERATOR_APPLICATION.generator.sample()
1376-
_CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE] = (
1388+
_CACHE_DATA_ARCHIVE_TO_SAMPLES[cache_key] = (
13771389
_IDT_GENERATOR_APPLICATION.project_settings.data,
13781390
_IDT_GENERATOR_APPLICATION.generator.samples_analysis,
13791391
_IDT_GENERATOR_APPLICATION.generator.baseline_exposure,
@@ -1383,7 +1395,7 @@ def compute_idt_camera(
13831395
_IDT_GENERATOR_APPLICATION.project_settings.data,
13841396
_IDT_GENERATOR_APPLICATION.generator._samples_analysis, # noqa: SLF001
13851397
_IDT_GENERATOR_APPLICATION.generator._baseline_exposure, # noqa: SLF001
1386-
) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[_HASH_IDT_ARCHIVE]
1398+
) = _CACHE_DATA_ARCHIVE_TO_SAMPLES[cache_key]
13871399

13881400
generator = _IDT_GENERATOR_APPLICATION.generator
13891401
project_settings = _IDT_GENERATOR_APPLICATION.project_settings

tests/resources/example_from_folder.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"cleanup": true,
3030
"reference_colour_checker": "ISO 17321-1",
3131
"illuminant": "D60",
32+
"illuminant_custom_temperature": 6500,
3233
"file_type": "",
3334
"ev_weights": [],
3435
"optimization_kwargs": {}

tests/resources/synthetic_001/test_project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"camera_model": "",
88
"iso": 800,
99
"temperature": 6000,
10+
"illuminant_custom_temperature": 6500,
1011
"additional_camera_settings": "",
1112
"lighting_setup_description": "",
1213
"debayering_platform": "",

tests/test_application.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def test__str__(self) -> None:
102102
Flatten_Clf : False
103103
Grey_Card_Reference : [ 0.18 0.18 0.18]
104104
Illuminant : D60
105+
Illuminant_Custom_Temperature : 6500
105106
Illuminant_Interpolator : Linear
106107
Include_Exposure_Factor_In_Clf : False
107108
Include_White_Balance_In_Clf : False

tests/test_common.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
find_clipped_exposures,
1414
find_similar_rows,
1515
generate_reference_colour_checker,
16+
get_sds_illuminant,
1617
)
1718
from tests.test_utils import TestIDTBase
1819

@@ -28,6 +29,7 @@
2829
"TestCalculateCameraNpmAndPrimariesWp",
2930
"TestFindSimilarRows",
3031
"TestFindClippedExposures",
32+
"TestGetSdsIlluminant",
3133
]
3234

3335

@@ -360,3 +362,87 @@ def test_scenario_6(self) -> None:
360362
)
361363
expected_ev_keys = [-6.0, -5.0, -4.0, 4.0, 5.0, 6.0]
362364
self.assertEqual(removed_evs, expected_ev_keys)
365+
366+
367+
class TestGetSdsIlluminant:
368+
"""
369+
Define :func:`aces.idt.core.common.get_sds_illuminant` definition unit tests
370+
methods.
371+
"""
372+
373+
def test_get_sds_illuminant_valid_name(self) -> None:
374+
"""
375+
Test :func:`aces.idt.core.common.get_sds_illuminant` with a valid
376+
illuminant name.
377+
"""
378+
379+
# Test with D60, the default ACES illuminant
380+
illuminant = get_sds_illuminant("D60")
381+
assert illuminant is not None
382+
assert hasattr(illuminant, "wavelengths")
383+
assert hasattr(illuminant, "values")
384+
385+
def test_get_sds_illuminant_blackbody_without_temperature(self) -> None:
386+
"""
387+
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Blackbody'
388+
illuminant name without providing a temperature parameter.
389+
390+
This should raise a ValueError since temperature is required for
391+
Blackbody illuminants.
392+
"""
393+
394+
import pytest
395+
396+
with pytest.raises(ValueError, match="Temperature parameter is required"):
397+
get_sds_illuminant("Blackbody")
398+
399+
def test_get_sds_illuminant_blackbody_with_temperature(self) -> None:
400+
"""
401+
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Blackbody'
402+
illuminant name and a valid temperature parameter.
403+
404+
This test verifies that the Blackbody illuminant functionality works
405+
correctly when a temperature is provided.
406+
"""
407+
408+
# Test with common blackbody temperatures
409+
for temperature in [5500, 6500, 3200]:
410+
illuminant = get_sds_illuminant("Blackbody", temperature=temperature)
411+
assert illuminant is not None
412+
assert hasattr(illuminant, "wavelengths")
413+
assert hasattr(illuminant, "values")
414+
# Verify the illuminant name includes the temperature
415+
assert str(temperature) in illuminant.name
416+
417+
def test_get_sds_illuminant_daylight_without_temperature(self) -> None:
418+
"""
419+
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Daylight'
420+
illuminant name without providing a temperature parameter.
421+
422+
This should raise a ValueError since temperature is required for
423+
Daylight illuminants.
424+
"""
425+
426+
import pytest
427+
428+
with pytest.raises(ValueError, match="Temperature parameter is required"):
429+
get_sds_illuminant("Daylight")
430+
431+
def test_get_sds_illuminant_daylight_with_temperature(self) -> None:
432+
"""
433+
Test :func:`aces.idt.core.common.get_sds_illuminant` with 'Daylight'
434+
illuminant name and a valid temperature parameter.
435+
436+
This test verifies that the Daylight (CIE D Series) illuminant
437+
functionality works correctly when a temperature is provided.
438+
"""
439+
440+
# Test with common daylight temperatures
441+
for temperature in [5500, 6500, 7500]:
442+
illuminant = get_sds_illuminant("Daylight", temperature=temperature)
443+
assert illuminant is not None
444+
assert hasattr(illuminant, "wavelengths")
445+
assert hasattr(illuminant, "values")
446+
# Daylight illuminants should have spectral data
447+
assert len(illuminant.wavelengths) > 0
448+
assert len(illuminant.values) > 0

0 commit comments

Comments
 (0)