def test_offset(self):
        """
        Test :class:`colour.io.luts.operator.LUTOperatorMatrix.offset`
        property.
        """

        offset = zeros(3)

        lut_operator_matrix = LUTOperatorMatrix(np.identity(3), offset)
        np.testing.assert_array_equal(lut_operator_matrix.offset, zeros(4))
Exemple #2
0
def XYZ_to_xyY(XYZ,
               illuminant=CCS_ILLUMINANTS[
                   'CIE 1931 2 Degree Standard Observer']['D65']):
    """
    Converts from *CIE XYZ* tristimulus values to *CIE xyY* colourspace and
    reference *illuminant*.

    Parameters
    ----------
    XYZ : array_like
        *CIE XYZ* tristimulus values.
    illuminant : array_like, optional
        Reference *illuminant* chromaticity coordinates.

    Returns
    -------
    ndarray
        *CIE xyY* colourspace array.

    Notes
    -----

    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``XYZ``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    +------------+-----------------------+---------------+
    | **Range**  | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``xyY``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    References
    ----------
    :cite:`Lindbloom2003e`, :cite:`Wikipedia2005`

    Examples
    --------
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> XYZ_to_xyY(XYZ)  # doctest: +ELLIPSIS
    array([ 0.5436955...,  0.3210794...,  0.1219722...])
    """

    XYZ = to_domain_1(XYZ)
    X, Y, Z = tsplit(XYZ)
    xy_w = as_float_array(illuminant)

    XYZ_n = zeros(XYZ.shape)
    XYZ_n[..., 0:2] = xy_w

    xyY = np.where(
        np.all(XYZ == 0, axis=-1)[..., np.newaxis],
        XYZ_n,
        tstack([X / (X + Y + Z), Y / (X + Y + Z),
                from_range_1(Y)]),
    )

    return xyY
Exemple #3
0
def RGB_to_IHLS(RGB: ArrayLike) -> NDArray:
    """
    Convert from *RGB* colourspace to *IHLS* (Improved HLS) colourspace.

    Parameters
    ----------
    RGB
       *RGB* colourspace array.

    Returns
    -------
    :class:`numpy.ndarray`
        *HYS* colourspace array.

    Notes
    -----
    +------------+-----------------------+---------------+
    | **Domain** | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``RGB``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    +------------+-----------------------+---------------+
    | **Range**  | **Scale - Reference** | **Scale - 1** |
    +============+=======================+===============+
    | ``HYS``    | [0, 1]                | [0, 1]        |
    +------------+-----------------------+---------------+

    References
    ----------
    :cite:`Hanbury2003`

    Examples
    --------
    >>> RGB = np.array([0.45595571, 0.03039702, 0.04087245])
    >>> RGB_to_IHLS(RGB)  # doctest: +ELLIPSIS
    array([ 6.2616051...,  0.1216271...,  0.4255586...])
    """

    RGB = to_domain_1(RGB)
    R, G, B = tsplit(RGB)

    Y, C_1, C_2 = tsplit(vector_dot(MATRIX_RGB_TO_YC_1_C_2, RGB))

    C = np.sqrt(C_1**2 + C_2**2)

    acos_C_1_C_2 = zeros(C.shape)
    acos_C_1_C_2[C != 0] = np.arccos(C_1[C != 0] / C[C != 0])
    H = np.where(C_2 <= 0, acos_C_1_C_2, (np.pi * 2) - acos_C_1_C_2)

    S = np.maximum(np.maximum(R, G), B) - np.minimum(np.minimum(R, G), B)

    HYS = tstack([H, Y, S])

    return from_range_1(HYS)
Exemple #4
0
    def offset(self, value: ArrayLike):
        """Setter for the **self.offset** property."""

        value = as_float_array(value)

        shape_t = value.shape[-1]

        attest(
            value.shape in [(3, ), (4, )],
            f'"offset" property: "{value}" shape is not (3, ) or (4, )!',
        )

        offset = zeros(4)
        offset[:shape_t] = value

        self._offset = offset
Exemple #5
0
    def __init__(
        self,
        matrix: Optional[ArrayLike] = None,
        offset: Optional[ArrayLike] = None,
        *args: Any,
        **kwargs: Any,
    ):
        super().__init__(*args, **kwargs)

        # TODO: Remove pragma when https://github.com/python/mypy/issues/3004
        # is resolved.
        self._matrix: NDArray = np.diag(ones(4))
        self.matrix = cast(ArrayLike,
                           optional(matrix,
                                    self._matrix))  # type: ignore[assignment]
        self._offset: NDArray = zeros(4)
        self.offset = cast(ArrayLike,
                           optional(offset,
                                    self._offset))  # type: ignore[assignment]
Exemple #6
0
    def test_LUT3D_Jakob2019(self):
        """
        Tests the entirety of the
        :class:`colour.recovery.jakob2019.LUT3D_Jakob2019`class.
        """

        LUT = LUT3D_Jakob2019()
        LUT.generate(self._RGB_colourspace, self._cmfs, self._sd_D65, 5)

        path = os.path.join(self._temporary_directory, 'Test_Jakob2019.coeff')

        LUT.write(path)
        LUT.read(path)

        for RGB in [
                np.array([1, 0, 0]),
                np.array([0, 1, 0]),
                np.array([0, 0, 1]),
                zeros(3),
                full(3, 0.5),
                ones(3),
        ]:
            XYZ = RGB_to_XYZ(RGB, self._RGB_colourspace.whitepoint,
                             self._xy_D65,
                             self._RGB_colourspace.matrix_RGB_to_XYZ)
            Lab = XYZ_to_Lab(XYZ, self._xy_D65)

            recovered_sd = LUT.RGB_to_sd(RGB)
            recovered_XYZ = sd_to_XYZ(recovered_sd, self._cmfs,
                                      self._sd_D65) / 100
            recovered_Lab = XYZ_to_Lab(recovered_XYZ, self._xy_D65)

            error = delta_E_CIE1976(Lab, recovered_Lab)

            if error > 2 * JND_CIE1976 / 100:
                self.fail(
                    'Delta E for RGB={0} in colourspace {1} is {2}!'.format(
                        RGB, self._RGB_colourspace.name, error))
Exemple #7
0
def primitive_grid(
    width: Floating = 1,
    height: Floating = 1,
    width_segments: Integer = 1,
    height_segments: Integer = 1,
    axis: Literal["-x", "+x", "-y", "+y", "-z", "+z", "xy", "xz", "yz", "yx",
                  "zx", "zy"] = "+z",
    dtype_vertices: Optional[Type[DTypeFloating]] = None,
    dtype_indexes: Optional[Type[DTypeInteger]] = None,
) -> Tuple[NDArray, NDArray, NDArray]:
    """
    Generate vertices and indexes for a filled and outlined grid primitive.

    Parameters
    ----------
    width
        Grid width.
    height
        Grid height.
    width_segments
        Grid segments count along the width.
    height_segments
        Grid segments count along the height.
    axis
        Axis the primitive will be normal to, or plane the primitive will be
        co-planar with.
    dtype_vertices
        :class:`numpy.dtype` to use for the grid vertices, default to
        the :class:`numpy.dtype` defined by the
        :attr:`colour.constant.DEFAULT_FLOAT_DTYPE` attribute.
    dtype_indexes
        :class:`numpy.dtype` to use for the grid indexes, default to
        the :class:`numpy.dtype` defined by the
        :attr:`colour.constant.DEFAULT_INT_DTYPE` attribute.

    Returns
    -------
    :class:`tuple`
        Tuple of grid vertices, face indexes to produce a filled grid and
        outline indexes to produce an outline of the faces of the grid.

    References
    ----------
    :cite:`Cabello2015`

    Examples
    --------
    >>> vertices, faces, outline = primitive_grid()
    >>> print(vertices)
    [([-0.5,  0.5,  0. ], [ 0.,  1.], [ 0.,  0.,  1.], [ 0.,  1.,  0.,  1.])
     ([ 0.5,  0.5,  0. ], [ 1.,  1.], [ 0.,  0.,  1.], [ 1.,  1.,  0.,  1.])
     ([-0.5, -0.5,  0. ], [ 0.,  0.], [ 0.,  0.,  1.], [ 0.,  0.,  0.,  1.])
     ([ 0.5, -0.5,  0. ], [ 1.,  0.], [ 0.,  0.,  1.], [ 1.,  0.,  0.,  1.])]
    >>> print(faces)
    [[0 2 1]
     [2 3 1]]
    >>> print(outline)
    [[0 2]
     [2 3]
     [3 1]
     [1 0]]
    """

    axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()

    dtype_vertices = cast(Type[DTypeFloating],
                          optional(dtype_vertices, DEFAULT_FLOAT_DTYPE))
    dtype_indexes = cast(Type[DTypeInteger],
                         optional(dtype_indexes, DEFAULT_INT_DTYPE))

    x_grid = width_segments
    y_grid = height_segments

    x_grid1 = int(x_grid + 1)
    y_grid1 = int(y_grid + 1)

    # Positions, normals and uvs.
    positions = zeros(x_grid1 * y_grid1 * 3)
    normals = zeros(x_grid1 * y_grid1 * 3)
    uvs = zeros(x_grid1 * y_grid1 * 2)

    y = np.arange(y_grid1) * height / y_grid - height / 2
    x = np.arange(x_grid1) * width / x_grid - width / 2

    positions[::3] = np.tile(x, y_grid1)
    positions[1::3] = -np.repeat(y, x_grid1)

    normals[2::3] = 1

    uvs[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1)
    uvs[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1)

    # Faces and outline.
    faces_indexes = []
    outline_indexes = []
    for i_y in range(y_grid):
        for i_x in range(x_grid):
            a = i_x + x_grid1 * i_y
            b = i_x + x_grid1 * (i_y + 1)
            c = (i_x + 1) + x_grid1 * (i_y + 1)
            d = (i_x + 1) + x_grid1 * i_y

            faces_indexes.extend([(a, b, d), (b, c, d)])
            outline_indexes.extend([(a, b), (b, c), (c, d), (d, a)])

    faces = np.reshape(as_int_array(faces_indexes, dtype_indexes), (-1, 3))
    outline = np.reshape(as_int_array(outline_indexes, dtype_indexes), (-1, 2))

    positions = np.reshape(positions, (-1, 3))
    uvs = np.reshape(uvs, (-1, 2))
    normals = np.reshape(normals, (-1, 3))

    if axis in ("-x", "+x"):
        shift, zero_axis = 1, 0
    elif axis in ("-y", "+y"):
        shift, zero_axis = -1, 1
    elif axis in ("-z", "+z"):
        shift, zero_axis = 0, 2

    sign = -1 if "-" in axis else 1

    positions = np.roll(positions, shift, -1)
    normals = np.roll(normals, shift, -1) * sign
    vertex_colours = np.ravel(positions)
    vertex_colours = np.hstack([
        np.reshape(
            np.interp(
                vertex_colours,
                (np.min(vertex_colours), np.max(vertex_colours)),
                (0, 1),
            ),
            positions.shape,
        ),
        ones((positions.shape[0], 1)),
    ])
    vertex_colours[..., zero_axis] = 0

    vertices = zeros(
        positions.shape[0],
        [
            ("position", dtype_vertices, 3),
            ("uv", dtype_vertices, 2),
            ("normal", dtype_vertices, 3),
            ("colour", dtype_vertices, 4),
        ],  # type: ignore[arg-type]
    )

    vertices["position"] = positions
    vertices["uv"] = uvs
    vertices["normal"] = normals
    vertices["colour"] = vertex_colours

    return vertices, faces, outline
