def msds_constant( k: Floating, labels: Sequence, shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, **kwargs: Any, ) -> MultiSpectralDistributions: """ Return the multi-spectral distributions with given labels and given spectral shape filled with constant :math:`k` values. Parameters ---------- k Constant :math:`k` to fill the multi-spectral distributions with. labels Names to use for the :class:`colour.SpectralDistribution` class instances. shape Spectral shape used to create the multi-spectral distributions. Other Parameters ---------------- kwargs {:class:`colour.MultiSpectralDistributions`}, See the documentation of the previously listed class. Returns ------- :class:`colour.MultiSpectralDistributions` Constant :math:`k` filled multi-spectral distributions. Notes ----- - By default, the multi-spectral distributions will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> msds = msds_constant(100, labels=['a', 'b', 'c']) >>> msds.shape SpectralShape(360.0, 780.0, 1.0) >>> msds[400] array([ 100., 100., 100.]) >>> msds.labels # doctest: +SKIP ['a', 'b', 'c'] """ settings = {"name": f"{k} Constant"} settings.update(kwargs) wavelengths = shape.range() values = full((len(wavelengths), len(labels)), k) return MultiSpectralDistributions(values, wavelengths, labels=labels, **settings)
def msds_constant(k, labels, shape=SPECTRAL_SHAPE_DEFAULT, dtype=None): """ Returns the multi-spectral distributions with given labels and given spectral shape filled with constant :math:`k` values. Parameters ---------- k : numeric Constant :math:`k` to fill the multi-spectral distributions with. labels : array_like Names to use for the :class:`colour.SpectralDistribution` class instances. shape : SpectralShape, optional Spectral shape used to create the multi-spectral distributions. dtype : type Data type used for the multi-spectral distributions. Returns ------- MultiSpectralDistributions Constant :math:`k` filled multi-spectral distributions. Notes ----- - By default, the multi-spectral distributions will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> msds = msds_constant(100, labels=['a', 'b', 'c']) >>> msds.shape SpectralShape(360.0, 780.0, 1.0) >>> msds[400] array([ 100., 100., 100.]) >>> msds.labels # doctest: +SKIP ['a', 'b', 'c'] """ if dtype is None: dtype = DEFAULT_FLOAT_DTYPE wavelengths = shape.range(dtype) values = full([len(wavelengths), len(labels)], k, dtype) name = '{0} Constant'.format(k) return MultiSpectralDistributions( values, wavelengths, name=name, labels=labels, dtype=dtype)
def sd_constant(k: Floating, shape: SpectralShape = SPECTRAL_SHAPE_DEFAULT, **kwargs: Any) -> SpectralDistribution: """ Return a spectral distribution of given spectral shape filled with constant :math:`k` values. Parameters ---------- k Constant :math:`k` to fill the spectral distribution with. shape Spectral shape used to create the spectral distribution. Other Parameters ---------------- kwargs {:class:`colour.SpectralDistribution`}, See the documentation of the previously listed class. Returns ------- :class:`colour.SpectralDistribution` Constant :math:`k` filled spectral distribution. Notes ----- - By default, the spectral distribution will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> sd = sd_constant(100) >>> sd.shape SpectralShape(360.0, 780.0, 1.0) >>> sd[400] 100.0 """ settings = {"name": f"{k} Constant"} settings.update(kwargs) wavelengths = shape.range() values = full(len(wavelengths), k) return SpectralDistribution(values, wavelengths, **settings)
def sd_constant(k, shape=SPECTRAL_SHAPE_DEFAULT, dtype=None): """ Returns a spectral distribution of given spectral shape filled with constant :math:`k` values. Parameters ---------- k : numeric Constant :math:`k` to fill the spectral distribution with. shape : SpectralShape, optional Spectral shape used to create the spectral distribution. dtype : type Data type used for the spectral distribution. Returns ------- SpectralDistribution Constant :math:`k` filled spectral distribution. Notes ----- - By default, the spectral distribution will use the shape given by :attr:`colour.SPECTRAL_SHAPE_DEFAULT` attribute. Examples -------- >>> sd = sd_constant(100) >>> sd.shape SpectralShape(360.0, 780.0, 1.0) >>> sd[400] 100.0 """ if dtype is None: dtype = DEFAULT_FLOAT_DTYPE wavelengths = shape.range(dtype) values = full(len(wavelengths), k, dtype) name = '{0} Constant'.format(k) return SpectralDistribution(values, wavelengths, name=name, dtype=dtype)
def uv_to_UCS(uv, V=1): """ Returns the *CIE 1960 UCS* colourspace array from given *uv* chromaticity coordinates. Parameters ---------- uv : array_like *uv* chromaticity coordinates. V : numeric, optional Optional :math:`V` *luminance* value used to construct the *CIE 1960 UCS* colourspace array, the default :math:`V` *luminance* is set to 1. Returns ------- ndarray *CIE 1960 UCS* colourspace array. References ---------- :cite:`Wikipedia2008c` Examples -------- >>> import numpy as np >>> uv = np.array([0.37720213, 0.33413508]) >>> uv_to_UCS(uv) # doctest: +ELLIPSIS array([ 1.1288911..., 1. , 0.8639104...]) """ u, v = tsplit(uv) V = full(u.shape, V) U = V * u / v W = -V * (u + v - 1) / v UVW = tstack([U, V, W]) return from_range_1(UVW)
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 uv_to_UCS(uv: ArrayLike, V: Floating = 1) -> NDArray: """ Return the *CIE 1960 UCS* colourspace array from given *uv* chromaticity coordinates. Parameters ---------- uv *uv* chromaticity coordinates. V Optional :math:`V` *luminance* value used to construct the *CIE 1960 UCS* colourspace array, the default :math:`V` *luminance* is set to 1. Returns ------- :class:`numpy.ndarray` *CIE 1960 UCS* colourspace array. References ---------- :cite:`Wikipedia2008c` Examples -------- >>> import numpy as np >>> uv = np.array([0.37720213, 0.33413508]) >>> uv_to_UCS(uv) # doctest: +ELLIPSIS array([ 1.1288911..., 1. , 0.8639104...]) """ u, v = tsplit(uv) V = as_float_scalar(to_domain_1(V)) UVW = tstack([V * u / v, full(u.shape, V), -V * (u + v - 1) / v]) return from_range_1(UVW)
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 test_process_image_OpenColorIO(self): """Test :func:`colour.io.ocio.process_image_OpenColorIO` definition.""" # TODO: Remove when "Pypi" wheel compatible with "ARM" on "macOS" is # released. if not is_opencolorio_installed(): # pragma: no cover return import PyOpenColorIO as ocio config = os.path.join( RESOURCES_DIRECTORY, "config-aces-reference.ocio.yaml" ) a = full([4, 2, 3], 0.18) np.testing.assert_almost_equal( process_image_OpenColorIO( a, "ACES - ACES2065-1", "ACES - ACEScct", config=config ), np.array( [ [ [0.41358781, 0.41358781, 0.41358781], [0.41358781, 0.41358781, 0.41358781], ], [ [0.41358781, 0.41358781, 0.41358781], [0.41358781, 0.41358781, 0.41358781], ], [ [0.41358781, 0.41358781, 0.41358781], [0.41358781, 0.41358781, 0.41358781], ], [ [0.41358781, 0.41358781, 0.41358781], [0.41358781, 0.41358781, 0.41358781], ], ] ), decimal=5, ) np.testing.assert_almost_equal( process_image_OpenColorIO( a, "ACES - ACES2065-1", "Display - sRGB", "Output - SDR Video - ACES 1.0", ocio.TRANSFORM_DIR_FORWARD, config=config, ), np.array( [ [ [0.35595229, 0.35595256, 0.35595250], [0.35595229, 0.35595256, 0.35595250], ], [ [0.35595229, 0.35595256, 0.35595250], [0.35595229, 0.35595256, 0.35595250], ], [ [0.35595229, 0.35595256, 0.35595250], [0.35595229, 0.35595256, 0.35595250], ], [ [0.35595229, 0.35595256, 0.35595250], [0.35595229, 0.35595256, 0.35595250], ], ] ), decimal=5, )
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 convert_experiment_results_Breneman1987(experiment): """ Converts *Breneman (1987)* experiment results to a :class:`colour.CorrespondingColourDataset` class instance. Parameters ---------- experiment : integer {1, 2, 3, 4, 6, 8, 9, 11, 12} *Breneman (1987)* experiment number. Returns ------- CorrespondingColourDataset :class:`colour.CorrespondingColourDataset` class instance. Examples -------- >>> from pprint import pprint >>> pprint(tuple(convert_experiment_results_Breneman1987(2))) ... # doctest: +ELLIPSIS (2, array([ 0.9582463..., 1. , 0.9436325...]), array([ 0.9587332..., 1. , 0.4385796...]), array([[ 388.125 , 405. , 345.625 ], [ 266.8957925..., 135. , 28.5983365...], [ 474.5717821..., 405. , 222.75 ...], [ 538.3899082..., 405. , 24.8944954...], [ 178.7430167..., 135. , 19.6089385...], [ 436.6749547..., 405. , 26.5483725...], [ 124.7746282..., 135. , 36.1965613...], [ 77.0794172..., 135. , 60.5850563...], [ 279.9390889..., 405. , 455.8395127...], [ 149.5808157..., 135. , 498.7046827...], [ 372.1113689..., 405. , 669.9883990...], [ 212.3638968..., 135. , 414.6704871...]]), array([[ 400.1039651..., 405. , 191.7287234...], [ 271.0384615..., 135. , 13.5 ...], [ 495.4705323..., 405. , 119.7290874...], [ 580.7967033..., 405. , 6.6758241...], [ 190.1933701..., 135. , 7.4585635...], [ 473.7184115..., 405. , 10.2346570...], [ 135.4936014..., 135. , 20.2376599...], [ 86.4689781..., 135. , 35.2281021...], [ 283.5396281..., 405. , 258.1775929...], [ 119.7044335..., 135. , 282.6354679...], [ 359.9532224..., 405. , 381.0031185...], [ 181.8271461..., 135. , 204.0661252...]]), array(1500), array(1500), 0.3, 0.3, {}) """ valid_experiment_results = (1, 2, 3, 4, 6, 8, 9, 11, 12) assert experiment in valid_experiment_results, ( '"Breneman (1987)" experiment result must be one of "{0}"!'.format( valid_experiment_results)) samples_luminance = [ 0.270, 0.090, 0.270, 0.270, 0.090, 0.270, 0.090, 0.090, 0.270, 0.090, 0.270, 0.090, ] experiment_results = list(BRENEMAN_EXPERIMENTS[experiment]) illuminant_chromaticities = experiment_results.pop(0) Y_r = Y_t = BRENEMAN_EXPERIMENT_PRIMARIES_CHROMATICITIES[experiment].Y B_r = B_t = 0.3 XYZ_t, XYZ_r = xy_to_XYZ( np.hstack( [Luv_uv_to_xy(illuminant_chromaticities[1:3]), full([2, 1], Y_r)])) / Y_r xyY_cr, xyY_ct = [], [] for i, experiment_result in enumerate(experiment_results): xyY_cr.append( np.hstack([ Luv_uv_to_xy(experiment_result[2]), samples_luminance[i] * Y_r ])) xyY_ct.append( np.hstack([ Luv_uv_to_xy(experiment_result[1]), samples_luminance[i] * Y_t ])) XYZ_cr = xyY_to_XYZ(xyY_cr) XYZ_ct = xyY_to_XYZ(xyY_ct) return CorrespondingColourDataset(experiment, XYZ_r, XYZ_t, XYZ_cr, XYZ_ct, Y_r, Y_t, B_r, B_t, {})
def arithmetical_operation(self, a, operation, in_place=False): """ Performs given arithmetical operation with :math:`a` operand, the operation can be either performed on a copy or in-place. Parameters ---------- a : numeric or ndarray or Signal Operand. operation : object Operation to perform. in_place : bool, optional Operation happens in place. Returns ------- Signal Continuous signal. Examples -------- Adding a single *numeric* variable: >>> range_ = np.linspace(10, 100, 10) >>> signal_1 = Signal(range_) >>> print(signal_1) [[ 0. 10.] [ 1. 20.] [ 2. 30.] [ 3. 40.] [ 4. 50.] [ 5. 60.] [ 6. 70.] [ 7. 80.] [ 8. 90.] [ 9. 100.]] >>> print(signal_1.arithmetical_operation(10, '+', True)) [[ 0. 20.] [ 1. 30.] [ 2. 40.] [ 3. 50.] [ 4. 60.] [ 5. 70.] [ 6. 80.] [ 7. 90.] [ 8. 100.] [ 9. 110.]] Adding an *array_like* variable: >>> a = np.linspace(10, 100, 10) >>> print(signal_1.arithmetical_operation(a, '+', True)) [[ 0. 30.] [ 1. 50.] [ 2. 70.] [ 3. 90.] [ 4. 110.] [ 5. 130.] [ 6. 150.] [ 7. 170.] [ 8. 190.] [ 9. 210.]] Adding a :class:`colour.continuous.Signal` class: >>> signal_2 = Signal(range_) >>> print(signal_1.arithmetical_operation(signal_2, '+', True)) [[ 0. 40.] [ 1. 70.] [ 2. 100.] [ 3. 130.] [ 4. 160.] [ 5. 190.] [ 6. 220.] [ 7. 250.] [ 8. 280.] [ 9. 310.]] """ operation, ioperator = { '+': (add, iadd), '-': (sub, isub), '*': (mul, imul), '/': (div, idiv), '**': (pow, ipow) }[operation] if in_place: if isinstance(a, Signal): self[self._domain] = operation(self._range, a[self._domain]) exclusive_or = np.setxor1d(self._domain, a.domain) self[exclusive_or] = full(exclusive_or.shape, np.nan) else: self.range = ioperator(self.range, a) return self else: copy = ioperator(self.copy(), a) return copy
def arithmetical_operation( self, a: Union[FloatingOrArrayLike, AbstractContinuousFunction], operation: Literal["+", "-", "*", "/", "**"], in_place: Boolean = False, ) -> AbstractContinuousFunction: """ Perform given arithmetical operation with operand :math:`a`, the operation can be either performed on a copy or in-place. Parameters ---------- a Operand :math:`a`. operation Operation to perform. in_place Operation happens in place. Returns ------- :class:`colour.continuous.Signal` Continuous signal. Examples -------- Adding a single *numeric* variable: >>> range_ = np.linspace(10, 100, 10) >>> signal_1 = Signal(range_) >>> print(signal_1) [[ 0. 10.] [ 1. 20.] [ 2. 30.] [ 3. 40.] [ 4. 50.] [ 5. 60.] [ 6. 70.] [ 7. 80.] [ 8. 90.] [ 9. 100.]] >>> print(signal_1.arithmetical_operation(10, '+', True)) [[ 0. 20.] [ 1. 30.] [ 2. 40.] [ 3. 50.] [ 4. 60.] [ 5. 70.] [ 6. 80.] [ 7. 90.] [ 8. 100.] [ 9. 110.]] Adding an `ArrayLike` variable: >>> a = np.linspace(10, 100, 10) >>> print(signal_1.arithmetical_operation(a, '+', True)) [[ 0. 30.] [ 1. 50.] [ 2. 70.] [ 3. 90.] [ 4. 110.] [ 5. 130.] [ 6. 150.] [ 7. 170.] [ 8. 190.] [ 9. 210.]] Adding a :class:`colour.continuous.Signal` class: >>> signal_2 = Signal(range_) >>> print(signal_1.arithmetical_operation(signal_2, '+', True)) [[ 0. 40.] [ 1. 70.] [ 2. 100.] [ 3. 130.] [ 4. 160.] [ 5. 190.] [ 6. 220.] [ 7. 250.] [ 8. 280.] [ 9. 310.]] """ operator, ioperator = { "+": (add, iadd), "-": (sub, isub), "*": (mul, imul), "/": (truediv, itruediv), "**": (pow, ipow), }[operation] if in_place: if isinstance(a, Signal): self[self._domain] = operator(self._range, a[self._domain]) exclusive_or = np.setxor1d(self._domain, a.domain) self[exclusive_or] = full(exclusive_or.shape, np.nan) else: self.range = ioperator(self.range, a) return self else: copy = ioperator(self.copy(), a) return copy
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 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 nadir_grid(limits=None, segments=10, labels=None, axes=None, **kwargs): """ Returns a grid on *CIE xy* plane made of quad geometric elements and its associated faces and edges colours. Ticks and labels are added to the given axes according to the extended grid settings. Parameters ---------- limits : array_like, optional Extended grid limits. segments : int, optional Edge segments count for the extended grid. labels : array_like, optional Axis labels. axes : matplotlib.axes.Axes, optional Axes to add the grid. Other Parameters ---------------- grid_face_colours : array_like, optional Grid face colours array such as `grid_face_colours = (0.25, 0.25, 0.25)`. grid_edge_colours : array_like, optional Grid edge colours array such as `grid_edge_colours = (0.25, 0.25, 0.25)`. grid_face_alpha : numeric, optional Grid face opacity value such as `grid_face_alpha = 0.1`. grid_edge_alpha : numeric, optional Grid edge opacity value such as `grid_edge_alpha = 0.5`. x_axis_colour : array_like, optional *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`. y_axis_colour : array_like, optional *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`. x_ticks_colour : array_like, optional *X* axis ticks colour array such as `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. y_ticks_colour : array_like, optional *Y* axis ticks colour array such as `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. x_label_colour : array_like, optional *X* axis label colour array such as `x_label_colour = (0.0, 0.0, 0.0, 0.85)`. y_label_colour : array_like, optional *Y* axis label colour array such as `y_label_colour = (0.0, 0.0, 0.0, 0.85)`. ticks_and_label_location : array_like, optional Location of the *X* and *Y* axis ticks and labels such as `ticks_and_label_location = ('-x', '-y')`. Returns ------- tuple Grid quads, faces colours, edges colours. Examples -------- >>> nadir_grid(segments=1) (array([[[-1. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[-1. , -1. , 0. ], [ 0. , -1. , 0. ], [ 0. , 0. , 0. ], [-1. , 0. , 0. ]], <BLANKLINE> [[-1. , 0. , 0. ], [ 0. , 0. , 0. ], [ 0. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[ 0. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 0. , 0. ], [ 0. , 0. , 0. ]], <BLANKLINE> [[ 0. , 0. , 0. ], [ 1. , 0. , 0. ], [ 1. , 1. , 0. ], [ 0. , 1. , 0. ]], <BLANKLINE> [[-1. , -0.001, 0. ], [ 1. , -0.001, 0. ], [ 1. , 0.001, 0. ], [-1. , 0.001, 0. ]], <BLANKLINE> [[-0.001, -1. , 0. ], [ 0.001, -1. , 0. ], [ 0.001, 1. , 0. ], [-0.001, 1. , 0. ]]]), array([[ 0.25, 0.25, 0.25, 0.1 ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]]), array([[ 0.5 , 0.5 , 0.5 , 0.5 ], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]])) """ if limits is None: limits = np.array([[-1, 1], [-1, 1]]) if labels is None: labels = ('x', 'y') extent = np.max(np.abs(limits[..., 1] - limits[..., 0])) settings = Structure( **{ 'grid_face_colours': (0.25, 0.25, 0.25), 'grid_edge_colours': (0.50, 0.50, 0.50), 'grid_face_alpha': 0.1, 'grid_edge_alpha': 0.5, 'x_axis_colour': (0.0, 0.0, 0.0, 1.0), 'y_axis_colour': (0.0, 0.0, 0.0, 1.0), 'x_ticks_colour': (0.0, 0.0, 0.0, 0.85), 'y_ticks_colour': (0.0, 0.0, 0.0, 0.85), 'x_label_colour': (0.0, 0.0, 0.0, 0.85), 'y_label_colour': (0.0, 0.0, 0.0, 0.85), 'ticks_and_label_location': ('-x', '-y') }) settings.update(**kwargs) # Outer grid. quads_g = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments, width_segments=segments) RGB_g = ones([quads_g.shape[0], quads_g.shape[-1]]) RGB_gf = RGB_g * settings.grid_face_colours RGB_gf = np.hstack( [RGB_gf, full([RGB_gf.shape[0], 1], settings.grid_face_alpha)]) RGB_ge = RGB_g * settings.grid_edge_colours RGB_ge = np.hstack( [RGB_ge, full([RGB_ge.shape[0], 1], settings.grid_edge_alpha)]) # Inner grid. quads_gs = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments * 2, width_segments=segments * 2) RGB_gs = ones([quads_gs.shape[0], quads_gs.shape[-1]]) RGB_gsf = RGB_gs * 0 RGB_gsf = np.hstack([RGB_gsf, full([RGB_gsf.shape[0], 1], 0)]) RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1) RGB_gse = np.hstack((RGB_gse, full([RGB_gse.shape[0], 1], settings.grid_edge_alpha / 2))) # Axis. thickness = extent / 1000 quad_x = primitive_vertices_grid_mpl( origin=(limits[0, 0], -thickness / 2), width=extent, height=thickness) RGB_x = ones([quad_x.shape[0], quad_x.shape[-1] + 1]) RGB_x = RGB_x * settings.x_axis_colour quad_y = primitive_vertices_grid_mpl( origin=(-thickness / 2, limits[1, 0]), width=thickness, height=extent) RGB_y = ones([quad_y.shape[0], quad_y.shape[-1] + 1]) RGB_y = RGB_y * settings.y_axis_colour if axes is not None: # Ticks. x_s = 1 if '+x' in settings.ticks_and_label_location else -1 y_s = 1 if '+y' in settings.ticks_and_label_location else -1 for i, axis in enumerate('xy'): h_a = 'center' if axis == 'x' else 'left' if x_s == 1 else 'right' v_a = 'center' ticks = list(sorted(set(quads_g[..., 0, i]))) ticks += [ticks[-1] + ticks[-1] - ticks[-2]] for tick in ticks: x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 25) if i else tick) y = (tick if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 25)) tick = DEFAULT_INT_DTYPE(tick) if DEFAULT_FLOAT_DTYPE( tick).is_integer() else tick c = settings['{0}_ticks_colour'.format(axis)] axes.text( x, y, 0, tick, 'x', horizontalalignment=h_a, verticalalignment=v_a, color=c, clip_on=True) # Labels. for i, axis in enumerate('xy'): h_a = 'center' if axis == 'x' else 'left' if x_s == 1 else 'right' v_a = 'center' x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 10) if i else 0) y = (0 if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 10)) c = settings['{0}_label_colour'.format(axis)] axes.text( x, y, 0, labels[i], 'x', horizontalalignment=h_a, verticalalignment=v_a, color=c, size=20, clip_on=True) quads = np.vstack([quads_g, quads_gs, quad_x, quad_y]) RGB_f = np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y]) RGB_e = np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y]) return quads, RGB_f, RGB_e
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 uv_to_Luv(uv, illuminant=CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer'] ['D65'], Y=1): """ Returns the *CIE L\\*u\\*v\\** colourspace array from given :math:`uv^p` chromaticity coordinates by extending the array last dimension with given :math:`L` *Lightness*. Parameters ---------- uv : array_like :math:`uv^p` chromaticity coordinates. illuminant : array_like, optional Reference *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY* colourspace array. Y : numeric, optional Optional :math:`Y` *luminance* value used to construct the intermediate *CIE XYZ* colourspace array, the default :math:`Y` *luminance* value is 1. Returns ------- ndarray *CIE L\\*u\\*v\\** colourspace array. Notes ----- +----------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +================+=======================+=================+ | ``Luv`` | ``L`` : [0, 100] | ``L`` : [0, 1] | | | | | | | ``u`` : [-100, 100] | ``u`` : [-1, 1] | | | | | | | ``v`` : [-100, 100] | ``v`` : [-1, 1] | +----------------+-----------------------+-----------------+ | ``illuminant`` | [0, 1] | [0, 1] | +----------------+-----------------------+-----------------+ References ---------- :cite:`CIETC1-482004j` Examples -------- >>> import numpy as np >>> uv = np.array([0.37720213, 0.50120264]) >>> uv_to_Luv(uv) # doctest: +ELLIPSIS array([ 100. , 233.1837603..., 42.7474385...]) """ u, v = tsplit(uv) Y = to_domain_1(Y) X = 9 * u / (4 * v) Z = (-5 * Y * v - 3 * u / 4 + 3) / v Y = full(u.shape, Y) return XYZ_to_Luv(from_range_1(tstack([X, Y, Z])), illuminant)
def nadir_grid( limits: Optional[ArrayLike] = None, segments: Integer = 10, labels: Optional[Sequence[str]] = None, axes: Optional[plt.Axes] = None, **kwargs: Any, ) -> Tuple[NDArray, NDArray, NDArray]: """ Return a grid on *CIE xy* plane made of quad geometric elements and its associated faces and edges colours. Ticks and labels are added to the given axes according to the extended grid settings. Parameters ---------- limits Extended grid limits. segments Edge segments count for the extended grid. labels Axis labels. axes Axes to add the grid. Other Parameters ---------------- grid_edge_alpha Grid edge opacity value such as `grid_edge_alpha = 0.5`. grid_edge_colours Grid edge colours array such as `grid_edge_colours = (0.25, 0.25, 0.25)`. grid_face_alpha Grid face opacity value such as `grid_face_alpha = 0.1`. grid_face_colours Grid face colours array such as `grid_face_colours = (0.25, 0.25, 0.25)`. ticks_and_label_location Location of the *X* and *Y* axis ticks and labels such as `ticks_and_label_location = ('-x', '-y')`. x_axis_colour *X* axis colour array such as `x_axis_colour = (0.0, 0.0, 0.0, 1.0)`. x_label_colour *X* axis label colour array such as `x_label_colour = (0.0, 0.0, 0.0, 0.85)`. x_ticks_colour *X* axis ticks colour array such as `x_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. y_axis_colour *Y* axis colour array such as `y_axis_colour = (0.0, 0.0, 0.0, 1.0)`. y_label_colour *Y* axis label colour array such as `y_label_colour = (0.0, 0.0, 0.0, 0.85)`. y_ticks_colour *Y* axis ticks colour array such as `y_ticks_colour = (0.0, 0.0, 0.0, 0.85)`. Returns ------- :class:`tuple` Grid quads, faces colours, edges colours. Examples -------- >>> nadir_grid(segments=1) (array([[[-1. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[-1. , -1. , 0. ], [ 0. , -1. , 0. ], [ 0. , 0. , 0. ], [-1. , 0. , 0. ]], <BLANKLINE> [[-1. , 0. , 0. ], [ 0. , 0. , 0. ], [ 0. , 1. , 0. ], [-1. , 1. , 0. ]], <BLANKLINE> [[ 0. , -1. , 0. ], [ 1. , -1. , 0. ], [ 1. , 0. , 0. ], [ 0. , 0. , 0. ]], <BLANKLINE> [[ 0. , 0. , 0. ], [ 1. , 0. , 0. ], [ 1. , 1. , 0. ], [ 0. , 1. , 0. ]], <BLANKLINE> [[-1. , -0.001, 0. ], [ 1. , -0.001, 0. ], [ 1. , 0.001, 0. ], [-1. , 0.001, 0. ]], <BLANKLINE> [[-0.001, -1. , 0. ], [ 0.001, -1. , 0. ], [ 0.001, 1. , 0. ], [-0.001, 1. , 0. ]]]), array([[ 0.25, 0.25, 0.25, 0.1 ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]]), array([[ 0.5 , 0.5 , 0.5 , 0.5 ], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.75, 0.25], [ 0. , 0. , 0. , 1. ], [ 0. , 0. , 0. , 1. ]])) """ limits = as_float_array( cast(ArrayLike, optional(limits, np.array([[-1, 1], [-1, 1]])))) labels = cast(Sequence, optional(labels, ("x", "y"))) extent = np.max(np.abs(limits[..., 1] - limits[..., 0])) settings = Structure( **{ "grid_face_colours": (0.25, 0.25, 0.25), "grid_edge_colours": (0.50, 0.50, 0.50), "grid_face_alpha": 0.1, "grid_edge_alpha": 0.5, "x_axis_colour": (0.0, 0.0, 0.0, 1.0), "y_axis_colour": (0.0, 0.0, 0.0, 1.0), "x_ticks_colour": (0.0, 0.0, 0.0, 0.85), "y_ticks_colour": (0.0, 0.0, 0.0, 0.85), "x_label_colour": (0.0, 0.0, 0.0, 0.85), "y_label_colour": (0.0, 0.0, 0.0, 0.85), "ticks_and_label_location": ("-x", "-y"), }) settings.update(**kwargs) # Outer grid. quads_g = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments, width_segments=segments, ) RGB_g = ones((quads_g.shape[0], quads_g.shape[-1])) RGB_gf = RGB_g * settings.grid_face_colours RGB_gf = np.hstack( [RGB_gf, full((RGB_gf.shape[0], 1), settings.grid_face_alpha)]) RGB_ge = RGB_g * settings.grid_edge_colours RGB_ge = np.hstack( [RGB_ge, full((RGB_ge.shape[0], 1), settings.grid_edge_alpha)]) # Inner grid. quads_gs = primitive_vertices_grid_mpl( origin=(-extent / 2, -extent / 2), width=extent, height=extent, height_segments=segments * 2, width_segments=segments * 2, ) RGB_gs = ones((quads_gs.shape[0], quads_gs.shape[-1])) RGB_gsf = RGB_gs * 0 RGB_gsf = np.hstack([RGB_gsf, full((RGB_gsf.shape[0], 1), 0)]) RGB_gse = np.clip(RGB_gs * settings.grid_edge_colours * 1.5, 0, 1) RGB_gse = np.hstack( (RGB_gse, full((RGB_gse.shape[0], 1), settings.grid_edge_alpha / 2))) # Axis. thickness = extent / 1000 quad_x = primitive_vertices_grid_mpl(origin=(limits[0, 0], -thickness / 2), width=extent, height=thickness) RGB_x = ones((quad_x.shape[0], quad_x.shape[-1] + 1)) RGB_x = RGB_x * settings.x_axis_colour quad_y = primitive_vertices_grid_mpl(origin=(-thickness / 2, limits[1, 0]), width=thickness, height=extent) RGB_y = ones((quad_y.shape[0], quad_y.shape[-1] + 1)) RGB_y = RGB_y * settings.y_axis_colour if axes is not None: # Ticks. x_s = 1 if "+x" in settings.ticks_and_label_location else -1 y_s = 1 if "+y" in settings.ticks_and_label_location else -1 for i, axis in enumerate("xy"): h_a = "center" if axis == "x" else "left" if x_s == 1 else "right" v_a = "center" ticks = list(sorted(set(quads_g[..., 0, i]))) ticks += [ticks[-1] + ticks[-1] - ticks[-2]] for tick in ticks: x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 25) if i else tick) y = (tick if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 25)) tick = as_int_scalar(tick) if is_integer(tick) else tick c = settings[f"{axis}_ticks_colour"] axes.text( x, y, 0, tick, "x", horizontalalignment=h_a, verticalalignment=v_a, color=c, clip_on=True, ) # Labels. for i, axis in enumerate("xy"): h_a = "center" if axis == "x" else "left" if x_s == 1 else "right" v_a = "center" x = (limits[1, 1 if x_s == 1 else 0] + (x_s * extent / 10) if i else 0) y = (0 if i else limits[0, 1 if y_s == 1 else 0] + (y_s * extent / 10)) c = settings[f"{axis}_label_colour"] axes.text( x, y, 0, labels[i], "x", horizontalalignment=h_a, verticalalignment=v_a, color=c, size=20, clip_on=True, ) quads = np.vstack([quads_g, quads_gs, quad_x, quad_y]) RGB_f = np.vstack([RGB_gf, RGB_gsf, RGB_x, RGB_y]) RGB_e = np.vstack([RGB_ge, RGB_gse, RGB_x, RGB_y]) return quads, RGB_f, RGB_e
def xy_to_xyY(xy, Y=1): """ Converts from *CIE xy* chromaticity coordinates to *CIE xyY* colourspace by extending the array last dimension with given :math:`Y` *luminance*. ``xy`` argument with last dimension being equal to 3 will be assumed to be a *CIE xyY* colourspace array argument and will be returned directly by the definition. Parameters ---------- xy : array_like *CIE xy* chromaticity coordinates or *CIE xyY* colourspace array. Y : numeric, optional Optional :math:`Y` *luminance* value used to construct the *CIE xyY* colourspace array, the default :math:`Y` *luminance* value is 1. Returns ------- ndarray *CIE xyY* colourspace array. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``xy`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``xyY`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ - This definition is a convenient object provided to implement support of illuminant argument *luminance* value in various :mod:`colour.models` package objects such as :func:`colour.Lab_to_XYZ` or :func:`colour.Luv_to_XYZ`. References ---------- :cite:`Wikipedia2005` Examples -------- >>> xy = np.array([0.54369557, 0.32107944]) >>> xy_to_xyY(xy) # doctest: +ELLIPSIS array([ 0.5436955..., 0.3210794..., 1. ]) >>> xy = np.array([0.54369557, 0.32107944, 1.00000000]) >>> xy_to_xyY(xy) # doctest: +ELLIPSIS array([ 0.5436955..., 0.3210794..., 1. ]) >>> xy = np.array([0.54369557, 0.32107944]) >>> xy_to_xyY(xy, 100) # doctest: +ELLIPSIS array([ 0.5436955..., 0.3210794..., 100. ]) """ xy = as_float_array(xy) Y = to_domain_1(Y) shape = xy.shape # Assuming ``xy`` is actually a *CIE xyY* colourspace array argument and # returning it directly. if shape[-1] == 3: return xy x, y = tsplit(xy) Y = full(x.shape, from_range_1(Y)) xyY = tstack([x, y, Y]) return xyY
def plot_hull_section_colours( hull: trimesh.Trimesh, # type: ignore[name-defined] # noqa model: 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", axis: Union[Literal["+z", "+x", "+y"], str] = "+z", origin: Floating = 0.5, normalise: Boolean = True, section_colours: Optional[Union[ArrayLike, str]] = None, section_opacity: Floating = 1, convert_kwargs: Optional[Dict] = None, samples: Integer = 256, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the section colours of given *trimesh* hull along given axis and origin. Parameters ---------- hull *Trimesh* hull. model Colourspace model, see :attr:`colour.COLOURSPACE_MODELS` attribute for the list of supported colourspace models. axis Axis the hull section will be normal to. origin Coordinate along ``axis`` at which to plot the hull section. normalise Whether to normalise ``axis`` to the extent of the hull along it. section_colours Colours of the hull section, if ``section_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. section_opacity Opacity of the hull section colours. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. samples Samples count on one axis when computing the hull section colours. 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 -------- >>> from colour.models import RGB_COLOURSPACE_sRGB >>> from colour.utilities import is_trimesh_installed >>> vertices, faces, _outline = primitive_cube(1, 1, 1, 64, 64, 64) >>> XYZ_vertices = RGB_to_XYZ( ... vertices['position'] + 0.5, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.whitepoint, ... RGB_COLOURSPACE_sRGB.matrix_RGB_to_XYZ, ... ) >>> if is_trimesh_installed: ... import trimesh ... hull = trimesh.Trimesh(XYZ_vertices, faces, process=False) ... plot_hull_section_colours(hull, section_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Hull_Section_Colours.png :align: center :alt: plot_hull_section_colours """ axis = validate_method( axis, ["+z", "+x", "+y"], '"{0}" axis is invalid, it must be one of {1}!', ) hull = hull.copy() settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) section_colours = cast( ArrayLike, optional(section_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)), ) convert_kwargs = optional(convert_kwargs, {}) # Luminance / Lightness reordered along "z" axis. with suppress_warnings(python_warnings=True): ijk_vertices = colourspace_model_axis_reorder( convert(hull.vertices, "CIE XYZ", model, **convert_kwargs), model) ijk_vertices = np.nan_to_num(ijk_vertices) ijk_vertices *= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[ model] hull.vertices = ijk_vertices if axis == "+x": index_origin = 0 elif axis == "+y": index_origin = 1 elif axis == "+z": index_origin = 2 plane = MAPPING_AXIS_TO_PLANE[axis] section = hull_section(hull, axis, origin, normalise) padding = 0.1 * np.mean( COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[model]) min_x = np.min(ijk_vertices[..., plane[0]]) - padding max_x = np.max(ijk_vertices[..., plane[0]]) + padding min_y = np.min(ijk_vertices[..., plane[1]]) - padding max_y = np.max(ijk_vertices[..., plane[1]]) + padding extent = (min_x, max_x, min_y, max_y) use_RGB_section_colours = str(section_colours).upper() == "RGB" if use_RGB_section_colours: ii, jj = np.meshgrid( np.linspace(min_x, max_x, samples), np.linspace(max_y, min_y, samples), ) ij = tstack([ii, jj]) ijk_section = full((samples, samples, 3), np.median(section[..., index_origin])) ijk_section[..., plane] = ij ijk_section /= COLOURSPACE_MODELS_DOMAIN_RANGE_SCALE_1_TO_REFERENCE[ model] XYZ_section = convert( colourspace_model_axis_reorder(ijk_section, model, "Inverse"), model, "CIE XYZ", **convert_kwargs, ) RGB_section = XYZ_to_plotting_colourspace(XYZ_section) else: section_colours = np.hstack([section_colours, section_opacity]) facecolor = "none" if use_RGB_section_colours else section_colours polygon = Polygon( section[..., plane], facecolor=facecolor, edgecolor="none", zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.add_patch(polygon) if use_RGB_section_colours: image = axes.imshow( np.clip(RGB_section, 0, 1), interpolation="bilinear", extent=extent, clip_path=None, alpha=section_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) image.set_clip_path(polygon) settings = { "axes": axes, "bounding_box": extent, } settings.update(kwargs) return render(**settings)
def uv_to_Luv( uv: ArrayLike, illuminant: ArrayLike = CCS_ILLUMINANTS[ "CIE 1931 2 Degree Standard Observer"]["D65"], Y: Floating = 1, ) -> NDArray: """ Return the *CIE L\\*u\\*v\\** colourspace array from given :math:`uv^p` chromaticity coordinates by extending the array last dimension with given :math:`L` *Lightness*. Parameters ---------- uv :math:`uv^p` chromaticity coordinates. illuminant Reference *illuminant* *CIE xy* chromaticity coordinates or *CIE xyY* colourspace array. Y Optional :math:`Y` *luminance* value used to construct the intermediate *CIE XYZ* colourspace array, the default :math:`Y` *luminance* value is 1. Returns ------- :class:`numpy.ndarray` *CIE L\\*u\\*v\\** colourspace array. Notes ----- +----------------+-----------------------+-----------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +================+=======================+=================+ | ``Luv`` | ``L`` : [0, 100] | ``L`` : [0, 1] | | | | | | | ``u`` : [-100, 100] | ``u`` : [-1, 1] | | | | | | | ``v`` : [-100, 100] | ``v`` : [-1, 1] | +----------------+-----------------------+-----------------+ | ``illuminant`` | [0, 1] | [0, 1] | +----------------+-----------------------+-----------------+ References ---------- :cite:`CIETC1-482004j` Examples -------- >>> import numpy as np >>> uv = np.array([0.37720213, 0.50120264]) >>> uv_to_Luv(uv) # doctest: +ELLIPSIS array([ 100. , 233.1837603..., 42.7474385...]) """ u, v = tsplit(uv) Y = as_float_scalar(to_domain_1(Y)) X = 9 * u / (4 * v) Z = (-5 * Y * v - 3 * u / 4 + 3) / v XYZ = tstack([X, full(u.shape, Y), Z]) return XYZ_to_Luv(from_range_1(XYZ), illuminant)
def test_full(self): """ Tests :func:`colour.utilities.array.full` definition. """ np.testing.assert_equal(full(3, 0.5), np.full(3, 0.5))
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