diff options
Diffstat (limited to 'silx/gui/colors.py')
-rwxr-xr-x | silx/gui/colors.py | 1326 |
1 files changed, 0 insertions, 1326 deletions
diff --git a/silx/gui/colors.py b/silx/gui/colors.py deleted file mode 100755 index db837b5..0000000 --- a/silx/gui/colors.py +++ /dev/null @@ -1,1326 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides API to manage colors. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent", "H.Payno"] -__license__ = "MIT" -__date__ = "29/01/2019" - -import numpy -import logging -import collections -import warnings - -from silx.gui import qt -from silx.gui.utils import blockSignals -from silx.math.combo import min_max -from silx.math import colormap as _colormap -from silx.utils.exceptions import NotEditableError -from silx.utils import deprecation -from silx.resources import resource_filename as _resource_filename - - -_logger = logging.getLogger(__name__) - -try: - import silx.gui.utils.matplotlib # noqa Initalize matplotlib - from matplotlib import cm as _matplotlib_cm - from matplotlib.pyplot import colormaps as _matplotlib_colormaps -except ImportError: - _logger.info("matplotlib not available, only embedded colormaps available") - _matplotlib_cm = None - _matplotlib_colormaps = None - - -_COLORDICT = {} -"""Dictionary of common colors.""" - -_COLORDICT['b'] = _COLORDICT['blue'] = '#0000ff' -_COLORDICT['r'] = _COLORDICT['red'] = '#ff0000' -_COLORDICT['g'] = _COLORDICT['green'] = '#00ff00' -_COLORDICT['k'] = _COLORDICT['black'] = '#000000' -_COLORDICT['w'] = _COLORDICT['white'] = '#ffffff' -_COLORDICT['pink'] = '#ff66ff' -_COLORDICT['brown'] = '#a52a2a' -_COLORDICT['orange'] = '#ff9900' -_COLORDICT['violet'] = '#6600ff' -_COLORDICT['gray'] = _COLORDICT['grey'] = '#a0a0a4' -# _COLORDICT['darkGray'] = _COLORDICT['darkGrey'] = '#808080' -# _COLORDICT['lightGray'] = _COLORDICT['lightGrey'] = '#c0c0c0' -_COLORDICT['y'] = _COLORDICT['yellow'] = '#ffff00' -_COLORDICT['m'] = _COLORDICT['magenta'] = '#ff00ff' -_COLORDICT['c'] = _COLORDICT['cyan'] = '#00ffff' -_COLORDICT['darkBlue'] = '#000080' -_COLORDICT['darkRed'] = '#800000' -_COLORDICT['darkGreen'] = '#008000' -_COLORDICT['darkBrown'] = '#660000' -_COLORDICT['darkCyan'] = '#008080' -_COLORDICT['darkYellow'] = '#808000' -_COLORDICT['darkMagenta'] = '#800080' -_COLORDICT['transparent'] = '#00000000' - - -# FIXME: It could be nice to expose a functional API instead of that attribute -COLORDICT = _COLORDICT - - -_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color", "preferred"]) -"""Description of a LUT for internal purpose.""" - - -_AVAILABLE_LUTS = collections.OrderedDict([ - ('gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('reversed gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('red', _LUT_DESCRIPTION('builtin', 'green', True)), - ('green', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)), - ('viridis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('cividis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('magma', _LUT_DESCRIPTION('resource', 'green', True)), - ('inferno', _LUT_DESCRIPTION('resource', 'green', True)), - ('plasma', _LUT_DESCRIPTION('resource', 'green', True)), - ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)), - ('hsv', _LUT_DESCRIPTION('matplotlib', 'black', True)), -]) -"""Description for internal porpose of all the default LUT provided by the library.""" - - -DEFAULT_MIN_LIN = 0 -"""Default min value if in linear normalization""" -DEFAULT_MAX_LIN = 1 -"""Default max value if in linear normalization""" - - -def rgba(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A) - of floats. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - if colorDict is None: - colorDict = _COLORDICT - - if hasattr(color, 'getRgbF'): # QColor support - color = color.getRgbF() - - values = numpy.asarray(color).ravel() - - if values.dtype.kind in 'iuf': # integer or float - # Color is an array - assert len(values) in (3, 4) - - # Convert from integers in [0, 255] to float in [0, 1] - if values.dtype.kind in 'iu': - values = values / 255. - - # Clip to [0, 1] - values[values < 0.] = 0. - values[values > 1.] = 1. - - if len(values) == 3: - return values[0], values[1], values[2], 1. - else: - return tuple(values) - - # We assume color is a string - if not color.startswith('#'): - color = colorDict[color] - - assert len(color) in (7, 9) and color[0] == '#' - r = int(color[1:3], 16) / 255. - g = int(color[3:5], 16) / 255. - b = int(color[5:7], 16) / 255. - a = int(color[7:9], 16) / 255. if len(color) == 9 else 1. - return r, g, b, a - - -def greyed(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color - (R, G, B, A). - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - r, g, b, a = rgba(color=color, colorDict=colorDict) - g = 0.21 * r + 0.72 * g + 0.07 * b - return g, g, g, a - - -def asQColor(color): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a `qt.QColor`. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :rtype: qt.QColor - """ - color = rgba(color) - return qt.QColor.fromRgbF(*color) - - -def cursorColorForColormap(colormapName): - """Get a color suitable for overlay over a colormap. - - :param str colormapName: The name of the colormap. - :return: Name of the color. - :rtype: str - """ - description = _AVAILABLE_LUTS.get(colormapName, None) - if description is not None: - color = description.cursor_color - if color is not None: - return color - return 'black' - - -# Colormap loader - -_COLORMAP_CACHE = {} -"""Cache already used colormaps as name: color LUT""" - - -def _arrayToRgba8888(colors): - """Convert colors from a numpy array using float (0..1) int or uint - (0..255) to uint8 RGBA. - - :param numpy.ndarray colors: Array of float int or uint colors to convert - :return: colors as uint8 - :rtype: numpy.ndarray - """ - assert len(colors.shape) == 2 - assert colors.shape[1] in (3, 4) - - if colors.dtype == numpy.uint8: - pass - elif colors.dtype.kind == 'f': - # Each bin is [N, N+1[ except the last one: [255, 256] - colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.) - colors = colors.astype(numpy.uint8) - elif colors.dtype.kind in 'iu': - colors = numpy.clip(colors, 0, 255) - colors = colors.astype(numpy.uint8) - - if colors.shape[1] == 3: - tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8) - tmp[:, 0:3] = colors - tmp[:, 3] = 255 - colors = tmp - - return colors - - -def _createColormapLut(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - description = _AVAILABLE_LUTS.get(name) - use_mpl = False - if description is not None: - if description.source == "builtin": - # Build colormap LUT - lut = numpy.zeros((256, 4), dtype=numpy.uint8) - lut[:, 3] = 255 - - if name == 'gray': - lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'reversed gray': - lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'red': - lut[:, 0] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'green': - lut[:, 1] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'blue': - lut[:, 2] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'temperature': - # Red - lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8) - lut[192:, 0] = 255 - # Green - lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8) - lut[64:192, 1] = 255 - lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8) - # Blue - lut[:64, 2] = 255 - lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8) - else: - raise RuntimeError("Built-in colormap not implemented") - return lut - - elif description.source == "resource": - # Load colormap LUT - colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name)) - # Convert to uint8 and add alpha channel - lut = _arrayToRgba8888(colors) - return lut - - elif description.source == "matplotlib": - use_mpl = True - - else: - raise RuntimeError("Internal LUT source '%s' unsupported" % description.source) - - # Here it expect a matplotlib LUTs - - if use_mpl: - # matplotlib is mandatory - if _matplotlib_cm is None: - raise ValueError("The colormap '%s' expect matplotlib, but matplotlib is not installed" % name) - - if _matplotlib_cm is not None: # Try to load with matplotlib - colormap = _matplotlib_cm.get_cmap(name) - lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True)) - lut = _arrayToRgba8888(lut) - return lut - - raise ValueError("Unknown colormap '%s'" % name) - - -def _getColormap(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - name = str(name) - if name not in _COLORMAP_CACHE: - lut = _createColormapLut(name) - _COLORMAP_CACHE[name] = lut - return _COLORMAP_CACHE[name] - - -# Normalizations - -class _NormalizationMixIn: - """Colormap normalization mix-in class""" - - DEFAULT_RANGE = 0, 1 - """Fallback for (vmin, vmax)""" - - def isValid(self, value): - """Check if a value is in the valid range for this normalization. - - Override in subclass. - - :param Union[float,numpy.ndarray] value: - :rtype: Union[bool,numpy.ndarray] - """ - if isinstance(value, collections.abc.Iterable): - return numpy.ones_like(value, dtype=numpy.bool_) - else: - return True - - def autoscale(self, data, mode): - """Returns range for given data and autoscale mode. - - :param Union[None,numpy.ndarray] data: - :param str mode: Autoscale mode, see :class:`Colormap` - :returns: Range as (min, max) - :rtype: Tuple[float,float] - """ - data = None if data is None else numpy.array(data, copy=False) - if data is None or data.size == 0: - return self.DEFAULT_RANGE - - if mode == Colormap.MINMAX: - vmin, vmax = self.autoscaleMinMax(data) - elif mode == Colormap.STDDEV3: - dmin, dmax = self.autoscaleMinMax(data) - stdmin, stdmax = self.autoscaleMean3Std(data) - if dmin is None: - vmin = stdmin - elif stdmin is None: - vmin = dmin - else: - vmin = max(dmin, stdmin) - - if dmax is None: - vmax = stdmax - elif stdmax is None: - vmax = dmax - else: - vmax = min(dmax, stdmax) - - else: - raise ValueError('Unsupported mode: %s' % mode) - - # Check returned range and handle fallbacks - if vmin is None or not numpy.isfinite(vmin): - vmin = self.DEFAULT_RANGE[0] - if vmax is None or not numpy.isfinite(vmax): - vmax = self.DEFAULT_RANGE[1] - if vmax < vmin: - vmax = vmin - return float(vmin), float(vmax) - - def autoscaleMinMax(self, data): - """Autoscale using min/max - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - data = data[self.isValid(data)] - if data.size == 0: - return None, None - result = min_max(data, min_positive=False, finite=True) - return result.minimum, result.maximum - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - This implementation only works for normalization that do NOT - use the data range. - Override this method for normalization using the range. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - # Use [0, 1] as data range for normalization not using range - normdata = self.apply(data, 0., 1.) - if normdata.dtype.kind == 'f': # Replaces inf by NaN - normdata[numpy.isfinite(normdata) == False] = numpy.nan - if normdata.size == 0: # Fallback - return None, None - - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(normdata), numpy.nanstd(normdata) - - return self.revert(mean - 3 * std, 0., 1.), self.revert(mean + 3 * std, 0., 1.) - - -class _LinearNormalizationMixIn(_NormalizationMixIn): - """Colormap normalization mix-in class specific to autoscale taken from initial range""" - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - Do the autoscale on the data itself, not the normalized data. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - if data.dtype.kind == 'f': # Replaces inf by NaN - data = numpy.array(data, copy=True) # Work on a copy - data[numpy.isfinite(data) == False] = numpy.nan - if data.size == 0: # Fallback - return None, None - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(data), numpy.nanstd(data) - return mean - 3 * std, mean + 3 * std - - -class _LinearNormalization(_colormap.LinearNormalization, _LinearNormalizationMixIn): - """Linear normalization""" - def __init__(self): - _colormap.LinearNormalization.__init__(self) - _LinearNormalizationMixIn.__init__(self) - - -class _LogarithmicNormalization(_colormap.LogarithmicNormalization, _NormalizationMixIn): - """Logarithm normalization""" - - DEFAULT_RANGE = 1, 10 - - def __init__(self): - _colormap.LogarithmicNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value > 0. - - def autoscaleMinMax(self, data): - result = min_max(data, min_positive=True, finite=True) - return result.min_positive, result.maximum - - -class _SqrtNormalization(_colormap.SqrtNormalization, _NormalizationMixIn): - """Square root normalization""" - - DEFAULT_RANGE = 0, 1 - - def __init__(self): - _colormap.SqrtNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value >= 0. - - -class _GammaNormalization(_colormap.PowerNormalization, _LinearNormalizationMixIn): - """Gamma correction normalization: - - Linear normalization to [0, 1] followed by power normalization. - - :param gamma: Gamma correction factor - """ - def __init__(self, gamma): - _colormap.PowerNormalization.__init__(self, gamma) - _LinearNormalizationMixIn.__init__(self) - - -class _ArcsinhNormalization(_colormap.ArcsinhNormalization, _NormalizationMixIn): - """Inverse hyperbolic sine normalization""" - - def __init__(self): - _colormap.ArcsinhNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - -class Colormap(qt.QObject): - """Description of a colormap - - If no `name` nor `colors` are provided, a default gray LUT is used. - - :param str name: Name of the colormap - :param tuple colors: optional, custom colormap. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - If 'name' is None, then this array is used as the colormap. - :param str normalization: Normalization: 'linear' (default) or 'log' - :param vmin: Lower bound of the colormap or None for autoscale (default) - :type vmin: Union[None, float] - :param vmax: Upper bounds of the colormap or None for autoscale (default) - :type vmax: Union[None, float] - """ - - LINEAR = 'linear' - """constant for linear normalization""" - - LOGARITHM = 'log' - """constant for logarithmic normalization""" - - SQRT = 'sqrt' - """constant for square root normalization""" - - GAMMA = 'gamma' - """Constant for gamma correction normalization""" - - ARCSINH = 'arcsinh' - """constant for inverse hyperbolic sine normalization""" - - _BASIC_NORMALIZATIONS = { - LINEAR: _LinearNormalization(), - LOGARITHM: _LogarithmicNormalization(), - SQRT: _SqrtNormalization(), - ARCSINH: _ArcsinhNormalization(), - } - """Normalizations without parameters""" - - NORMALIZATIONS = LINEAR, LOGARITHM, SQRT, GAMMA, ARCSINH - """Tuple of managed normalizations""" - - MINMAX = 'minmax' - """constant for autoscale using min/max data range""" - - STDDEV3 = 'stddev3' - """constant for autoscale using mean +/- 3*std(data) - with a clamp on min/max of the data""" - - AUTOSCALE_MODES = (MINMAX, STDDEV3) - """Tuple of managed auto scale algorithms""" - - sigChanged = qt.Signal() - """Signal emitted when the colormap has changed.""" - - _DEFAULT_NAN_COLOR = 255, 255, 255, 0 - - def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX): - qt.QObject.__init__(self) - self._editable = True - self.__gamma = 2.0 - # Default NaN color: fully transparent white - self.__nanColor = numpy.array(self._DEFAULT_NAN_COLOR, dtype=numpy.uint8) - - assert normalization in Colormap.NORMALIZATIONS - assert autoscaleMode in Colormap.AUTOSCALE_MODES - - if normalization is Colormap.LOGARITHM: - if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0): - m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale." - m += ' Autoscale will be performed.' - m = m % (vmin, vmax) - _logger.warning(m) - vmin = None - vmax = None - - self._name = None - self._colors = None - - if colors is not None and name is not None: - deprecation.deprecated_warning("Argument", - name="silx.gui.plot.Colors", - reason="name and colors can't be used at the same time", - since_version="0.10.0", - skip_backtrace_count=1) - - colors = None - - if name is not None: - self.setName(name) # And resets colormap LUT - elif colors is not None: - self.setColormapLUT(colors) - else: - # Default colormap is grey - self.setName("gray") - - self._normalization = str(normalization) - self._autoscaleMode = str(autoscaleMode) - self._vmin = float(vmin) if vmin is not None else None - self._vmax = float(vmax) if vmax is not None else None - - def setFromColormap(self, other): - """Set this colormap using information from the `other` colormap. - - :param ~silx.gui.colors.Colormap other: Colormap to use as reference. - """ - if not self.isEditable(): - raise NotEditableError('Colormap is not editable') - if self == other: - return - with blockSignals(self): - name = other.getName() - if name is not None: - self.setName(name) - else: - self.setColormapLUT(other.getColormapLUT()) - self.setNaNColor(other.getNaNColor()) - self.setNormalization(other.getNormalization()) - self.setGammaNormalizationParameter( - other.getGammaNormalizationParameter()) - self.setAutoscaleMode(other.getAutoscaleMode()) - self.setVRange(*other.getVRange()) - self.setEditable(other.isEditable()) - self.sigChanged.emit() - - def getNColors(self, nbColors=None): - """Returns N colors computed by sampling the colormap regularly. - - :param nbColors: - The number of colors in the returned array or None for the default value. - The default value is the size of the colormap LUT. - :type nbColors: int or None - :return: 2D array of uint8 of shape (nbColors, 4) - :rtype: numpy.ndarray - """ - # Handle default value for nbColors - if nbColors is None: - return numpy.array(self._colors, copy=True) - else: - nbColors = int(nbColors) - colormap = self.copy() - colormap.setNormalization(Colormap.LINEAR) - colormap.setVRange(vmin=0, vmax=nbColors - 1) - colors = colormap.applyToData( - numpy.arange(nbColors, dtype=numpy.int32)) - return colors - - def getName(self): - """Return the name of the colormap - :rtype: str - """ - return self._name - - def setName(self, name): - """Set the name of the colormap to use. - - :param str name: The name of the colormap. - At least the following names are supported: 'gray', - 'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet', - 'viridis', 'magma', 'inferno', 'plasma'. - """ - name = str(name) - if self._name == name: - return - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if name not in self.getSupportedColormaps(): - raise ValueError("Colormap name '%s' is not supported" % name) - self._name = name - self._colors = _getColormap(self._name) - self.sigChanged.emit() - - def getColormapLUT(self, copy=True): - """Return the list of colors for the colormap or None if not set. - - This returns None if the colormap was set with :meth:`setName`. - Use :meth:`getNColors` to get the colormap LUT for any colormap. - - :param bool copy: If true a copy of the numpy array is provided - :return: the list of colors for the colormap or None if not set - :rtype: numpy.ndarray or None - """ - if self._name is None: - return numpy.array(self._colors, copy=copy) - else: - return None - - def setColormapLUT(self, colors): - """Set the colors of the colormap. - - :param numpy.ndarray colors: the colors of the LUT. - If float, it is converted from [0, 1] to uint8 range. - Otherwise it is casted to uint8. - - .. warning: this will set the value of name to None - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert colors is not None - - colors = numpy.array(colors, copy=False) - if colors.shape == (): - raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors)) - assert len(colors) != 0 - assert colors.ndim >= 2 - colors.shape = -1, colors.shape[-1] - self._colors = _arrayToRgba8888(colors) - self._name = None - self.sigChanged.emit() - - def getNaNColor(self): - """Returns the color to use for Not-A-Number floating point value. - - :rtype: QColor - """ - return qt.QColor(*self.__nanColor) - - def setNaNColor(self, color): - """Set the color to use for Not-A-Number floating point value. - - :param color: RGB(A) color to use for NaN values - :type color: QColor, str, tuple of uint8 or float in [0., 1.] - """ - color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8) - if not numpy.array_equal(self.__nanColor, color): - self.__nanColor = color - self.sigChanged.emit() - - def getNormalization(self): - """Return the normalization of the colormap. - - See :meth:`setNormalization` for returned values. - - :return: the normalization of the colormap - :rtype: str - """ - return self._normalization - - def setNormalization(self, norm): - """Set the colormap normalization. - - Accepted normalizations: 'log', 'linear', 'sqrt' - - :param str norm: the norm to set - """ - assert norm in self.NORMALIZATIONS - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - self._normalization = str(norm) - self.sigChanged.emit() - - def setGammaNormalizationParameter(self, gamma: float) -> None: - """Set the gamma correction parameter. - - Only used for gamma correction normalization. - - :param float gamma: - :raise ValueError: If gamma is not valid - """ - if gamma < 0. or not numpy.isfinite(gamma): - raise ValueError("Gamma value not supported") - if gamma != self.__gamma: - self.__gamma = gamma - self.sigChanged.emit() - - def getGammaNormalizationParameter(self) -> float: - """Returns the gamma correction parameter value. - - :rtype: float - """ - return self.__gamma - - def getAutoscaleMode(self): - """Return the autoscale mode of the colormap ('minmax' or 'stddev3') - - :rtype: str - """ - return self._autoscaleMode - - def setAutoscaleMode(self, mode): - """Set the autoscale mode: either 'minmax' or 'stddev3' - - :param str mode: the mode to set - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert mode in self.AUTOSCALE_MODES - if mode != self._autoscaleMode: - self._autoscaleMode = mode - self.sigChanged.emit() - - def isAutoscale(self): - """Return True if both min and max are in autoscale mode""" - return self._vmin is None and self._vmax is None - - def getVMin(self): - """Return the lower bound of the colormap - - :return: the lower bound of the colormap - :rtype: float or None - """ - return self._vmin - - def setVMin(self, vmin): - """Set the minimal value of the colormap - - :param float vmin: Lower bound of the colormap or None for autoscale - (default) - value) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None: - if self._vmax is not None and vmin > self._vmax: - err = "Can't set vmin because vmin >= vmax. " \ - "vmin = %s, vmax = %s" % (vmin, self._vmax) - raise ValueError(err) - - self._vmin = vmin - self.sigChanged.emit() - - def getVMax(self): - """Return the upper bounds of the colormap or None - - :return: the upper bounds of the colormap or None - :rtype: float or None - """ - return self._vmax - - def setVMax(self, vmax): - """Set the maximal value of the colormap - - :param float vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmax is not None: - if self._vmin is not None and vmax < self._vmin: - err = "Can't set vmax because vmax <= vmin. " \ - "vmin = %s, vmax = %s" % (self._vmin, vmax) - raise ValueError(err) - - self._vmax = vmax - self.sigChanged.emit() - - def isEditable(self): - """ Return if the colormap is editable or not - - :return: editable state of the colormap - :rtype: bool - """ - return self._editable - - def setEditable(self, editable): - """ - Set the editable state of the colormap - - :param bool editable: is the colormap editable - """ - assert type(editable) is bool - self._editable = editable - self.sigChanged.emit() - - def _getNormalizer(self): - """Returns normalizer object""" - normalization = self.getNormalization() - if normalization == self.GAMMA: - return _GammaNormalization(self.getGammaNormalizationParameter()) - else: - return self._BASIC_NORMALIZATIONS[normalization] - - def _computeAutoscaleRange(self, data): - """Compute the data range which will be used in autoscale mode. - - :param numpy.ndarray data: The data for which to compute the range - :return: (vmin, vmax) range - """ - return self._getNormalizer().autoscale( - data, mode=self.getAutoscaleMode()) - - def getColormapRange(self, data=None): - """Return (vmin, vmax) the range of the colormap for the given data or item. - - :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixIn] data: - The data or item to use for autoscale bounds. - :return: (vmin, vmax) corresponding to the colormap applied to data if provided. - :rtype: tuple - """ - vmin = self._vmin - vmax = self._vmax - assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters - - normalizer = self._getNormalizer() - - # Handle invalid bounds as autoscale - if vmin is not None and not normalizer.isValid(vmin): - _logger.info( - 'Invalid vmin, switching to autoscale for lower bound') - vmin = None - if vmax is not None and not normalizer.isValid(vmax): - _logger.info( - 'Invalid vmax, switching to autoscale for upper bound') - vmax = None - - if vmin is None or vmax is None: # Handle autoscale - from .plot.items.core import ColormapMixIn # avoid cyclic import - if isinstance(data, ColormapMixIn): - min_, max_ = data._getColormapAutoscaleRange(self) - # Make sure min_, max_ are not None - min_ = normalizer.DEFAULT_RANGE[0] if min_ is None else min_ - max_ = normalizer.DEFAULT_RANGE[1] if max_ is None else max_ - else: - min_, max_ = normalizer.autoscale( - data, mode=self.getAutoscaleMode()) - - if vmin is None: # Set vmin respecting provided vmax - vmin = min_ if vmax is None else min(min_, vmax) - - if vmax is None: - vmax = max(max_, vmin) # Handle max_ <= 0 for log scale - - return vmin, vmax - - def getVRange(self): - """Get the bounds of the colormap - - :rtype: Tuple(Union[float,None],Union[float,None]) - :returns: A tuple of 2 values for min and max. Or None instead of float - for autoscale - """ - return self.getVMin(), self.getVMax() - - def setVRange(self, vmin, vmax): - """Set the bounds of the colormap - - :param vmin: Lower bound of the colormap or None for autoscale - (default) - :param vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None and vmax is not None: - if vmin > vmax: - err = "Can't set vmin and vmax because vmin >= vmax " \ - "vmin = %s, vmax = %s" % (vmin, vmax) - raise ValueError(err) - - if self._vmin == vmin and self._vmax == vmax: - return - - self._vmin = vmin - self._vmax = vmax - self.sigChanged.emit() - - def __getitem__(self, item): - if item == 'autoscale': - return self.isAutoscale() - elif item == 'name': - return self.getName() - elif item == 'normalization': - return self.getNormalization() - elif item == 'vmin': - return self.getVMin() - elif item == 'vmax': - return self.getVMax() - elif item == 'colors': - return self.getColormapLUT() - elif item == 'autoscaleMode': - return self.getAutoscaleMode() - else: - raise KeyError(item) - - def _toDict(self): - """Return the equivalent colormap as a dictionary - (old colormap representation) - - :return: the representation of the Colormap as a dictionary - :rtype: dict - """ - return { - 'name': self._name, - 'colors': self.getColormapLUT(), - 'vmin': self._vmin, - 'vmax': self._vmax, - 'autoscale': self.isAutoscale(), - 'normalization': self.getNormalization(), - 'autoscaleMode': self.getAutoscaleMode(), - } - - def _setFromDict(self, dic): - """Set values to the colormap from a dictionary - - :param dict dic: the colormap as a dictionary - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - name = dic['name'] if 'name' in dic else None - colors = dic['colors'] if 'colors' in dic else None - if name is not None and colors is not None: - if isinstance(colors, int): - # Filter out argument which was supported but never used - _logger.info("Unused 'colors' from colormap dictionary filterer.") - colors = None - vmin = dic['vmin'] if 'vmin' in dic else None - vmax = dic['vmax'] if 'vmax' in dic else None - if 'normalization' in dic: - normalization = dic['normalization'] - else: - warn = 'Normalization not given in the dictionary, ' - warn += 'set by default to ' + Colormap.LINEAR - _logger.warning(warn) - normalization = Colormap.LINEAR - - if name is None and colors is None: - err = 'The colormap should have a name defined or a tuple of colors' - raise ValueError(err) - if normalization not in Colormap.NORMALIZATIONS: - err = 'Given normalization is not recognized (%s)' % normalization - raise ValueError(err) - - autoscaleMode = dic.get('autoscaleMode', Colormap.MINMAX) - if autoscaleMode not in Colormap.AUTOSCALE_MODES: - err = 'Given autoscale mode is not recognized (%s)' % autoscaleMode - raise ValueError(err) - - # If autoscale, then set boundaries to None - if dic.get('autoscale', False): - vmin, vmax = None, None - - if name is not None: - self.setName(name) - else: - self.setColormapLUT(colors) - self._vmin = vmin - self._vmax = vmax - self._autoscale = True if (vmin is None and vmax is None) else False - self._normalization = normalization - self._autoscaleMode = autoscaleMode - - self.sigChanged.emit() - - @staticmethod - def _fromDict(dic): - colormap = Colormap() - colormap._setFromDict(dic) - return colormap - - def copy(self): - """Return a copy of the Colormap. - - :rtype: silx.gui.colors.Colormap - """ - colormap = Colormap(name=self._name, - colors=self.getColormapLUT(), - vmin=self._vmin, - vmax=self._vmax, - normalization=self.getNormalization(), - autoscaleMode=self.getAutoscaleMode()) - colormap.setNaNColor(self.getNaNColor()) - colormap.setGammaNormalizationParameter( - self.getGammaNormalizationParameter()) - colormap.setEditable(self.isEditable()) - return colormap - - def applyToData(self, data, reference=None): - """Apply the colormap to the data - - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn] data: - The data to convert or the item for which to apply the colormap. - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn,None] reference: - The data or item to use as reference to compute autoscale - """ - if reference is None: - reference = data - vmin, vmax = self.getColormapRange(reference) - - if hasattr(data, "getColormappedData"): # Use item's data - data = data.getColormappedData(copy=False) - - return _colormap.cmap( - data, - self._colors, - vmin, - vmax, - self._getNormalizer(), - self.__nanColor) - - @staticmethod - def getSupportedColormaps(): - """Get the supported colormap names as a tuple of str. - - The list should at least contain and start by: - - ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue', - 'viridis', 'magma', 'inferno', 'plasma') - - :rtype: tuple - """ - colormaps = set() - if _matplotlib_colormaps is not None: - colormaps.update(_matplotlib_colormaps()) - colormaps.update(_AVAILABLE_LUTS.keys()) - - colormaps = tuple(cmap for cmap in sorted(colormaps) - if cmap not in _AVAILABLE_LUTS.keys()) - - return tuple(_AVAILABLE_LUTS.keys()) + colormaps - - def __str__(self): - return str(self._toDict()) - - def __eq__(self, other): - """Compare colormap values and not pointers""" - if other is None: - return False - if not isinstance(other, Colormap): - return False - if self.getNormalization() != other.getNormalization(): - return False - if self.getNormalization() == self.GAMMA: - delta = self.getGammaNormalizationParameter() - other.getGammaNormalizationParameter() - if abs(delta) > 0.001: - return False - return (self.getName() == other.getName() and - self.getAutoscaleMode() == other.getAutoscaleMode() and - self.getVMin() == other.getVMin() and - self.getVMax() == other.getVMax() and - numpy.array_equal(self.getColormapLUT(), other.getColormapLUT()) - ) - - _SERIAL_VERSION = 3 - - def restoreState(self, byteArray): - """ - Read the colormap state from a QByteArray. - - :param qt.QByteArray byteArray: Stream containing the state - :return: True if the restoration sussseed - :rtype: bool - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - stream = qt.QDataStream(byteArray, qt.QIODevice.ReadOnly) - - className = stream.readQString() - if className != self.__class__.__name__: - _logger.warning("Classname mismatch. Found %s." % className) - return False - - version = stream.readUInt32() - if version not in numpy.arange(1, self._SERIAL_VERSION+1): - _logger.warning("Serial version mismatch. Found %d." % version) - return False - - name = stream.readQString() - isNull = stream.readBool() - if not isNull: - vmin = stream.readQVariant() - else: - vmin = None - isNull = stream.readBool() - if not isNull: - vmax = stream.readQVariant() - else: - vmax = None - - normalization = stream.readQString() - if normalization == Colormap.GAMMA: - gamma = stream.readFloat() - else: - gamma = None - - if version == 1: - autoscaleMode = Colormap.MINMAX - else: - autoscaleMode = stream.readQString() - - if version <= 2: - nanColor = self._DEFAULT_NAN_COLOR - else: - nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32() - - # emit change event only once - old = self.blockSignals(True) - try: - self.setName(name) - self.setNormalization(normalization) - self.setAutoscaleMode(autoscaleMode) - self.setVRange(vmin, vmax) - if gamma is not None: - self.setGammaNormalizationParameter(gamma) - self.setNaNColor(nanColor) - finally: - self.blockSignals(old) - self.sigChanged.emit() - return True - - def saveState(self): - """ - Save state of the colomap into a QDataStream. - - :rtype: qt.QByteArray - """ - data = qt.QByteArray() - stream = qt.QDataStream(data, qt.QIODevice.WriteOnly) - - stream.writeQString(self.__class__.__name__) - stream.writeUInt32(self._SERIAL_VERSION) - stream.writeQString(self.getName()) - stream.writeBool(self.getVMin() is None) - if self.getVMin() is not None: - stream.writeQVariant(self.getVMin()) - stream.writeBool(self.getVMax() is None) - if self.getVMax() is not None: - stream.writeQVariant(self.getVMax()) - stream.writeQString(self.getNormalization()) - if self.getNormalization() == Colormap.GAMMA: - stream.writeFloat(self.getGammaNormalizationParameter()) - stream.writeQString(self.getAutoscaleMode()) - nanColor = self.getNaNColor() - stream.writeInt32(nanColor.red()) - stream.writeInt32(nanColor.green()) - stream.writeInt32(nanColor.blue()) - stream.writeInt32(nanColor.alpha()) - - return data - - -_PREFERRED_COLORMAPS = None -""" -Tuple of preferred colormap names accessed with :meth:`preferredColormaps`. -""" - - -def preferredColormaps(): - """Returns the name of the preferred colormaps. - - This list is used by widgets allowing to change the colormap - like the :class:`ColormapDialog` as a subset of colormap choices. - - :rtype: tuple of str - """ - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is None: - # Initialize preferred colormaps - default_preferred = [] - for name, info in _AVAILABLE_LUTS.items(): - if (info.preferred and - (info.source != 'matplotlib' or _matplotlib_cm is not None)): - default_preferred.append(name) - setPreferredColormaps(default_preferred) - return tuple(_PREFERRED_COLORMAPS) - - -def setPreferredColormaps(colormaps): - """Set the list of preferred colormap names. - - Warning: If a colormap name is not available - it will be removed from the list. - - :param colormaps: Not empty list of colormap names - :type colormaps: iterable of str - :raise ValueError: if the list of available preferred colormaps is empty. - """ - supportedColormaps = Colormap.getSupportedColormaps() - colormaps = [cmap for cmap in colormaps if cmap in supportedColormaps] - if len(colormaps) == 0: - raise ValueError("Cannot set preferred colormaps to an empty list") - - global _PREFERRED_COLORMAPS - _PREFERRED_COLORMAPS = colormaps - - -def registerLUT(name, colors, cursor_color='black', preferred=True): - """Register a custom LUT to be used with `Colormap` objects. - - It can override existing LUT names. - - :param str name: Name of the LUT as defined to configure colormaps - :param numpy.ndarray colors: The custom LUT to register. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - :param bool preferred: If true, this LUT will be displayed as part of the - preferred colormaps in dialogs. - :param str cursor_color: Color used to display overlay over images using - colormap with this LUT. - """ - description = _LUT_DESCRIPTION('user', cursor_color, preferred=preferred) - colors = _arrayToRgba8888(colors) - _AVAILABLE_LUTS[name] = description - - if preferred: - # Invalidate the preferred cache - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is not None: - if name not in _PREFERRED_COLORMAPS: - _PREFERRED_COLORMAPS.append(name) - else: - # The cache is not yet loaded, it's fine - pass - - # Register the cache as the LUT was already loaded - _COLORMAP_CACHE[name] = colors |