"""
RGB Colourspace Volume Computation
==================================
Define various RGB colourspace volume computation objects:
- :func:`colour.RGB_colourspace_limits`
- :func:`colour.RGB_colourspace_volume_MonteCarlo`
- :func:`colour.RGB_colourspace_volume_coverage_MonteCarlo`
- :func:`colour.RGB_colourspace_pointer_gamut_coverage_MonteCarlo`
- :func:`colour.RGB_colourspace_visible_spectrum_coverage_MonteCarlo`
"""
from __future__ import annotations
import itertools
import typing
import numpy as np
from colour.algebra import random_triplet_generator
from colour.colorimetry import CCS_ILLUMINANTS
from colour.constants import DTYPE_INT_DEFAULT
if typing.TYPE_CHECKING:
from colour.hints import (
ArrayLike,
Callable,
LiteralChromaticAdaptationTransform,
NDArrayFloat,
)
from colour.models import (
Lab_to_XYZ,
RGB_Colourspace,
RGB_to_XYZ,
XYZ_to_Lab,
XYZ_to_RGB,
)
from colour.utilities import as_float_array, multiprocessing_pool
from colour.volume import is_within_pointer_gamut, is_within_visible_spectrum
__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 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__ = [
"sample_RGB_colourspace_volume_MonteCarlo",
"RGB_colourspace_limits",
"RGB_colourspace_volume_MonteCarlo",
"RGB_colourspace_volume_coverage_MonteCarlo",
"RGB_colourspace_pointer_gamut_coverage_MonteCarlo",
"RGB_colourspace_visible_spectrum_coverage_MonteCarlo",
]
def _wrapper_RGB_colourspace_volume_MonteCarlo(arguments: tuple) -> int:
"""
Call the :func:`colour.volume.rgb.sample_RGB_colourspace_volume_MonteCarlo`
definition with multiple arguments.
Parameters
----------
arguments
Arguments.
Returns
-------
:class:`int`
Inside *RGB* colourspace volume sample count.
"""
return sample_RGB_colourspace_volume_MonteCarlo(*arguments)
def sample_RGB_colourspace_volume_MonteCarlo(
colourspace: RGB_Colourspace,
samples: int = 1000000,
limits: ArrayLike = ([0, 100], [-150, 150], [-150, 150]),
illuminant_Lab: ArrayLike = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"][
"D65"
],
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
random_generator: Callable = random_triplet_generator,
random_state: np.random.RandomState | None = None,
) -> int:
"""
Randomly sample the *CIE L\\*a\\*b\\** colourspace volume and returns the
ratio of samples within the given *RGB* colourspace volume.
Parameters
----------
colourspace
*RGB* colourspace to compute the volume of.
samples
Sample count.
limits
*CIE L\\*a\\*b\\** colourspace volume.
illuminant_Lab
*CIE L\\*a\\*b\\** colourspace *illuminant* chromaticity coordinates.
chromatic_adaptation_transform
*Chromatic adaptation* transform.
random_generator
Random triplet generator providing the random samples within the
*CIE L\\*a\\*b\\** colourspace volume.
random_state
Mersenne Twister pseudo-random number generator to use in the random
number generator.
Returns
-------
:class:`int`
Within *RGB* colourspace volume sample count.
Notes
-----
- The doctest is assuming that :func:`np.random.RandomState` definition
will return the same sequence no matter which *OS* or *Python*
version is used. There is however no formal promise about the *prng*
sequence reproducibility of either *Python* or *Numpy*
implementations: Laurent. (2012). Reproducibility of python
pseudo-random numbers across systems and versions? Retrieved January
20, 2015, from http://stackoverflow.com/questions/8786084/\
reproducibility-of-python-pseudo-random-numbers-across-systems-and-versions
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> prng = np.random.RandomState(2)
>>> sample_RGB_colourspace_volume_MonteCarlo(sRGB, 10e3, random_state=prng)
... # doctest: +ELLIPSIS
9...
"""
random_state = random_state if random_state is not None else np.random.RandomState()
Lab = random_generator(DTYPE_INT_DEFAULT(samples), limits, random_state)
RGB = XYZ_to_RGB(
Lab_to_XYZ(Lab, illuminant_Lab),
colourspace,
illuminant_Lab,
chromatic_adaptation_transform,
)
RGB_w = RGB[np.logical_and(np.min(RGB, axis=-1) >= 0, np.max(RGB, axis=-1) <= 1)]
return len(RGB_w)
[docs]
def RGB_colourspace_limits(colourspace: RGB_Colourspace) -> NDArrayFloat:
"""
Compute given *RGB* colourspace volume limits in *CIE L\\*a\\*b\\**
colourspace.
Parameters
----------
colourspace
*RGB* colourspace to compute the volume of.
Returns
-------
:class:`numpy.ndarray`
*RGB* colourspace volume limits.
Notes
-----
The limits are computed for the given *RGB* colourspace illuminant. This is
important to account for, if the intent is to compare various *RGB*
colourspaces together. In this instance, they must be chromatically adapted
to the same illuminant before-hand.
See :meth:`colour.RGB_Colourspace.chromatically_adapt` method for more
information.
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> RGB_colourspace_limits(sRGB) # doctest: +ELLIPSIS
array([[ 0. ..., 100. ...],
[ -86.182855 ..., 98.2563272...],
[-107.8503557..., 94.4894974...]])
"""
Lab = np.array(
[
XYZ_to_Lab(
RGB_to_XYZ(combination, colourspace),
colourspace.whitepoint,
)
for combination in list(itertools.product([0, 1], repeat=3))
]
)
limits = [(np.min(Lab[..., i]), np.max(Lab[..., i])) for i in np.arange(3)]
return np.array(limits)
[docs]
def RGB_colourspace_volume_MonteCarlo(
colourspace: RGB_Colourspace,
samples: int = 1000000,
limits: ArrayLike = ([0, 100], [-150, 150], [-150, 150]),
illuminant_Lab: ArrayLike = CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"][
"D65"
],
chromatic_adaptation_transform: (
LiteralChromaticAdaptationTransform | str | None
) = "CAT02",
random_generator: Callable = random_triplet_generator,
random_state: np.random.RandomState | None = None,
) -> float:
"""
Perform given *RGB* colourspace volume computation using *Monte Carlo*
method and multiprocessing.
Parameters
----------
colourspace\
*RGB* colourspace to compute the volume of.
samples\
Sample count.
limits\
*CIE L\\*a\\*b\\** colourspace volume.
illuminant_Lab\
*CIE L\\*a\\*b\\** colourspace *illuminant* chromaticity coordinates.
chromatic_adaptation_transform\
*Chromatic adaptation* method.
random_generator\
Random triplet generator providing the random samples within the
*CIE L\\*a\\*b\\** colourspace volume.
random_state\
Mersenne Twister pseudo-random number generator to use in the random
number generator.
Returns
-------
:class:`float`
*RGB* colourspace volume.
Notes
-----
- The doctest is assuming that :func:`np.random.RandomState` definition
will return the same sequence no matter which *OS* or *Python*
version is used. There is however no formal promise about the *prng*
sequence reproducibility of either *Python* or *Numpy*
implementations: Laurent. (2012). Reproducibility of python
pseudo-random numbers across systems and versions? Retrieved January
20, 2015, from http://stackoverflow.com/questions/8786084/\
reproducibility-of-python-pseudo-random-numbers-across-systems-and-versions
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> from colour.utilities import disable_multiprocessing
>>> prng = np.random.RandomState(2)
>>> with disable_multiprocessing():
... RGB_colourspace_volume_MonteCarlo(sRGB, 10e3, random_state=prng)
... # doctest: +SKIP
...
8...
"""
import multiprocessing
processes = multiprocessing.cpu_count()
process_samples = DTYPE_INT_DEFAULT(np.round(samples / processes))
arguments = (
colourspace,
process_samples,
limits,
illuminant_Lab,
chromatic_adaptation_transform,
random_generator,
random_state,
)
with multiprocessing_pool() as pool:
results = pool.map(
_wrapper_RGB_colourspace_volume_MonteCarlo,
[arguments for _ in range(processes)],
)
Lab_volume = np.prod([np.sum(np.abs(x)) for x in as_float_array(limits)])
return Lab_volume * np.sum(results) / (process_samples * processes)
[docs]
def RGB_colourspace_volume_coverage_MonteCarlo(
colourspace: RGB_Colourspace,
coverage_sampler: Callable,
samples: int = 1000000,
random_generator: Callable = random_triplet_generator,
random_state: np.random.RandomState | None = None,
) -> float:
"""
Return given *RGB* colourspace percentage coverage of an arbitrary volume.
Parameters
----------
colourspace
*RGB* colourspace to compute the volume coverage percentage.
coverage_sampler
Python object responsible for checking the volume coverage.
samples
Sample count.
random_generator
Random triplet generator providing the random samples.
random_state
Mersenne Twister pseudo-random number generator to use in the random
number generator.
Returns
-------
:class:`float`
Percentage coverage of volume.
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> prng = np.random.RandomState(2)
>>> RGB_colourspace_volume_coverage_MonteCarlo(
... sRGB, is_within_pointer_gamut, 10e3, random_state=prng
... )
... # doctest: +ELLIPSIS
81...
"""
random_state = random_state if random_state is not None else np.random.RandomState()
XYZ = random_generator(DTYPE_INT_DEFAULT(samples), random_state=random_state)
XYZ_vs = XYZ[coverage_sampler(XYZ)]
RGB = XYZ_to_RGB(XYZ_vs, colourspace)
RGB_c = RGB[np.logical_and(np.min(RGB, axis=-1) >= 0, np.max(RGB, axis=-1) <= 1)]
return 100 * RGB_c.size / XYZ_vs.size
[docs]
def RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
colourspace: RGB_Colourspace,
samples: int = 1000000,
random_generator: Callable = random_triplet_generator,
random_state: np.random.RandomState | None = None,
) -> float:
"""
Return given *RGB* colourspace percentage coverage of Pointer's Gamut
volume using *Monte Carlo* method.
Parameters
----------
colourspace
*RGB* colourspace to compute the *Pointer's Gamut* coverage percentage.
samples
Sample count.
random_generator
Random triplet generator providing the random samples.
random_state
Mersenne Twister pseudo-random number generator to use in the random
number generator.
Returns
-------
:class:`float`
Percentage coverage of *Pointer's Gamut* volume.
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> prng = np.random.RandomState(2)
>>> RGB_colourspace_pointer_gamut_coverage_MonteCarlo(
... sRGB, 10e3, random_state=prng
... ) # doctest: +ELLIPSIS
81...
"""
return RGB_colourspace_volume_coverage_MonteCarlo(
colourspace,
is_within_pointer_gamut,
samples,
random_generator,
random_state,
)
[docs]
def RGB_colourspace_visible_spectrum_coverage_MonteCarlo(
colourspace: RGB_Colourspace,
samples: int = 1000000,
random_generator: Callable = random_triplet_generator,
random_state: np.random.RandomState | None = None,
) -> float:
"""
Return given *RGB* colourspace percentage coverage of visible spectrum
volume using *Monte Carlo* method.
Parameters
----------
colourspace
*RGB* colourspace to compute the visible spectrum coverage percentage.
samples
Sample count.
random_generator
Random triplet generator providing the random samples.
random_state
Mersenne Twister pseudo-random number generator to use in the random
number generator.
Returns
-------
:class:`float`
Percentage coverage of visible spectrum volume.
Examples
--------
>>> from colour.models import RGB_COLOURSPACE_sRGB as sRGB
>>> prng = np.random.RandomState(2)
>>> RGB_colourspace_visible_spectrum_coverage_MonteCarlo(
... sRGB, 10e3, random_state=prng
... ) # doctest: +ELLIPSIS
46...
"""
return RGB_colourspace_volume_coverage_MonteCarlo(
colourspace,
is_within_visible_spectrum,
samples,
random_generator,
random_state,
)