Exemple #8
0
    def generate(
        self,
        colourspace: RGB_Colourspace,
        cmfs: Optional[MultiSpectralDistributions] = None,
        illuminant: Optional[SpectralDistribution] = None,
        size: Integer = 64,
        print_callable: Callable = print,
    ):
        """
        Generate the lookup table data for given *RGB* colourspace, colour
        matching functions, illuminant and given size.

        Parameters
        ----------
        colourspace
            The *RGB* colourspace to create a lookup table for.
        cmfs
            Standard observer colour matching functions, default to the
            *CIE 1931 2 Degree Standard Observer*.
        illuminant
            Illuminant spectral distribution, default to
            *CIE Standard Illuminant D65*.
        size
            The resolution of the lookup table. Higher values will decrease
            errors but at the cost of a much longer run time. The published
            *\\*.coeff* files have a resolution of 64.
        print_callable
            Callable used to print progress and diagnostic information.

        Examples
        --------
        >>> from colour import MSDS_CMFS, SDS_ILLUMINANTS
        >>> from colour.models import RGB_COLOURSPACE_sRGB
        >>> from colour.utilities import numpy_print_options
        >>> cmfs = (
        ...     MSDS_CMFS['CIE 1931 2 Degree Standard Observer'].
        ...     copy().align(SpectralShape(360, 780, 10))
        ... )
        >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
        >>> LUT = LUT3D_Jakob2019()
        >>> print(LUT.interpolator)
        None
        >>> LUT.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, 3)
        ======================================================================\
=========
        *                                                                     \
        *
        *   "Jakob et al. (2018)" LUT Optimisation                            \
        *
        *                                                                     \
        *
        ======================================================================\
=========
        <BLANKLINE>
        Optimising 27 coefficients...
        <BLANKLINE>
        >>> print(LUT.interpolator)
        ... # doctest: +ELLIPSIS
        <scipy.interpolate...RegularGridInterpolator object at 0x...>
        """

        cmfs, illuminant = handle_spectral_arguments(
            cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
        )
        shape = cmfs.shape

        xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs))

        # It could be interesting to have different resolutions for lightness
        # and chromaticity, but the current file format doesn't allow it.
        lightness_steps = size
        chroma_steps = size

        self._lightness_scale = lightness_scale(lightness_steps)
        self._coefficients = np.empty(
            [3, chroma_steps, chroma_steps, lightness_steps, 3]
        )

        cube_indexes = np.ndindex(3, chroma_steps, chroma_steps)
        total_coefficients = chroma_steps**2 * 3

        # First, create a list of all the fully bright colours with the order
        # matching cube_indexes.
        samples = np.linspace(0, 1, chroma_steps)
        ij = np.reshape(
            np.transpose(np.meshgrid([1], samples, samples, indexing="ij")),
            (-1, 3),
        )
        chromas = np.concatenate(
            [
                as_float_array(ij),
                np.roll(ij, 1, axis=1),
                np.roll(ij, 2, axis=1),
            ]
        )

        message_box(
            '"Jakob et al. (2018)" LUT Optimisation',
            print_callable=print_callable,
        )

        print_callable(f"\nOptimising {total_coefficients} coefficients...\n")

        def optimize(ijkL: ArrayLike, coefficients_0: ArrayLike) -> NDArray:
            """
            Solve for a specific lightness and stores the result in the
            appropriate cell.
            """

            i, j, k, L = tsplit(ijkL, dtype=DEFAULT_INT_DTYPE)

            RGB = self._lightness_scale[L] * chroma

            XYZ = RGB_to_XYZ(
                RGB,
                colourspace.whitepoint,
                xy_n,
                colourspace.matrix_RGB_to_XYZ,
            )

            coefficients, _error = find_coefficients_Jakob2019(
                XYZ, cmfs, illuminant, coefficients_0, dimensionalise=False
            )

            self._coefficients[i, L, j, k, :] = dimensionalise_coefficients(
                coefficients, shape
            )

            return coefficients

        with tqdm(total=total_coefficients) as progress:
            for ijk, chroma in zip(cube_indexes, chromas):
                progress.update()

                # Starts from somewhere in the middle, similarly to how
                # feedback works in "colour.recovery.\
                # find_coefficients_Jakob2019" definition.
                L_middle = lightness_steps // 3
                coefficients_middle = optimize(
                    np.hstack([ijk, L_middle]), zeros(3)
                )

                # Down the lightness scale.
                coefficients_0 = coefficients_middle
                for L in reversed(range(0, L_middle)):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0
                    )

                # Up the lightness scale.
                coefficients_0 = coefficients_middle
                for L in range(L_middle + 1, lightness_steps):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0
                    )

        self._size = size
        self._create_interpolator()
Exemple #9
0
def find_coefficients_Jakob2019(
    XYZ: ArrayLike,
    cmfs: Optional[MultiSpectralDistributions] = None,
    illuminant: Optional[SpectralDistribution] = None,
    coefficients_0: ArrayLike = zeros(3),
    max_error: Floating = JND_CIE1976 / 100,
    dimensionalise: Boolean = True,
) -> Tuple[NDArray, Floating]:
    """
    Compute the coefficients for *Jakob and Hanika (2019)* reflectance
    spectral model.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values to find the coefficients for.
    cmfs
        Standard observer colour matching functions, default to the
        *CIE 1931 2 Degree Standard Observer*.
    illuminant
        Illuminant spectral distribution, default to
        *CIE Standard Illuminant D65*.
    coefficients_0
        Starting coefficients for the solver.
    max_error
        Maximal acceptable error. Set higher to save computational time.
        If *None*, the solver will keep going until it is very close to the
        minimum. The default is ``ACCEPTABLE_DELTA_E``.
    dimensionalise
        If *True*, returned coefficients are dimensionful and will not work
        correctly if fed back as ``coefficients_0``. The default is *True*.

    Returns
    -------
    :class:`tuple`
        Tuple of computed coefficients that best fit the given colour and
        :math:`\\Delta E_{76}` between the target colour and the colour
        corresponding to the computed coefficients.

    References
    ----------
    :cite:`Jakob2019`

    Examples
    --------
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> find_coefficients_Jakob2019(XYZ)  # doctest: +ELLIPSIS
    (array([  1.3723791...e-04,  -1.3514399...e-01,   3.0838973...e+01]), \
0.0141941...)
    """

    coefficients_0 = as_float_array(coefficients_0)

    cmfs, illuminant = handle_spectral_arguments(
        cmfs, illuminant, shape_default=SPECTRAL_SHAPE_JAKOB2019
    )

    def optimize(
        target_o: NDArray, coefficients_0_o: NDArray
    ) -> Tuple[NDArray, Floating]:
        """Minimise the error function using *L-BFGS-B* method."""

        try:
            result = minimize(
                error_function,
                coefficients_0_o,
                (target_o, cmfs, illuminant, max_error),
                method="L-BFGS-B",
                jac=True,
            )

            return result.x, result.fun
        except StopMinimizationEarly as error:
            return error.coefficients, error.error

    xy_n = XYZ_to_xy(sd_to_XYZ_integration(illuminant, cmfs))

    XYZ_good = full(3, 0.5)
    coefficients_good = zeros(3)

    divisions = 3
    while divisions < 10:
        XYZ_r = XYZ_good
        coefficient_r = coefficients_good
        keep_divisions = False

        coefficients_0 = coefficient_r
        for i in range(1, divisions):
            XYZ_i = (XYZ - XYZ_r) * i / (divisions - 1) + XYZ_r
            Lab_i = XYZ_to_Lab(XYZ_i)

            coefficients_0, error = optimize(Lab_i, coefficients_0)

            if error > max_error:
                break
            else:
                XYZ_good = XYZ_i
                coefficients_good = coefficients_0
                keep_divisions = True
        else:
            break

        if not keep_divisions:
            divisions += 2

    target = XYZ_to_Lab(XYZ, xy_n)
    coefficients, error = optimize(target, coefficients_0)

    if dimensionalise:
        coefficients = dimensionalise_coefficients(coefficients, cmfs.shape)

    return coefficients, error
Exemple #10
0
    def generate(self,
                 colourspace,
                 cmfs=MSDS_CMFS_STANDARD_OBSERVER[
                     'CIE 1931 2 Degree Standard Observer']
                 .copy().align(SPECTRAL_SHAPE_JAKOB2019),
                 illuminant=SDS_ILLUMINANTS['D65'].copy().align(
                     SPECTRAL_SHAPE_JAKOB2019),
                 size=64,
                 print_callable=print):
        """
        Generates the lookup table data for given *RGB* colourspace, colour
        matching functions, illuminant and given size.

        Parameters
        ----------
        colourspace: RGB_Colourspace
            The *RGB* colourspace to create a lookup table for.
        cmfs : XYZ_ColourMatchingFunctions, optional
            Standard observer colour matching functions.
        illuminant : SpectralDistribution, optional
            Illuminant spectral distribution.
        size : int, optional
            The resolution of the lookup table. Higher values will decrease
            errors but at the cost of a much longer run time. The published
            *\\*.coeff* files have a resolution of 64.
        print_callable : callable, optional
            Callable used to print progress and diagnostic information.

        Examples
        --------
        >>> from colour.utilities import numpy_print_options
        >>> from colour.models import RGB_COLOURSPACE_sRGB
        >>> cmfs = MSDS_CMFS_STANDARD_OBSERVER[
        ...         'CIE 1931 2 Degree Standard Observer'].copy().align(
        ...             SpectralShape(360, 780, 10))
        >>> illuminant = SDS_ILLUMINANTS['D65'].copy().align(cmfs.shape)
        >>> LUT = LUT3D_Jakob2019()
        >>> print(LUT.interpolator)
        None
        >>> LUT.generate(RGB_COLOURSPACE_sRGB, cmfs, illuminant, 3)
        ======================================================================\
=========
        *                                                                     \
        *
        *   "Jakob et al. (2018)" LUT Optimisation                            \
        *
        *                                                                     \
        *
        ======================================================================\
=========
        <BLANKLINE>
        Optimising 27 coefficients...
        <BLANKLINE>
        >>> print(LUT.interpolator)
        ... # doctest: +ELLIPSIS
        <scipy.interpolate.interpolate.RegularGridInterpolator object at 0x...>
        """

        shape = cmfs.shape

        if illuminant.shape != shape:
            runtime_warning(
                'Aligning "{0}" illuminant shape to "{1}" colour matching '
                'functions shape.'.format(illuminant.name, cmfs.name))
            illuminant = illuminant.copy().align(cmfs.shape)

        xy_n = XYZ_to_xy(sd_to_XYZ(illuminant, cmfs))

        # It could be interesting to have different resolutions for lightness
        # and chromaticity, but the current file format doesn't allow it.
        lightness_steps = size
        chroma_steps = size

        self._lightness_scale = lightness_scale(lightness_steps)
        self._coefficients = np.empty(
            [3, chroma_steps, chroma_steps, lightness_steps, 3])

        cube_indexes = np.ndindex(3, chroma_steps, chroma_steps)
        total_coefficients = chroma_steps ** 2 * 3

        # First, create a list of all the fully bright colours with the order
        # matching cube_indexes.
        samples = np.linspace(0, 1, chroma_steps)
        ij = np.meshgrid(*[[1], samples, samples], indexing='ij')
        ij = np.transpose(ij).reshape(-1, 3)
        chromas = np.concatenate(
            [ij, np.roll(ij, 1, axis=1),
             np.roll(ij, 2, axis=1)])

        message_box(
            '"Jakob et al. (2018)" LUT Optimisation',
            print_callable=print_callable)

        print_callable(
            '\nOptimising {0} coefficients...\n'.format(total_coefficients))

        def optimize(ijkL, coefficients_0):
            """
            Solves for a specific lightness and stores the result in the
            appropriate cell.
            """

            i, j, k, L = ijkL

            RGB = self._lightness_scale[L] * chroma

            XYZ = RGB_to_XYZ(RGB, colourspace.whitepoint, xy_n,
                             colourspace.matrix_RGB_to_XYZ)

            coefficients, error = find_coefficients_Jakob2019(
                XYZ, cmfs, illuminant, coefficients_0, dimensionalise=False)

            self._coefficients[i, L, j, k, :] = dimensionalise_coefficients(
                coefficients, shape)

            return coefficients

        with tqdm(total=total_coefficients) as progress:
            for ijk, chroma in zip(cube_indexes, chromas):
                progress.update()

                # Starts from somewhere in the middle, similarly to how
                # feedback works in "colour.recovery.\
                # find_coefficients_Jakob2019" definition.
                L_middle = lightness_steps // 3
                coefficients_middle = optimize(
                    np.hstack([ijk, L_middle]), zeros(3))

                # Goes down the lightness scale.
                coefficients_0 = coefficients_middle
                for L in reversed(range(0, L_middle)):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0)

                # Goes up the lightness scale.
                coefficients_0 = coefficients_middle
                for L in range(L_middle + 1, lightness_steps):
                    coefficients_0 = optimize(
                        np.hstack([ijk, L]), coefficients_0)

        self._size = size
        self._create_interpolator()
