# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. import copy as cp import os import os.path as op import re from collections import defaultdict from colorsys import hsv_to_rgb, rgb_to_hsv import numpy as np from scipy import linalg, sparse from .fixes import _safe_svd from .morph_map import read_morph_map from .parallel import parallel_func from .source_estimate import ( SourceEstimate, VolSourceEstimate, _center_of_mass, extract_label_time_course, spatial_src_adjacency, ) from .source_space._source_space import ( SourceSpaces, _ensure_src, add_source_space_distances, ) from .stats.cluster_level import _find_clusters, _get_components from .surface import ( _mesh_borders, complete_surface_info, fast_cross_3d, mesh_dist, mesh_edges, read_surface, ) from .utils import ( _check_fname, _check_option, _check_subject, _validate_type, check_random_state, fill_doc, get_subjects_dir, logger, verbose, warn, ) def _blend_colors(color_1, color_2): """Blend two colors in HSV space. Parameters ---------- color_1, color_2 : None | tuple RGBA tuples with values between 0 and 1. None if no color is available. If both colors are None, the output is None. If only one is None, the output is the other color. Returns ------- color : None | tuple RGBA tuple of the combined color. Saturation, value and alpha are averaged, whereas the new hue is determined as angle half way between the two input colors' hues. """ if color_1 is None and color_2 is None: return None elif color_1 is None: return color_2 elif color_2 is None: return color_1 r_1, g_1, b_1, a_1 = color_1 h_1, s_1, v_1 = rgb_to_hsv(r_1, g_1, b_1) r_2, g_2, b_2, a_2 = color_2 h_2, s_2, v_2 = rgb_to_hsv(r_2, g_2, b_2) hue_diff = abs(h_1 - h_2) if hue_diff < 0.5: h = min(h_1, h_2) + hue_diff / 2.0 else: h = max(h_1, h_2) + (1.0 - hue_diff) / 2.0 h %= 1.0 s = (s_1 + s_2) / 2.0 v = (v_1 + v_2) / 2.0 r, g, b = hsv_to_rgb(h, s, v) a = (a_1 + a_2) / 2.0 color = (r, g, b, a) return color def _split_colors(color, n): """Create n colors in HSV space that occupy a gradient in value. Parameters ---------- color : tuple RGBA tuple with values between 0 and 1. n : int >= 2 Number of colors on the gradient. Returns ------- colors : tuple of tuples, len = n N RGBA tuples that occupy a gradient in value (low to high) but share saturation and hue with the input color. """ r, g, b, a = color h, s, v = rgb_to_hsv(r, g, b) gradient_range = np.sqrt(n / 10.0) if v > 0.5: v_max = min(0.95, v + gradient_range / 2) v_min = max(0.05, v_max - gradient_range) else: v_min = max(0.05, v - gradient_range / 2) v_max = min(0.95, v_min + gradient_range) hsv_colors = ((h, s, v_) for v_ in np.linspace(v_min, v_max, n)) rgb_colors = (hsv_to_rgb(h_, s_, v_) for h_, s_, v_ in hsv_colors) rgba_colors = ( ( r_, g_, b_, a, ) for r_, g_, b_ in rgb_colors ) return tuple(rgba_colors) def _n_colors(n, bytes_=False, cmap="hsv"): """Produce a list of n unique RGBA color tuples based on a colormap. Parameters ---------- n : int Number of colors. bytes : bool Return colors as integers values between 0 and 255 (instead of floats between 0 and 1). cmap : str Which colormap to use. Returns ------- colors : array, shape (n, 4) RGBA color values. """ n_max = 2**10 if n > n_max: raise NotImplementedError("Can't produce more than %i unique colors" % n_max) from .viz.utils import _get_cmap cm = _get_cmap(cmap) pos = np.linspace(0, 1, n, False) colors = cm(pos, bytes=bytes_) if bytes_: # make sure colors are unique for ii, c in enumerate(colors): if np.any(np.all(colors[:ii] == c, 1)): raise RuntimeError( "Could not get %d unique colors from %s " "colormap. Try using a different colormap." % (n, cmap) ) return colors @fill_doc class Label: """A FreeSurfer/MNE label with vertices restricted to one hemisphere. Labels can be combined with the ``+`` operator: * Duplicate vertices are removed. * If duplicate vertices have conflicting position values, an error is raised. * Values of duplicate vertices are summed. Parameters ---------- vertices : array, shape (N,) Vertex indices (0 based). pos : array, shape (N, 3) | None Locations in meters. If None, then zeros are used. values : array, shape (N,) | None Values at the vertices. If None, then ones are used. hemi : 'lh' | 'rh' Hemisphere to which the label applies. comment : str Kept as information but not used by the object itself. name : str Kept as information but not used by the object itself. filename : str Kept as information but not used by the object itself. %(subject_label)s color : None | matplotlib color Default label color and alpha (e.g., ``(1., 0., 0., 1.)`` for red). %(verbose)s Attributes ---------- color : None | tuple Default label color, represented as RGBA tuple with values between 0 and 1. comment : str Comment from the first line of the label file. hemi : 'lh' | 'rh' Hemisphere. name : None | str A name for the label. It is OK to change that attribute manually. pos : array, shape (N, 3) Locations in meters. subject : str | None The label subject. It is best practice to set this to the proper value on initialization, but it can also be set manually. values : array, shape (N,) Values at the vertices. vertices : array, shape (N,) Vertex indices (0 based) """ @verbose def __init__( self, vertices=(), pos=None, values=None, hemi=None, comment="", name=None, filename=None, subject=None, color=None, *, verbose=None, ): # check parameters if not isinstance(hemi, str): raise ValueError(f"hemi must be a string, not {type(hemi)}") vertices = np.asarray(vertices, int) if np.any(np.diff(vertices.astype(int)) <= 0): raise ValueError("Vertices must be ordered in increasing order.") if color is not None: from matplotlib.colors import colorConverter color = colorConverter.to_rgba(color) if values is None: values = np.ones(len(vertices)) else: values = np.asarray(values) if pos is None: pos = np.zeros((len(vertices), 3)) else: pos = np.asarray(pos) if not (len(vertices) == len(values) == len(pos)): raise ValueError( "vertices, values and pos need to have same " "length (number of vertices)" ) # name if name is None and filename is not None: name = op.basename(filename[:-6]) self.vertices = vertices self.pos = pos self.values = values self.hemi = hemi self.comment = comment self.subject = _check_subject(None, subject, raise_error=False) self.color = color self.name = name self.filename = filename def __setstate__(self, state): # noqa: D105 self.vertices = state["vertices"] self.pos = state["pos"] self.values = state["values"] self.hemi = state["hemi"] self.comment = state["comment"] self.subject = state.get("subject", None) self.color = state.get("color", None) self.name = state["name"] self.filename = state["filename"] def __getstate__(self): # noqa: D105 out = dict( vertices=self.vertices, pos=self.pos, values=self.values, hemi=self.hemi, comment=self.comment, subject=self.subject, color=self.color, name=self.name, filename=self.filename, ) return out def __repr__(self): # noqa: D105 name = "unknown, " if self.subject is None else self.subject + ", " name += repr(self.name) if self.name is not None else "unnamed" n_vert = len(self) return "