summaryrefslogtreecommitdiff
path: root/silx/gui/colors.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/colors.py')
-rwxr-xr-xsilx/gui/colors.py1326
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