def test_XYZ_to_plotting_colourspace(self): """ Tests :func:`colour.plotting.common.XYZ_to_plotting_colourspace` definition. """ XYZ = np.random.random(3) np.testing.assert_almost_equal( XYZ_to_sRGB(XYZ), XYZ_to_plotting_colourspace(XYZ), decimal=7)
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, )
def add_rainbow(axis, wavelengths, values, opacity=100): # sanity check: if not hasattr(axis, 'plot') and not hasattr(axis, 'add_patch'): raise Exception("ERROR:\tFirst argument needs to have method \"plot\".") from colour.plotting import XYZ_to_plotting_colourspace, filter_cmfs, CONSTANTS_COLOUR_STYLE from colour.colorimetry import CCS_ILLUMINANTS, wavelength_to_XYZ from colour.utilities import first_item, normalise_maximum from matplotlib.patches import Polygon col_map_f = "CIE 1931 2 Degree Standard Observer" cmfs = first_item(filter_cmfs(col_map_f).values()) wlen_cmfs = [n for n in wavelengths if n > cmfs.shape.start and n < cmfs.shape.end] clr = XYZ_to_plotting_colourspace( wavelength_to_XYZ(wlen_cmfs, cmfs), CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer']['E'], apply_cctf_encoding=False) clr = normalise_maximum(clr) clr = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(clr) polygon = Polygon( np.vstack([ [min(wavelengths), 0], np.array([wavelengths, values]).T.tolist(), [max(wavelengths), 0], ]), facecolor='none', edgecolor='none') axis.add_patch(polygon) if opacity < 100: padding = 0 else: padding = 0.1 for dom, col in [(wavelengths - padding, 'black'), (wlen_cmfs, clr)]: axis.bar( x=dom, height=max(values), width=1 + padding, color=col, align='edge', alpha=opacity/100, clip_path=polygon ) pass
def plot_simple_rainbow(ax=None, wavelength=None, flux=None, cmfs="CIE 1931 2 Degree Standard Observer", **kwargs): """ Plot a simple horizontal rainbow, based off colour's plot_single_sd. Parameters ---------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow should be drawn. cmfs : string The color matching function(s?) to use. Returns ------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow was drawn. """ if ax is None: ax = plt.gca() # pull out the CMFss cmfs = first_item(filter_cmfs(cmfs).values()) if wavelength is None: # create a grid of wavelengths (at which CMFss are useful) wavelength = cmfs.wavelengths ok = (wavelength >= np.min(cmfs.wavelengths)) & (wavelength <= np.max( cmfs.wavelengths)) # create colors at theose wavelengths colours = XYZ_to_plotting_colourspace( wavelength_to_XYZ(wavelength[ok], cmfs), CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["E"], ) # normalize the colors to their maximum? colours = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding( normalise_maximum(colours)) # create y values that will be plotted (these could be spectrum) if flux is None: flux = np.ones_like(wavelength) x_min, x_max = min(wavelength), max(wavelength) y_min, y_max = 0, max(flux) + max(flux) * 0.05 # create a polygon to define the top of the bars? polygon = Polygon( np.vstack([ (x_min, 0), tstack([wavelength[ok], flux[ok]]), (x_max, 0), ]), facecolor="none", edgecolor="none", ) ax.add_patch(polygon) # draw bars, with the colors at each vertical stripe padding = 0.2 ax.bar( x=wavelength[ok] - padding / 2, height=max(flux[ok]), width=1 + padding, color=colours, align="edge", ) return ax
def plot_sds_in_chromaticity_diagram( sds, cmfs='CIE 1931 2 Degree Standard Observer', chromaticity_diagram_callable=plot_chromaticity_diagram, method='CIE 1931', annotate_kwargs=None, plot_kwargs=None, **kwargs): """ Plots given spectral distribution chromaticity coordinates into the *Chromaticity Diagram* using given method. Parameters ---------- sds : array_like or MultiSpectralDistributions Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. 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. chromaticity_diagram_callable : callable, optional Callable responsible for drawing the *Chromaticity Diagram*. method : unicode, optional **{'CIE 1931', 'CIE 1960 UCS', 'CIE 1976 UCS'}**, *Chromaticity Diagram* method. annotate_kwargs : dict or array_like, optional Keyword arguments for the :func:`plt.annotate` definition, used to annotate the resulting chromaticity coordinates with their respective spectral distribution names. ``annotate_kwargs`` can be either a single dictionary applied to all the arrows with same settings or a sequence of dictionaries with different settings for each spectral distribution. The following special keyword arguments can also be used: - *annotate* : bool, whether to annotate the spectral distributions. plot_kwargs : dict or array_like, optional Keyword arguments for the :func:`plt.plot` definition, used to control the style of the plotted spectral distributions. ``plot_kwargs`` can be either a single dictionary applied to all the plotted spectral distributions with same settings or a sequence of dictionaries with different settings for each plotted spectral distributions. The following special keyword arguments can also be used: - *illuminant* : unicode or :class:`colour.SpectralDistribution`, the illuminant used to compute the spectral distributions colours. The default is the illuminant associated with the whitepoint of the default plotting colourspace. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - *cmfs* : unicode, the standard observer colour matching functions used for computing the spectral distributions colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - *normalise_sd_colours* : bool, whether to normalise the computed spectral distributions colours. The default is *True*. - *use_sd_colours* : bool, whether to use the computed spectral distributions colours under the plotting colourspace illuminant. Alternatively, it is possible to use the :func:`plt.plot` definition ``color`` argument with pre-computed values. The default is *True*. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.diagrams.plot_chromaticity_diagram`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Also handles keywords arguments for deprecation management. Returns ------- tuple Current figure and axes. Examples -------- >>> A = SDS_ILLUMINANTS['A'] >>> D65 = SDS_ILLUMINANTS['D65'] >>> annotate_kwargs = [ ... {'xytext': (-25, 15), 'arrowprops':{'arrowstyle':'-'}}, ... {} ... ] >>> plot_kwargs = [ ... { ... 'illuminant': SDS_ILLUMINANTS['E'], ... 'markersize' : 15, ... 'normalise_sd_colours': True, ... 'use_sd_colours': True ... }, ... {'illuminant': SDS_ILLUMINANTS['E']}, ... ] >>> plot_sds_in_chromaticity_diagram( ... [A, D65], annotate_kwargs=annotate_kwargs, plot_kwargs=plot_kwargs) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_SDS_In_Chromaticity_Diagram.png :align: center :alt: plot_sds_in_chromaticity_diagram """ annotate_kwargs = handle_arguments_deprecation( { 'ArgumentRenamed': [['annotate_parameters', 'annotate_kwargs']], }, **kwargs).get('annotate_kwargs', annotate_kwargs) sds = sds_and_msds_to_sds(sds) settings = {'uniform': True} settings.update(kwargs) _figure, axes = artist(**settings) method = method.upper() settings.update({ 'axes': axes, 'standalone': False, 'method': method, 'cmfs': cmfs, }) chromaticity_diagram_callable(**settings) if method == 'CIE 1931': def XYZ_to_ij(XYZ): """ Converts given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return XYZ_to_xy(XYZ) bounding_box = (-0.1, 0.9, -0.1, 0.9) elif method == 'CIE 1960 UCS': def XYZ_to_ij(XYZ): """ Converts given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return UCS_to_uv(XYZ_to_UCS(XYZ)) bounding_box = (-0.1, 0.7, -0.2, 0.6) elif method == 'CIE 1976 UCS': def XYZ_to_ij(XYZ): """ Converts given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return Luv_to_uv(XYZ_to_Luv(XYZ)) bounding_box = (-0.1, 0.7, -0.1, 0.7) else: raise ValueError( 'Invalid method: "{0}", must be one of ' '[\'CIE 1931\', \'CIE 1960 UCS\', \'CIE 1976 UCS\']'.format( method)) annotate_settings_collection = [{ 'annotate': True, 'xytext': (-50, 30), 'textcoords': 'offset points', 'arrowprops': CONSTANTS_ARROW_STYLE, } for _ in range(len(sds))] if annotate_kwargs is not None: update_settings_collection(annotate_settings_collection, annotate_kwargs, len(sds)) plot_settings_collection = [{ 'color': CONSTANTS_COLOUR_STYLE.colour.brightest, 'label': '{0}'.format(sd.strict_name), 'marker': 'o', 'markeredgecolor': CONSTANTS_COLOUR_STYLE.colour.dark, 'markeredgewidth': CONSTANTS_COLOUR_STYLE.geometry.short * 0.75, 'markersize': (CONSTANTS_COLOUR_STYLE.geometry.short * 6 + CONSTANTS_COLOUR_STYLE.geometry.short * 0.75), 'cmfs': cmfs, 'illuminant': SDS_ILLUMINANTS[ CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint_name], 'use_sd_colours': False, 'normalise_sd_colours': False, } for sd in sds] if plot_kwargs is not None: update_settings_collection(plot_settings_collection, plot_kwargs, len(sds)) for i, sd in enumerate(sds): plot_settings = plot_settings_collection[i] cmfs = first_item(filter_cmfs(plot_settings.pop('cmfs')).values()) illuminant = first_item( filter_illuminants(plot_settings.pop('illuminant')).values()) normalise_sd_colours = plot_settings.pop('normalise_sd_colours') use_sd_colours = plot_settings.pop('use_sd_colours') with domain_range_scale('1'): XYZ = sd_to_XYZ(sd, cmfs, illuminant) if use_sd_colours: if normalise_sd_colours: XYZ /= XYZ[..., 1] plot_settings['color'] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1) ij = XYZ_to_ij(XYZ) axes.plot(ij[0], ij[1], **plot_settings) if (sd.name is not None and annotate_settings_collection[i]['annotate']): annotate_settings = annotate_settings_collection[i] annotate_settings.pop('annotate') axes.annotate(sd.name, xy=ij, **annotate_settings) settings.update({'standalone': True, 'bounding_box': bounding_box}) settings.update(kwargs) return render(**settings)
def plot_blackbody_spectral_radiance( temperature=3500, cmfs='CIE 1931 2 Degree Standard Observer', blackbody='VY Canis Major', **kwargs): """ Plots given blackbody spectral radiance. Parameters ---------- temperature : numeric, optional Blackbody temperature. cmfs : unicode, optional Standard observer colour matching functions. blackbody : unicode, optional Blackbody name. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> plot_blackbody_spectral_radiance(3500, blackbody='VY Canis Major') ... # doctest: +ELLIPSIS (<Figure size ... with 2 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Blackbody_Spectral_Radiance.png :align: center :alt: plot_blackbody_spectral_radiance """ figure = plt.figure() figure.subplots_adjust(hspace=COLOUR_STYLE_CONSTANTS.geometry.short / 2) cmfs = first_item(filter_cmfs(cmfs).values()) sd = sd_blackbody(temperature, cmfs.shape) axes = figure.add_subplot(211) settings = { 'axes': axes, 'title': '{0} - Spectral Radiance'.format(blackbody), 'y_label': 'W / (sr m$^2$) / m', } settings.update(kwargs) settings['standalone'] = False plot_single_sd(sd, cmfs.name, **settings) axes = figure.add_subplot(212) with domain_range_scale('1'): XYZ = sd_to_XYZ(sd, cmfs) RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ)) settings = { 'axes': axes, 'aspect': None, 'title': '{0} - Colour'.format(blackbody), 'x_label': '{0}K'.format(temperature), 'y_label': '', 'x_ticker': False, 'y_ticker': False, } settings.update(kwargs) settings['standalone'] = False figure, axes = plot_single_colour_swatch(ColourSwatch(name='', RGB=RGB), **settings) settings = {'axes': axes, 'standalone': True} settings.update(kwargs) return render(**settings)
def plot_chromaticity_diagram_colours( samples=256, diagram_opacity=1.0, diagram_clipping_path=None, cmfs='CIE 1931 2 Degree Standard Observer', method='CIE 1931', **kwargs): """ Plots the *Chromaticity Diagram* colours according to given method. Parameters ---------- samples : numeric, optional Samples count on one axis. diagram_opacity : numeric, optional Opacity of the *Chromaticity Diagram* colours. diagram_clipping_path : array_like, optional Path of points used to clip the *Chromaticity Diagram* colours. cmfs : unicode, optional Standard observer colour matching functions used for *Chromaticity Diagram* bounds. 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_chromaticity_diagram_colours() # doctest: +SKIP .. image:: ../_static/Plotting_Plot_Chromaticity_Diagram_Colours.png :align: center :alt: plot_chromaticity_diagram_colours """ settings = {'uniform': True} settings.update(kwargs) figure, axes = artist(**settings) method = method.upper() cmfs = first_item(filter_cmfs(cmfs).values()) illuminant = COLOUR_STYLE_CONSTANTS.colour.colourspace.whitepoint ii, jj = np.meshgrid( np.linspace(0, 1, samples), np.linspace(1, 0, samples)) ij = tstack([ii, jj]) if method == 'CIE 1931': XYZ = xy_to_XYZ(ij) spectral_locus = XYZ_to_xy(cmfs.values, illuminant) elif method == 'CIE 1960 UCS': XYZ = xy_to_XYZ(UCS_uv_to_xy(ij)) spectral_locus = UCS_to_uv(XYZ_to_UCS(cmfs.values)) elif method == 'CIE 1976 UCS': XYZ = xy_to_XYZ(Luv_uv_to_xy(ij)) spectral_locus = Luv_to_uv( XYZ_to_Luv(cmfs.values, illuminant), illuminant) else: raise ValueError( 'Invalid method: "{0}", must be one of ' '{\'CIE 1931\', \'CIE 1960 UCS\', \'CIE 1976 UCS\'}'.format( method)) RGB = normalise_maximum( XYZ_to_plotting_colourspace(XYZ, illuminant), axis=-1) polygon = Polygon( spectral_locus if diagram_clipping_path is None else diagram_clipping_path, facecolor='none', edgecolor='none') axes.add_patch(polygon) # Preventing bounding box related issues as per # https://github.com/matplotlib/matplotlib/issues/10529 image = axes.imshow( RGB, interpolation='bilinear', extent=(0, 1, 0, 1), clip_path=None, alpha=diagram_opacity) image.set_clip_path(polygon) settings = {'axes': axes} settings.update(kwargs) return render(**kwargs)
def plot_hull_section_contour( 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, contour_colours: Optional[Union[ArrayLike, str]] = None, contour_opacity: Floating = 1, convert_kwargs: Optional[Dict] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the section contour 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. contour_colours Colours of the hull section contour, if ``contour_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. contour_opacity Opacity of the hull section contour. convert_kwargs Keyword arguments for the :func:`colour.convert` definition. 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_contour(hull, contour_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Hull_Section_Contour.png :align: center :alt: plot_hull_section_contour """ hull = hull.copy() contour_colours = cast( Union[ArrayLike, str], optional(contour_colours, CONSTANTS_COLOUR_STYLE.colour.dark), ) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) convert_kwargs = optional(convert_kwargs, {}) # Luminance / Lightness is re-ordered along "z-up" 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 plane = MAPPING_AXIS_TO_PLANE[axis] 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_contour_colours = str(contour_colours).upper() == "RGB" section = hull_section(hull, axis, origin, normalise) if use_RGB_contour_colours: ijk_section = 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, ) contour_colours = np.clip(XYZ_to_plotting_colourspace(XYZ_section), 0, 1) section = np.reshape(section[..., plane], (-1, 1, 2)) line_collection = LineCollection( np.concatenate([section[:-1], section[1:]], axis=1), colors=contour_colours, alpha=contour_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.add_collection(line_collection) settings = { "axes": axes, "bounding_box": extent, } settings.update(kwargs) return render(**settings)
def plot_multi_sds(sds, cmfs='CIE 1931 2 Degree Standard Observer', use_sds_colours=False, normalise_sds_colours=False, **kwargs): """ Plots given spectral distributions. Parameters ---------- sds : array_like or MultiSpectralDistributions Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. cmfs : unicode, optional Standard observer colour matching functions used for spectrum creation. use_sds_colours : bool, optional Whether to use spectral distributions colours. normalise_sds_colours : bool Whether to normalise spectral distributions colours. 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 -------- >>> from colour import SpectralDistribution >>> data_1 = { ... 500: 0.004900, ... 510: 0.009300, ... 520: 0.063270, ... 530: 0.165500, ... 540: 0.290400, ... 550: 0.433450, ... 560: 0.594500 ... } >>> data_2 = { ... 500: 0.323000, ... 510: 0.503000, ... 520: 0.710000, ... 530: 0.862000, ... 540: 0.954000, ... 550: 0.994950, ... 560: 0.995000 ... } >>> sd_1 = SpectralDistribution(data_1, name='Custom 1') >>> sd_2 = SpectralDistribution(data_2, name='Custom 2') >>> plot_multi_sds([sd_1, sd_2]) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Multi_SDS.png :align: center :alt: plot_multi_sds """ _figure, axes = artist(**kwargs) sds = sds_and_multi_sds_to_sds(sds) cmfs = first_item(filter_cmfs(cmfs).values()) illuminant = ILLUMINANTS_SDS[ COLOUR_STYLE_CONSTANTS.colour.colourspace.illuminant] x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], [] for sd in sds: wavelengths, values = sd.wavelengths, sd.values shape = sd.shape x_limit_min.append(shape.start) x_limit_max.append(shape.end) y_limit_min.append(min(values)) y_limit_max.append(max(values)) if use_sds_colours: with domain_range_scale('1'): XYZ = sd_to_XYZ(sd, cmfs, illuminant) if normalise_sds_colours: XYZ = normalise_maximum(XYZ, clip=False) RGB = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1) axes.plot(wavelengths, values, color=RGB, label=sd.strict_name) else: axes.plot(wavelengths, values, label=sd.strict_name) bounding_box = (min(x_limit_min), max(x_limit_max), min(y_limit_min), max(y_limit_max) + max(y_limit_max) * 0.05) settings = { 'axes': axes, 'bounding_box': bounding_box, 'legend': True, 'x_label': 'Wavelength $\\lambda$ (nm)', 'y_label': 'Spectral Distribution', } settings.update(kwargs) return render(**settings)
def plot_multi_sds( sds: Union[Sequence[Union[SpectralDistribution, MultiSpectralDistributions]], MultiSpectralDistributions, ], plot_kwargs: Optional[Union[Dict, List[Dict]]] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given spectral distributions. Parameters ---------- sds Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. plot_kwargs Keyword arguments for the :func:`matplotlib.pyplot.plot` definition, used to control the style of the plotted spectral distributions. `plot_kwargs`` can be either a single dictionary applied to all the plotted spectral distributions with the same settings or a sequence of dictionaries with different settings for each plotted spectral distributions. The following special keyword arguments can also be used: - ``illuminant`` : The illuminant used to compute the spectral distributions colours. The default is the illuminant associated with the whitepoint of the default plotting colourspace. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - ``cmfs`` : The standard observer colour matching functions used for computing the spectral distributions colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - ``normalise_sd_colours`` : Whether to normalise the computed spectral distributions colours. The default is *True*. - ``use_sd_colours`` : Whether to use the computed spectral distributions colours under the plotting colourspace illuminant. Alternatively, it is possible to use the :func:`matplotlib.pyplot.plot` definition ``color`` argument with pre-computed values. The default is *True*. 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 import SpectralDistribution >>> data_1 = { ... 500: 0.004900, ... 510: 0.009300, ... 520: 0.063270, ... 530: 0.165500, ... 540: 0.290400, ... 550: 0.433450, ... 560: 0.594500 ... } >>> data_2 = { ... 500: 0.323000, ... 510: 0.503000, ... 520: 0.710000, ... 530: 0.862000, ... 540: 0.954000, ... 550: 0.994950, ... 560: 0.995000 ... } >>> sd_1 = SpectralDistribution(data_1, name='Custom 1') >>> sd_2 = SpectralDistribution(data_2, name='Custom 2') >>> plot_kwargs = [ ... {'use_sd_colours': True}, ... {'use_sd_colours': True, 'linestyle': 'dashed'}, ... ] >>> plot_multi_sds([sd_1, sd_2], plot_kwargs=plot_kwargs) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Multi_SDS.png :align: center :alt: plot_multi_sds """ _figure, axes = artist(**kwargs) sds_converted = sds_and_msds_to_sds(sds) plot_settings_collection = [{ "label": f"{sd.strict_name}", "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_line, "cmfs": "CIE 1931 2 Degree Standard Observer", "illuminant": SDS_ILLUMINANTS[ CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint_name], "use_sd_colours": False, "normalise_sd_colours": False, } for sd in sds_converted] if plot_kwargs is not None: update_settings_collection(plot_settings_collection, plot_kwargs, len(sds_converted)) x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], [] for i, sd in enumerate(sds_converted): plot_settings = plot_settings_collection[i] cmfs = cast( MultiSpectralDistributions, first_item(filter_cmfs(plot_settings.pop("cmfs")).values()), ) illuminant = cast( SpectralDistribution, first_item( filter_illuminants(plot_settings.pop("illuminant")).values()), ) normalise_sd_colours = plot_settings.pop("normalise_sd_colours") use_sd_colours = plot_settings.pop("use_sd_colours") wavelengths, values = sd.wavelengths, sd.values shape = sd.shape x_limit_min.append(shape.start) x_limit_max.append(shape.end) y_limit_min.append(min(values)) y_limit_max.append(max(values)) if use_sd_colours: with domain_range_scale("1"): XYZ = sd_to_XYZ(sd, cmfs, illuminant) if normalise_sd_colours: XYZ /= XYZ[..., 1] plot_settings["color"] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1) axes.plot(wavelengths, values, **plot_settings) bounding_box = ( min(x_limit_min), max(x_limit_max), min(y_limit_min), max(y_limit_max) + np.max(y_limit_max) * 0.05, ) settings: Dict[str, Any] = { "axes": axes, "bounding_box": bounding_box, "legend": True, "x_label": "Wavelength $\\lambda$ (nm)", "y_label": "Spectral Distribution", } settings.update(kwargs) return render(**settings)
def plot_with_rainbow_fill(ax=None, wavelength=None, flux=None, cmfs="CIE 1931 2 Degree Standard Observer", rainbowtop=np.inf, **kwargs): """ (This is still *real* blarg-y*.) Plot a spectrum, with a rainbow underneath. Parameters ---------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow should be drawn. cmfs : string The color matching function(s?) to use. Returns ------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow was drawn. """ if ax is None: ax = plt.gca() if wavelength is None: # create a grid of wavelengths (at which CMFss are useful) wavelength = CMFs.wavelengths # create y values that will be plotted (these could be spectrum) if flux is None: flux = np.ones_like(wavelength) # pull out only the values that *can* be converted to colors ok = (wavelength >= np.min(CMFs.wavelengths)) & (wavelength <= np.max( CMFs.wavelengths)) w, f = wavelength[ok], flux[ok] # clip the top of the box? f = np.minimum(f, rainbowtop) XYZ = wavelength_to_XYZ(w) # create colors at theose wavelengths colours = XYZ_to_plotting_colourspace(XYZ) # normalize the colors to their maximum? # colours = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding( # normalise_maximum(colours)) colours = np.maximum(0, colours / np.max(colours)) x_min, x_max = min(w), max(w) y_min, y_max = 0, max(f) + max(f) * 0.05 # create a polygon to define the top of the bars? polygon = Polygon( np.vstack([ (x_min, 0), tstack([w, f]), (x_max, 0), ]), facecolor="none", edgecolor="none", ) ax.add_patch(polygon) # draw bars, with the colors at each vertical stripe padding = 0.0 dw = np.mean(np.diff(w)) ax.bar( x=w, height=f, width=dw, color=colours, clip_path=polygon, align="edge", clip_on=True, ) return ax
def plot_blackbody_colours( shape: SpectralShape = SpectralShape(150, 12500, 50), cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot blackbody colours. Parameters ---------- shape Spectral shape to use as plot boundaries. cmfs Standard observer colour matching functions used for computing the blackbody colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. 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_blackbody_colours(SpectralShape(150, 12500, 50)) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Blackbody_Colours.png :align: center :alt: plot_blackbody_colours """ _figure, axes = artist(**kwargs) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) RGB = [] temperatures = [] for temperature in shape: sd = sd_blackbody(temperature, cmfs.shape) with domain_range_scale("1"): XYZ = sd_to_XYZ(sd, cmfs) RGB.append(normalise_maximum(XYZ_to_plotting_colourspace(XYZ))) temperatures.append(temperature) x_min, x_max = min(temperatures), max(temperatures) y_min, y_max = 0, 1 padding = 0.1 axes.bar( x=np.array(temperatures) - padding, height=1, width=shape.interval + (padding * shape.interval), color=RGB, align="edge", zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) settings: Dict[str, Any] = { "axes": axes, "bounding_box": (x_min, x_max, y_min, y_max), "title": "Blackbody Colours", "x_label": "Temperature K", "y_label": None, } settings.update(kwargs) return render(**settings)
def plot_single_sd( sd: SpectralDistribution, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", out_of_gamut_clipping: Boolean = True, modulate_colours_with_sd_amplitude: Boolean = False, equalize_sd_amplitude: Boolean = False, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given spectral distribution. Parameters ---------- sd Spectral distribution to plot. cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. out_of_gamut_clipping Whether to clip out of gamut colours otherwise, the colours will be offset by the absolute minimal colour leading to a rendering on gray background, less saturated and smoother. modulate_colours_with_sd_amplitude Whether to modulate the colours with the spectral distribution amplitude. equalize_sd_amplitude Whether to equalize the spectral distribution amplitude. Equalization occurs after the colours modulation thus setting both arguments to *True* will generate a spectrum strip where each wavelength colour is modulated by the spectral distribution amplitude. The usual 5% margin above the spectral distribution is also omitted. 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. References ---------- :cite:`Spiker2015a` Examples -------- >>> from colour import SpectralDistribution >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360 ... } >>> sd = SpectralDistribution(data, name='Custom') >>> plot_single_sd(sd) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Single_SD.png :align: center :alt: plot_single_sd """ _figure, axes = artist(**kwargs) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) sd = cast(SpectralDistribution, sd.copy()) sd.interpolator = LinearInterpolator wavelengths = cmfs.wavelengths[np.logical_and( cmfs.wavelengths >= max(min(cmfs.wavelengths), min(sd.wavelengths)), cmfs.wavelengths <= min(max(cmfs.wavelengths), max(sd.wavelengths)), )] values = as_float_array(sd[wavelengths]) RGB = XYZ_to_plotting_colourspace( wavelength_to_XYZ(wavelengths, cmfs), CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["E"], apply_cctf_encoding=False, ) if not out_of_gamut_clipping: RGB += np.abs(np.min(RGB)) RGB = normalise_maximum(RGB) if modulate_colours_with_sd_amplitude: RGB *= (values / np.max(values))[..., np.newaxis] RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(RGB) if equalize_sd_amplitude: values = ones(values.shape) margin = 0 if equalize_sd_amplitude else 0.05 x_min, x_max = min(wavelengths), max(wavelengths) y_min, y_max = 0, max(values) + max(values) * margin polygon = Polygon( np.vstack([ (x_min, 0), tstack([wavelengths, values]), (x_max, 0), ]), facecolor="none", edgecolor="none", zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.add_patch(polygon) padding = 0.1 axes.bar( x=wavelengths - padding, height=max(values), width=1 + padding, color=RGB, align="edge", clip_path=polygon, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.plot( wavelengths, values, color=CONSTANTS_COLOUR_STYLE.colour.dark, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) settings: Dict[str, Any] = { "axes": axes, "bounding_box": (x_min, x_max, y_min, y_max), "title": f"{sd.strict_name} - {cmfs.strict_name}", "x_label": "Wavelength $\\lambda$ (nm)", "y_label": "Spectral Distribution", } settings.update(kwargs) return render(**settings)
def plot_sds_in_chromaticity_diagram( sds: Union[Sequence[Union[SpectralDistribution, MultiSpectralDistributions]], MultiSpectralDistributions, ], cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", chromaticity_diagram_callable: Callable = plot_chromaticity_diagram, method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"], str] = "CIE 1931", annotate_kwargs: Optional[Union[Dict, List[Dict]]] = None, plot_kwargs: Optional[Union[Dict, List[Dict]]] = None, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given spectral distribution chromaticity coordinates into the *Chromaticity Diagram* using given method. Parameters ---------- sds Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. 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. chromaticity_diagram_callable Callable responsible for drawing the *Chromaticity Diagram*. method *Chromaticity Diagram* method. annotate_kwargs Keyword arguments for the :func:`matplotlib.pyplot.annotate` definition, used to annotate the resulting chromaticity coordinates with their respective spectral distribution names. ``annotate_kwargs`` can be either a single dictionary applied to all the arrows with same settings or a sequence of dictionaries with different settings for each spectral distribution. The following special keyword arguments can also be used: - ``annotate`` : Whether to annotate the spectral distributions. plot_kwargs Keyword arguments for the :func:`matplotlib.pyplot.plot` definition, used to control the style of the plotted spectral distributions. `plot_kwargs`` can be either a single dictionary applied to all the plotted spectral distributions with the same settings or a sequence of dictionaries with different settings for each plotted spectral distributions. The following special keyword arguments can also be used: - ``illuminant`` : The illuminant used to compute the spectral distributions colours. The default is the illuminant associated with the whitepoint of the default plotting colourspace. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - ``cmfs`` : The standard observer colour matching functions used for computing the spectral distributions colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - ``normalise_sd_colours`` : Whether to normalise the computed spectral distributions colours. The default is *True*. - ``use_sd_colours`` : Whether to use the computed spectral distributions colours under the plotting colourspace illuminant. Alternatively, it is possible to use the :func:`matplotlib.pyplot.plot` definition ``color`` argument with pre-computed values. The default is *True*. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.diagrams.plot_chromaticity_diagram`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> A = SDS_ILLUMINANTS['A'] >>> D65 = SDS_ILLUMINANTS['D65'] >>> annotate_kwargs = [ ... {'xytext': (-25, 15), 'arrowprops':{'arrowstyle':'-'}}, ... {} ... ] >>> plot_kwargs = [ ... { ... 'illuminant': SDS_ILLUMINANTS['E'], ... 'markersize' : 15, ... 'normalise_sd_colours': True, ... 'use_sd_colours': True ... }, ... {'illuminant': SDS_ILLUMINANTS['E']}, ... ] >>> plot_sds_in_chromaticity_diagram( ... [A, D65], annotate_kwargs=annotate_kwargs, plot_kwargs=plot_kwargs) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_SDS_In_Chromaticity_Diagram.png :align: center :alt: plot_sds_in_chromaticity_diagram """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) sds_converted = sds_and_msds_to_sds(sds) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) settings.update({ "axes": axes, "standalone": False, "method": method, "cmfs": cmfs, }) chromaticity_diagram_callable(**settings) if method == "cie 1931": def XYZ_to_ij(XYZ: NDArray) -> NDArray: """ Convert given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return XYZ_to_xy(XYZ) bounding_box = (-0.1, 0.9, -0.1, 0.9) elif method == "cie 1960 ucs": def XYZ_to_ij(XYZ: NDArray) -> NDArray: """ Convert given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return UCS_to_uv(XYZ_to_UCS(XYZ)) bounding_box = (-0.1, 0.7, -0.2, 0.6) elif method == "cie 1976 ucs": def XYZ_to_ij(XYZ: NDArray) -> NDArray: """ Convert given *CIE XYZ* tristimulus values to *ij* chromaticity coordinates. """ return Luv_to_uv(XYZ_to_Luv(XYZ)) bounding_box = (-0.1, 0.7, -0.1, 0.7) annotate_settings_collection = [{ "annotate": True, "xytext": (-50, 30), "textcoords": "offset points", "arrowprops": CONSTANTS_ARROW_STYLE, "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_annotation, } for _ in range(len(sds_converted))] if annotate_kwargs is not None: update_settings_collection(annotate_settings_collection, annotate_kwargs, len(sds_converted)) plot_settings_collection = [{ "color": CONSTANTS_COLOUR_STYLE.colour.brightest, "label": f"{sd.strict_name}", "marker": "o", "markeredgecolor": CONSTANTS_COLOUR_STYLE.colour.dark, "markeredgewidth": CONSTANTS_COLOUR_STYLE.geometry.short * 0.75, "markersize": (CONSTANTS_COLOUR_STYLE.geometry.short * 6 + CONSTANTS_COLOUR_STYLE.geometry.short * 0.75), "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_line, "cmfs": cmfs, "illuminant": SDS_ILLUMINANTS[ CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint_name], "use_sd_colours": False, "normalise_sd_colours": False, } for sd in sds_converted] if plot_kwargs is not None: update_settings_collection(plot_settings_collection, plot_kwargs, len(sds_converted)) for i, sd in enumerate(sds_converted): plot_settings = plot_settings_collection[i] cmfs = cast( MultiSpectralDistributions, first_item(filter_cmfs(plot_settings.pop("cmfs")).values()), ) illuminant = cast( SpectralDistribution, first_item( filter_illuminants(plot_settings.pop("illuminant")).values()), ) normalise_sd_colours = plot_settings.pop("normalise_sd_colours") use_sd_colours = plot_settings.pop("use_sd_colours") with domain_range_scale("1"): XYZ = sd_to_XYZ(sd, cmfs, illuminant) if use_sd_colours: if normalise_sd_colours: XYZ /= XYZ[..., 1] plot_settings["color"] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1) ij = XYZ_to_ij(XYZ) axes.plot(ij[0], ij[1], **plot_settings) if sd.name is not None and annotate_settings_collection[i]["annotate"]: annotate_settings = annotate_settings_collection[i] annotate_settings.pop("annotate") axes.annotate(sd.name, xy=ij, **annotate_settings) settings.update({"standalone": True, "bounding_box": bounding_box}) settings.update(kwargs) return render(**settings)
def plot_chromaticity_diagram_colours( samples: Integer = 256, diagram_colours: Optional[Union[ArrayLike, str]] = None, diagram_opacity: Floating = 1, diagram_clipping_path: Optional[ArrayLike] = None, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", method: Union[Literal["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"], str] = "CIE 1931", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the *Chromaticity Diagram* colours according to given method. Parameters ---------- samples Samples count on one axis when computing the *Chromaticity Diagram* colours. diagram_colours Colours of the *Chromaticity Diagram*, if ``diagram_colours`` is set to *RGB*, the colours will be computed according to the corresponding coordinates. diagram_opacity Opacity of the *Chromaticity Diagram*. diagram_clipping_path Path of points used to clip the *Chromaticity Diagram* colours. 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. 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_chromaticity_diagram_colours(diagram_colours='RGB') ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Chromaticity_Diagram_Colours.png :align: center :alt: plot_chromaticity_diagram_colours """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) diagram_colours = cast( ArrayLike, optional(diagram_colours, HEX_to_RGB(CONSTANTS_COLOUR_STYLE.colour.average)), ) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint if method == "cie 1931": spectral_locus = XYZ_to_xy(cmfs.values, illuminant) elif method == "cie 1960 ucs": spectral_locus = UCS_to_uv(XYZ_to_UCS(cmfs.values)) elif method == "cie 1976 ucs": spectral_locus = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) use_RGB_diagram_colours = str(diagram_colours).upper() == "RGB" if use_RGB_diagram_colours: ii, jj = np.meshgrid(np.linspace(0, 1, samples), np.linspace(1, 0, samples)) ij = tstack([ii, jj]) # NOTE: Various values in the grid have potential to generate # zero-divisions, they could be avoided by perturbing the grid, e.g. # adding a small epsilon. It was decided instead to disable warnings. with suppress_warnings(python_warnings=True): if method == "cie 1931": XYZ = xy_to_XYZ(ij) elif method == "cie 1960 ucs": XYZ = xy_to_XYZ(UCS_uv_to_xy(ij)) elif method == "cie 1976 ucs": XYZ = xy_to_XYZ(Luv_uv_to_xy(ij)) diagram_colours = normalise_maximum(XYZ_to_plotting_colourspace( XYZ, illuminant), axis=-1) polygon = Polygon( spectral_locus if diagram_clipping_path is None else diagram_clipping_path, facecolor="none" if use_RGB_diagram_colours else np.hstack( [diagram_colours, diagram_opacity]), edgecolor="none" if use_RGB_diagram_colours else np.hstack( [diagram_colours, diagram_opacity]), zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) axes.add_patch(polygon) if use_RGB_diagram_colours: # Preventing bounding box related issues as per # https://github.com/matplotlib/matplotlib/issues/10529 image = axes.imshow( diagram_colours, interpolation="bilinear", extent=(0, 1, 0, 1), clip_path=None, alpha=diagram_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) image.set_clip_path(polygon) settings = {"axes": axes} settings.update(kwargs) return render(**kwargs)
def plot_spectral_locus( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", spectral_locus_colours: Optional[Union[ArrayLike, str]] = None, spectral_locus_opacity: Floating = 1, spectral_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 *Spectral Locus* according to given method. Parameters ---------- 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. spectral_locus_colours Colours of the *Spectral Locus*, if ``spectral_locus_colours`` is set to *RGB*, the colours will be computed according to the corresponding chromaticity coordinates. spectral_locus_opacity Opacity of the *Spectral Locus*. spectral_locus_labels Array of wavelength labels used to customise which labels will be drawn around the spectral locus. Passing an empty array will result in no wavelength labels 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_spectral_locus(spectral_locus_colours='RGB') # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Spectral_Locus.png :align: center :alt: plot_spectral_locus """ method = validate_method(method, ["CIE 1931", "CIE 1960 UCS", "CIE 1976 UCS"]) spectral_locus_colours = optional(spectral_locus_colours, CONSTANTS_COLOUR_STYLE.colour.dark) settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint wavelengths = list(cmfs.wavelengths) equal_energy = np.array([1 / 3] * 2) if method == "cie 1931": ij = XYZ_to_xy(cmfs.values, illuminant) labels = cast( Tuple, optional( spectral_locus_labels, ( 390, 460, 470, 480, 490, 500, 510, 520, 540, 560, 580, 600, 620, 700, ), ), ) elif method == "cie 1960 ucs": ij = UCS_to_uv(XYZ_to_UCS(cmfs.values)) labels = cast( Tuple, optional( spectral_locus_labels, ( 420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680, ), ), ) elif method == "cie 1976 ucs": ij = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) labels = cast( Tuple, optional( spectral_locus_labels, ( 420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680, ), ), ) pl_ij = np.reshape( tstack([ np.linspace(ij[0][0], ij[-1][0], 20), np.linspace(ij[0][1], ij[-1][1], 20), ]), (-1, 1, 2), ) sl_ij = np.copy(ij).reshape(-1, 1, 2) purple_line_colours: Optional[Union[ArrayLike, str]] if str(spectral_locus_colours).upper() == "RGB": spectral_locus_colours = normalise_maximum(XYZ_to_plotting_colourspace( cmfs.values), axis=-1) if method == "cie 1931": XYZ = xy_to_XYZ(pl_ij) elif method == "cie 1960 ucs": XYZ = xy_to_XYZ(UCS_uv_to_xy(pl_ij)) elif method == "cie 1976 ucs": XYZ = xy_to_XYZ(Luv_uv_to_xy(pl_ij)) purple_line_colours = normalise_maximum(XYZ_to_plotting_colourspace( np.reshape(XYZ, (-1, 3))), axis=-1) else: purple_line_colours = spectral_locus_colours for slp_ij, slp_colours in ( (pl_ij, purple_line_colours), (sl_ij, spectral_locus_colours), ): line_collection = LineCollection( np.concatenate([slp_ij[:-1], slp_ij[1:]], axis=1), colors=slp_colours, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_scatter, ) axes.add_collection(line_collection) wl_ij = dict(zip(wavelengths, ij)) for label in labels: ij_l = wl_ij.get(label) if ij_l is None: continue ij_l = as_float_array([ij_l]) i, j = tsplit(ij_l) index = bisect.bisect(wavelengths, label) left = wavelengths[index - 1] if index >= 0 else wavelengths[index] right = (wavelengths[index] if index < len(wavelengths) else wavelengths[-1]) dx = wl_ij[right][0] - wl_ij[left][0] dy = wl_ij[right][1] - wl_ij[left][1] direction = np.array([-dy, dx]) normal = (np.array([-dy, dx]) if np.dot( normalise_vector(ij_l - equal_energy), normalise_vector(direction), ) > 0 else np.array([dy, -dx])) normal = normalise_vector(normal) / 30 label_colour = ( spectral_locus_colours if is_string(spectral_locus_colours) else spectral_locus_colours[index] # type: ignore[index] ) axes.plot( (i, i + normal[0] * 0.75), (j, j + normal[1] * 0.75), color=label_colour, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.plot( i, j, "o", color=label_colour, alpha=spectral_locus_opacity, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_line, ) axes.text( i + normal[0], j + normal[1], label, clip_on=True, ha="left" if normal[0] >= 0 else "right", va="center", fontdict={"size": "small"}, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_label, ) settings = {"axes": axes} settings.update(kwargs) return render(**kwargs)
def plot_colour_quality_bars(specifications, labels=True, hatching=None, hatching_repeat=2, **kwargs): """ Plots the colour quality data of given illuminants or light sources colour quality specifications. Parameters ---------- specifications : array_like Array of illuminants or light sources colour quality specifications. labels : bool, optional Add labels above bars. hatching : bool or None, optional Use hatching for the bars. hatching_repeat : int, optional Hatching pattern repeat. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.quality.plot_colour_quality_bars`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> from colour import (ILLUMINANTS_SDS, ... LIGHT_SOURCES_SDS, SpectralShape) >>> illuminant = ILLUMINANTS_SDS['FL2'] >>> light_source = LIGHT_SOURCES_SDS['Kinoton 75P'] >>> light_source = light_source.copy().align(SpectralShape(360, 830, 1)) >>> cqs_i = colour_quality_scale(illuminant, additional_data=True) >>> cqs_l = colour_quality_scale(light_source, additional_data=True) >>> plot_colour_quality_bars([cqs_i, cqs_l]) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Colour_Quality_Bars.png :align: center :alt: plot_colour_quality_bars """ settings = {'uniform': True} settings.update(kwargs) _figure, axes = artist(**settings) bar_width = 0.5 y_ticks_interval = 10 count_s, count_Q_as = len(specifications), 0 patterns = cycle(COLOUR_STYLE_CONSTANTS.hatch.patterns) if hatching is None: hatching = False if count_s == 1 else True for i, specification in enumerate(specifications): Q_a, Q_as, colorimetry_data = (specification.Q_a, specification.Q_as, specification.colorimetry_data) count_Q_as = len(Q_as) colours = ([[1] * 3] + [ np.clip(XYZ_to_plotting_colourspace(x.XYZ), 0, 1) for x in colorimetry_data[0] ]) x = (i + np.arange(0, (count_Q_as + 1) * (count_s + 1), (count_s + 1), dtype=DEFAULT_FLOAT_DTYPE)) * bar_width y = [s[1].Q_a for s in sorted(Q_as.items(), key=lambda s: s[0])] y = np.array([Q_a] + list(y)) bars = axes.bar(x, np.abs(y), color=colours, width=bar_width, edgecolor=COLOUR_STYLE_CONSTANTS.colour.dark, label=specification.name) hatches = ([next(patterns) * hatching_repeat] * (count_Q_as + 1) if hatching else np.where( y < 0, next(patterns), None).tolist()) for j, bar in enumerate(bars.patches): bar.set_hatch(hatches[j]) if labels: label_rectangles( ['{0:.1f}'.format(y_v) for y_v in y], bars, rotation='horizontal' if count_s == 1 else 'vertical', offset=(0 if count_s == 1 else 3 / 100 * count_s + 65 / 1000, 0.025), text_size=-5 / 7 * count_s + 12.5, axes=axes) axes.axhline(y=100, color=COLOUR_STYLE_CONSTANTS.colour.dark, linestyle='--') axes.set_xticks( (np.arange(0, (count_Q_as + 1) * (count_s + 1), (count_s + 1), dtype=DEFAULT_FLOAT_DTYPE) * bar_width + (count_s * bar_width / 2)), ['Qa'] + ['Q{0}'.format(index + 1) for index in range(0, count_Q_as + 1, 1)]) axes.set_yticks(range(0, 100 + y_ticks_interval, y_ticks_interval)) aspect = 1 / (120 / (bar_width + len(Q_as) + bar_width * 2)) bounding_box = (-bar_width, ((count_Q_as + 1) * (count_s + 1)) / 2, 0, 120) settings = { 'axes': axes, 'aspect': aspect, 'bounding_box': bounding_box, 'legend': hatching, 'title': 'Colour Quality', } settings.update(kwargs) return render(**settings)
def plot_blackbody_spectral_radiance( temperature: Floating = 3500, cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", blackbody: str = "VY Canis Major", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot given blackbody spectral radiance. Parameters ---------- temperature Blackbody temperature. cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. blackbody Blackbody name. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_blackbody_spectral_radiance(3500, blackbody='VY Canis Major') ... # doctest: +ELLIPSIS (<Figure size ... with 2 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Blackbody_Spectral_Radiance.png :align: center :alt: plot_blackbody_spectral_radiance """ figure = plt.figure() figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) sd = sd_blackbody(temperature, cmfs.shape) axes = figure.add_subplot(211) settings: Dict[str, Any] = { "axes": axes, "title": f"{blackbody} - Spectral Radiance", "y_label": "W / (sr m$^2$) / m", } settings.update(kwargs) settings["standalone"] = False plot_single_sd(sd, cmfs.name, **settings) axes = figure.add_subplot(212) with domain_range_scale("1"): XYZ = sd_to_XYZ(sd, cmfs) RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ)) settings = { "axes": axes, "aspect": None, "title": f"{blackbody} - Colour", "x_label": f"{temperature}K", "y_label": "", "x_ticker": False, "y_ticker": False, } settings.update(kwargs) settings["standalone"] = False figure, axes = plot_single_colour_swatch(RGB, **settings) settings = {"axes": axes, "standalone": True} settings.update(kwargs) return render(**settings)
def plot_multi_colour_checkers( colour_checkers: Union[ColourChecker, str, Sequence[Union[ColourChecker, str]]], **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot and compares given colour checkers. Parameters ---------- colour_checkers Color checker to plot, count must be less than or equal to 2. ``colour_checkers`` elements can be of any type or form supported by the :func:`colour.plotting.filter_colour_checkers` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_multi_colour_swatches`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_multi_colour_checkers(['ColorChecker 1976', 'ColorChecker 2005']) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Multi_Colour_Checkers.png :align: center :alt: plot_multi_colour_checkers """ filtered_colour_checkers = list( filter_colour_checkers(colour_checkers).values()) attest( len(filtered_colour_checkers) <= 2, "Only two colour checkers can be compared at a time!", ) _figure, axes = artist(**kwargs) compare_swatches = len(filtered_colour_checkers) == 2 colour_swatches = [] colour_checker_names = [] for colour_checker in filtered_colour_checkers: colour_checker_names.append(colour_checker.name) for label, xyY in colour_checker.data.items(): XYZ = xyY_to_XYZ(xyY) RGB = XYZ_to_plotting_colourspace(XYZ, colour_checker.illuminant) colour_swatches.append( ColourSwatch(np.clip(np.ravel(RGB), 0, 1), label.title())) if compare_swatches: colour_swatches = [ swatch for pairs in zip( colour_swatches[0:len(colour_swatches) // 2], colour_swatches[len(colour_swatches) // 2:], ) for swatch in pairs ] background_colour = "0.1" width = height = 1.0 spacing = 0.25 columns = 6 settings: Dict[str, Any] = { "axes": axes, "width": width, "height": height, "spacing": spacing, "columns": columns, "direction": "-y", "text_kwargs": { "size": 8 }, "background_colour": background_colour, "compare_swatches": "Stacked" if compare_swatches else None, } settings.update(kwargs) settings["standalone"] = False plot_multi_colour_swatches(colour_swatches, **settings) axes.text( 0.5, 0.005, (f"{', '.join(colour_checker_names)} - " f"{CONSTANTS_COLOUR_STYLE.colour.colourspace.name} - " f"Colour Rendition Chart"), transform=axes.transAxes, color=CONSTANTS_COLOUR_STYLE.colour.bright, ha="center", va="bottom", zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_label, ) settings.update({ "axes": axes, "standalone": True, "title": ", ".join(colour_checker_names), }) return render(**settings)
def plot_the_blue_sky(cmfs='CIE 1931 2 Degree Standard Observer', **kwargs): """ Plots the blue sky. Parameters ---------- cmfs : unicode, optional Standard observer colour matching functions. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.plot_multi_colour_swatches`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> plot_the_blue_sky() # doctest: +SKIP .. image:: ../_static/Plotting_Plot_The_Blue_Sky.png :align: center :alt: plot_the_blue_sky """ figure = plt.figure() figure.subplots_adjust(hspace=COLOUR_STYLE_CONSTANTS.geometry.short / 2) cmfs = first_item(filter_cmfs(cmfs).values()) ASTM_G_173_sd = ASTM_G_173_ETR.copy() rayleigh_sd = sd_rayleigh_scattering() ASTM_G_173_sd.align(rayleigh_sd.shape) sd = rayleigh_sd * ASTM_G_173_sd axes = figure.add_subplot(211) settings = { 'axes': axes, 'title': 'The Blue Sky - Synthetic Spectral Distribution', 'y_label': u'W / m-2 / nm-1', } settings.update(kwargs) settings['standalone'] = False plot_single_sd(sd, cmfs, **settings) axes = figure.add_subplot(212) x_label = ('The sky is blue because molecules in the atmosphere ' 'scatter shorter wavelengths more than longer ones.\n' 'The synthetic spectral distribution is computed as ' 'follows: ' '(ASTM G-173 ETR * Standard Air Rayleigh Scattering).') settings = { 'axes': axes, 'aspect': None, 'title': 'The Blue Sky - Colour', 'x_label': x_label, 'y_label': '', 'x_ticker': False, 'y_ticker': False, } settings.update(kwargs) settings['standalone'] = False blue_sky_color = XYZ_to_plotting_colourspace(sd_to_XYZ(sd)) figure, axes = plot_single_colour_swatch( ColourSwatch('', normalise_maximum(blue_sky_color)), **settings) settings = {'axes': axes, 'standalone': True} settings.update(kwargs) return render(**settings)
def plot_colour_quality_bars( specifications: Sequence[ Union[ ColourRendering_Specification_CQS, ColourRendering_Specification_CRI, ] ], labels: Boolean = True, hatching: Optional[Boolean] = None, hatching_repeat: Integer = 2, **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the colour quality data of given illuminants or light sources colour quality specifications. Parameters ---------- specifications Array of illuminants or light sources colour quality specifications. labels Add labels above bars. hatching Use hatching for the bars. hatching_repeat Hatching pattern repeat. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.quality.plot_colour_quality_bars`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> from colour import (SDS_ILLUMINANTS, ... SDS_LIGHT_SOURCES, SpectralShape) >>> illuminant = SDS_ILLUMINANTS['FL2'] >>> light_source = SDS_LIGHT_SOURCES['Kinoton 75P'] >>> light_source = light_source.copy().align(SpectralShape(360, 830, 1)) >>> cqs_i = colour_quality_scale(illuminant, additional_data=True) >>> cqs_l = colour_quality_scale(light_source, additional_data=True) >>> plot_colour_quality_bars([cqs_i, cqs_l]) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Colour_Quality_Bars.png :align: center :alt: plot_colour_quality_bars """ settings: Dict[str, Any] = {"uniform": True} settings.update(kwargs) _figure, axes = artist(**settings) bar_width = 0.5 y_ticks_interval = 10 count_s, count_Q_as = len(specifications), 0 patterns = cycle(CONSTANTS_COLOUR_STYLE.hatch.patterns) if hatching is None: hatching = False if count_s == 1 else True for i, specification in enumerate(specifications): Q_a, Q_as, colorimetry_data = ( specification.Q_a, specification.Q_as, specification.colorimetry_data, ) count_Q_as = len(Q_as) RGB = [[1] * 3] + [ np.clip(XYZ_to_plotting_colourspace(x.XYZ), 0, 1) for x in colorimetry_data[0] ] x = ( as_float_array( i + np.arange( 0, (count_Q_as + 1) * (count_s + 1), (count_s + 1), dtype=DEFAULT_FLOAT_DTYPE, ) ) * bar_width ) y = as_float_array( [Q_a] + [ s[1].Q_a # type: ignore[attr-defined] for s in sorted(Q_as.items(), key=lambda s: s[0]) ] ) bars = axes.bar( x, np.abs(y), color=RGB, width=bar_width, edgecolor=CONSTANTS_COLOUR_STYLE.colour.dark, label=specification.name, zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon, ) hatches = ( [next(patterns) * hatching_repeat] * (count_Q_as + 1) if hatching else list( np.where(y < 0, next(patterns), None) # type: ignore[call-overload] ) ) for j, bar in enumerate(bars.patches): bar.set_hatch(hatches[j]) if labels: label_rectangles( [f"{y_v:.1f}" for y_v in y], bars, rotation="horizontal" if count_s == 1 else "vertical", offset=( 0 if count_s == 1 else 3 / 100 * count_s + 65 / 1000, 0.025, ), text_size=-5 / 7 * count_s + 12.5, axes=axes, ) axes.axhline( y=100, color=CONSTANTS_COLOUR_STYLE.colour.dark, linestyle="--", zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line, ) axes.set_xticks( ( np.arange( 0, (count_Q_as + 1) * (count_s + 1), (count_s + 1), dtype=DEFAULT_FLOAT_DTYPE, ) - bar_width ) * bar_width + (count_s * bar_width / 2) ) axes.set_xticklabels( ["Qa"] + [f"Q{index + 1}" for index in range(0, count_Q_as, 1)] ) axes.set_yticks(range(0, 100 + y_ticks_interval, y_ticks_interval)) aspect = 1 / (120 / (bar_width + len(Q_as) + bar_width * 2)) bounding_box = ( -bar_width, ((count_Q_as + 1) * (count_s + 1)) / 2 - bar_width, 0, 120, ) settings = { "axes": axes, "aspect": aspect, "bounding_box": bounding_box, "legend": hatching, "title": "Colour Quality", } settings.update(kwargs) return render(**settings)
def plot_multi_colour_checkers(colour_checkers=None, **kwargs): """ Plots and compares given colour checkers. Parameters ---------- colour_checkers : array_like, optional Color checker names, must be less than or equal to 2 names. Other Parameters ---------------- \\**kwargs : dict, optional {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_multi_colour_swatches`, :func:`colour.plotting.render`}, Please refer to the documentation of the previously listed definitions. Returns ------- tuple Current figure and axes. Examples -------- >>> plot_multi_colour_checkers(['ColorChecker 1976', 'ColorChecker 2005']) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Multi_Colour_Checkers.png :align: center :alt: plot_multi_colour_checkers """ if colour_checkers is None: colour_checkers = ['ColorChecker 1976', 'ColorChecker 2005'] else: assert len(colour_checkers) <= 2, ( 'Only two colour checkers can be compared at a time!') colour_checkers = filter_colour_checkers(colour_checkers).values() _figure, axes = artist(**kwargs) compare_swatches = len(colour_checkers) == 2 colour_swatches = [] colour_checker_names = [] for colour_checker in colour_checkers: colour_checker_names.append(colour_checker.name) for label, xyY in colour_checker.data.items(): XYZ = xyY_to_XYZ(xyY) RGB = XYZ_to_plotting_colourspace(XYZ, colour_checker.illuminant) colour_swatches.append( ColourSwatch(label.title(), np.clip(np.ravel(RGB), 0, 1))) if compare_swatches: colour_swatches = [ swatch for pairs in zip(colour_swatches[0:len(colour_swatches) // 2], colour_swatches[len(colour_swatches) // 2:]) for swatch in pairs ] background_colour = '0.1' width = height = 1.0 spacing = 0.25 columns = 6 settings = { 'axes': axes, 'width': width, 'height': height, 'spacing': spacing, 'columns': columns, 'text_parameters': { 'size': 8 }, 'background_colour': background_colour, 'compare_swatches': 'Stacked' if compare_swatches else None, } settings.update(kwargs) settings['standalone'] = False plot_multi_colour_swatches(colour_swatches, **settings) axes.text(0.5, 0.005, '{0} - {1} - Colour Rendition Chart'.format( ', '.join(colour_checker_names), COLOUR_STYLE_CONSTANTS.colour.colourspace.name), transform=axes.transAxes, color=COLOUR_STYLE_CONSTANTS.colour.bright, ha='center', va='bottom') settings.update({ 'axes': axes, 'standalone': True, 'title': ', '.join(colour_checker_names), }) return render(**settings)
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 plot_the_blue_sky( cmfs: Union[MultiSpectralDistributions, str, Sequence[Union[ MultiSpectralDistributions, str]], ] = "CIE 1931 2 Degree Standard Observer", **kwargs: Any, ) -> Tuple[plt.Figure, plt.Axes]: """ Plot the blue sky. Parameters ---------- cmfs Standard observer colour matching functions used for computing the spectrum domain and colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. Other Parameters ---------------- kwargs {:func:`colour.plotting.artist`, :func:`colour.plotting.plot_single_sd`, :func:`colour.plotting.plot_multi_colour_swatches`, :func:`colour.plotting.render`}, See the documentation of the previously listed definitions. Returns ------- :class:`tuple` Current figure and axes. Examples -------- >>> plot_the_blue_sky() # doctest: +ELLIPSIS (<Figure size ... with 2 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_The_Blue_Sky.png :align: center :alt: plot_the_blue_sky """ figure = plt.figure() figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2) cmfs = cast(MultiSpectralDistributions, first_item(filter_cmfs(cmfs).values())) ASTMG173_sd = cast(SpectralDistribution, SD_ASTMG173_ETR.copy()) rayleigh_sd = sd_rayleigh_scattering() ASTMG173_sd.align(rayleigh_sd.shape) sd = rayleigh_sd * ASTMG173_sd axes = figure.add_subplot(211) settings: Dict[str, Any] = { "axes": axes, "title": "The Blue Sky - Synthetic Spectral Distribution", "y_label": "W / m-2 / nm-1", } settings.update(kwargs) settings["standalone"] = False plot_single_sd(sd, cmfs, **settings) axes = figure.add_subplot(212) x_label = ("The sky is blue because molecules in the atmosphere " "scatter shorter wavelengths more than longer ones.\n" "The synthetic spectral distribution is computed as " "follows: " "(ASTM G-173 ETR * Standard Air Rayleigh Scattering).") settings = { "axes": axes, "aspect": None, "title": "The Blue Sky - Colour", "x_label": x_label, "y_label": "", "x_ticker": False, "y_ticker": False, } settings.update(kwargs) settings["standalone"] = False blue_sky_color = XYZ_to_plotting_colourspace( sd_to_XYZ(cast(SpectralDistribution, sd))) figure, axes = plot_single_colour_swatch( ColourSwatch(normalise_maximum(blue_sky_color)), **settings) settings = {"axes": axes, "standalone": True} settings.update(kwargs) return render(**settings)
def plot_single_sd(sd, cmfs='CIE 1931 2 Degree Standard Observer', out_of_gamut_clipping=True, modulate_colours_with_sd_amplitude=False, equalize_sd_amplitude=False, **kwargs): """ Plots given spectral distribution. Parameters ---------- sd : SpectralDistribution Spectral distribution to plot. out_of_gamut_clipping : bool, optional Whether to clip out of gamut colours otherwise, the colours will be offset by the absolute minimal colour leading to a rendering on gray background, less saturated and smoother. modulate_colours_with_sd_amplitude : bool, optional Whether to modulate the colours with the spectral distribution amplitude. equalize_sd_amplitude : bool, optional Whether to equalize the spectral distribution amplitude. Equalization occurs after the colours modulation thus setting both arguments to *True* will generate a spectrum strip where each wavelength colour is modulated by the spectral distribution amplitude. The usual 5% margin above the spectral distribution is also omitted. cmfs : unicode Standard observer colour matching functions used for spectrum creation. 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. References ---------- :cite:`Spiker2015a` Examples -------- >>> from colour import SpectralDistribution >>> data = { ... 500: 0.0651, ... 520: 0.0705, ... 540: 0.0772, ... 560: 0.0870, ... 580: 0.1128, ... 600: 0.1360 ... } >>> sd = SpectralDistribution(data, name='Custom') >>> plot_single_sd(sd) # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Single_SD.png :align: center :alt: plot_single_sd """ _figure, axes = artist(**kwargs) cmfs = first_item(filter_cmfs(cmfs).values()) sd = sd.copy() sd.interpolator = LinearInterpolator wavelengths = cmfs.wavelengths[np.logical_and( cmfs.wavelengths >= max(min(cmfs.wavelengths), min(sd.wavelengths)), cmfs.wavelengths <= min(max(cmfs.wavelengths), max(sd.wavelengths)), )] values = sd[wavelengths] colours = XYZ_to_plotting_colourspace( wavelength_to_XYZ(wavelengths, cmfs), ILLUMINANTS['CIE 1931 2 Degree Standard Observer']['E'], apply_cctf_encoding=False) if not out_of_gamut_clipping: colours += np.abs(np.min(colours)) colours = normalise_maximum(colours) if modulate_colours_with_sd_amplitude: colours *= (values / np.max(values))[..., np.newaxis] colours = COLOUR_STYLE_CONSTANTS.colour.colourspace.cctf_encoding(colours) if equalize_sd_amplitude: values = np.ones(values.shape) margin = 0 if equalize_sd_amplitude else 0.05 x_min, x_max = min(wavelengths), max(wavelengths) y_min, y_max = 0, max(values) + max(values) * margin polygon = Polygon(np.vstack([ (x_min, 0), tstack([wavelengths, values]), (x_max, 0), ]), facecolor='none', edgecolor='none') axes.add_patch(polygon) padding = 0.1 axes.bar(x=wavelengths - padding, height=max(values), width=1 + padding, color=colours, align='edge', clip_path=polygon) axes.plot(wavelengths, values, color=COLOUR_STYLE_CONSTANTS.colour.dark) settings = { 'axes': axes, 'bounding_box': (x_min, x_max, y_min, y_max), 'title': '{0} - {1}'.format(sd.strict_name, cmfs.strict_name), 'x_label': 'Wavelength $\\lambda$ (nm)', 'y_label': 'Spectral Distribution', } settings.update(kwargs) return render(**settings)
def plot_multi_sds(sds, plot_kwargs=None, **kwargs): """ Plots given spectral distributions. Parameters ---------- sds : array_like or MultiSpectralDistributions Spectral distributions or multi-spectral distributions to plot. `sds` can be a single :class:`colour.MultiSpectralDistributions` class instance, a list of :class:`colour.MultiSpectralDistributions` class instances or a list of :class:`colour.SpectralDistribution` class instances. plot_kwargs : dict or array_like, optional Keyword arguments for the :func:`plt.plot` definition, used to control the style of the plotted spectral distributions. ``plot_kwargs`` can be either a single dictionary applied to all the plotted spectral distributions with same settings or a sequence of dictionaries with different settings for each plotted spectral distributions. The following special keyword arguments can also be used: - *illuminant* : unicode or :class:`colour.SpectralDistribution`, the illuminant used to compute the spectral distributions colours. The default is the illuminant associated with the whitepoint of the default plotting colourspace. ``illuminant`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - *cmfs* : unicode, the standard observer colour matching functions used for computing the spectral distributions colours. ``cmfs`` can be of any type or form supported by the :func:`colour.plotting.filter_cmfs` definition. - *normalise_sd_colours* : bool, whether to normalise the computed spectral distributions colours. The default is *True*. - *use_sd_colours* : bool, whether to use the computed spectral distributions colours under the plotting colourspace illuminant. Alternatively, it is possible to use the :func:`plt.plot` definition ``color`` argument with pre-computed values. The default is *True*. 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 -------- >>> from colour import SpectralDistribution >>> data_1 = { ... 500: 0.004900, ... 510: 0.009300, ... 520: 0.063270, ... 530: 0.165500, ... 540: 0.290400, ... 550: 0.433450, ... 560: 0.594500 ... } >>> data_2 = { ... 500: 0.323000, ... 510: 0.503000, ... 520: 0.710000, ... 530: 0.862000, ... 540: 0.954000, ... 550: 0.994950, ... 560: 0.995000 ... } >>> sd_1 = SpectralDistribution(data_1, name='Custom 1') >>> sd_2 = SpectralDistribution(data_2, name='Custom 2') >>> plot_kwargs = [ ... {'use_sd_colours': True}, ... {'use_sd_colours': True, 'linestyle': 'dashed'}, ... ] >>> plot_multi_sds([sd_1, sd_2], plot_kwargs=plot_kwargs) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Multi_SDS.png :align: center :alt: plot_multi_sds """ handle_arguments_deprecation( {'ArgumentRemoved': ['normalise_sd_colours', 'use_sds_colours']}, **kwargs) _figure, axes = artist(**kwargs) sds = sds_and_msds_to_sds(sds) plot_settings_collection = [{ 'label': '{0}'.format(sd.strict_name), 'cmfs': 'CIE 1931 2 Degree Standard Observer', 'illuminant': SDS_ILLUMINANTS[ CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint_name], 'use_sd_colours': False, 'normalise_sd_colours': False, } for sd in sds] if plot_kwargs is not None: update_settings_collection(plot_settings_collection, plot_kwargs, len(sds)) x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], [] for i, sd in enumerate(sds): plot_settings = plot_settings_collection[i] cmfs = first_item(filter_cmfs(plot_settings.pop('cmfs')).values()) illuminant = first_item( filter_illuminants(plot_settings.pop('illuminant')).values()) normalise_sd_colours = plot_settings.pop('normalise_sd_colours') use_sd_colours = plot_settings.pop('use_sd_colours') wavelengths, values = sd.wavelengths, sd.values shape = sd.shape x_limit_min.append(shape.start) x_limit_max.append(shape.end) y_limit_min.append(min(values)) y_limit_max.append(max(values)) if use_sd_colours: with domain_range_scale('1'): XYZ = sd_to_XYZ(sd, cmfs, illuminant) if normalise_sd_colours: XYZ /= XYZ[..., 1] plot_settings['color'] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1) axes.plot(wavelengths, values, **plot_settings) bounding_box = (min(x_limit_min), max(x_limit_max), min(y_limit_min), max(y_limit_max) + max(y_limit_max) * 0.05) settings = { 'axes': axes, 'bounding_box': bounding_box, 'legend': True, 'x_label': 'Wavelength $\\lambda$ (nm)', 'y_label': 'Spectral Distribution', } settings.update(kwargs) return render(**settings)
def plot_blackbody_colours(shape=SpectralShape(150, 12500, 50), cmfs='CIE 1931 2 Degree Standard Observer', **kwargs): """ Plots blackbody colours. Parameters ---------- shape : SpectralShape, optional Spectral shape to use as plot boundaries. cmfs : unicode, optional Standard observer colour matching functions. 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_blackbody_colours(SpectralShape(150, 12500, 50)) ... # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, \ <matplotlib.axes._subplots.AxesSubplot object at 0x...>) .. image:: ../_static/Plotting_Plot_Blackbody_Colours.png :align: center :alt: plot_blackbody_colours """ _figure, axes = artist(**kwargs) cmfs = first_item(filter_cmfs(cmfs).values()) colours = [] temperatures = [] for temperature in shape: sd = sd_blackbody(temperature, cmfs.shape) with domain_range_scale('1'): XYZ = sd_to_XYZ(sd, cmfs) RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ)) colours.append(RGB) temperatures.append(temperature) x_min, x_max = min(temperatures), max(temperatures) y_min, y_max = 0, 1 padding = 0.1 axes.bar(x=np.array(temperatures) - padding, height=1, width=shape.interval + (padding * shape.interval), color=colours, align='edge') settings = { 'axes': axes, 'bounding_box': (x_min, x_max, y_min, y_max), 'title': 'Blackbody Colours', 'x_label': 'Temperature K', 'y_label': None, } settings.update(kwargs) return render(**settings)
def plot_chromaticity_diagram_colours( samples=256, diagram_opacity=1.0, diagram_clipping_path=None, cmfs='CIE 1931 2 Degree Standard Observer', method='CIE 1931', **kwargs): """ Plots the *Chromaticity Diagram* colours according to given method. Parameters ---------- samples : numeric, optional Samples count on one axis. diagram_opacity : numeric, optional Opacity of the *Chromaticity Diagram* colours. diagram_clipping_path : array_like, optional Path of points used to clip the *Chromaticity Diagram* colours. 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. 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_chromaticity_diagram_colours() # doctest: +ELLIPSIS (<Figure size ... with 1 Axes>, <...AxesSubplot...>) .. image:: ../_static/Plotting_Plot_Chromaticity_Diagram_Colours.png :align: center :alt: plot_chromaticity_diagram_colours """ settings = {'uniform': True} settings.update(kwargs) _figure, axes = artist(**settings) method = method.upper() cmfs = first_item(filter_cmfs(cmfs).values()) illuminant = CONSTANTS_COLOUR_STYLE.colour.colourspace.whitepoint ii, jj = np.meshgrid(np.linspace(0, 1, samples), np.linspace(1, 0, samples)) ij = tstack([ii, jj]) # NOTE: Various values in the grid have potential to generate # zero-divisions, they could be avoided by perturbing the grid, e.g. adding # a small epsilon. It was decided instead to disable warnings. with suppress_warnings(python_warnings=True): if method == 'CIE 1931': XYZ = xy_to_XYZ(ij) spectral_locus = XYZ_to_xy(cmfs.values, illuminant) elif method == 'CIE 1960 UCS': XYZ = xy_to_XYZ(UCS_uv_to_xy(ij)) spectral_locus = UCS_to_uv(XYZ_to_UCS(cmfs.values)) elif method == 'CIE 1976 UCS': XYZ = xy_to_XYZ(Luv_uv_to_xy(ij)) spectral_locus = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) else: raise ValueError( 'Invalid method: "{0}", must be one of ' '[\'CIE 1931\', \'CIE 1960 UCS\', \'CIE 1976 UCS\']'.format( method)) RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ, illuminant), axis=-1) polygon = Polygon(spectral_locus if diagram_clipping_path is None else diagram_clipping_path, facecolor='none', edgecolor='none') axes.add_patch(polygon) # Preventing bounding box related issues as per # https://github.com/matplotlib/matplotlib/issues/10529 image = axes.imshow(RGB, interpolation='bilinear', extent=(0, 1, 0, 1), clip_path=None, alpha=diagram_opacity) image.set_clip_path(polygon) settings = {'axes': axes} settings.update(kwargs) return render(**kwargs)
def plot_spectral_locus(cmfs='CIE 1931 2 Degree Standard Observer', spectral_locus_colours=None, spectral_locus_labels=None, method='CIE 1931', **kwargs): """ Plots the *Spectral Locus* according to given method. Parameters ---------- cmfs : unicode, optional Standard observer colour matching functions defining the *Spectral Locus*. spectral_locus_colours : array_like or unicode, optional *Spectral Locus* colours, if ``spectral_locus_colours`` is set to *RGB*, the colours will be computed according to the corresponding chromaticity coordinates. spectral_locus_labels : array_like, optional Array of wavelength labels used to customise which labels will be drawn around the spectral locus. Passing an empty array will result in no wavelength labels being drawn. 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_spectral_locus(spectral_locus_colours='RGB') # doctest: +SKIP .. image:: ../_static/Plotting_Plot_Spectral_Locus.png :align: center :alt: plot_spectral_locus """ if spectral_locus_colours is None: spectral_locus_colours = COLOUR_STYLE_CONSTANTS.colour.dark settings = {'uniform': True} settings.update(kwargs) figure, axes = artist(**settings) method = method.upper() cmfs = first_item(filter_cmfs(cmfs).values()) illuminant = COLOUR_STYLE_CONSTANTS.colour.colourspace.whitepoint wavelengths = cmfs.wavelengths equal_energy = np.array([1 / 3] * 2) if method == 'CIE 1931': ij = XYZ_to_xy(cmfs.values, illuminant) labels = ((390, 460, 470, 480, 490, 500, 510, 520, 540, 560, 580, 600, 620, 700) if spectral_locus_labels is None else spectral_locus_labels) elif method == 'CIE 1960 UCS': ij = UCS_to_uv(XYZ_to_UCS(cmfs.values)) labels = ((420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680) if spectral_locus_labels is None else spectral_locus_labels) elif method == 'CIE 1976 UCS': ij = Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant) labels = ((420, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 645, 680) if spectral_locus_labels is None else spectral_locus_labels) else: raise ValueError( 'Invalid method: "{0}", must be one of ' '{\'CIE 1931\', \'CIE 1960 UCS\', \'CIE 1976 UCS\'}'.format( method)) pl_ij = tstack([ np.linspace(ij[0][0], ij[-1][0], 20), np.linspace(ij[0][1], ij[-1][1], 20) ]).reshape(-1, 1, 2) sl_ij = np.copy(ij).reshape(-1, 1, 2) if spectral_locus_colours.upper() == 'RGB': spectral_locus_colours = normalise_maximum( XYZ_to_plotting_colourspace(cmfs.values), axis=-1) if method == 'CIE 1931': XYZ = xy_to_XYZ(pl_ij) elif method == 'CIE 1960 UCS': XYZ = xy_to_XYZ(UCS_uv_to_xy(pl_ij)) elif method == 'CIE 1976 UCS': XYZ = xy_to_XYZ(Luv_uv_to_xy(pl_ij)) purple_line_colours = normalise_maximum( XYZ_to_plotting_colourspace(XYZ.reshape(-1, 3)), axis=-1) else: purple_line_colours = spectral_locus_colours for slp_ij, slp_colours in ((pl_ij, purple_line_colours), (sl_ij, spectral_locus_colours)): line_collection = LineCollection( np.concatenate([slp_ij[:-1], slp_ij[1:]], axis=1), colors=slp_colours) axes.add_collection(line_collection) wl_ij = dict(tuple(zip(wavelengths, ij))) for label in labels: i, j = wl_ij[label] index = bisect.bisect(wavelengths, label) left = wavelengths[index - 1] if index >= 0 else wavelengths[index] right = (wavelengths[index] if index < len(wavelengths) else wavelengths[-1]) dx = wl_ij[right][0] - wl_ij[left][0] dy = wl_ij[right][1] - wl_ij[left][1] ij = np.array([i, j]) direction = np.array([-dy, dx]) normal = (np.array([-dy, dx]) if np.dot( normalise_vector(ij - equal_energy), normalise_vector(direction)) > 0 else np.array([dy, -dx])) normal = normalise_vector(normal) / 30 label_colour = (spectral_locus_colours if is_string(spectral_locus_colours) else spectral_locus_colours[index]) axes.plot( (i, i + normal[0] * 0.75), (j, j + normal[1] * 0.75), color=label_colour) axes.plot(i, j, 'o', color=label_colour) axes.text( i + normal[0], j + normal[1], label, clip_on=True, ha='left' if normal[0] >= 0 else 'right', va='center', fontdict={'size': 'small'}) settings = {'axes': axes} settings.update(kwargs) return render(**kwargs)
def plot_as_slit_rainbow(ax=None, wavelength=None, flux=None, cmfs="CIE 1931 2 Degree Standard Observer", **kwargs): """ (This is still *real* blarg-y*.) Plot a spectrum as a light source would be seen through a slit spectrometer, with vertical bands of light that are brighter or fainter depending on the intensity of the spectrum at that particular wavelength. Parameters ---------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow should be drawn. wavelength : array The wavelengths to include in the spectrum. In units of nm, but not as astropy units. flux : array The fluxes to include in the spectrum. In units of whatever, but not as astropy units. cmfs : string The color matching function(s?) to use. vector : bool Returns ------- ax : matplotlib.axes._subplots.AxesSubplot The ax into which the rainbow was drawn. """ # make sure our plotting ax is defined if ax is None: ax = plt.gca() # make sure we have a grid of wavelengths defined if wavelength is None: # create a grid of wavelengths (at which CMFss are useful) wavelength = CMFs.wavelengths # create y values that will be plotted (these could be spectrum) if flux is None: flux = np.ones_like(wavelength) # pull out only the values that *can* be converted to colors ok = (wavelength >= np.min(CMFs.wavelengths)) & (wavelength <= np.max( CMFs.wavelengths)) w, f = wavelength[ok], flux[ok] # get the XYZ for the wavelengths XYZ = wavelength_to_XYZ(w) # create colors at those wavelengths colours = XYZ_to_plotting_colourspace(XYZ) # normalize the colors to their maximum? # colours = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding( # normalise_maximum(colours)) # normalize the brightness by the flux colours *= f[:, np.newaxis] # normalize to the brightest line colours = np.maximum(0, colours / np.max(colours)) # draw as an RGB color image with imshow ax.imshow( colours[np.newaxis, :, :], aspect="auto", extent=[np.min(w), np.max(w), 0, 1], interpolation="nearest", ) return ax