Exemple #11
0
def find_coefficients_Jakob2019(
        XYZ,
        cmfs=MSDS_CMFS_STANDARD_OBSERVER['CIE 1931 2 Degree Standard Observer']
        .copy().align(SPECTRAL_SHAPE_JAKOB2019),
        illuminant=SDS_ILLUMINANTS['D65'].copy().align(
            SPECTRAL_SHAPE_JAKOB2019),
        coefficients_0=zeros(3),
        max_error=JND_CIE1976 / 100,
        dimensionalise=True):
    """
    Computes the coefficients for *Jakob and Hanika (2019)* reflectance
    spectral model.

    Parameters
    ----------
    XYZ : array_like, (3,)
        *CIE XYZ* tristimulus values to find the coefficients for.
    cmfs : XYZ_ColourMatchingFunctions
        Standard observer colour matching functions.
    illuminant : SpectralDistribution
        Illuminant spectral distribution.
    coefficients_0 : array_like, (3,), optional
        Starting coefficients for the solver.
    max_error : float, optional
        Maximal acceptable error. Set higher to save computational time.
        If *None*, the solver will keep going until it is very close to the
        minimum. The default is ``ACCEPTABLE_DELTA_E``.
    dimensionalise : bool, optional
        If *True*, returned coefficients are dimensionful and will not work
        correctly if fed back as ``coefficients_0``. The default is *True*.

    Returns
    -------
    coefficients : ndarray, (3,)
        Computed coefficients that best fit the given colour.
    error : float
        :math:`\\Delta E_{76}` between the target colour and the colour
        corresponding to the computed coefficients.

    References
    ----------
    :cite:`Jakob2019`

    Examples
    --------
    >>> XYZ = np.array([0.20654008, 0.12197225, 0.05136952])
    >>> find_coefficients_Jakob2019(XYZ)  # doctest: +ELLIPSIS
    (array([  1.3723791...e-04,  -1.3514399...e-01,   3.0838973...e+01]), \
0.0141941...)
    """

    shape = cmfs.shape

    if illuminant.shape != shape:
        runtime_warning(
            'Aligning "{0}" illuminant shape to "{1}" colour matching '
            'functions shape.'.format(illuminant.name, cmfs.name))
        illuminant = illuminant.copy().align(cmfs.shape)

    def optimize(target_o, coefficients_0_o):
        """
        Minimises the error function using *L-BFGS-B* method.
        """

        try:
            result = minimize(
                error_function,
                coefficients_0_o, (target_o, cmfs, illuminant, max_error),
                method='L-BFGS-B',
                jac=True)

            return result.x, result.fun
        except StopMinimizationEarly as error:
            return error.coefficients, error.error

    xy_n = XYZ_to_xy(sd_to_XYZ(illuminant, cmfs))

    XYZ_good = full(3, 0.5)
    coefficients_good = zeros(3)

    divisions = 3
    while divisions < 10:
        XYZ_r = XYZ_good
        coefficient_r = coefficients_good
        keep_divisions = False

        coefficients_0 = coefficient_r
        for i in range(1, divisions):
            XYZ_i = (XYZ - XYZ_r) * i / (divisions - 1) + XYZ_r
            Lab_i = XYZ_to_Lab(XYZ_i)

            coefficients_0, error = optimize(Lab_i, coefficients_0)

            if error > max_error:
                break
            else:
                XYZ_good = XYZ_i
                coefficients_good = coefficients_0
                keep_divisions = True
        else:
            break

        if not keep_divisions:
            divisions += 2

    target = XYZ_to_Lab(XYZ, xy_n)
    coefficients, error = optimize(target, coefficients_0)

    if dimensionalise:
        coefficients = dimensionalise_coefficients(coefficients, shape)

    return coefficients, error
Exemple #12
0
def primitive_grid(width=1,
                   height=1,
                   width_segments=1,
                   height_segments=1,
                   axis='+z'):
    """
    Generates vertices and indices for a filled and outlined grid primitive.

    Parameters
    ----------
    width : float, optional
        Grid width.
    height : float, optional
        Grid height.
    width_segments : int, optional
        Grid segments count along the width.
    height_segments : float, optional
        Grid segments count along the height.
    axis : unicode, optional
        **{'+z', '-x', '+x', '-y', '+y', '-z',
        'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**,
        Axis the primitive will be normal to, or plane the primitive will be
        co-planar with.

    Returns
    -------
    tuple
        Tuple of grid vertices, face indices to produce a filled grid and
        outline indices to produce an outline of the faces of the grid.

    References
    ----------
    :cite:`Cabello2015`

    Examples
    --------
    >>> vertices, faces, outline = primitive_grid()
    >>> print(vertices)
    [([-0.5,  0.5,  0. ], [ 0.,  1.], [ 0.,  0.,  1.], [ 0.,  1.,  0.,  1.])
     ([ 0.5,  0.5,  0. ], [ 1.,  1.], [ 0.,  0.,  1.], [ 1.,  1.,  0.,  1.])
     ([-0.5, -0.5,  0. ], [ 0.,  0.], [ 0.,  0.,  1.], [ 0.,  0.,  0.,  1.])
     ([ 0.5, -0.5,  0. ], [ 1.,  0.], [ 0.,  0.,  1.], [ 1.,  0.,  0.,  1.])]
    >>> print(faces)
    [[0 2 1]
     [2 3 1]]
    >>> print(outline)
    [[0 2]
     [2 3]
     [3 1]
     [1 0]]
    """

    axis = PLANE_TO_AXIS_MAPPING.get(axis, axis).lower()

    x_grid = width_segments
    y_grid = height_segments

    x_grid1 = x_grid + 1
    y_grid1 = y_grid + 1

    # Positions, normals and uvs.
    positions = zeros(x_grid1 * y_grid1 * 3)
    normals = zeros(x_grid1 * y_grid1 * 3)
    uvs = zeros(x_grid1 * y_grid1 * 2)

    y = np.arange(y_grid1) * height / y_grid - height / 2
    x = np.arange(x_grid1) * width / x_grid - width / 2

    positions[::3] = np.tile(x, y_grid1)
    positions[1::3] = -np.repeat(y, x_grid1)

    normals[2::3] = 1

    uvs[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1)
    uvs[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1)

    # Faces and outline.
    faces, outline = [], []
    for i_y in range(y_grid):
        for i_x in range(x_grid):
            a = i_x + x_grid1 * i_y
            b = i_x + x_grid1 * (i_y + 1)
            c = (i_x + 1) + x_grid1 * (i_y + 1)
            d = (i_x + 1) + x_grid1 * i_y

            faces.extend([(a, b, d), (b, c, d)])
            outline.extend([(a, b), (b, c), (c, d), (d, a)])

    positions = np.reshape(positions, (-1, 3))
    uvs = np.reshape(uvs, (-1, 2))
    normals = np.reshape(normals, (-1, 3))

    faces = np.reshape(faces, (-1, 3)).astype(np.uint32)
    outline = np.reshape(outline, (-1, 2)).astype(np.uint32)

    if axis in ('-x', '+x'):
        shift, zero_axis = 1, 0
    elif axis in ('-y', '+y'):
        shift, zero_axis = -1, 1
    elif axis in ('-z', '+z'):
        shift, zero_axis = 0, 2

    sign = -1 if '-' in axis else 1

    positions = np.roll(positions, shift, -1)
    normals = np.roll(normals, shift, -1) * sign
    vertex_colours = np.ravel(positions)
    vertex_colours = np.hstack([
        np.reshape(
            np.interp(vertex_colours,
                      (np.min(vertex_colours), np.max(vertex_colours)),
                      (0, 1)), positions.shape),
        ones([positions.shape[0], 1])
    ])
    vertex_colours[..., zero_axis] = 0

    vertices = zeros(positions.shape[0], [
        ('position', DEFAULT_FLOAT_DTYPE, 3),
        ('uv', DEFAULT_FLOAT_DTYPE, 2),
        ('normal', DEFAULT_FLOAT_DTYPE, 3),
        ('colour', DEFAULT_FLOAT_DTYPE, 4),
    ])

    vertices['position'] = positions
    vertices['uv'] = uvs
    vertices['normal'] = normals
    vertices['colour'] = vertex_colours

    return vertices, faces, outline
Exemple #13
0
def chromatic_adaptation(
    XYZ: ArrayLike,
    XYZ_w: ArrayLike,
    XYZ_b: ArrayLike,
    L_A,
    F_L,
    XYZ_p: ArrayLike = None,
    p: Optional[FloatingOrArrayLike] = None,
    helson_judd_effect: Boolean = False,
    discount_illuminant: Boolean = True,
) -> NDArray:
    """
    Apply chromatic adaptation to given *CIE XYZ* tristimulus values.

    Parameters
    ----------
    XYZ
        *CIE XYZ* tristimulus values of test sample.
    XYZ_b
        *CIE XYZ* tristimulus values of background.
    XYZ_w
        *CIE XYZ* tristimulus values of reference white.
    L_A
        Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`.
    F_L
        Luminance adaptation factor :math:`F_L`.
    XYZ_p
        *CIE XYZ* tristimulus values of proximal field, assumed to be equal to
        background if not specified.
    p
        Simultaneous contrast / assimilation factor :math:`p` with value
        normalised to  domain [-1, 0] when simultaneous contrast occurs and
        normalised to domain [0, 1] when assimilation occurs.
    helson_judd_effect
        Truth value indicating whether the *Helson-Judd* effect should be
        accounted for.
    discount_illuminant
       Truth value indicating if the illuminant should be discounted.

    Returns
    -------
    :class:`numpy.ndarray`
        Adapted *CIE XYZ* tristimulus values.

    Examples
    --------
    >>> XYZ = np.array([19.01, 20.00, 21.78])
    >>> XYZ_b = np.array([95.05, 100.00, 108.88])
    >>> XYZ_w = np.array([95.05, 100.00, 108.88])
    >>> L_A = 318.31
    >>> F_L = 1.16754446415
    >>> chromatic_adaptation(XYZ, XYZ_w, XYZ_b, L_A, F_L)  # doctest: +ELLIPSIS
    array([ 6.8959454...,  6.8959991...,  6.8965708...])

    # Coverage Doctests

    >>> chromatic_adaptation(XYZ, XYZ_w, XYZ_b, L_A, F_L,
    ...                      discount_illuminant=False)  # doctest: +ELLIPSIS
    array([ 6.8525880...,  6.8874417...,  6.9461478...])
    >>> chromatic_adaptation(XYZ, XYZ_w, XYZ_b, L_A, F_L,
    ...                      helson_judd_effect=True)  # doctest: +ELLIPSIS
    array([ 6.8959454...,  6.8959991...,  6.8965708...])
    >>> chromatic_adaptation(XYZ, XYZ_w, XYZ_b, L_A, F_L,
    ...                      XYZ_p=XYZ_b, p=0.5)  # doctest: +ELLIPSIS
    array([ 9.2069020...,  9.2070219...,  9.2078373...])
    """

    XYZ_w = as_float_array(XYZ_w)
    XYZ_b = as_float_array(XYZ_b)
    L_A = as_float_array(L_A)
    F_L = as_float_array(F_L)

    rgb = XYZ_to_rgb(XYZ)
    rgb_w = XYZ_to_rgb(XYZ_w)
    Y_w = XYZ_w[..., 1]
    Y_b = XYZ_b[..., 1]

    h_rgb = 3 * rgb_w / np.sum(rgb_w, axis=-1)[..., np.newaxis]

    # Computing chromatic adaptation factors.
    if not discount_illuminant:
        L_A_p = spow(L_A, 1 / 3)
        F_rgb = (1 + L_A_p + h_rgb) / (1 + L_A_p + (1 / h_rgb))
    else:
        F_rgb = ones(h_rgb.shape)

    # Computing Helson-Judd effect parameters.
    if helson_judd_effect:
        D_rgb = f_n((Y_b / Y_w) * F_L * F_rgb[..., 1]) - f_n(
            (Y_b / Y_w) * F_L * F_rgb)
    else:
        D_rgb = zeros(F_rgb.shape)

    # Computing cone bleach factors.
    B_rgb = (10**7) / ((10**7) + 5 * L_A[..., np.newaxis] * (rgb_w / 100))

    # Computing adjusted reference white signals.
    if XYZ_p is not None and p is not None:
        rgb_p = XYZ_to_rgb(XYZ_p)
        rgb_w = adjusted_reference_white_signals(rgb_p, B_rgb, rgb_w, p)

    # Computing adapted cone responses.
    rgb_a = 1 + B_rgb * (f_n(F_L[..., np.newaxis] * F_rgb * rgb / rgb_w) +
                         D_rgb)

    return rgb_a
