Source code for silx.gui.colors

# /*##########################################################################
#
# Copyright (c) 2015-2023 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 annotations

__authors__ = ["T. Vincent", "H.Payno"]
__license__ = "MIT"
__date__ = "29/01/2019"


import numpy
import logging
import numbers
import re
from collections.abc import Iterable
from typing import Any, Sequence, Tuple, Union

import silx
from silx.gui import qt
from silx.gui.utils import blockSignals
from silx.math import colormap as _colormap
from silx.utils.exceptions import NotEditableError


_logger = logging.getLogger(__name__)

try:
    import silx.gui.utils.matplotlib  # noqa  Initalize matplotlib

    try:
        from matplotlib import colormaps as _matplotlib_colormaps
    except ImportError:  # For matplotlib < 3.5
        from matplotlib import cm as _matplotlib_cm
        from matplotlib.pyplot import colormaps as _matplotlib_colormaps
    else:
        _matplotlib_cm = None
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


DEFAULT_MIN_LIN = 0
"""Default min value if in linear normalization"""
DEFAULT_MAX_LIN = 1
"""Default max value if in linear normalization"""


_INDEXED_COLOR_PATTERN = re.compile(r"C(?P<index>[0-9]+)")


ColorType = Union[str, Sequence[numbers.Real], qt.QColor]
"""Type of :func:`rgba`'s color argument"""


RGBAColorType = Tuple[float, float, float, float]
"""Type of :func:`rgba` return value"""


