# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2018 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
from silx.gui import qt
from silx import config
from silx.math.combo import min_max
from silx.math.colormap import cmap as _cmap
from silx.utils.exceptions import NotEditableError
from silx.utils import deprecation
from silx.resources import resource_filename as _resource_filename
_logger = logging.getLogger(__file__)
try:
from matplotlib import cm as _matplotlib_cm
except ImportError:
_logger.info("matplotlib not available, only embedded colormaps available")
_matplotlib_cm = 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)),
('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)),
('red', _LUT_DESCRIPTION('builtin', 'green', True)),
('green', _LUT_DESCRIPTION('builtin', 'pink', True)),
('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)),
('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)),
('viridis', _LUT_DESCRIPTION('resource', 'pink', True)),
('magma', _LUT_DESCRIPTION('resource', 'green', True)),
('inferno', _LUT_DESCRIPTION('resource', 'green', True)),
('plasma', _LUT_DESCRIPTION('resource', 'green', 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"""
DEFAULT_MIN_LOG = 1
"""Default min value if in log normalization"""
DEFAULT_MAX_LOG = 10
"""Default max value if in log normalization"""
[docs]def rgba(color, colorDict=None):
"""Convert color code '#RRGGBB' and '#RRGGBBAA' to (R, G, B, A)
It also convert RGB(A) values from uint8 to float in [0, 1] and
accept a 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
[docs]def greyed(color, colorDict=None):
"""Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color
(R, G, B, A).
It also convert RGB(A) values from uint8 to float in [0, 1] and
accept a 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
[docs]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]
[docs]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 float vmin:
Lower bound of the colormap or None for autoscale (default)
:param float vmax:
Upper bounds of the colormap or None for autoscale (default)
"""
LINEAR = 'linear'
"""constant for linear normalization"""
LOGARITHM = 'log'
"""constant for logarithmic normalization"""
NORMALIZATIONS = (LINEAR, LOGARITHM)
"""Tuple of managed normalizations"""
sigChanged = qt.Signal()
"""Signal emitted when the colormap has changed."""
def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None):
qt.QObject.__init__(self)
self._editable = True
assert normalization in Colormap.NORMALIZATIONS
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._vmin = float(vmin) if vmin is not None else None
self._vmax = float(vmax) if vmax is not None else None
[docs] def setFromColormap(self, other):
"""Set this colormap using information from the `other` colormap.
:param Colormap other: Colormap to use as reference.
"""
if not self.isEditable():
raise NotEditableError('Colormap is not editable')
if self == other:
return
old = self.blockSignals(True)
name = other.getName()
if name is not None:
self.setName(name)
else:
self.setColormapLUT(other.getColormapLUT())
self.setNormalization(other.getNormalization())
self.setVRange(other.getVMin(), other.getVMax())
self.blockSignals(old)
self.sigChanged.emit()
[docs] 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:
colormap = self.copy()
colormap.setNormalization(Colormap.LINEAR)
colormap.setVRange(vmin=None, vmax=None)
colors = colormap.applyToData(
numpy.arange(int(nbColors), dtype=numpy.int))
return colors
[docs] def getName(self):
"""Return the name of the colormap
:rtype: str
"""
return self._name
[docs] 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()
[docs] 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
[docs] 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()
[docs] def getNormalization(self):
"""Return the normalization of the colormap ('log' or 'linear')
:return: the normalization of the colormap
:rtype: str
"""
return self._normalization
[docs] def setNormalization(self, norm):
"""Set the norm ('log', 'linear')
:param str norm: the norm to set
"""
if self.isEditable() is False:
raise NotEditableError('Colormap is not editable')
self._normalization = str(norm)
self.sigChanged.emit()
[docs] def isAutoscale(self):
"""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):
"""Return the lower bound of the colormap
:return: the lower bound of the colormap
:rtype: float or None
"""
return self._vmin
[docs] 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()
[docs] 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
[docs] 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()
[docs] def isEditable(self):
""" Return if the colormap is editable or not
:return: editable state of the colormap
:rtype: bool
"""
return self._editable
[docs] 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()
[docs] def getColormapRange(self, data=None):
"""Return (vmin, vmax)
:return: the tuple vmin, vmax fitting vmin, vmax, normalization and
data if any given
:rtype: tuple
"""
vmin = self._vmin
vmax = self._vmax
assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters
if self.getNormalization() == self.LOGARITHM:
# Handle negative bounds as autoscale
if vmin is not None and (vmin is not None and vmin <= 0.):
mess = 'negative vmin, moving to autoscale for lower bound'
_logger.warning(mess)
vmin = None
if vmax is not None and (vmax is not None and vmax <= 0.):
mess = 'negative vmax, moving to autoscale for upper bound'
_logger.warning(mess)
vmax = None
if vmin is None or vmax is None: # Handle autoscale
# Get min/max from data
if data is not None:
data = numpy.array(data, copy=False)
if data.size == 0: # Fallback an array but no data
min_, max_ = self._getDefaultMin(), self._getDefaultMax()
else:
if self.getNormalization() == self.LOGARITHM:
result = min_max(data, min_positive=True, finite=True)
min_ = result.min_positive # >0 or None
max_ = result.maximum # can be <= 0
else:
min_, max_ = min_max(data, min_positive=False, finite=True)
# Handle fallback
if min_ is None or not numpy.isfinite(min_):
min_ = self._getDefaultMin()
if max_ is None or not numpy.isfinite(max_):
max_ = self._getDefaultMax()
else: # Fallback if no data is provided
min_, max_ = self._getDefaultMin(), self._getDefaultMax()
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 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()
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._normalization
}
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 recoginized (%s)' % normalization
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.sigChanged.emit()
@staticmethod
def _fromDict(dic):
colormap = Colormap()
colormap._setFromDict(dic)
return colormap
[docs] def copy(self):
"""Return a copy of the Colormap.
:rtype: silx.gui.colors.Colormap
"""
return Colormap(name=self._name,
colors=self.getColormapLUT(),
vmin=self._vmin,
vmax=self._vmax,
normalization=self._normalization)
[docs] def applyToData(self, data):
"""Apply the colormap to the data
:param numpy.ndarray data: The data to convert.
"""
vmin, vmax = self.getColormapRange(data)
normalization = self.getNormalization()
return _cmap(data, self._colors, vmin, vmax, normalization)
[docs] @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_cm is not None:
colormaps.update(_matplotlib_cm.cmap_d.keys())
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 _getDefaultMin(self):
return DEFAULT_MIN_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MIN_LOG
def _getDefaultMax(self):
return DEFAULT_MAX_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MAX_LOG
def __eq__(self, other):
"""Compare colormap values and not pointers"""
if other is None:
return False
if not isinstance(other, Colormap):
return False
return (self.getName() == other.getName() and
self.getNormalization() == other.getNormalization() and
self.getVMin() == other.getVMin() and
self.getVMax() == other.getVMax() and
numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
)
_SERIAL_VERSION = 1
[docs] 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 != self._SERIAL_VERSION:
_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()
# emit change event only once
old = self.blockSignals(True)
try:
self.setName(name)
self.setNormalization(normalization)
self.setVRange(vmin, vmax)
finally:
self.blockSignals(old)
self.sigChanged.emit()
return True
[docs] 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())
return data
_PREFERRED_COLORMAPS = None
"""
Tuple of preferred colormap names accessed with :meth:`preferredColormaps`.
"""
[docs]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 = [k for k in _AVAILABLE_LUTS.keys() if _AVAILABLE_LUTS[k].preferred]
setPreferredColormaps(default_preferred)
return tuple(_PREFERRED_COLORMAPS)
[docs]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
[docs]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