"""
HDRI Generation
===============
Define the HDRI generation objects:
- :func:`colour_hdri.image_stack_to_HDRI`
See Also
--------
`Colour - HDRI - Examples Jupyter Notebooks
<https://github.com/colour-science/colour-hdri/\
blob/master/colour_hdri/examples>`__
References
----------
- :cite:`Banterle2011n` : Banterle, F., Artusi, A., Debattista, K., &
Chalmers, A. (2011). 2.1.1 Generating HDR Content by Combining Multiple
Exposures. In Advanced High Dynamic Range Imaging. A K Peters/CRC Press.
ISBN:978-1-56881-719-4
"""
from __future__ import annotations
import gc
import typing
import numpy as np
from colour.constants import EPSILON
if typing.TYPE_CHECKING:
from colour.hints import ArrayLike, Callable, NDArrayFloat
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
if typing.TYPE_CHECKING:
from colour_hdri.utilities import ImageStack
__author__ = "Colour Developers"
__copyright__ = "Copyright 2015 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"
__all__ = [
"image_stack_to_HDRI",
]
[docs]
def image_stack_to_HDRI(
image_stack: ImageStack,
weighting_function: Callable = weighting_function_Debevec1997,
camera_response_functions: ArrayLike | None = None,
) -> NDArrayFloat:
"""
Generate a HDRI from given image stack.
Parameters
----------
image_stack
Stack of single channel or multichannel floating point images. The
stack is assumed to be representing linear values except if
``camera_response_functions`` argument is provided.
weighting_function
Weighting function :math:`w`.
camera_response_functions
Camera response functions :math:`g(z)` of the imaging system / camera
if the stack is representing non-linear values.
Returns
-------
:class:`numpy.ndarray`
HDRI.
Warnings
--------
If the image stack contains images with negative or equal to zero values,
unpredictable results may occur and NaNs might be generated. It is
thus recommended encoding the images in a wider RGB colourspace. This
definition avoids NaNs creation by ensuring that all values are greater or
equal to current floating point format epsilon. In practical applications
such as HDRI merging with photographic material there should never be a
pixel with a value exactly equal to zero. Ideally, the process should not
be presented by any negative photometric quantity even though RGB
colourspace encodings allows to do so.
References
----------
:cite:`Banterle2011n`
"""
attest(len(image_stack) > 0, "Image stack cannot be empty!")
attest(image_stack.is_valid(), "Image stack is invalid!")
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
image.metadata.iso, # pyright: ignore
)
if np.any(image.data <= 0): # pyright: ignore
warning(
f'"{image.path}" image channels contain negative or equal '
f"to zero values, unpredictable results may occur! Please "
f"consider encoding your images in a wider gamut RGB "
f"colourspace."
)
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[data >= 0.5] = 1
if i == len(image_stack) - 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(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])
data = tstack([R, G, B])
image_c += weights * data / L
weight_c += weights
del data, weights
return image_c / weight_c