[docs] def rgba( color: ColorType, colorDict: dict[str, str] | None = None, colors: Sequence[str] | None = None, ) -> RGBAColorType: """Convert different kind of color definition to a tuple (R, G, B, A) of floats. It supports: - color names: e.g., 'green' - color codes: '#RRGGBB' and '#RRGGBBAA' - indexed color names: e.g., 'C0' - RGB(A) sequence of uint8 in [0, 255] or float in [0, 1] - QColor :param color: The color to convert :param colorDict: A dictionary of color name conversion to color code :param colors: Sequence of colors to use for ` :returns: RGBA colors as floats in [0., 1.] :raises ValueError: if the input is not a valid color """ if isinstance(color, str): # From name colorFromDict = (_COLORDICT if colorDict is None else colorDict).get(color) if colorFromDict is not None: return rgba(colorFromDict, colorDict, colors) # From indexed color name: color{index} match = _INDEXED_COLOR_PATTERN.fullmatch(color) if match is not None: if colors is None: colors = silx.config.DEFAULT_PLOT_CURVE_COLORS index = int(match["index"]) % len(colors) return rgba(colors[index], colorDict, colors) # From #code if len(color) in (7, 9) and color[0] == "#": r = int(color[1:3], 16) / 255.0 g = int(color[3:5], 16) / 255.0 b = int(color[5:7], 16) / 255.0 a = int(color[7:9], 16) / 255.0 if len(color) == 9 else 1.0 return r, g, b, a raise ValueError(f"The string '{color}' is not a valid color") # From QColor if isinstance(color, qt.QColor): return rgba(color.getRgb(), colorDict, colors) # From array values = numpy.asarray(color).ravel() if values.dtype.kind not in "iuf": raise ValueError( f"The array color must be integer/unsigned or float. Found '{values.dtype.kind}'" ) if len(values) not in (3, 4): raise ValueError( f"The array color must have 3 or 4 compound. Found '{len(values)}'" ) # Convert from integers in [0, 255] to float in [0, 1] if values.dtype.kind in "iu": values = values / 255.0 values = numpy.clip(values, 0.0, 1.0) if len(values) == 3: return values[0], values[1], values[2], 1.0 return tuple(values)
[docs] def greyed( color: ColorType, colorDict: dict[str, str] | None = None, ) -> RGBAColorType: """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 color: The color to convert :param colorDict: A dictionary of color name conversion to color code :returns: RGBA colors as floats in [0., 1.] """ r, g, b, a = rgba(color=color, colorDict=colorDict) g = 0.21 * r + 0.72 * g + 0.07 * b return g, g, g, a
[docs] def asQColor(color: ColorType) -> qt.QColor: """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 color: The color to convert """ color = rgba(color) return qt.QColor.fromRgbF(*color)
[docs] def cursorColorForColormap(colormapName: str) -> str: """Get a color suitable for overlay over a colormap. :param colormapName: The name of the colormap. :return: Name of the color. """ return _colormap.get_colormap_cursor_color(colormapName)
# Colormap loader def _registerColormapFromMatplotlib( name: str, cursor_color: str = "black", preferred: bool = False, ): if _matplotlib_cm is not None: colormap = _matplotlib_cm.get_cmap(name) else: # matplotlib >= 3.5 colormap = _matplotlib_colormaps[name] lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True)) colors = _colormap.array_to_rgba8888(lut) registerLUT(name, colors, cursor_color, preferred) def _getColormap(name: str) -> numpy.ndarray: """Returns the color LUT corresponding to a colormap name :param name: Name of the colormap to load :returns: Corresponding table of colors :raise ValueError: If no colormap corresponds to name """ name = str(name) try: return _colormap.get_colormap_lut(name) except ValueError: # Colormap is not available, try to load it from matplotlib _registerColormapFromMatplotlib(name, "black", False) return _colormap.get_colormap_lut(name) class _Colormappable: """Class for objects that can be colormapped by a :class:`Colormap` Used by silx.gui.plot.items.core.ColormapMixIn """ def _getColormapAutoscaleRange( self, colormap: Colormap | None, ) -> tuple[float | None, float | None]: """Returns the autoscale range for given colormap. :param colormap: The colormap for which to compute the autoscale range. If None, the default, the colormap of the item is used :return: (vmin, vmax) range """ raise NotImplementedError("This method must be implemented in subclass") def getColormappedData(copy: bool = False) -> numpy.ndarray | None: """Returns the data used to compute the displayed colors :param copy: True to get a copy, False to get internal data (do not modify!). """ raise NotImplementedError("This method must be implemented in subclass")
[docs] class Colormap(qt.QObject): """Description of a colormap If no `name` nor `colors` are provided, a default gray LUT is used. :param name: Name of the colormap :param 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 normalization: Normalization: 'linear' (default) or 'log' :param vmin: Lower bound of the colormap or None for autoscale (default) :param vmax: Upper bounds of the colormap or None for autoscale (default) """ 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: _colormap.LinearNormalization(), LOGARITHM: _colormap.LogarithmicNormalization(), SQRT: _colormap.SqrtNormalization(), ARCSINH: _colormap.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: str | None = None, colors: numpy.ndarray | None = None, normalization: str = LINEAR, vmin: float | None = None, vmax: float | None = None, autoscaleMode: str = 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: raise ValueError("name and colors arguments can't be set at the same time") 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 self.__warnBadVmin = True self.__warnBadVmax = True
[docs] def setFromColormap(self, other: Colormap): """Set this colormap using information from the `other` colormap. :param 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()
[docs] def getNColors(self, nbColors: int | None = None) -> numpy.ndarray: """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. :return: 2D array of uint8 of shape (nbColors, 4) """ # 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
[docs] def getName(self) -> str | None: """Return the name of the colormap""" return self._name
[docs] def setName(self, name: str): """Set the name of the colormap to use. :param 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()
[docs] def getColormapLUT(self, copy: bool = True) -> numpy.ndarray | None: """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 copy: If true a copy of the numpy array is provided :return: the list of colors for the colormap or None if not set """ if self._name is None: return numpy.array(self._colors, copy=copy) return None
[docs] def setColormapLUT(self, colors: numpy.ndarray): """Set the colors of the colormap. :param 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 = _colormap.array_to_rgba8888(colors) self._name = None self.sigChanged.emit()
[docs] def getNaNColor(self) -> qt.QColor: """Returns the color to use for Not-A-Number floating point value.""" return qt.QColor(*self.__nanColor)
[docs] def setNaNColor(self, color: ColorType): """Set the color to use for Not-A-Number floating point value. :param color: RGB(A) color to use for NaN values """ color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8) if not numpy.array_equal(self.__nanColor, color): self.__nanColor = color self.sigChanged.emit()
[docs] def getNormalization(self) -> str: """Return the normalization of the colormap. See :meth:`setNormalization` for returned values. :return: the normalization of the colormap """ return self._normalization
[docs] def setNormalization(self, norm: str): """Set the colormap normalization. Accepted normalizations: 'log', 'linear', 'sqrt' :param norm: the norm to set """ assert norm in self.NORMALIZATIONS if self.isEditable() is False: raise NotEditableError("Colormap is not editable") norm = str(norm) if norm != self._normalization: self._normalization = norm self.__warnBadVmin = True self.__warnBadVmax = True self.sigChanged.emit()
[docs] def setGammaNormalizationParameter(self, gamma: float): """Set the gamma correction parameter. Only used for gamma correction normalization. :raise ValueError: If gamma is not valid """ if gamma < 0.0 or not numpy.isfinite(gamma): raise ValueError("Gamma value not supported") if gamma != self.__gamma: self.__gamma = gamma self.sigChanged.emit()
[docs] def getGammaNormalizationParameter(self) -> float: """Returns the gamma correction parameter value.""" return self.__gamma
[docs] def getAutoscaleMode(self) -> str: """Return the autoscale mode of the colormap ('minmax' or 'stddev3')""" return self._autoscaleMode
[docs] def setAutoscaleMode(self, mode: str): """Set the autoscale mode: either 'minmax' or 'stddev3' :param 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()
[docs] def isAutoscale(self) -> bool: """Return True if both min and max are in autoscale mode""" return self._vmin is None and self._vmax is None
[docs] def getVMin(self) -> float | None: """Return the lower bound of the colormap :return: the lower bound of the colormap """ return self._vmin
[docs] def setVMin(self, vmin: float | None): """Set the minimal value of the colormap :param vmin: Lower bound of the colormap or None for autoscale (initial 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) if vmin != self._vmin: self._vmin = vmin self.__warnBadVmin = True self.sigChanged.emit()
[docs] def getVMax(self) -> float | None: """Return the upper bounds of the colormap or None :return: the upper bounds of the colormap or None """ return self._vmax
[docs] def setVMax(self, vmax: float | None): """Set the maximal value of the colormap :param vmax: Upper bounds of the colormap or None for autoscale (initial value) """ 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) if vmax != self._vmax: self._vmax = vmax self.__warnBadVmax = True self.sigChanged.emit()
[docs] def isEditable(self) -> bool: """Return if the colormap is editable or not :return: editable state of the colormap """ return self._editable
[docs] def setEditable(self, editable: bool): """ Set the editable state of the colormap :param editable: is the colormap editable """ assert type(editable) is bool self._editable = editable self.sigChanged.emit()
def _getNormalizer(self): # TODO """Returns normalizer object""" normalization = self.getNormalization() if normalization == self.GAMMA: return _colormap.GammaNormalization(self.getGammaNormalizationParameter()) else: return self._BASIC_NORMALIZATIONS[normalization] def _computeAutoscaleRange(self, data: numpy.ndarray): """Compute the data range which will be used in autoscale mode. :param data: The data for which to compute the range :return: (vmin, vmax) range """ return self._getNormalizer().autoscale(data, mode=self.getAutoscaleMode())
[docs] def getColormapRange( self, data: numpy.ndarray | _Colormappable | None = None, ) -> tuple[float, float]: """Return (vmin, vmax) the range of the colormap for the given data or item. :param data: The data or item to use for autoscale bounds. :return: (vmin, vmax) corresponding to the colormap applied to data if provided. """ 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.is_valid(vmin): if self.__warnBadVmin: self.__warnBadVmin = False _logger.info("Invalid vmin, switching to autoscale for lower bound") vmin = None if vmax is not None and not normalizer.is_valid(vmax): if self.__warnBadVmax: self.__warnBadVmax = False _logger.info("Invalid vmax, switching to autoscale for upper bound") vmax = None if vmin is None or vmax is None: # Handle autoscale if isinstance(data, _Colormappable): 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
[docs] def getVRange(self) -> tuple[float | None, float | None]: """Get the bounds of the colormap :returns: A tuple of 2 values for min and max. Or None instead of float for autoscale """ return self.getVMin(), self.getVMax()
[docs] def setVRange(self, vmin: float | None, vmax: float | None): """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 not numpy.isfinite(vmin)) or ( vmax is not None and not numpy.isfinite(vmax) ): err = ( "Can't set vmin and vmax because vmin or vmax are not finite " "vmin = %s, vmax = %s" % (vmin, vmax) ) raise ValueError(err) 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 if vmin != self._vmin: self.__warnBadVmin = True self._vmin = vmin if vmax != self._vmax: self.__warnBadVmax = True self._vmax = vmax self.sigChanged.emit()
def __getitem__(self, item: str): 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) -> dict: """Return the equivalent colormap as a dictionary (old colormap representation) :return: the representation of the Colormap as a dictionary """ 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: dict): """Set values to the colormap from a dictionary :param 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.__warnBadVmin = True self.__warnBadVmax = True self.sigChanged.emit() @staticmethod def _fromDict(dic: dict): colormap = Colormap() colormap._setFromDict(dic) return colormap
[docs] def copy(self) -> Colormap: """Return a copy of the 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
[docs] def applyToData( self, data: numpy.ndarray | _Colormappable, reference: numpy.ndarray | _Colormappable | None = None, ) -> numpy.ndarray: """Apply the colormap to the data :param data: The data to convert or the item for which to apply the colormap. :param reference: The data or item to use as reference to compute autoscale """ if reference is None: reference = data vmin, vmax = self.getColormapRange(reference) if isinstance(data, _Colormappable): # Use item's data data = data.getColormappedData(copy=False) return _colormap.cmap( data, self._colors, vmin, vmax, self._getNormalizer(), self.__nanColor )
[docs] @staticmethod def getSupportedColormaps() -> tuple[str, ...]: """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') """ registered_colormaps = _colormap.get_registered_colormaps() colormaps = set(registered_colormaps) if _matplotlib_colormaps is not None: colormaps.update(_matplotlib_colormaps()) # Put registered_colormaps first colormaps = tuple( cmap for cmap in sorted(colormaps) if cmap not in registered_colormaps ) return registered_colormaps + colormaps
def __str__(self) -> str: return str(self._toDict()) def __eq__(self, other: Any): """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
[docs] def restoreState(self, byteArray: qt.QByteArray) -> bool: """ Read the colormap state from a QByteArray. :param byteArray: Stream containing the state :return: True if the restoration sussseed """ 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
[docs] def saveState(self) -> qt.QByteArray: """Save state of the colomap into a QDataStream.""" 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`. """ _DEFAULT_PREFERRED_COLORMAPS = ( "gray", "reversed gray", "red", "green", "blue", "viridis", "cividis", "magma", "inferno", "plasma", "temperature", "jet", "hsv", )
[docs] def preferredColormaps() -> tuple[str, ...]: """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. """ global _PREFERRED_COLORMAPS if _PREFERRED_COLORMAPS is None: # Initialize preferred colormaps setPreferredColormaps(_DEFAULT_PREFERRED_COLORMAPS) return tuple(_PREFERRED_COLORMAPS)
[docs] def setPreferredColormaps(colormaps: Iterable[str]): """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 :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
[docs] def registerLUT( name: str, colors: numpy.ndarray, cursor_color: str = "black", preferred: bool = True, ): """Register a custom LUT to be used with `Colormap` objects. It can override existing LUT names. :param name: Name of the LUT as defined to configure colormaps :param colors: The custom LUT to register. Nx3 or Nx4 numpy array of RGB(A) colors, either uint8 or float in [0, 1]. :param preferred: If true, this LUT will be displayed as part of the preferred colormaps in dialogs. :param cursor_color: Color used to display overlay over images using colormap with this LUT. """ _colormap.register_colormap(name, colors, cursor_color) 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
# Load some colormaps from matplotlib by default if _matplotlib_cm is not None: _registerColormapFromMatplotlib("jet", cursor_color="pink", preferred=True) _registerColormapFromMatplotlib("hsv", cursor_color="black", preferred=True)