Skip to content

Commit

Permalink
Merge branch 'feature/v0.2.6' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
KelSolaar committed Dec 25, 2024
2 parents dc02758 + 8af4fe3 commit 79caf87
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 41 deletions.
34 changes: 23 additions & 11 deletions colour_hdri/generation/hdri.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from __future__ import annotations

import gc
import typing

import numpy as np
Expand All @@ -30,7 +31,7 @@
if typing.TYPE_CHECKING:
from colour.hints import ArrayLike, Callable, NDArrayFloat

from colour.utilities import as_float_array, attest, tsplit, tstack, warning
from colour.utilities import as_float_array, attest, tsplit, tstack, warning, zeros

from colour_hdri.exposure import average_luminance
from colour_hdri.generation import weighting_function_Debevec1997
Expand Down Expand Up @@ -96,10 +97,17 @@ def image_stack_to_HDRI(

attest(image_stack.is_valid(), "Image stack is invalid!")

image_c = np.zeros(image_stack[0].data.shape) # pyright: ignore
weight_c = np.zeros(image_stack[0].data.shape) # pyright: ignore
image_c = as_float_array([])
weight_c = as_float_array([])

for i, image in enumerate(image_stack):
if image.data is None:
image.read_data(image_stack.cctf_decoding)

if image_c.size == 0:
image_c = zeros(image.data.shape) # pyright: ignore
weight_c = zeros(image.data.shape) # pyright: ignore

L = 1 / average_luminance(
image.metadata.f_number, # pyright: ignore
image.metadata.exposure_time, # pyright: ignore
Expand All @@ -114,28 +122,32 @@ def image_stack_to_HDRI(
f"colourspace."
)

image_data = np.clip(image.data, EPSILON, 1) # pyright: ignore
weights = np.clip(weighting_function(image_data), EPSILON, 1)
data = np.clip(image.data, EPSILON, 1) # pyright: ignore
weights = np.clip(weighting_function(data), EPSILON, 1)

# Invoking garbage collection to free memory.
image.data = None
gc.collect()

if i == 0:
weights[image_data >= 0.5] = 1
weights[data >= 0.5] = 1

if i == len(image_stack) - 1:
weights[image_data <= 0.5] = 1
weights[data <= 0.5] = 1

if camera_response_functions is not None:
camera_response_functions = as_float_array(camera_response_functions)
samples = np.linspace(0, 1, camera_response_functions.shape[0])

R, G, B = tsplit(image_data)
R, G, B = tsplit(data)
R = np.interp(R, samples, camera_response_functions[..., 0])
G = np.interp(G, samples, camera_response_functions[..., 1])
B = np.interp(B, samples, camera_response_functions[..., 2])
image_data = tstack([R, G, B])
data = tstack([R, G, B])

image_c += weights * image_data / L
image_c += weights * data / L
weight_c += weights

del image_data, weights
del data, weights

return image_c / weight_c
19 changes: 17 additions & 2 deletions colour_hdri/generation/tests/test_hdri.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def test_image_stack_to_HDRI(self) -> None:
image_stack = ImageStack.from_files(IMAGES_JPG)
image_stack.data = RGB_COLOURSPACES["sRGB"].cctf_decoding(image_stack.data)

# Lower precision for unit tests under *travis-ci*.
np.testing.assert_allclose(
image_stack_to_HDRI(image_stack),
np.load(
Expand All @@ -66,7 +65,6 @@ def test_image_stack_to_HDRI(self) -> None:
atol=0.0001,
)

# Lower precision for unit tests under *travis-ci*.
image_stack = ImageStack.from_files(IMAGES_JPG)
np.testing.assert_allclose(
image_stack_to_HDRI(
Expand All @@ -83,3 +81,20 @@ def test_image_stack_to_HDRI(self) -> None:
),
atol=0.0001,
)

image_stack = ImageStack.from_files(IMAGES_JPG, read_data=False)
np.testing.assert_allclose(
image_stack_to_HDRI(
image_stack,
camera_response_functions=(
camera_response_functions_Debevec1997(image_stack)
),
),
np.load(
os.path.join(
ROOT_RESOURCES_GENERATION,
"test_image_stack_to_hdri_crfs.npy",
)
),
atol=0.0001,
)
4 changes: 2 additions & 2 deletions colour_hdri/network/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,8 +1013,8 @@ def process(self, **kwargs: Any) -> None:
"output",
list(
zip(
self.nodes["ParallelForMultiprocess"].get_output("results"), # pyright: ignore
self.nodes["GraphPostMergeHDRI"].get_output("output"), # pyright: ignore
self.nodes["ParallelForMultiprocess"].get_output("results"),
self.nodes["GraphPostMergeHDRI"].get_output("output"),
strict=False,
)
),
Expand Down
7 changes: 5 additions & 2 deletions colour_hdri/network/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1430,7 +1430,10 @@ def process(self, **kwargs: Any) -> None: # noqa: ARG002
return

self.set_output(
"image_stack", ImageStack.from_files(paths, self.get_input("cctf_decoding"))
"image_stack",
ImageStack.from_files(
paths, self.get_input("cctf_decoding"), read_data=False
),
)

self.dirty = False
Expand Down Expand Up @@ -1508,7 +1511,7 @@ def process(self, **kwargs: Any) -> None: # noqa: ARG002
self.log(f'"{image_path}" image does not exist!')
return

median.append(np.median(read_image_OpenImageIO(image_path)))
median.append(np.median(read_image_OpenImageIO(image_path))) # pyright: ignore

normalising_factor = np.median(median)

Expand Down
2 changes: 1 addition & 1 deletion colour_hdri/sampling/variance_minimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def light_probe_sampling_variance_minimization_Viriyothai2009(
Light_Specification(
(c / np.array(Y.shape))[::-1],
np.sum(np.sum(light_probe_c, 0), 0),
c, # pyright: ignore
c,
)
)

Expand Down
112 changes: 89 additions & 23 deletions colour_hdri/utilities/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,40 @@ def read_metadata(self) -> Metadata:
return metadata


def _luminance_average_key(image: Image) -> NDArrayFloat | None:
"""Comparison key function."""

metadata = cast(Metadata, image.metadata)

f_number = metadata.f_number
exposure_time = metadata.exposure_time
iso = metadata.iso

if f_number is None or exposure_time is None or iso is None:
warning(
f'"{image.path}" exposure data is missing, average '
f"luminance sorting is inapplicable!"
)
return None

return 1 / average_luminance(f_number, exposure_time, iso)


class ImageStack(MutableSequence[Image]):
"""
Define a convenient image stack storing a sequence of images for HDRI / radiance
images generation.
Parameters
----------
cctf_decoding
Decoding colour component transfer function (Decoding CCTF) or
electro-optical transfer function (EOTF / EOCF).
Attributes
----------
- :attr:`~colour_hdri.ImageStack.cctf_decoding`
Methods
-------
- :meth:`colour_hdri.ImageStack.__init__`
Expand All @@ -385,8 +414,44 @@ class ImageStack(MutableSequence[Image]):
- :meth:`colour_hdri.ImageStack.clear_metadata`
"""

def __init__(self) -> None:
def __init__(self, cctf_decoding: Callable | None = None) -> None:
self._data: List = []
self._cctf_decoding: Callable | None = None
self.cctf_decoding = cctf_decoding

@property
def cctf_decoding(self) -> Callable | None:
"""
Getter and setter property for the decoding colour component transfer
function (Decoding CCTF) / electro-optical transfer function
(EOTF).
Parameters
----------
value
Decoding colour component transfer function (Decoding CCTF) /
electro-optical transfer function (EOTF).
Returns
-------
:py:data:`None` or Callable
Decoding colour component transfer function (Decoding CCTF) /
electro-optical transfer function (EOTF).
"""

return self._cctf_decoding

@cctf_decoding.setter
def cctf_decoding(self, value: Callable | None) -> None:
"""Setter for the **self.cctf_decoding** property."""

if value is not None:
attest(
callable(value),
f'"cctf_decoding" property: "{value}" is not callable!',
)

self._cctf_decoding = value

def __getitem__(self, index: int | slice) -> Image | List[Image]: # pyright: ignore
"""
Expand Down Expand Up @@ -461,6 +526,11 @@ def __getattr__(self, attribute: str) -> Any:
return self.__dict__[attribute]
except KeyError as exception:
if hasattr(Image, attribute):
if attribute == "data":
for image in self:
if image.data is None:
image.read_data()

value = [getattr(image, attribute) for image in self]

if attribute == "data":
Expand Down Expand Up @@ -533,7 +603,10 @@ def sort(self, key: Callable | None = None) -> None:

@staticmethod
def from_files(
image_files: Sequence[str], cctf_decoding: Callable | None = None
image_files: Sequence[str],
cctf_decoding: Callable | None = None,
read_data: bool = True,
read_metadata: bool = True,
) -> ImageStack:
"""
Return a :class:`colour_hdri.ImageStack` instance from given image
Expand All @@ -546,52 +619,45 @@ def from_files(
cctf_decoding
Decoding colour component transfer function (Decoding CCTF) or
electro-optical transfer function (EOTF / EOCF).
read_data
Whether to read the image data.
read_metadata
Whether to read the image metadata.
Returns
-------
:class:`colour_hdri.ImageStack`
"""

image_stack = ImageStack()
image_stack = ImageStack(cctf_decoding)
for image_file in image_files:
image = Image(image_file)
image.read_data(cctf_decoding)
image.read_metadata()
image_stack.append(image)

def luminance_average_key(image: Image) -> NDArrayFloat | None:
"""Comparison key function."""

metadata = cast(Metadata, image.metadata)
if read_data:
image.read_data(image_stack.cctf_decoding)

f_number = metadata.f_number
exposure_time = metadata.exposure_time
iso = metadata.iso
if read_metadata:
image.read_metadata()

if f_number is None or exposure_time is None or iso is None:
warning(
f'"{image.path}" exposure data is missing, average '
f"luminance sorting is inapplicable!"
)
return None
return 1 / average_luminance(f_number, exposure_time, iso)
image_stack.append(image)

image_stack.sort(luminance_average_key)
if read_metadata:
image_stack.sort(_luminance_average_key)

return image_stack

def is_valid(self) -> bool:
"""
Return whether the image stack is valid, i.e., whether all the image
data and metadata is defined.
metadata is defined.
Returns
-------
:class:`bool`
Whether the image stack is valid.
"""

return all(not (image.data is None or image.metadata is None) for image in self)
return all(image.metadata is not None for image in self)

def clear_data(self) -> None:
"""Clear the image stack image data."""
Expand Down
8 changes: 8 additions & 0 deletions colour_hdri/utilities/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def setup_method(self) -> None:

self._image_stack = ImageStack().from_files(self._test_jpg_images)

def test_required_attributes(self) -> None:
"""Test the presence of required attributes."""

required_attributes = ("cctf_decoding",)

for attribute in required_attributes:
assert attribute in dir(Image)

def test_required_methods(self) -> None:
"""Test the presence of required methods."""

Expand Down

0 comments on commit 79caf87

Please sign in to comment.