Exemple #14
0
def plot_RGB_colourspaces_gamuts(
    colourspaces: Union[RGB_Colourspace, str, Sequence[Union[RGB_Colourspace,
                                                             str]]],
    reference_colourspace: Union[Literal["CAM02LCD", "CAM02SCD", "CAM02UCS",
                                         "CAM16LCD", "CAM16SCD", "CAM16UCS",
                                         "CIE XYZ", "CIE xyY", "CIE Lab",
                                         "CIE Luv", "CIE UCS", "CIE UVW",
                                         "DIN99", "Hunter Lab", "Hunter Rdab",
                                         "ICaCb", "ICtCp", "IPT", "IgPgTg",
                                         "Jzazbz", "OSA UCS", "Oklab",
                                         "hdr-CIELAB", "hdr-IPT", ],
                                 str, ] = "CIE xyY",
    segments: Integer = 8,
    show_grid: Boolean = True,
    grid_segments: Integer = 10,
    show_spectral_locus: Boolean = False,
    spectral_locus_colour: Optional[Union[ArrayLike, str]] = None,
    cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[
        MultiSpectralDistributions,
        str]], ] = "CIE 1931 2 Degree Standard Observer",
    chromatically_adapt: Boolean = False,
    convert_kwargs: Optional[Dict] = None,
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot given *RGB* colourspaces gamuts in given reference colourspace.

    Parameters
    ----------
    colourspaces
        *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements
        can be of any type or form supported by the
        :func:`colour.plotting.filter_RGB_colourspaces` definition.
    reference_colourspace
        Reference colourspace model to plot the gamuts into, see
        :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported
        colourspace models.
    segments
        Edge segments count for each *RGB* colourspace cubes.
    show_grid
        Whether to show a grid at the bottom of the *RGB* colourspace cubes.
    grid_segments
        Edge segments count for the grid.
    show_spectral_locus
        Whether to show the spectral locus.
    spectral_locus_colour
        Spectral locus colour.
    cmfs
        Standard observer colour matching functions used for computing the
        spectral locus boundaries. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.filter_cmfs` definition.
    chromatically_adapt
        Whether to chromatically adapt the *RGB* colourspaces given in
        ``colourspaces`` to the whitepoint of the default plotting colourspace.
    convert_kwargs
        Keyword arguments for the :func:`colour.convert` definition.

    Other Parameters
    ----------------
    edge_colours
        Edge colours array such as `edge_colours = (None, (0.5, 0.5, 1.0))`.
    edge_alpha
        Edge opacity value such as `edge_alpha = (0.0, 1.0)`.
    face_alpha
        Face opacity value such as `face_alpha = (0.5, 1.0)`.
    face_colours
        Face colours array such as `face_colours = (None, (0.5, 0.5, 1.0))`.
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.volume.nadir_grid`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_RGB_colourspaces_gamuts(['ITU-R BT.709', 'ACEScg', 'S-Gamut'])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>)

    .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png
        :align: center
        :alt: plot_RGB_colourspaces_gamuts
    """

    colourspaces = cast(
        List[RGB_Colourspace],
        list(filter_RGB_colourspaces(colourspaces).values()),
    )

    convert_kwargs = optional(convert_kwargs, {})

    count_c = len(colourspaces)

    title = (
        f"{', '.join([colourspace.name for colourspace in colourspaces])} "
        f"- {reference_colourspace} Reference Colourspace")

    illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint

    convert_settings = {"illuminant": illuminant}
    convert_settings.update(convert_kwargs)

    settings = Structure(
        **{
            "face_colours": [None] * count_c,
            "edge_colours": [None] * count_c,
            "face_alpha": [1] * count_c,
            "edge_alpha": [1] * count_c,
            "title": title,
        })
    settings.update(kwargs)

    figure = plt.figure()
    axes = figure.add_subplot(111, projection="3d")

    points = zeros((4, 3))
    if show_spectral_locus:
        cmfs = cast(MultiSpectralDistributions,
                    first_item(filter_cmfs(cmfs).values()))
        XYZ = cmfs.values

        points = colourspace_model_axis_reorder(
            convert(XYZ, "CIE XYZ", reference_colourspace, **convert_settings),
            reference_colourspace,
        )

        points[np.isnan(points)] = 0

        c = ((0.0, 0.0, 0.0,
              0.5) if spectral_locus_colour is None else spectral_locus_colour)

        axes.plot(
            points[..., 0],
            points[..., 1],
            points[..., 2],
            color=c,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
        )
        axes.plot(
            (points[-1][0], points[0][0]),
            (points[-1][1], points[0][1]),
            (points[-1][2], points[0][2]),
            color=c,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
        )

    plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace

    quads_c: List = []
    RGB_cf: List = []
    RGB_ce: List = []
    for i, colourspace in enumerate(colourspaces):

        if chromatically_adapt and not np.array_equal(
                colourspace.whitepoint, plotting_colourspace.whitepoint):
            colourspace = colourspace.chromatically_adapt(
                plotting_colourspace.whitepoint,
                plotting_colourspace.whitepoint_name,
            )

        quads_cb, RGB = RGB_identity_cube(
            width_segments=segments,
            height_segments=segments,
            depth_segments=segments,
        )

        XYZ = RGB_to_XYZ(
            quads_cb,
            colourspace.whitepoint,
            colourspace.whitepoint,
            colourspace.matrix_RGB_to_XYZ,
        )

        convert_settings = {"illuminant": colourspace.whitepoint}
        convert_settings.update(convert_kwargs)

        quads_c.extend(
            colourspace_model_axis_reorder(
                convert(XYZ, "CIE XYZ", reference_colourspace,
                        **convert_settings),
                reference_colourspace,
            ))

        if settings.face_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.face_colours[i]

        RGB_cf.extend(
            np.hstack([RGB,
                       full((RGB.shape[0], 1), settings.face_alpha[i])]))

        if settings.edge_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.edge_colours[i]

        RGB_ce.extend(
            np.hstack([RGB,
                       full((RGB.shape[0], 1), settings.edge_alpha[i])]))

    quads = as_float_array(quads_c)
    RGB_f = as_float_array(RGB_cf)
    RGB_e = as_float_array(RGB_ce)

    quads[np.isnan(quads)] = 0

    if quads.size != 0:
        for i, axis in enumerate("xyz"):
            min_a = np.minimum(np.min(quads[..., i]), np.min(points[..., i]))
            max_a = np.maximum(np.max(quads[..., i]), np.max(points[..., i]))
            getattr(axes, f"set_{axis}lim")((min_a, max_a))

    labels = np.array(
        COLOURSPACE_MODELS_AXIS_LABELS[reference_colourspace])[as_int_array(
            colourspace_model_axis_reorder([0, 1, 2], reference_colourspace))]
    for i, axis in enumerate("xyz"):
        getattr(axes, f"set_{axis}label")(labels[i])

    if show_grid:
        limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])

        quads_g, RGB_gf, RGB_ge = nadir_grid(limits, grid_segments, labels,
                                             axes, **settings)
        quads = np.vstack([quads_g, quads])
        RGB_f = np.vstack([RGB_gf, RGB_f])
        RGB_e = np.vstack([RGB_ge, RGB_e])

    collection = Poly3DCollection(quads)
    collection.set_facecolors(RGB_f)
    collection.set_edgecolors(RGB_e)

    axes.add_collection3d(collection)

    settings.update({
        "axes": axes,
        "axes_visible": False,
        "camera_aspect": "equal"
    })
    settings.update(kwargs)

    return render(**settings)
Exemple #15
0
def plot_RGB_colourspaces_gamuts(colourspaces,
                                 reference_colourspace='CIE xyY',
                                 segments=8,
                                 show_grid=True,
                                 grid_segments=10,
                                 show_spectral_locus=False,
                                 spectral_locus_colour=None,
                                 cmfs='CIE 1931 2 Degree Standard Observer',
                                 chromatically_adapt=False,
                                 **kwargs):
    """
    Plots given *RGB* colourspaces gamuts in given reference colourspace.

    Parameters
    ----------
    colourspaces : unicode or RGB_Colourspace or array_like
        *RGB* colourspaces to plot the gamuts. ``colourspaces`` elements
        can be of any type or form supported by the
        :func:`colour.plotting.filter_RGB_colourspaces` definition.
    reference_colourspace : unicode, optional
        **{'CIE XYZ', 'CIE xyY', 'CIE xy', 'CIE Lab', 'CIE LCHab', 'CIE Luv',
        'CIE Luv uv', 'CIE LCHuv', 'CIE UCS', 'CIE UCS uv', 'CIE UVW',
        'DIN 99', 'Hunter Lab', 'Hunter Rdab', 'IPT', 'JzAzBz', 'OSA UCS',
        'hdr-CIELAB', 'hdr-IPT'}**,
        Reference colourspace to plot the gamuts into.
    segments : int, optional
        Edge segments count for each *RGB* colourspace cubes.
    show_grid : bool, optional
        Whether to show a grid at the bottom of the *RGB* colourspace cubes.
    grid_segments : bool, optional
        Edge segments count for the grid.
    show_spectral_locus : bool, optional
        Whether to show the spectral locus.
    spectral_locus_colour : array_like, optional
        Spectral locus colour.
    cmfs : unicode or XYZ_ColourMatchingFunctions, optional
        Standard observer colour matching functions used for computing the
        spectral locus boundaries. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.filter_cmfs` definition.
    chromatically_adapt : bool, optional
        Whether to chromatically adapt the *RGB* colourspaces given in
        ``colourspaces`` to the whitepoint of the default plotting colourspace.

    Other Parameters
    ----------------
    \\**kwargs : dict, optional
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.volume.nadir_grid`},
        Please refer to the documentation of the previously listed definitions.
    face_colours : array_like, optional
        Face colours array such as `face_colours = (None, (0.5, 0.5, 1.0))`.
    edge_colours : array_like, optional
        Edge colours array such as `edge_colours = (None, (0.5, 0.5, 1.0))`.
    face_alpha : numeric, optional
        Face opacity value such as `face_alpha = (0.5, 1.0)`.
    edge_alpha : numeric, optional
        Edge opacity value such as `edge_alpha = (0.0, 1.0)`.

    Returns
    -------
    tuple
        Current figure and axes.

    Examples
    --------
    >>> plot_RGB_colourspaces_gamuts(['ITU-R BT.709', 'ACEScg', 'S-Gamut'])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes3DSubplot...>)

    .. image:: ../_static/Plotting_Plot_RGB_Colourspaces_Gamuts.png
        :align: center
        :alt: plot_RGB_colourspaces_gamuts
    """

    colourspaces = filter_RGB_colourspaces(colourspaces).values()

    count_c = len(colourspaces)

    title = '{0} - {1} Reference Colourspace'.format(
        ', '.join([colourspace.name for colourspace in colourspaces]),
        reference_colourspace,
    )

    settings = Structure(
        **{
            'face_colours': [None] * count_c,
            'edge_colours': [None] * count_c,
            'face_alpha': [1] * count_c,
            'edge_alpha': [1] * count_c,
            'title': title,
        })
    settings.update(kwargs)

    figure = plt.figure()
    axes = figure.add_subplot(111, projection='3d')

    illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint

    points = zeros([4, 3])
    if show_spectral_locus:
        cmfs = first_item(filter_cmfs(cmfs).values())
        XYZ = cmfs.values

        points = common_colourspace_model_axis_reorder(
            XYZ_to_colourspace_model(XYZ, illuminant, reference_colourspace),
            reference_colourspace)

        points[np.isnan(points)] = 0

        c = ((0.0, 0.0, 0.0, 0.5)
             if spectral_locus_colour is None else spectral_locus_colour)

        axes.plot(
            points[..., 0], points[..., 1], points[..., 2], color=c, zorder=10)
        axes.plot(
            (points[-1][0], points[0][0]), (points[-1][1], points[0][1]),
            (points[-1][2], points[0][2]),
            color=c,
            zorder=10)

    plotting_colourspace = CONSTANTS_COLOUR_STYLE.colour.colourspace

    quads, RGB_f, RGB_e = [], [], []
    for i, colourspace in enumerate(colourspaces):

        if chromatically_adapt and not np.array_equal(
                colourspace.whitepoint, plotting_colourspace.whitepoint):
            colourspace = colourspace.chromatically_adapt(
                plotting_colourspace.whitepoint,
                plotting_colourspace.whitepoint_name)

        quads_c, RGB = RGB_identity_cube(
            width_segments=segments,
            height_segments=segments,
            depth_segments=segments)

        XYZ = RGB_to_XYZ(
            quads_c,
            colourspace.whitepoint,
            colourspace.whitepoint,
            colourspace.matrix_RGB_to_XYZ,
        )

        quads.extend(
            common_colourspace_model_axis_reorder(
                XYZ_to_colourspace_model(
                    XYZ,
                    colourspace.whitepoint,
                    reference_colourspace,
                ), reference_colourspace))

        if settings.face_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.face_colours[i]

        RGB_f.extend(
            np.hstack([RGB,
                       full([RGB.shape[0], 1], settings.face_alpha[i])]))

        if settings.edge_colours[i] is not None:
            RGB = ones(RGB.shape) * settings.edge_colours[i]

        RGB_e.extend(
            np.hstack([RGB,
                       full([RGB.shape[0], 1], settings.edge_alpha[i])]))

    quads = as_float_array(quads)
    quads[np.isnan(quads)] = 0

    if quads.size != 0:
        for i, axis in enumerate('xyz'):
            min_a = min(np.min(quads[..., i]), np.min(points[..., i]))
            max_a = max(np.max(quads[..., i]), np.max(points[..., i]))
            getattr(axes, 'set_{}lim'.format(axis))((min_a, max_a))

    labels = np.array(
        COLOURSPACE_MODELS_AXIS_LABELS[reference_colourspace])[as_int_array(
            common_colourspace_model_axis_reorder([0, 1, 2],
                                                  reference_colourspace))]
    for i, axis in enumerate('xyz'):
        getattr(axes, 'set_{}label'.format(axis))(labels[i])

    if show_grid:
        limits = np.array([[-1.5, 1.5], [-1.5, 1.5]])

        quads_g, RGB_gf, RGB_ge = nadir_grid(limits, grid_segments, labels,
                                             axes, **settings)
        quads = np.vstack([quads_g, quads])
        RGB_f = np.vstack([RGB_gf, RGB_f])
        RGB_e = np.vstack([RGB_ge, RGB_e])

    collection = Poly3DCollection(quads)
    collection.set_facecolors(RGB_f)
    collection.set_edgecolors(RGB_e)

    axes.add_collection3d(collection)

    settings.update({
        'axes': axes,
        'axes_visible': False,
        'camera_aspect': 'equal'
    })
    settings.update(kwargs)

    return render(**settings)
Exemple #16
0
def plot_planckian_locus(planckian_locus_colours=None,
                         method='CIE 1931',
                         **kwargs):
    """
    Plots the *Planckian Locus* according to given method.

    Parameters
    ----------
    planckian_locus_colours : array_like or unicode, optional
        *Planckian Locus* colours.
    method : unicode, optional
        **{'CIE 1931', 'CIE 1960 UCS', 'CIE 1976 UCS'}**,
        *Chromaticity Diagram* method.

    Other Parameters
    ----------------
    \\**kwargs : dict, optional
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        Please refer to the documentation of the previously listed definitions.

    Returns
    -------
    tuple
        Current figure and axes.

    Examples
    --------
    >>> plot_planckian_locus()  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...AxesSubplot...>)

    .. image:: ../_static/Plotting_Plot_Planckian_Locus.png
        :align: center
        :alt: plot_planckian_locus
    """

    if planckian_locus_colours is None:
        planckian_locus_colours = CONSTANTS_COLOUR_STYLE.colour.dark

    settings = {'uniform': True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    if method == 'CIE 1931':

        def uv_to_ij(uv):
            """
            Converts given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return UCS_uv_to_xy(uv)

        D_uv = 0.025
    elif method == 'CIE 1960 UCS':

        def uv_to_ij(uv):
            """
            Converts given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return uv

        D_uv = 0.025
    else:
        raise ValueError('Invalid method: "{0}", must be one of '
                         '[\'CIE 1931\', \'CIE 1960 UCS\']'.format(method))

    start, end = 1667, 100000
    CCT = np.arange(start, end + 250, 250)
    CCT_D_uv = tstack([CCT, zeros(CCT.shape)])
    ij = uv_to_ij(CCT_to_uv(CCT_D_uv, 'Robertson 1968'))

    axes.plot(ij[..., 0], ij[..., 1], color=planckian_locus_colours)

    for i in (1667, 2000, 2500, 3000, 4000, 6000, 10000):
        i0, j0 = uv_to_ij(CCT_to_uv(np.array([i, -D_uv]), 'Robertson 1968'))
        i1, j1 = uv_to_ij(CCT_to_uv(np.array([i, D_uv]), 'Robertson 1968'))
        axes.plot((i0, i1), (j0, j1), color=planckian_locus_colours)
        axes.annotate('{0}K'.format(i),
                      xy=(i0, j0),
                      xytext=(0, -10),
                      textcoords='offset points',
                      size='x-small')

    settings = {'axes': axes}
    settings.update(kwargs)

    return render(**settings)
Exemple #17
0
def generate_pulse_waves(
    bins: Integer,
    pulse_order: Union[Literal["Bins", "Pulse Wave Width"], str] = "Bins",
    filter_jagged_pulses: Boolean = False,
) -> NDArray:
    """
    Generate the pulse waves of given number of bins necessary to totally
    stimulate the colour matching functions and produce the *Rösch-MacAdam*
    colour solid.

    Assuming 5 bins, a first set of SPDs would be as follows::

        1 0 0 0 0
        0 1 0 0 0
        0 0 1 0 0
        0 0 0 1 0
        0 0 0 0 1

    The second one::

        1 1 0 0 0
        0 1 1 0 0
        0 0 1 1 0
        0 0 0 1 1
        1 0 0 0 1

    The third:

        1 1 1 0 0
        0 1 1 1 0
        0 0 1 1 1
        1 0 0 1 1
        1 1 0 0 1

    Etc...

    Parameters
    ----------
    bins
        Number of bins of the pulse waves.
    pulse_order
        Method for ordering the pulse waves. *Bins* is the default order, with
        *Pulse Wave Width* ordering, instead of iterating over the pulse wave
        widths first, iteration occurs over the bins, producing blocks of pulse
        waves with increasing width.
    filter_jagged_pulses
        Whether to filter jagged pulses. When ``pulse_order`` is set to
        *Pulse Wave Width*, the pulses are ordered by increasing width. Because
        of the discrete nature of the underlying signal, the resulting pulses
        will be jagged. For example assuming 5 bins, the center block with
        the two extreme values added would be as follows::

            0 0 0 0 0
            0 0 1 0 0
            0 0 1 1 0 <--
            0 1 1 1 0
            0 1 1 1 1 <--
            1 1 1 1 1

        Setting the ``filter_jagged_pulses`` parameter to `True` will result
        in the removal of the two marked pulse waves above thus avoiding jagged
        lines when plotting and having to resort to excessive ``bins`` values.

    Returns
    -------
    :class:`numpy.ndarray`
        Pulse waves.

    References
    ----------
    :cite:`Lindbloom2015`, :cite:`Mansencal2018`, :cite:`Martinez-Verdu2007`

    Examples
    --------
    >>> generate_pulse_waves(5)
    array([[ 0.,  0.,  0.,  0.,  0.],
           [ 1.,  0.,  0.,  0.,  0.],
           [ 0.,  1.,  0.,  0.,  0.],
           [ 0.,  0.,  1.,  0.,  0.],
           [ 0.,  0.,  0.,  1.,  0.],
           [ 0.,  0.,  0.,  0.,  1.],
           [ 1.,  1.,  0.,  0.,  0.],
           [ 0.,  1.,  1.,  0.,  0.],
           [ 0.,  0.,  1.,  1.,  0.],
           [ 0.,  0.,  0.,  1.,  1.],
           [ 1.,  0.,  0.,  0.,  1.],
           [ 1.,  1.,  1.,  0.,  0.],
           [ 0.,  1.,  1.,  1.,  0.],
           [ 0.,  0.,  1.,  1.,  1.],
           [ 1.,  0.,  0.,  1.,  1.],
           [ 1.,  1.,  0.,  0.,  1.],
           [ 1.,  1.,  1.,  1.,  0.],
           [ 0.,  1.,  1.,  1.,  1.],
           [ 1.,  0.,  1.,  1.,  1.],
           [ 1.,  1.,  0.,  1.,  1.],
           [ 1.,  1.,  1.,  0.,  1.],
           [ 1.,  1.,  1.,  1.,  1.]])
    >>> generate_pulse_waves(5, 'Pulse Wave Width')
    array([[ 0.,  0.,  0.,  0.,  0.],
           [ 1.,  0.,  0.,  0.,  0.],
           [ 1.,  1.,  0.,  0.,  0.],
           [ 1.,  1.,  0.,  0.,  1.],
           [ 1.,  1.,  1.,  0.,  1.],
           [ 0.,  1.,  0.,  0.,  0.],
           [ 0.,  1.,  1.,  0.,  0.],
           [ 1.,  1.,  1.,  0.,  0.],
           [ 1.,  1.,  1.,  1.,  0.],
           [ 0.,  0.,  1.,  0.,  0.],
           [ 0.,  0.,  1.,  1.,  0.],
           [ 0.,  1.,  1.,  1.,  0.],
           [ 0.,  1.,  1.,  1.,  1.],
           [ 0.,  0.,  0.,  1.,  0.],
           [ 0.,  0.,  0.,  1.,  1.],
           [ 0.,  0.,  1.,  1.,  1.],
           [ 1.,  0.,  1.,  1.,  1.],
           [ 0.,  0.,  0.,  0.,  1.],
           [ 1.,  0.,  0.,  0.,  1.],
           [ 1.,  0.,  0.,  1.,  1.],
           [ 1.,  1.,  0.,  1.,  1.],
           [ 1.,  1.,  1.,  1.,  1.]])
    >>> generate_pulse_waves(5, 'Pulse Wave Width', True)
    array([[ 0.,  0.,  0.,  0.,  0.],
           [ 1.,  0.,  0.,  0.,  0.],
           [ 1.,  1.,  0.,  0.,  1.],
           [ 0.,  1.,  0.,  0.,  0.],
           [ 1.,  1.,  1.,  0.,  0.],
           [ 0.,  0.,  1.,  0.,  0.],
           [ 0.,  1.,  1.,  1.,  0.],
           [ 0.,  0.,  0.,  1.,  0.],
           [ 0.,  0.,  1.,  1.,  1.],
           [ 0.,  0.,  0.,  0.,  1.],
           [ 1.,  0.,  0.,  1.,  1.],
           [ 1.,  1.,  1.,  1.,  1.]])
    """

    pulse_order = validate_method(
        pulse_order,
        ["Bins", "Pulse Wave Width"],
        '"{0}" pulse order is invalid, it must be one of {1}!',
    )

    square_waves = []
    square_waves_basis = np.tril(
        np.ones((bins, bins), dtype=DEFAULT_FLOAT_DTYPE))[0:-1, :]

    if pulse_order.lower() == "bins":
        for square_wave_basis in square_waves_basis:
            for i in range(bins):
                square_waves.append(np.roll(square_wave_basis, i))
    else:
        for i in range(bins):
            for j, square_wave_basis in enumerate(square_waves_basis):
                square_waves.append(np.roll(square_wave_basis, i - j // 2))

        if filter_jagged_pulses:
            square_waves = square_waves[::2]

    return np.vstack([
        zeros(bins),
        np.vstack(square_waves),
        np.ones(bins, dtype=DEFAULT_FLOAT_DTYPE),
    ])
Exemple #18
0
def spectral_similarity_index(sd_test, sd_reference):
    """
    Returns the *Academy Spectral Similarity Index* (SSI) of given test
    spectral distribution with given reference spectral distribution.

    Parameters
    ----------
    sd_test : SpectralDistribution
        Test spectral distribution.
    sd_reference : SpectralDistribution
        Reference spectral distribution.

    Returns
    -------
    numeric
        *Academy Spectral Similarity Index* (SSI).

    References
    ----------
    :cite:`TheAcademyofMotionPictureArtsandSciences2019`

    Examples
    --------
    >>> from colour import SDS_ILLUMINANTS
    >>> sd_test = SDS_ILLUMINANTS['C']
    >>> sd_reference = SDS_ILLUMINANTS['D65']
    >>> spectral_similarity_index(sd_test, sd_reference)
    94.0
    """

    global _MATRIX_INTEGRATION

    if _MATRIX_INTEGRATION is None:
        _MATRIX_INTEGRATION = zeros([
            len(_SPECTRAL_SHAPE_SSI_LARGE.range()),
            len(SPECTRAL_SHAPE_SSI.range())
        ])

        weights = np.array([0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.5])

        for i in range(_MATRIX_INTEGRATION.shape[0]):
            _MATRIX_INTEGRATION[i, (10 * i):(10 * i + 11)] = weights

    settings = {
        'interpolator': LinearInterpolator,
        'extrapolator_kwargs': {
            'left': 0,
            'right': 0
        }
    }

    sd_test = sd_test.copy().align(SPECTRAL_SHAPE_SSI, **settings)
    sd_reference = sd_reference.copy().align(SPECTRAL_SHAPE_SSI, **settings)

    test_i = np.dot(_MATRIX_INTEGRATION, sd_test.values)
    reference_i = np.dot(_MATRIX_INTEGRATION, sd_reference.values)

    test_i /= np.sum(test_i)
    reference_i /= np.sum(reference_i)

    d_i = test_i - reference_i
    dr_i = d_i / (reference_i + np.mean(reference_i))
    wdr_i = dr_i * [
        12 / 45, 22 / 45, 32 / 45, 40 / 45, 44 / 45, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11 / 15, 3 / 15
    ]
    c_wdr_i = convolve1d(np.hstack([0, wdr_i, 0]), [0.22, 0.56, 0.22])
    m_v = np.sum(c_wdr_i**2)

    SSI = np.around(100 - 32 * np.sqrt(m_v))

    return SSI
Exemple #19
0
def primitive_vertices_sphere(radius=0.5,
                              segments=8,
                              intermediate=False,
                              origin=np.array([0, 0, 0]),
                              axis='+z'):
    """
    Returns the vertices of a latitude-longitude sphere primitive.

    Parameters
    ----------
    radius: numeric, optional
        Sphere radius.
    segments: numeric, optional
        Latitude-longitude segments, if the ``intermediate`` argument is
        *True*, then the sphere will have one less segment along its longitude.
    intermediate: bool, optional
        Whether to generate the sphere vertices at the center of the faces
        outlined by the segments of a regular sphere generated without
        the ``intermediate`` argument set to *True*. The resulting sphere is
        inscribed on the regular sphere faces but possesses the same poles.
    origin: array_like, optional
        Sphere origin on the construction plane.
    axis : array_like, optional
        **{'+z', '+x', '+y', 'yz', 'xz', 'xy'}**,
        Axis (or normal of the plane) the poles of the sphere will be aligned
        with.

    Returns
    -------
    ndarray
        Sphere primitive vertices.

    Notes
    -----
    -   The sphere poles have latitude segments count - 1 co-located vertices.

    Examples
    --------
    >>> primitive_vertices_sphere(segments=4)  # doctest: +ELLIPSIS
    array([[[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [ -3.5355339...e-01,  -4.3297802...e-17,   3.5355339...e-01],
            [ -5.0000000...e-01,  -6.1232340...e-17,   3.0616170...e-17],
            [ -3.5355339...e-01,  -4.3297802...e-17,  -3.5355339...e-01],
            [ -6.1232340...e-17,  -7.4987989...e-33,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,  -3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,  -5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,  -3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,  -6.1232340...e-17,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  3.5355339...e-01,   0.0000000...e+00,   3.5355339...e-01],
            [  5.0000000...e-01,   0.0000000...e+00,   3.0616170...e-17],
            [  3.5355339...e-01,   0.0000000...e+00,  -3.5355339...e-01],
            [  6.1232340...e-17,   0.0000000...e+00,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,   3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,   5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,   3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,   6.1232340...e-17,  -5.0000000...e-01]]])
    """

    axis = PLANE_TO_AXIS_MAPPING.get(axis, axis).lower()

    if not intermediate:
        theta = np.tile(np.radians(np.linspace(0, 180, segments + 1)),
                        (segments + 1, 1))
        phi = np.transpose(
            np.tile(np.radians(np.linspace(-180, 180, segments + 1)),
                    (segments + 1, 1)))
    else:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]),
            (segments + 1, 1))
        theta = np.hstack([
            zeros([segments + 1, 1]),
            theta,
            full([segments + 1, 1], np.pi),
        ])
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)) +
                np.radians(360 / segments / 2), (segments, 1)))

    rho = ones(phi.shape) * radius
    rho_theta_phi = tstack([rho, theta, phi])

    vertices = spherical_to_cartesian(rho_theta_phi)

    # Removing extra longitude vertices.
    vertices = vertices[:-1, :, :]

    if axis == '+z':
        pass
    elif axis == '+y':
        vertices = np.roll(vertices, 2, -1)
    elif axis == '+x':
        vertices = np.roll(vertices, 1, -1)
    else:
        raise ValueError('Axis must be one of "{0}"!'.format(
            ['+x', '+y', '+z']))

    vertices += origin

    return vertices
Exemple #20
0
def plot_planckian_locus(
    planckian_locus_colours: Optional[Union[ArrayLike, str]] = None,
    planckian_locus_opacity: Floating = 1,
    planckian_locus_labels: Optional[Sequence] = None,
    method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"],
                  str] = "CIE 1931",
    **kwargs: Any,
) -> Tuple[plt.Figure, plt.Axes]:
    """
    Plot the *Planckian Locus* according to given method.

    Parameters
    ----------
    planckian_locus_colours
        Colours of the *Planckian Locus*, if ``planckian_locus_colours`` is set
        to *RGB*, the colours will be computed according to the corresponding
        chromaticity coordinates.
    planckian_locus_opacity
       Opacity of the *Planckian Locus*.
    planckian_locus_labels
        Array of labels used to customise which iso-temperature lines will be
        drawn along the *Planckian Locus*. Passing an empty array will result
        in no iso-temperature lines being drawn.
    method
        *Chromaticity Diagram* method.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_planckian_locus(planckian_locus_colours='RGB')
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...AxesSubplot...>)

    .. image:: ../_static/Plotting_Plot_Planckian_Locus.png
        :align: center
        :alt: plot_planckian_locus
    """

    method = validate_method(method,
                             ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"])

    planckian_locus_colours = optional(planckian_locus_colours,
                                       CONSTANTS_COLOUR_STYLE.colour.dark)

    labels = cast(
        Tuple,
        optional(
            planckian_locus_labels,
            (10**6 / 600, 2000, 2500, 3000, 4000, 6000, 10**6 / 100),
        ),
    )
    D_uv = 0.05

    settings: Dict[str, Any] = {"uniform": True}
    settings.update(kwargs)

    _figure, axes = artist(**settings)

    if method == "cie 1931":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return UCS_uv_to_xy(uv)

    elif method == "cie 1960 ucs":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return uv

    elif method == "cie 1976 ucs":

        def uv_to_ij(uv: NDArray) -> NDArray:
            """
            Convert given *uv* chromaticity coordinates to *ij* chromaticity
            coordinates.
            """

            return xy_to_Luv_uv(UCS_uv_to_xy(uv))

    def CCT_D_uv_to_plotting_colourspace(CCT_D_uv):
        """
        Convert given *uv* chromaticity coordinates to the default plotting
        colourspace.
        """

        return normalise_maximum(
            XYZ_to_plotting_colourspace(
                xy_to_XYZ(UCS_uv_to_xy(CCT_to_uv(CCT_D_uv,
                                                 "Robertson 1968")))),
            axis=-1,
        )

    start, end = 10**6 / 600, 10**6 / 10
    CCT = np.arange(start, end + 100, 100)
    CCT_D_uv = np.reshape(tstack([CCT, zeros(CCT.shape)]), (-1, 1, 2))
    ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968"))

    use_RGB_planckian_locus_colours = (
        str(planckian_locus_colours).upper() == "RGB")
    if use_RGB_planckian_locus_colours:
        pl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv)
    else:
        pl_colours = planckian_locus_colours

    line_collection = LineCollection(
        np.concatenate([ij[:-1], ij[1:]], axis=1),
        colors=pl_colours,
        alpha=planckian_locus_opacity,
        zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
    )
    axes.add_collection(line_collection)

    for label in labels:
        CCT_D_uv = np.reshape(
            tstack([full(10, label),
                    np.linspace(-D_uv, D_uv, 10)]), (-1, 1, 2))

        if use_RGB_planckian_locus_colours:
            itl_colours = CCT_D_uv_to_plotting_colourspace(CCT_D_uv)
        else:
            itl_colours = planckian_locus_colours

        ij = uv_to_ij(CCT_to_uv(CCT_D_uv, "Robertson 1968"))

        line_collection = LineCollection(
            np.concatenate([ij[:-1], ij[1:]], axis=1),
            colors=itl_colours,
            alpha=planckian_locus_opacity,
            zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
        )
        axes.add_collection(line_collection)
        axes.annotate(
            f"{as_int_scalar(label)}K",
            xy=(ij[-1, :, 0], ij[-1, :, 1]),
            xytext=(0, CONSTANTS_COLOUR_STYLE.geometry.long / 2),
            textcoords="offset points",
            size="x-small",
            zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_label,
        )

    settings = {"axes": axes}
    settings.update(kwargs)

    return render(**settings)
Exemple #21
0
def generate_pulse_waves(bins):
    """
    Generates the pulse waves of given number of bins necessary to totally
    stimulate the colour matching functions.

    Assuming 5 bins, a first set of SPDs would be as follows::

        1 0 0 0 0
        0 1 0 0 0
        0 0 1 0 0
        0 0 0 1 0
        0 0 0 0 1

    The second one::

        1 1 0 0 0
        0 1 1 0 0
        0 0 1 1 0
        0 0 0 1 1
        1 0 0 0 1

    The third:

        1 1 1 0 0
        0 1 1 1 0
        0 0 1 1 1
        1 0 0 1 1
        1 1 0 0 1

    Etc...

    Parameters
    ----------
    bins : int
        Number of bins of the pulse waves.

    Returns
    -------
    ndarray
        Pulse waves.

    References
    ----------
    :cite:`Lindbloom2015`, :cite:`Mansencal2018`

    Examples
    --------
    >>> generate_pulse_waves(5)
    array([[ 0.,  0.,  0.,  0.,  0.],
           [ 1.,  0.,  0.,  0.,  0.],
           [ 0.,  1.,  0.,  0.,  0.],
           [ 0.,  0.,  1.,  0.,  0.],
           [ 0.,  0.,  0.,  1.,  0.],
           [ 0.,  0.,  0.,  0.,  1.],
           [ 1.,  1.,  0.,  0.,  0.],
           [ 0.,  1.,  1.,  0.,  0.],
           [ 0.,  0.,  1.,  1.,  0.],
           [ 0.,  0.,  0.,  1.,  1.],
           [ 1.,  0.,  0.,  0.,  1.],
           [ 1.,  1.,  1.,  0.,  0.],
           [ 0.,  1.,  1.,  1.,  0.],
           [ 0.,  0.,  1.,  1.,  1.],
           [ 1.,  0.,  0.,  1.,  1.],
           [ 1.,  1.,  0.,  0.,  1.],
           [ 1.,  1.,  1.,  1.,  0.],
           [ 0.,  1.,  1.,  1.,  1.],
           [ 1.,  0.,  1.,  1.,  1.],
           [ 1.,  1.,  0.,  1.,  1.],
           [ 1.,  1.,  1.,  0.,  1.],
           [ 1.,  1.,  1.,  1.,  1.]])
    """

    square_waves = []
    square_waves_basis = np.tril(
        np.ones((bins, bins), dtype=DEFAULT_FLOAT_DTYPE))[0:-1, :]
    for square_wave_basis in square_waves_basis:
        for i in range(bins):
            square_waves.append(np.roll(square_wave_basis, i))

    return np.vstack([
        zeros(bins),
        np.vstack(square_waves),
        np.ones(bins, dtype=DEFAULT_FLOAT_DTYPE)
    ])
Exemple #22
0
def primitive_cube(
    width: Floating = 1,
    height: Floating = 1,
    depth: Floating = 1,
    width_segments: Integer = 1,
    height_segments: Integer = 1,
    depth_segments: Integer = 1,
    planes: Optional[Literal["-x", "+x", "-y", "+y", "-z", "+z", "xy", "xz",
                             "yz", "yx", "zx", "zy", ]] = None,
    dtype_vertices: Optional[Type[DTypeFloating]] = None,
    dtype_indexes: Optional[Type[DTypeInteger]] = None,
) -> Tuple[NDArray, NDArray, NDArray]:
    """
    Generate vertices and indexes for a filled and outlined cube primitive.

    Parameters
    ----------
    width
        Cube width.
    height
        Cube height.
    depth
        Cube depth.
    width_segments
        Cube segments count along the width.
    height_segments
        Cube segments count along the height.
    depth_segments
        Cube segments count along the depth.
    planes
        Grid primitives to include in the cube construction.
    dtype_vertices
        :class:`numpy.dtype` to use for the grid vertices, default to
        the :class:`numpy.dtype` defined by the
        :attr:`colour.constant.DEFAULT_FLOAT_DTYPE` attribute.
    dtype_indexes
        :class:`numpy.dtype` to use for the grid indexes, default to
        the :class:`numpy.dtype` defined by the
        :attr:`colour.constant.DEFAULT_INT_DTYPE` attribute.

    Returns
    -------
    :class:`tuple`
        Tuple of cube vertices, face indexes to produce a filled cube and
        outline indexes to produce an outline of the faces of the cube.

    Examples
    --------
    >>> vertices, faces, outline = primitive_cube()
    >>> print(vertices)
    [([-0.5,  0.5, -0.5], [ 0.,  1.], [-0., -0., -1.], [ 0.,  1.,  0.,  1.])
     ([ 0.5,  0.5, -0.5], [ 1.,  1.], [-0., -0., -1.], [ 1.,  1.,  0.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-0., -0., -1.], [ 0.,  0.,  0.,  1.])
     ([ 0.5, -0.5, -0.5], [ 1.,  0.], [-0., -0., -1.], [ 1.,  0.,  0.,  1.])
     ([-0.5,  0.5,  0.5], [ 0.,  1.], [ 0.,  0.,  1.], [ 0.,  1.,  1.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 0.,  0.,  1.], [ 1.,  1.,  1.,  1.])
     ([-0.5, -0.5,  0.5], [ 0.,  0.], [ 0.,  0.,  1.], [ 0.,  0.,  1.,  1.])
     ([ 0.5, -0.5,  0.5], [ 1.,  0.], [ 0.,  0.,  1.], [ 1.,  0.,  1.,  1.])
     ([ 0.5, -0.5, -0.5], [ 0.,  1.], [-0., -1., -0.], [ 1.,  0.,  0.,  1.])
     ([ 0.5, -0.5,  0.5], [ 1.,  1.], [-0., -1., -0.], [ 1.,  0.,  1.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-0., -1., -0.], [ 0.,  0.,  0.,  1.])
     ([-0.5, -0.5,  0.5], [ 1.,  0.], [-0., -1., -0.], [ 0.,  0.,  1.,  1.])
     ([ 0.5,  0.5, -0.5], [ 0.,  1.], [ 0.,  1.,  0.], [ 1.,  1.,  0.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 0.,  1.,  0.], [ 1.,  1.,  1.,  1.])
     ([-0.5,  0.5, -0.5], [ 0.,  0.], [ 0.,  1.,  0.], [ 0.,  1.,  0.,  1.])
     ([-0.5,  0.5,  0.5], [ 1.,  0.], [ 0.,  1.,  0.], [ 0.,  1.,  1.,  1.])
     ([-0.5, -0.5,  0.5], [ 0.,  1.], [-1., -0., -0.], [ 0.,  0.,  1.,  1.])
     ([-0.5,  0.5,  0.5], [ 1.,  1.], [-1., -0., -0.], [ 0.,  1.,  1.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-1., -0., -0.], [ 0.,  0.,  0.,  1.])
     ([-0.5,  0.5, -0.5], [ 1.,  0.], [-1., -0., -0.], [ 0.,  1.,  0.,  1.])
     ([ 0.5, -0.5,  0.5], [ 0.,  1.], [ 1.,  0.,  0.], [ 1.,  0.,  1.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 1.,  0.,  0.], [ 1.,  1.,  1.,  1.])
     ([ 0.5, -0.5, -0.5], [ 0.,  0.], [ 1.,  0.,  0.], [ 1.,  0.,  0.,  1.])
     ([ 0.5,  0.5, -0.5], [ 1.,  0.], [ 1.,  0.,  0.], [ 1.,  1.,  0.,  1.])]
    >>> print(faces)
    [[ 1  2  0]
     [ 1  3  2]
     [ 4  6  5]
     [ 6  7  5]
     [ 9 10  8]
     [ 9 11 10]
     [12 14 13]
     [14 15 13]
     [17 18 16]
     [17 19 18]
     [20 22 21]
     [22 23 21]]
    >>> print(outline)
    [[ 0  2]
     [ 2  3]
     [ 3  1]
     [ 1  0]
     [ 4  6]
     [ 6  7]
     [ 7  5]
     [ 5  4]
     [ 8 10]
     [10 11]
     [11  9]
     [ 9  8]
     [12 14]
     [14 15]
     [15 13]
     [13 12]
     [16 18]
     [18 19]
     [19 17]
     [17 16]
     [20 22]
     [22 23]
     [23 21]
     [21 20]]
    """

    axis = (
        sorted(list(MAPPING_PLANE_TO_AXIS.values())) if planes is None else
        [MAPPING_PLANE_TO_AXIS.get(plane, plane).lower() for plane in planes])

    dtype_vertices = cast(Type[DTypeFloating],
                          optional(dtype_vertices, DEFAULT_FLOAT_DTYPE))
    dtype_indexes = cast(Type[DTypeInteger],
                         optional(dtype_indexes, DEFAULT_INT_DTYPE))

    w_s, h_s, d_s = width_segments, height_segments, depth_segments

    planes_p = []
    if "-z" in axis:
        planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "-z")))
        planes_p[-1][0]["position"][..., 2] -= height / 2
        planes_p[-1][1] = np.fliplr(planes_p[-1][1])
    if "+z" in axis:
        planes_p.append(list(primitive_grid(width, depth, w_s, d_s, "+z")))
        planes_p[-1][0]["position"][..., 2] += height / 2

    if "-y" in axis:
        planes_p.append(list(primitive_grid(height, width, h_s, w_s, "-y")))
        planes_p[-1][0]["position"][..., 1] -= depth / 2
        planes_p[-1][1] = np.fliplr(planes_p[-1][1])
    if "+y" in axis:
        planes_p.append(list(primitive_grid(height, width, h_s, w_s, "+y")))
        planes_p[-1][0]["position"][..., 1] += depth / 2

    if "-x" in axis:
        planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "-x")))
        planes_p[-1][0]["position"][..., 0] -= width / 2
        planes_p[-1][1] = np.fliplr(planes_p[-1][1])
    if "+x" in axis:
        planes_p.append(list(primitive_grid(depth, height, d_s, h_s, "+x")))
        planes_p[-1][0]["position"][..., 0] += width / 2

    positions = zeros((0, 3))
    uvs = zeros((0, 2))
    normals = zeros((0, 3))

    faces = zeros((0, 3), dtype=dtype_indexes)
    outline = zeros((0, 2), dtype=dtype_indexes)

    offset = 0
    for vertices_p, faces_p, outline_p in planes_p:
        positions = np.vstack([positions, vertices_p["position"]])
        uvs = np.vstack([uvs, vertices_p["uv"]])
        normals = np.vstack([normals, vertices_p["normal"]])

        faces = np.vstack([faces, faces_p + offset])
        outline = np.vstack([outline, outline_p + offset])
        offset += vertices_p["position"].shape[0]

    vertices = zeros(
        positions.shape[0],
        [
            ("position", dtype_vertices, 3),
            ("uv", dtype_vertices, 2),
            ("normal", dtype_vertices, 3),
            ("colour", dtype_vertices, 4),
        ],  # type: ignore[arg-type]
    )

    vertex_colours = np.ravel(positions)
    vertex_colours = np.hstack([
        np.reshape(
            np.interp(
                vertex_colours,
                (np.min(vertex_colours), np.max(vertex_colours)),
                (0, 1),
            ),
            positions.shape,
        ),
        ones((positions.shape[0], 1)),
    ])

    vertices["position"] = positions
    vertices["uv"] = uvs
    vertices["normal"] = normals
    vertices["colour"] = vertex_colours

    return vertices, faces, outline
Exemple #23
0
def opponent_colour_dimensions_inverse(P_n: ArrayLike,
                                       h: FloatingOrArrayLike) -> NDArray:
    """
    Return opponent colour dimensions from given points :math:`P_n` and hue
    :math:`h` in degrees for inverse *CIECAM02* implementation.

    Parameters
    ----------
    P_n
        Points :math:`P_n`.
    h
        Hue :math:`h` in degrees.

    Returns
    -------
    :class:`numpy.ndarray`
        Opponent colour dimensions.

    Notes
    -----
    -   This definition implements negative values handling as per
        :cite:`Luo2013`.

    Examples
    --------
    >>> P_n = np.array([30162.89081534, 24.23720547, 1.05000000])
    >>> h = -140.95156734
    >>> opponent_colour_dimensions_inverse(P_n, h)  # doctest: +ELLIPSIS
    array([-0.0006241..., -0.0005062...])
    """

    P_1, P_2, P_3 = tsplit(P_n)
    hr = np.radians(h)

    sin_hr = np.sin(hr)
    cos_hr = np.cos(hr)

    P_4 = P_1 / sin_hr
    P_5 = P_1 / cos_hr
    n = P_2 * (2 + P_3) * (460 / 1403)

    a = zeros(hr.shape)
    b = zeros(hr.shape)

    b = np.where(
        np.isfinite(P_1) * np.abs(sin_hr) >= np.abs(cos_hr),
        (n / (P_4 + (2 + P_3) * (220 / 1403) * (cos_hr / sin_hr) -
              (27 / 1403) + P_3 * (6300 / 1403))),
        b,
    )

    a = np.where(
        np.isfinite(P_1) * np.abs(sin_hr) >= np.abs(cos_hr),
        b * (cos_hr / sin_hr),
        a,
    )

    a = np.where(
        np.isfinite(P_1) * np.abs(sin_hr) < np.abs(cos_hr),
        (n / (P_5 + (2 + P_3) * (220 / 1403) -
              ((27 / 1403) - P_3 * (6300 / 1403)) * (sin_hr / cos_hr))),
        a,
    )

    b = np.where(
        np.isfinite(P_1) * np.abs(sin_hr) < np.abs(cos_hr),
        a * (sin_hr / cos_hr),
        b,
    )

    ab = tstack([a, b])

    return ab
Exemple #24
0
def primitive_cube(width=1,
                   height=1,
                   depth=1,
                   width_segments=1,
                   height_segments=1,
                   depth_segments=1,
                   planes=None):
    """
    Generates vertices and indices for a filled and outlined cube primitive.

    Parameters
    ----------
    width : float, optional
        Cube width.
    height : float, optional
        Cube height.
    depth : float, optional
        Cube depth.
    width_segments : int, optional
        Cube segments count along the width.
    height_segments : float, optional
        Cube segments count along the height.
    depth_segments : float, optional
        Cube segments count along the depth.
    planes : array_like, optional
        **{'-x', '+x', '-y', '+y', '-z', '+z',
        'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**,
        Grid primitives to include in the cube construction.

    Returns
    -------
    tuple
        Tuple of cube vertices, face indices to produce a filled cube and
        outline indices to produce an outline of the faces of the cube.

    Examples
    --------
    >>> vertices, faces, outline = primitive_cube()
    >>> print(vertices)
    [([-0.5,  0.5, -0.5], [ 0.,  1.], [-0., -0., -1.], [ 0.,  1.,  0.,  1.])
     ([ 0.5,  0.5, -0.5], [ 1.,  1.], [-0., -0., -1.], [ 1.,  1.,  0.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-0., -0., -1.], [ 0.,  0.,  0.,  1.])
     ([ 0.5, -0.5, -0.5], [ 1.,  0.], [-0., -0., -1.], [ 1.,  0.,  0.,  1.])
     ([-0.5,  0.5,  0.5], [ 0.,  1.], [ 0.,  0.,  1.], [ 0.,  1.,  1.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 0.,  0.,  1.], [ 1.,  1.,  1.,  1.])
     ([-0.5, -0.5,  0.5], [ 0.,  0.], [ 0.,  0.,  1.], [ 0.,  0.,  1.,  1.])
     ([ 0.5, -0.5,  0.5], [ 1.,  0.], [ 0.,  0.,  1.], [ 1.,  0.,  1.,  1.])
     ([ 0.5, -0.5, -0.5], [ 0.,  1.], [-0., -1., -0.], [ 1.,  0.,  0.,  1.])
     ([ 0.5, -0.5,  0.5], [ 1.,  1.], [-0., -1., -0.], [ 1.,  0.,  1.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-0., -1., -0.], [ 0.,  0.,  0.,  1.])
     ([-0.5, -0.5,  0.5], [ 1.,  0.], [-0., -1., -0.], [ 0.,  0.,  1.,  1.])
     ([ 0.5,  0.5, -0.5], [ 0.,  1.], [ 0.,  1.,  0.], [ 1.,  1.,  0.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 0.,  1.,  0.], [ 1.,  1.,  1.,  1.])
     ([-0.5,  0.5, -0.5], [ 0.,  0.], [ 0.,  1.,  0.], [ 0.,  1.,  0.,  1.])
     ([-0.5,  0.5,  0.5], [ 1.,  0.], [ 0.,  1.,  0.], [ 0.,  1.,  1.,  1.])
     ([-0.5, -0.5,  0.5], [ 0.,  1.], [-1., -0., -0.], [ 0.,  0.,  1.,  1.])
     ([-0.5,  0.5,  0.5], [ 1.,  1.], [-1., -0., -0.], [ 0.,  1.,  1.,  1.])
     ([-0.5, -0.5, -0.5], [ 0.,  0.], [-1., -0., -0.], [ 0.,  0.,  0.,  1.])
     ([-0.5,  0.5, -0.5], [ 1.,  0.], [-1., -0., -0.], [ 0.,  1.,  0.,  1.])
     ([ 0.5, -0.5,  0.5], [ 0.,  1.], [ 1.,  0.,  0.], [ 1.,  0.,  1.,  1.])
     ([ 0.5,  0.5,  0.5], [ 1.,  1.], [ 1.,  0.,  0.], [ 1.,  1.,  1.,  1.])
     ([ 0.5, -0.5, -0.5], [ 0.,  0.], [ 1.,  0.,  0.], [ 1.,  0.,  0.,  1.])
     ([ 0.5,  0.5, -0.5], [ 1.,  0.], [ 1.,  0.,  0.], [ 1.,  1.,  0.,  1.])]
    >>> print(faces)
    [[ 1  2  0]
     [ 1  3  2]
     [ 4  6  5]
     [ 6  7  5]
     [ 9 10  8]
     [ 9 11 10]
     [12 14 13]
     [14 15 13]
     [17 18 16]
     [17 19 18]
     [20 22 21]
     [22 23 21]]
    >>> print(outline)
    [[ 0  2]
     [ 2  3]
     [ 3  1]
     [ 1  0]
     [ 4  6]
     [ 6  7]
     [ 7  5]
     [ 5  4]
     [ 8 10]
     [10 11]
     [11  9]
     [ 9  8]
     [12 14]
     [14 15]
     [15 13]
     [13 12]
     [16 18]
     [18 19]
     [19 17]
     [17 16]
     [20 22]
     [22 23]
     [23 21]
     [21 20]]
    """

    planes = (
        sorted(list(PLANE_TO_AXIS_MAPPING.values())) if planes is None else
        [PLANE_TO_AXIS_MAPPING.get(plane, plane).lower() for plane in planes])

    w_s, h_s, d_s = width_segments, height_segments, depth_segments

    planes_m = []
    if '-z' in planes:
        planes_m.append(list(primitive_grid(width, depth, w_s, d_s, '-z')))
        planes_m[-1][0]['position'][..., 2] -= height / 2
        planes_m[-1][1] = np.fliplr(planes_m[-1][1])
    if '+z' in planes:
        planes_m.append(list(primitive_grid(width, depth, w_s, d_s, '+z')))
        planes_m[-1][0]['position'][..., 2] += height / 2

    if '-y' in planes:
        planes_m.append(list(primitive_grid(height, width, h_s, w_s, '-y')))
        planes_m[-1][0]['position'][..., 1] -= depth / 2
        planes_m[-1][1] = np.fliplr(planes_m[-1][1])
    if '+y' in planes:
        planes_m.append(list(primitive_grid(height, width, h_s, w_s, '+y')))
        planes_m[-1][0]['position'][..., 1] += depth / 2

    if '-x' in planes:
        planes_m.append(list(primitive_grid(depth, height, d_s, h_s, '-x')))
        planes_m[-1][0]['position'][..., 0] -= width / 2
        planes_m[-1][1] = np.fliplr(planes_m[-1][1])
    if '+x' in planes:
        planes_m.append(list(primitive_grid(depth, height, d_s, h_s, '+x')))
        planes_m[-1][0]['position'][..., 0] += width / 2

    positions = zeros([0, 3])
    uvs = zeros([0, 2])
    normals = zeros([0, 3])

    faces = zeros([0, 3], dtype=DEFAULT_INT_DTYPE)
    outline = zeros([0, 2], dtype=DEFAULT_INT_DTYPE)

    offset = 0
    for vertices_p, faces_p, outline_p in planes_m:
        positions = np.vstack([positions, vertices_p['position']])
        uvs = np.vstack([uvs, vertices_p['uv']])
        normals = np.vstack([normals, vertices_p['normal']])

        faces = np.vstack([faces, faces_p + offset])
        outline = np.vstack([outline, outline_p + offset])
        offset += vertices_p['position'].shape[0]

    vertices = zeros(positions.shape[0], [('position', DEFAULT_FLOAT_DTYPE, 3),
                                          ('uv', DEFAULT_FLOAT_DTYPE, 2),
                                          ('normal', DEFAULT_FLOAT_DTYPE, 3),
                                          ('colour', DEFAULT_FLOAT_DTYPE, 4)])

    vertex_colours = np.ravel(positions)
    vertex_colours = np.hstack([
        np.reshape(
            np.interp(vertex_colours,
                      (np.min(vertex_colours), np.max(vertex_colours)),
                      (0, 1)), positions.shape),
        ones([positions.shape[0], 1])
    ])

    vertices['position'] = positions
    vertices['uv'] = uvs
    vertices['normal'] = normals
    vertices['colour'] = vertex_colours

    return vertices, faces, outline
Exemple #25
0
    def test_zeros(self):
        """
        Tests :func:`colour.utilities.array.zeros` definition.
        """

        np.testing.assert_equal(zeros(3), np.zeros(3))
Exemple #26
0
def primitive_vertices_sphere(
    radius: Floating = 0.5,
    segments: Integer = 8,
    intermediate: Boolean = False,
    origin: ArrayLike = np.array([0, 0, 0]),
    axis: Union[Literal["+z", "+x", "+y", "yz", "xz", "xy"], str] = "+z",
) -> NDArray:
    """
    Return the vertices of a latitude-longitude sphere primitive.

    Parameters
    ----------
    radius
        Sphere radius.
    segments
        Latitude-longitude segments, if the ``intermediate`` argument is
        *True*, then the sphere will have one less segment along its longitude.
    intermediate
        Whether to generate the sphere vertices at the center of the faces
        outlined by the segments of a regular sphere generated without
        the ``intermediate`` argument set to *True*. The resulting sphere is
        inscribed on the regular sphere faces but possesses the same poles.
    origin
        Sphere origin on the construction plane.
    axis
        Axis (or normal of the plane) the poles of the sphere will be aligned
        with.

    Returns
    -------
    :class:`numpy.ndarray`
        Sphere primitive vertices.

    Notes
    -----
    -   The sphere poles have latitude segments count - 1 co-located vertices.

    Examples
    --------
    >>> primitive_vertices_sphere(segments=4)  # doctest: +ELLIPSIS
    array([[[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [ -3.5355339...e-01,  -4.3297802...e-17,   3.5355339...e-01],
            [ -5.0000000...e-01,  -6.1232340...e-17,   3.0616170...e-17],
            [ -3.5355339...e-01,  -4.3297802...e-17,  -3.5355339...e-01],
            [ -6.1232340...e-17,  -7.4987989...e-33,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,  -3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,  -5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,  -3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,  -6.1232340...e-17,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  3.5355339...e-01,   0.0000000...e+00,   3.5355339...e-01],
            [  5.0000000...e-01,   0.0000000...e+00,   3.0616170...e-17],
            [  3.5355339...e-01,   0.0000000...e+00,  -3.5355339...e-01],
            [  6.1232340...e-17,   0.0000000...e+00,  -5.0000000...e-01]],
    <BLANKLINE>
           [[  0.0000000...e+00,   0.0000000...e+00,   5.0000000...e-01],
            [  2.1648901...e-17,   3.5355339...e-01,   3.5355339...e-01],
            [  3.0616170...e-17,   5.0000000...e-01,   3.0616170...e-17],
            [  2.1648901...e-17,   3.5355339...e-01,  -3.5355339...e-01],
            [  3.7493994...e-33,   6.1232340...e-17,  -5.0000000...e-01]]])
    """

    axis = MAPPING_PLANE_TO_AXIS.get(axis, axis).lower()
    axis = validate_method(axis, ["+x", "+y", "+z"],
                           '"{0}" axis invalid, it must be one of {1}!')

    if not intermediate:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments + 1)),
            (int(segments) + 1, 1),
        )
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)),
                (int(segments) + 1, 1),
            ))
    else:
        theta = np.tile(
            np.radians(np.linspace(0, 180, segments * 2 + 1)[1::2][1:-1]),
            (int(segments) + 1, 1),
        )
        theta = np.hstack([
            zeros((segments + 1, 1)),
            theta,
            full((segments + 1, 1), np.pi),
        ])
        phi = np.transpose(
            np.tile(
                np.radians(np.linspace(-180, 180, segments + 1)) +
                np.radians(360 / segments / 2),
                (int(segments), 1),
            ))

    rho = ones(phi.shape) * radius
    rho_theta_phi = tstack([rho, theta, phi])

    vertices = spherical_to_cartesian(rho_theta_phi)

    # Removing extra longitude vertices.
    vertices = vertices[:-1, :, :]

    if axis == "+z":
        pass
    elif axis == "+y":
        vertices = np.roll(vertices, 2, -1)
    elif axis == "+x":
        vertices = np.roll(vertices, 1, -1)

    vertices += origin

    return vertices