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))
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
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)
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
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]
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))
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
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()
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
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()
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
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
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
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)
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)
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)
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), ])
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
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
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)
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) ])
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
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
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
def test_zeros(self): """ Tests :func:`colour.utilities.array.zeros` definition. """ np.testing.assert_equal(zeros(3), np.zeros(3))
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