Skip to content

Commit fb2608d

Browse files
authored
Feature/optional pin white and black (#118)
* Introduce an optional white and black measure to help getting more accurate results at the extremes Black - with the lens cap on White - with the lens off These are optional samples which can be provided or not * In our log camera generator we insert the black as the first reference and sample In our log camera generator if we have a white sample we use this as our max * Move the verify into the overall validate settings so non matter if we are coming from a zip file or a directory all validations happen during the process function * Start to cleanup the code now we do the same verification on all pathways * Ensure we have file types before we do an iter * Ensure we validate the project_settings before we try and process anything
1 parent f5e408d commit fb2608d

File tree

9 files changed

+4446
-17
lines changed

9 files changed

+4446
-17
lines changed

aces/idt/application.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,34 @@ def _verify_directory(self, root_directory: Path | str) -> None:
241241
else:
242242
self.project_settings.data[DirectoryStructure.GREY_CARD] = []
243243

244+
if self.project_settings.data.get(DirectoryStructure.BLACK, []):
245+
images = [
246+
Path(root_directory) / image
247+
for image in self.project_settings.data.get(
248+
DirectoryStructure.BLACK, []
249+
)
250+
]
251+
for image in images:
252+
attest(image.exists())
253+
254+
self.project_settings.data[DirectoryStructure.BLACK] = images
255+
else:
256+
self.project_settings.data[DirectoryStructure.BLACK] = []
257+
258+
if self.project_settings.data.get(DirectoryStructure.WHITE, []):
259+
images = [
260+
Path(root_directory) / image
261+
for image in self.project_settings.data.get(
262+
DirectoryStructure.WHITE, []
263+
)
264+
]
265+
for image in images:
266+
attest(image.exists())
267+
268+
self.project_settings.data[DirectoryStructure.WHITE] = images
269+
else:
270+
self.project_settings.data[DirectoryStructure.WHITE] = []
271+
244272
def _verify_file_type(self) -> None:
245273
"""
246274
Verify that the *IDT* archive contains a unique file type and set the

aces/idt/core/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class DirectoryStructure:
5151
COLOUR_CHECKER: ClassVar[str] = "colour_checker"
5252
GREY_CARD: ClassVar[str] = "grey_card"
5353
FLATFIELD: ClassVar[str] = "flatfield"
54+
WHITE: ClassVar[str] = "white"
55+
BLACK: ClassVar[str] = "black"
5456

5557

5658
class UITypes:

aces/idt/framework/project_settings.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@ def from_directory(cls, directory: str) -> IDTProjectSettings:
697697
data = {
698698
DirectoryStructure.COLOUR_CHECKER: {},
699699
DirectoryStructure.GREY_CARD: [],
700+
DirectoryStructure.WHITE: [],
701+
DirectoryStructure.BLACK: [],
700702
}
701703

702704
# Validate folder paths for colour_checker and grey_card exist
@@ -707,6 +709,15 @@ def from_directory(cls, directory: str) -> IDTProjectSettings:
707709
directory, DirectoryStructure.DATA, DirectoryStructure.GREY_CARD
708710
)
709711

712+
white_path = os.path.join(
713+
directory, DirectoryStructure.DATA, DirectoryStructure.WHITE
714+
)
715+
716+
black_path = os.path.join(
717+
directory, DirectoryStructure.DATA, DirectoryStructure.BLACK
718+
)
719+
720+
# Check for non optional paths
710721
if not os.path.exists(colour_checker_path) or not os.path.exists(
711722
grey_card_path
712723
):
@@ -740,20 +751,46 @@ def from_directory(cls, directory: str) -> IDTProjectSettings:
740751

741752
data[DirectoryStructure.COLOUR_CHECKER] = sorted_colour_checker
742753

743-
# Populate grey_card data
744-
for root, _, files in os.walk(grey_card_path):
745-
files.sort()
746-
for file in files:
747-
if os.path.basename(file).startswith("."):
748-
continue
749-
absolute_file_path = os.path.join(root, file)
750-
data[DirectoryStructure.GREY_CARD].append(
751-
os.path.relpath(absolute_file_path, start=directory)
752-
)
754+
# Populate grey_card data along with white and black data if we have them
755+
cls.populate_data_from_path(
756+
grey_card_path, data, directory, DirectoryStructure.GREY_CARD
757+
)
758+
cls.populate_data_from_path(
759+
white_path, data, directory, DirectoryStructure.WHITE
760+
)
761+
cls.populate_data_from_path(
762+
black_path, data, directory, DirectoryStructure.BLACK
763+
)
753764

754765
instance.data = data
755766
return instance
756767

768+
@classmethod
769+
def populate_data_from_path(
770+
cls, folder_path: str, data: dict, directory: str, data_key: DirectoryStructure
771+
) -> None:
772+
"""
773+
774+
Parameters
775+
----------
776+
folder_path: The folder path to the sub folder for the given sequence
777+
data: The data dict that holds the paths to the image sequences
778+
directory
779+
The directory to the project root containing the image sequence
780+
directories.
781+
data_key: the key we want to store the data under in the data dict
782+
"""
783+
if os.path.exists(folder_path):
784+
for root, _, files in os.walk(folder_path):
785+
files.sort()
786+
for file in files:
787+
if os.path.basename(file).startswith("."):
788+
continue
789+
absolute_file_path = os.path.join(root, file)
790+
data[data_key].append(
791+
os.path.relpath(absolute_file_path, start=directory)
792+
)
793+
757794
def __str__(self) -> str:
758795
"""
759796
Return a formatted string representation of the project settings.

aces/idt/generators/base_generator.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class IDTBaseGenerator(ABC):
8686
- :attr:`~aces.idt.IDTBaseGenerator.image_grey_card_sampling`
8787
- :attr:`~aces.idt.IDTBaseGenerator.samples_camera`
8888
- :attr:`~aces.idt.IDTBaseGenerator.samples_reference`
89+
- :attr:`~aces.idt.IDTBaseGenerator.samples_white`
90+
- :attr:`~aces.idt.IDTBaseGenerator.samples_black`
8991
- :attr:`~aces.idt.IDTBaseGenerator.LUT_unfiltered`
9092
- :attr:`~aces.idt.IDTBaseGenerator.LUT_filtered`
9193
- :attr:`~aces.idt.IDTBaseGenerator.LUT_decoding`
@@ -117,6 +119,10 @@ def __init__(self, project_settings: IDTProjectSettings) -> None:
117119

118120
self._samples_camera = None
119121
self._samples_reference = None
122+
123+
self._samples_black = None
124+
self._samples_white = None
125+
120126
self._baseline_exposure = 0
121127

122128
self._LUT_unfiltered = None
@@ -201,6 +207,32 @@ def samples_camera(self) -> NDArrayFloat | None:
201207

202208
return self._samples_camera
203209

210+
@property
211+
def samples_black(self) -> NDArrayFloat | None:
212+
"""
213+
Getter property for the samples of the camera for the measured black which
214+
is optional
215+
216+
Returns
217+
-------
218+
:class:`NDArray` or :py:data:`None`
219+
Samples of the camera for the measured black level.
220+
"""
221+
return self._samples_black
222+
223+
@property
224+
def samples_white(self) -> NDArrayFloat | None:
225+
"""
226+
Getter property for the samples of the camera for the measured white which
227+
is optional
228+
229+
Returns
230+
-------
231+
:class:`NDArray` or :py:data:`None`
232+
Samples of the camera for the measured white level.
233+
"""
234+
return self._samples_white
235+
204236
@property
205237
def samples_reference(self) -> NDArrayFloat | None:
206238
"""
@@ -599,6 +631,62 @@ def _reformat_image(image: ArrayLike) -> NDArrayInt | NDArrayFloat:
599631
0,
600632
).tolist()
601633

634+
if self.project_settings.data.get(DirectoryStructure.BLACK, []):
635+
self._samples_analysis[DirectoryStructure.BLACK] = {}
636+
self._samples_analysis[DirectoryStructure.BLACK]["samples_median"] = []
637+
for path in self.project_settings.data[DirectoryStructure.BLACK]:
638+
with working_directory(self.project_settings.working_directory):
639+
LOGGER.info(
640+
'Reading "Black" from "%s"...',
641+
path,
642+
)
643+
644+
image = _reformat_image(read_image(path))
645+
self._samples_analysis[DirectoryStructure.BLACK][
646+
"samples_median"
647+
].append(
648+
np.median(
649+
as_float_array(image),
650+
(0, 1),
651+
).tolist()
652+
)
653+
self._samples_black = np.median(
654+
as_float_array(
655+
self._samples_analysis[DirectoryStructure.BLACK][
656+
"samples_median"
657+
]
658+
),
659+
axis=0,
660+
).tolist()
661+
662+
if self.project_settings.data.get(DirectoryStructure.WHITE, []):
663+
self._samples_analysis[DirectoryStructure.WHITE] = {}
664+
self._samples_analysis[DirectoryStructure.WHITE]["samples_median"] = []
665+
for path in self.project_settings.data[DirectoryStructure.WHITE]:
666+
with working_directory(self.project_settings.working_directory):
667+
LOGGER.info(
668+
'Reading "WHITE" from "%s"...',
669+
path,
670+
)
671+
672+
image = _reformat_image(read_image(path))
673+
self._samples_analysis[DirectoryStructure.WHITE][
674+
"samples_median"
675+
].append(
676+
np.median(
677+
as_float_array(image),
678+
(0, 1),
679+
).tolist()
680+
)
681+
self._samples_white = np.median(
682+
as_float_array(
683+
self._samples_analysis[DirectoryStructure.WHITE][
684+
"samples_median"
685+
]
686+
),
687+
axis=0,
688+
).tolist()
689+
602690
if self.project_settings.cleanup:
603691
shutil.rmtree(self.project_settings.working_directory)
604692

aces/idt/generators/log_camera.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,13 @@ def generate_LUT(self) -> LUT3x1D:
206206
LOGGER.info('Generating unfiltered "LUT3x1D" with "%s" size...', size)
207207

208208
self._LUT_unfiltered = LUT3x1D(size=size, name="LUT - Unfiltered")
209-
209+
if self.samples_black:
210+
self._samples_camera = np.insert(
211+
self._samples_camera, 0, self.samples_black, axis=0
212+
)
213+
self._samples_reference = np.insert(
214+
self._samples_reference, 0, np.zeros(3), axis=0
215+
)
210216
for i in range(3):
211217
x = self._samples_camera[..., i] * (size - 1)
212218
y = self._samples_reference[..., i]
@@ -218,12 +224,14 @@ def generate_LUT(self) -> LUT3x1D:
218224
LinearInterpolator(x, y), method="Constant"
219225
)(samples)
220226

227+
max_value = np.max(self._samples_camera)
228+
if self.samples_white:
229+
max_value = self.samples_white[i]
230+
221231
# Searching for the index of ~middle camera code value * 125%
222232
# We are trying to find the logarithmic slope of the camera middle
223233
# range.
224-
index_middle = np.searchsorted(
225-
samples / size, np.max(self._samples_camera) / 2 * 1.25
226-
)
234+
index_middle = np.searchsorted(samples / size, max_value / 2 * 1.25)
227235
padding = index_middle // 2
228236
samples_middle = np.log(np.copy(samples_linear))
229237
samples_middle[: index_middle - padding] = samples_middle[
@@ -242,7 +250,7 @@ def generate_LUT(self) -> LUT3x1D:
242250
# Preparing the mask to blend the logarithmic slope with the
243251
# extrapolated data.
244252
edge_left = index_middle - padding
245-
edge_right = np.searchsorted(samples / size, np.max(self._samples_camera))
253+
edge_right = np.searchsorted(samples / size, max_value)
246254
mask_samples = smoothstep_function(
247255
samples, edge_left, edge_right, clip=True
248256
)

tests/resources/download_manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@
33
"URL": "https://www.dropbox.com/scl/fi/srna8mw98wxn47np0adoj/FULL_STOPS_EXR.zip?rlkey=e2n0t59g0olu40gxk31mm01sh&dl=0",
44
"SHA256": "7f4bb72394e502815e85ef4a45c4a1c027d111e6a54b058b9d4d75e5b76e1127"
55
},
6+
"FULL_STOPS.zip": {
7+
"URL": "",
8+
"SHA256": "9a8f53d0ee191b040e1270d5e170bab6ad0b6002ee5d42fc7d71b97e81ff5710"
9+
},
610
"PTZ_160.zip": {
711
"URL": "https://www.dropbox.com/scl/fi/9ni9vcpwahrx8s3l1zqkf/PTZ_160.zip?rlkey=m3ddjysrhmkbfxx7ktgpmgfft&dl=0",
812
"SHA256": "4c3c85a293166ef4896713fc99f28870b90821c1f1d0118c6ceae3b40520d4c2"
13+
},
14+
"DPX_10_log.zip": {
15+
"URL": "",
16+
"SHA256": "7eef2787b5e4571810b508bc14d5673637e0507f638df8242d49830714dab9fa"
917
}
1018
}

tests/resources/example_from_folder.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
"data/grey_card/grey_card_0001.tif",
7676
"data/grey_card/grey_card_0002.tif",
7777
"data/grey_card/grey_card_0003.tif"
78-
]
78+
],
79+
"white": [],
80+
"black": []
7981
}
8082
}

tests/resources/synthetic_001/test_project.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
"data/grey_card/grey_card_0001.tif",
7676
"data/grey_card/grey_card_0002.tif",
7777
"data/grey_card/grey_card_0003.tif"
78-
]
78+
],
79+
"white": [],
80+
"black": []
7981
}
8082
}

0 commit comments

Comments
 (0)