# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-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.
#
# ###########################################################################*/
"""A QDialog widget to set-up the colormap.
It uses a description of colormaps as dict compatible with :class:`Plot`.
To run the following sample code, a QApplication must be initialized.
Create the colormap dialog and set the colormap description and data range:
>>> from silx.gui.dialog.ColormapDialog import ColormapDialog
>>> from silx.gui.colors import Colormap
>>> dialog = ColormapDialog()
>>> colormap = Colormap(name='red', normalization='log',
... vmin=1., vmax=2.)
>>> dialog.setColormap(colormap)
>>> colormap.setVRange(1., 100.) # This scale the width of the plot area
>>> dialog.show()
Get the colormap description (compatible with :class:`Plot`) from the dialog:
>>> cmap = dialog.getColormap()
>>> cmap.getName()
'red'
It is also possible to display an histogram of the image in the dialog.
This updates the data range with the range of the bins.
>>> import numpy
>>> image = numpy.random.normal(size=512 * 512).reshape(512, -1)
>>> hist, bin_edges = numpy.histogram(image, bins=10)
>>> dialog.setHistogram(hist, bin_edges)
The updates of the colormap description are also available through the signal:
:attr:`ColormapDialog.sigColormapChanged`.
""" # noqa
from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
__date__ = "27/11/2018"
import enum
import logging
import numpy
from .. import qt
from ..colors import Colormap, preferredColormaps
from ..plot import PlotWidget
from ..plot.items.axis import Axis
from silx.gui.widgets.FloatEdit import FloatEdit
import weakref
from silx.math.combo import min_max
from silx.gui import icons
from silx.math.histogram import Histogramnd
_logger = logging.getLogger(__name__)
_colormapIconPreview = {}
class _BoundaryWidget(qt.QWidget):
"""Widget to edit a boundary of the colormap (vmin, vmax)"""
sigValueChanged = qt.Signal(object)
"""Signal emitted when value is changed"""
def __init__(self, parent=None, value=0.0):
qt.QWidget.__init__(self, parent=None)
self.setLayout(qt.QHBoxLayout())
self.layout().setContentsMargins(0, 0, 0, 0)
self._numVal = FloatEdit(parent=self, value=value)
self.layout().addWidget(self._numVal)
self._autoCB = qt.QCheckBox('auto', parent=self)
self.layout().addWidget(self._autoCB)
self._autoCB.setChecked(False)
self._autoCB.toggled.connect(self._autoToggled)
self.sigValueChanged = self._autoCB.toggled
self.textEdited = self._numVal.textEdited
self.editingFinished = self._numVal.editingFinished
self._dataValue = None
def isAutoChecked(self):
return self._autoCB.isChecked()
def getValue(self):
return None if self._autoCB.isChecked() else self._numVal.value()
def getFiniteValue(self):
if not self._autoCB.isChecked():
return self._numVal.value()
elif self._dataValue is None:
return self._numVal.value()
else:
return self._dataValue
def _autoToggled(self, enabled):
self._numVal.setEnabled(not enabled)
self._updateDisplayedText()
def _updateDisplayedText(self):
# if dataValue is finite
if self._autoCB.isChecked() and self._dataValue is not None:
old = self._numVal.blockSignals(True)
self._numVal.setValue(self._dataValue)
self._numVal.blockSignals(old)
def setDataValue(self, dataValue):
self._dataValue = dataValue
self._updateDisplayedText()
def setFiniteValue(self, value):
assert(value is not None)
old = self._numVal.blockSignals(True)
self._numVal.setValue(value)
self._numVal.blockSignals(old)
def setValue(self, value, isAuto=False):
self._autoCB.setChecked(isAuto or value is None)
if value is not None:
self._numVal.setValue(value)
self._updateDisplayedText()
class _ColormapNameCombox(qt.QComboBox):
def __init__(self, parent=None):
qt.QComboBox.__init__(self, parent)
self.__initItems()
LUT_NAME = qt.Qt.UserRole + 1
LUT_COLORS = qt.Qt.UserRole + 2
def __initItems(self):
for colormapName in preferredColormaps():
index = self.count()
self.addItem(str.title(colormapName))
self.setItemIcon(index, self.getIconPreview(name=colormapName))
self.setItemData(index, colormapName, role=self.LUT_NAME)
def getIconPreview(self, name=None, colors=None):
"""Return an icon preview from a LUT name.
This icons are cached into a global structure.
:param str name: Name of the LUT
:param numpy.ndarray colors: Colors identify the LUT
:rtype: qt.QIcon
"""
if name is not None:
iconKey = name
else:
iconKey = tuple(colors)
icon = _colormapIconPreview.get(iconKey, None)
if icon is None:
icon = self.createIconPreview(name, colors)
_colormapIconPreview[iconKey] = icon
return icon
def createIconPreview(self, name=None, colors=None):
"""Create and return an icon preview from a LUT name.
This icons are cached into a global structure.
:param str name: Name of the LUT
:param numpy.ndarray colors: Colors identify the LUT
:rtype: qt.QIcon
"""
colormap = Colormap(name)
size = 32
if name is not None:
lut = colormap.getNColors(size)
else:
lut = colors
if len(lut) > size:
# Down sample
step = int(len(lut) / size)
lut = lut[::step]
elif len(lut) < size:
# Over sample
indexes = numpy.arange(size) / float(size) * (len(lut) - 1)
indexes = indexes.astype("int")
lut = lut[indexes]
if lut is None or len(lut) == 0:
return qt.QIcon()
pixmap = qt.QPixmap(size, size)
painter = qt.QPainter(pixmap)
for i in range(size):
rgb = lut[i]
r, g, b = rgb[0], rgb[1], rgb[2]
painter.setPen(qt.QColor(r, g, b))
painter.drawPoint(qt.QPoint(i, 0))
painter.drawPixmap(0, 1, size, size - 1, pixmap, 0, 0, size, 1)
painter.end()
return qt.QIcon(pixmap)
def getCurrentName(self):
return self.itemData(self.currentIndex(), self.LUT_NAME)
def getCurrentColors(self):
return self.itemData(self.currentIndex(), self.LUT_COLORS)
def findLutName(self, name):
return self.findData(name, role=self.LUT_NAME)
def findLutColors(self, lut):
for index in range(self.count()):
if self.itemData(index, role=self.LUT_NAME) is not None:
continue
colors = self.itemData(index, role=self.LUT_COLORS)
if colors is None:
continue
if numpy.array_equal(colors, lut):
return index
return -1
def setCurrentLut(self, colormap):
name = colormap.getName()
if name is not None:
self._setCurrentName(name)
else:
lut = colormap.getColormapLUT()
self._setCurrentLut(lut)
def _setCurrentLut(self, lut):
index = self.findLutColors(lut)
if index == -1:
index = self.count()
self.addItem("Custom")
self.setItemIcon(index, self.getIconPreview(colors=lut))
self.setItemData(index, None, role=self.LUT_NAME)
self.setItemData(index, lut, role=self.LUT_COLORS)
self.setCurrentIndex(index)
def _setCurrentName(self, name):
index = self.findLutName(name)
if index < 0:
index = self.count()
self.addItem(str.title(name))
self.setItemIcon(index, self.getIconPreview(name=name))
self.setItemData(index, name, role=self.LUT_NAME)
self.setCurrentIndex(index)
@enum.unique
class _DataInPlotMode(enum.Enum):
"""Enum for each mode of display of the data in the plot."""
NONE = 'none'
RANGE = 'range'
HISTOGRAM = 'histogram'
[docs]class ColormapDialog(qt.QDialog):
"""A QDialog widget to set the colormap.
:param parent: See :class:`QDialog`
:param str title: The QDialog title
"""
visibleChanged = qt.Signal(bool)
"""This event is sent when the dialog visibility change"""
def __init__(self, parent=None, title="Colormap Dialog"):
qt.QDialog.__init__(self, parent)
self.setWindowTitle(title)
self._colormap = None
self._data = None
self._dataInPlotMode = _DataInPlotMode.RANGE
self._ignoreColormapChange = False
"""Used as a semaphore to avoid editing the colormap object when we are
only attempt to display it.
Used instead of n connect and disconnect of the sigChanged. The
disconnection to sigChanged was also limiting when this colormapdialog
is used in the colormapaction and associated to the activeImageChanged.
(because the activeImageChanged is send when the colormap changed and
the self.setcolormap is a callback)
"""
self.__displayInvalidated = False
self._histogramData = None
self._minMaxWasEdited = False
self._initialRange = None
self._dataRange = None
"""If defined 3-tuple containing information from a data:
minimum, positive minimum, maximum"""
self._colormapStoredState = None
# Make the GUI
vLayout = qt.QVBoxLayout(self)
formWidget = qt.QWidget(parent=self)
vLayout.addWidget(formWidget)
formLayout = qt.QFormLayout(formWidget)
formLayout.setContentsMargins(10, 10, 10, 10)
formLayout.setSpacing(0)
# Colormap row
self._comboBoxColormap = _ColormapNameCombox(parent=formWidget)
self._comboBoxColormap.currentIndexChanged[int].connect(self._updateLut)
formLayout.addRow('Colormap:', self._comboBoxColormap)
# Normalization row
self._normButtonLinear = qt.QRadioButton('Linear')
self._normButtonLinear.setChecked(True)
self._normButtonLog = qt.QRadioButton('Log')
normButtonGroup = qt.QButtonGroup(self)
normButtonGroup.setExclusive(True)
normButtonGroup.addButton(self._normButtonLinear)
normButtonGroup.addButton(self._normButtonLog)
normButtonGroup.buttonClicked[qt.QAbstractButton].connect(self._updateNormalization)
normLayout = qt.QHBoxLayout()
normLayout.setContentsMargins(0, 0, 0, 0)
normLayout.setSpacing(10)
normLayout.addWidget(self._normButtonLinear)
normLayout.addWidget(self._normButtonLog)
formLayout.addRow('Normalization:', normLayout)
# Min row
self._minValue = _BoundaryWidget(parent=self, value=1.0)
self._minValue.textEdited.connect(self._minMaxTextEdited)
self._minValue.editingFinished.connect(self._minEditingFinished)
self._minValue.sigValueChanged.connect(self._updateMinMax)
formLayout.addRow('\tMin:', self._minValue)
# Max row
self._maxValue = _BoundaryWidget(parent=self, value=10.0)
self._maxValue.textEdited.connect(self._minMaxTextEdited)
self._maxValue.sigValueChanged.connect(self._updateMinMax)
self._maxValue.editingFinished.connect(self._maxEditingFinished)
formLayout.addRow('\tMax:', self._maxValue)
# Add plot for histogram
self._plotToolbar = qt.QToolBar(self)
self._plotToolbar.setFloatable(False)
self._plotToolbar.setMovable(False)
self._plotToolbar.setIconSize(qt.QSize(8, 8))
self._plotToolbar.setStyleSheet("QToolBar { border: 0px }")
self._plotToolbar.setOrientation(qt.Qt.Vertical)
group = qt.QActionGroup(self._plotToolbar)
group.setExclusive(True)
action = qt.QAction("Nothing", self)
action.setToolTip("No range nor histogram are displayed. No extra computation have to be done.")
action.setIcon(icons.getQIcon('colormap-none'))
action.setCheckable(True)
action.setData(_DataInPlotMode.NONE)
action.setChecked(action.data() == self._dataInPlotMode)
self._plotToolbar.addAction(action)
group.addAction(action)
action = qt.QAction("Data range", self)
action.setToolTip("Display the data range within the colormap range. A fast data processing have to be done.")
action.setIcon(icons.getQIcon('colormap-range'))
action.setCheckable(True)
action.setData(_DataInPlotMode.RANGE)
action.setChecked(action.data() == self._dataInPlotMode)
self._plotToolbar.addAction(action)
group.addAction(action)
action = qt.QAction("Histogram", self)
action.setToolTip("Display the data histogram within the colormap range. A slow data processing have to be done. ")
action.setIcon(icons.getQIcon('colormap-histogram'))
action.setCheckable(True)
action.setData(_DataInPlotMode.HISTOGRAM)
action.setChecked(action.data() == self._dataInPlotMode)
self._plotToolbar.addAction(action)
group.addAction(action)
group.triggered.connect(self._displayDataInPlotModeChanged)
self._plotBox = qt.QWidget(self)
self._plotInit()
plotBoxLayout = qt.QHBoxLayout()
plotBoxLayout.setContentsMargins(0, 0, 0, 0)
plotBoxLayout.setSpacing(2)
plotBoxLayout.addWidget(self._plotToolbar)
plotBoxLayout.addWidget(self._plot)
plotBoxLayout.setSizeConstraint(qt.QLayout.SetMinimumSize)
self._plotBox.setLayout(plotBoxLayout)
vLayout.addWidget(self._plotBox)
# define modal buttons
types = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel
self._buttonsModal = qt.QDialogButtonBox(parent=self)
self._buttonsModal.setStandardButtons(types)
self.layout().addWidget(self._buttonsModal)
self._buttonsModal.accepted.connect(self.accept)
self._buttonsModal.rejected.connect(self.reject)
# define non modal buttons
types = qt.QDialogButtonBox.Close | qt.QDialogButtonBox.Reset
self._buttonsNonModal = qt.QDialogButtonBox(parent=self)
self._buttonsNonModal.setStandardButtons(types)
self.layout().addWidget(self._buttonsNonModal)
self._buttonsNonModal.button(qt.QDialogButtonBox.Close).clicked.connect(self.accept)
self._buttonsNonModal.button(qt.QDialogButtonBox.Reset).clicked.connect(self.resetColormap)
# Set the colormap to default values
self.setColormap(Colormap(name='gray', normalization='linear',
vmin=None, vmax=None))
self.setModal(self.isModal())
vLayout.setSizeConstraint(qt.QLayout.SetMinimumSize)
self.setFixedSize(self.sizeHint())
self._applyColormap()
def _displayLater(self):
self.__displayInvalidated = True
def showEvent(self, event):
self.visibleChanged.emit(True)
super(ColormapDialog, self).showEvent(event)
if self.isVisible():
if self.__displayInvalidated:
self._applyColormap()
self._updateDataInPlot()
self.__displayInvalidated = False
def closeEvent(self, event):
if not self.isModal():
self.accept()
super(ColormapDialog, self).closeEvent(event)
def hideEvent(self, event):
self.visibleChanged.emit(False)
super(ColormapDialog, self).hideEvent(event)
def close(self):
self.accept()
qt.QDialog.close(self)
def setModal(self, modal):
assert type(modal) is bool
self._buttonsNonModal.setVisible(not modal)
self._buttonsModal.setVisible(modal)
qt.QDialog.setModal(self, modal)
def exec_(self):
wasModal = self.isModal()
self.setModal(True)
result = super(ColormapDialog, self).exec_()
self.setModal(wasModal)
return result
def _plotInit(self):
"""Init the plot to display the range and the values"""
self._plot = PlotWidget()
self._plot.setDataMargins(yMinMargin=0.125, yMaxMargin=0.125)
self._plot.getXAxis().setLabel("Data Values")
self._plot.getYAxis().setLabel("")
self._plot.setInteractiveMode('select', zoomOnWheel=False)
self._plot.setActiveCurveHandling(False)
self._plot.setMinimumSize(qt.QSize(250, 200))
self._plot.sigPlotSignal.connect(self._plotSlot)
self._plotUpdate()
def sizeHint(self):
return self.layout().minimumSize()
def _computeView(self, dataMin, dataMax):
"""Compute the location of the view according to the bound of the data
:rtype: Tuple(float, float)
"""
marginRatio = 1.0 / 6.0
scale = self._plot.getXAxis().getScale()
if self._dataRange is not None:
if scale == Axis.LOGARITHMIC:
minRange = self._dataRange[1]
else:
minRange = self._dataRange[0]
maxRange = self._dataRange[2]
if minRange is not None:
dataMin = min(dataMin, minRange)
dataMax = max(dataMax, maxRange)
if self._histogramData is not None:
info = min_max(self._histogramData[1])
if scale == Axis.LOGARITHMIC:
minHisto = info.min_positive
else:
minHisto = info.minimum
maxHisto = info.maximum
if minHisto is not None:
dataMin = min(dataMin, minHisto)
dataMax = max(dataMax, maxHisto)
if scale == Axis.LOGARITHMIC:
epsilon = numpy.finfo(numpy.float32).eps
if dataMin == 0:
dataMin = epsilon
if dataMax < dataMin:
dataMax = dataMin + epsilon
marge = marginRatio * abs(numpy.log10(dataMax) - numpy.log10(dataMin))
viewMin = 10**(numpy.log10(dataMin) - marge)
viewMax = 10**(numpy.log10(dataMax) + marge)
else: # scale == Axis.LINEAR:
marge = marginRatio * abs(dataMax - dataMin)
if marge < 0.0001:
# Smaller that the QLineEdit precision
marge = 0.0001
viewMin = dataMin - marge
viewMax = dataMax + marge
return viewMin, viewMax
def _plotUpdate(self, updateMarkers=True):
"""Update the plot content
:param bool updateMarkers: True to update markers, False otherwith
"""
colormap = self.getColormap()
if colormap is None:
if self._plotBox.isVisibleTo(self):
self._plotBox.setVisible(False)
self.setFixedSize(self.sizeHint())
return
if not self._plotBox.isVisibleTo(self):
self._plotBox.setVisible(True)
self.setFixedSize(self.sizeHint())
minData, maxData = self._minValue.getFiniteValue(), self._maxValue.getFiniteValue()
if minData > maxData:
# avoid a full collapse
minData, maxData = maxData, minData
minView, maxView = self._computeView(minData, maxData)
if updateMarkers:
# Save the state in we are not moving the markers
self._initialRange = minView, maxView
elif self._initialRange is not None:
minView = min(minView, self._initialRange[0])
maxView = max(maxView, self._initialRange[1])
if minView > minData:
# Hide the min range
minData = minView
x = [minView, minData, maxData, maxView]
y = [0, 0, 1, 1]
self._plot.addCurve(x, y,
legend="ConstrainedCurve",
color='black',
symbol='o',
linestyle='-',
resetzoom=False)
scale = self._plot.getXAxis().getScale()
if updateMarkers:
posMin = self._minValue.getFiniteValue()
posMax = self._maxValue.getFiniteValue()
def isDisplayable(pos):
if scale == Axis.LOGARITHMIC:
return pos > 0.0
return True
if isDisplayable(posMin):
minDraggable = (self._colormap().isEditable() and
not self._minValue.isAutoChecked())
self._plot.addXMarker(
posMin,
legend='Min',
text='Min',
draggable=minDraggable,
color='blue',
constraint=self._plotMinMarkerConstraint)
if isDisplayable(posMax):
maxDraggable = (self._colormap().isEditable() and
not self._maxValue.isAutoChecked())
self._plot.addXMarker(
posMax,
legend='Max',
text='Max',
draggable=maxDraggable,
color='blue',
constraint=self._plotMaxMarkerConstraint)
self._plot.resetZoom()
def _plotMinMarkerConstraint(self, x, y):
"""Constraint of the min marker"""
return min(x, self._maxValue.getFiniteValue()), y
def _plotMaxMarkerConstraint(self, x, y):
"""Constraint of the max marker"""
return max(x, self._minValue.getFiniteValue()), y
def _plotSlot(self, event):
"""Handle events from the plot"""
if event['event'] in ('markerMoving', 'markerMoved'):
value = float(str(event['xdata']))
if event['label'] == 'Min':
self._minValue.setValue(value)
elif event['label'] == 'Max':
self._maxValue.setValue(value)
# This will recreate the markers while interacting...
# It might break if marker interaction is changed
if event['event'] == 'markerMoved':
self._initialRange = None
self._updateMinMax()
else:
self._plotUpdate(updateMarkers=False)
[docs] @staticmethod
def computeDataRange(data):
"""Compute the data range as used by :meth:`setDataRange`.
:param data: The data to process
:rtype: Tuple(float, float, float)
"""
if data is None or len(data) == 0:
return None, None, None
dataRange = min_max(data, min_positive=True, finite=True)
if dataRange.minimum is None:
# Only non-finite data
dataRange = None
if dataRange is not None:
min_positive = dataRange.min_positive
if min_positive is None:
min_positive = float('nan')
dataRange = dataRange.minimum, min_positive, dataRange.maximum
if dataRange is None or len(dataRange) != 3:
qt.QMessageBox.warning(
None, "No Data",
"Image data does not contain any real value")
dataRange = 1., 1., 10.
return dataRange
[docs] @staticmethod
def computeHistogram(data, scale=Axis.LINEAR):
"""Compute the data histogram as used by :meth:`setHistogram`.
:param data: The data to process
:rtype: Tuple(List(float),List(float)
"""
_data = data
if _data.ndim == 3: # RGB(A) images
_logger.info('Converting current image from RGB(A) to grayscale\
in order to compute the intensity distribution')
_data = (_data[:, :, 0] * 0.299 +
_data[:, :, 1] * 0.587 +
_data[:, :, 2] * 0.114)
if len(_data) == 0:
return None, None
if scale == Axis.LOGARITHMIC:
_data = numpy.log10(_data)
xmin, xmax = min_max(_data, min_positive=False, finite=True)
if xmin is None:
return None, None
nbins = min(256, int(numpy.sqrt(_data.size)))
data_range = xmin, xmax
# bad hack: get 256 bins in the case we have a B&W
if numpy.issubdtype(_data.dtype, numpy.integer):
if nbins > xmax - xmin:
nbins = xmax - xmin
nbins = max(2, nbins)
_data = _data.ravel().astype(numpy.float32)
histogram = Histogramnd(_data, n_bins=nbins, histo_range=data_range)
bins = histogram.edges[0]
if scale == Axis.LOGARITHMIC:
bins = 10**bins
return histogram.histo, bins
def _getData(self):
if self._data is None:
return None
return self._data()
[docs] def setData(self, data):
"""Store the data as a weakref.
According to the state of the dialog, the data will be used to display
the data range or the histogram of the data using :meth:`setDataRange`
and :meth:`setHistogram`
"""
oldData = self._getData()
if oldData is data:
return
if data is None:
self._data = None
else:
self._data = weakref.ref(data, self._dataAboutToFinalize)
if self.isVisible():
self._updateDataInPlot()
else:
self._displayLater()
def _setDataInPlotMode(self, mode):
if self._dataInPlotMode == mode:
return
self._dataInPlotMode = mode
self._updateDataInPlot()
def _displayDataInPlotModeChanged(self, action):
mode = action.data()
self._setDataInPlotMode(mode)
def _updateDataInPlot(self):
data = self._getData()
if data is None:
self.setDataRange()
self.setHistogram()
return
if data.size == 0:
# One or more dimensions are equal to 0
self.setHistogram()
self.setDataRange()
return
mode = self._dataInPlotMode
if mode == _DataInPlotMode.NONE:
self.setHistogram()
self.setDataRange()
elif mode == _DataInPlotMode.RANGE:
result = self.computeDataRange(data)
self.setHistogram()
self.setDataRange(*result)
elif mode == _DataInPlotMode.HISTOGRAM:
# The histogram should be done in a worker thread
result = self.computeHistogram(data, scale=self._plot.getXAxis().getScale())
self.setHistogram(*result)
self.setDataRange()
def _invalidateHistogram(self):
"""Recompute the histogram if it is displayed"""
if self._dataInPlotMode == _DataInPlotMode.HISTOGRAM:
self._updateDataInPlot()
def _colormapAboutToFinalize(self, weakrefColormap):
"""Callback when the data weakref is about to be finalized."""
if self._colormap is weakrefColormap:
self.setColormap(None)
def _dataAboutToFinalize(self, weakrefData):
"""Callback when the data weakref is about to be finalized."""
if self._data is weakrefData:
self.setData(None)
[docs] def getHistogram(self):
"""Returns the counts and bin edges of the displayed histogram.
:return: (hist, bin_edges)
:rtype: 2-tuple of numpy arrays"""
if self._histogramData is None:
return None
else:
bins, counts = self._histogramData
return numpy.array(bins, copy=True), numpy.array(counts, copy=True)
[docs] def setHistogram(self, hist=None, bin_edges=None):
"""Set the histogram to display.
This update the data range with the bounds of the bins.
:param hist: array-like of counts or None to hide histogram
:param bin_edges: array-like of bins edges or None to hide histogram
"""
if hist is None or bin_edges is None:
self._histogramData = None
self._plot.remove(legend='Histogram', kind='histogram')
else:
hist = numpy.array(hist, copy=True)
bin_edges = numpy.array(bin_edges, copy=True)
self._histogramData = hist, bin_edges
norm_hist = hist / max(hist)
self._plot.addHistogram(norm_hist,
bin_edges,
legend="Histogram",
color='gray',
align='center',
fill=True)
self._updateMinMaxData()
[docs] def getColormap(self):
"""Return the colormap description.
:rtype: ~silx.gui.colors.Colormap
"""
if self._colormap is None:
return None
return self._colormap()
[docs] def resetColormap(self):
"""
Reset the colormap state before modification.
..note :: the colormap reference state is the state when set or the
state when validated
"""
colormap = self.getColormap()
if colormap is not None and self._colormapStoredState is not None:
if colormap != self._colormapStoredState:
self._ignoreColormapChange = True
colormap.setFromColormap(self._colormapStoredState)
self._ignoreColormapChange = False
self._applyColormap()
[docs] def setDataRange(self, minimum=None, positiveMin=None, maximum=None):
"""Set the range of data to use for the range of the histogram area.
:param float minimum: The minimum of the data
:param float positiveMin: The positive minimum of the data
:param float maximum: The maximum of the data
"""
scale = self._plot.getXAxis().getScale()
if scale == Axis.LOGARITHMIC:
dataMin, dataMax = positiveMin, maximum
else:
dataMin, dataMax = minimum, maximum
if dataMin is None or dataMax is None:
self._dataRange = None
self._plot.remove(legend='Range', kind='histogram')
else:
hist = numpy.array([1])
bin_edges = numpy.array([dataMin, dataMax])
self._plot.addHistogram(hist,
bin_edges,
legend="Range",
color='gray',
align='center',
fill=True)
self._dataRange = minimum, positiveMin, maximum
self._updateMinMaxData()
def _updateMinMaxData(self):
"""Update the min and max of the data according to the data range and
the histogram preset."""
colormap = self.getColormap()
minimum = float("+inf")
maximum = float("-inf")
if colormap is not None and colormap.getNormalization() == colormap.LOGARITHM:
# find a range in the positive part of the data
if self._dataRange is not None:
minimum = min(minimum, self._dataRange[1])
maximum = max(maximum, self._dataRange[2])
if self._histogramData is not None:
positives = list(filter(lambda x: x > 0, self._histogramData[1]))
if len(positives) > 0:
minimum = min(minimum, positives[0])
maximum = max(maximum, positives[-1])
else:
if self._dataRange is not None:
minimum = min(minimum, self._dataRange[0])
maximum = max(maximum, self._dataRange[2])
if self._histogramData is not None:
minimum = min(minimum, self._histogramData[1][0])
maximum = max(maximum, self._histogramData[1][-1])
if not numpy.isfinite(minimum):
minimum = None
if not numpy.isfinite(maximum):
maximum = None
self._minValue.setDataValue(minimum)
self._maxValue.setDataValue(maximum)
self._plotUpdate()
def accept(self):
self.storeCurrentState()
qt.QDialog.accept(self)
[docs] def storeCurrentState(self):
"""
save the current value sof the colormap if the user want to undo is
modifications
"""
colormap = self.getColormap()
if colormap is not None:
self._colormapStoredState = colormap.copy()
else:
self._colormapStoredState = None
def reject(self):
self.resetColormap()
qt.QDialog.reject(self)
[docs] def setColormap(self, colormap):
"""Set the colormap description
:param ~silx.gui.colors.Colormap colormap: the colormap to edit
"""
assert colormap is None or isinstance(colormap, Colormap)
if self._ignoreColormapChange is True:
return
oldColormap = self.getColormap()
if oldColormap is colormap:
return
if oldColormap is not None:
oldColormap.sigChanged.disconnect(self._applyColormap)
if colormap is not None:
colormap.sigChanged.connect(self._applyColormap)
colormap = weakref.ref(colormap, self._colormapAboutToFinalize)
self._colormap = colormap
self.storeCurrentState()
if self.isVisible():
self._applyColormap()
else:
self._updateResetButton()
self._displayLater()
def _updateResetButton(self):
resetButton = self._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
rStateEnabled = False
colormap = self.getColormap()
if colormap is not None and colormap.isEditable():
# can reset only in the case the colormap changed
rStateEnabled = colormap != self._colormapStoredState
resetButton.setEnabled(rStateEnabled)
def _applyColormap(self):
self._updateResetButton()
if self._ignoreColormapChange is True:
return
colormap = self.getColormap()
if colormap is None:
self._comboBoxColormap.setEnabled(False)
self._normButtonLinear.setEnabled(False)
self._normButtonLog.setEnabled(False)
self._minValue.setEnabled(False)
self._maxValue.setEnabled(False)
else:
self._ignoreColormapChange = True
self._comboBoxColormap.setCurrentLut(colormap)
self._comboBoxColormap.setEnabled(colormap.isEditable())
assert colormap.getNormalization() in Colormap.NORMALIZATIONS
self._normButtonLinear.setChecked(
colormap.getNormalization() == Colormap.LINEAR)
self._normButtonLog.setChecked(
colormap.getNormalization() == Colormap.LOGARITHM)
vmin = colormap.getVMin()
vmax = colormap.getVMax()
dataRange = colormap.getColormapRange()
self._normButtonLinear.setEnabled(colormap.isEditable())
self._normButtonLog.setEnabled(colormap.isEditable())
self._minValue.setValue(vmin or dataRange[0], isAuto=vmin is None)
self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)
self._minValue.setEnabled(colormap.isEditable())
self._maxValue.setEnabled(colormap.isEditable())
axis = self._plot.getXAxis()
scale = axis.LINEAR if colormap.getNormalization() == Colormap.LINEAR else axis.LOGARITHMIC
axis.setScale(scale)
self._ignoreColormapChange = False
self._plotUpdate()
def _updateMinMax(self):
if self._ignoreColormapChange is True:
return
vmin = self._minValue.getFiniteValue()
vmax = self._maxValue.getFiniteValue()
if vmax is not None and vmin is not None and vmax < vmin:
# If only one autoscale is checked constraints are too strong
# We have to edit a user value anyway it is not requested
# TODO: It would be better IMO to disable the auto checkbox before
# this case occur (valls)
cmin = self._minValue.isAutoChecked()
cmax = self._maxValue.isAutoChecked()
if cmin is False:
self._minValue.setFiniteValue(vmax)
if cmax is False:
self._maxValue.setFiniteValue(vmin)
vmin = self._minValue.getValue()
vmax = self._maxValue.getValue()
self._ignoreColormapChange = True
colormap = self._colormap()
if colormap is not None:
colormap.setVRange(vmin, vmax)
self._ignoreColormapChange = False
self._plotUpdate()
self._updateResetButton()
def _updateLut(self):
if self._ignoreColormapChange is True:
return
colormap = self._colormap()
if colormap is not None:
self._ignoreColormapChange = True
name = self._comboBoxColormap.getCurrentName()
if name is not None:
colormap.setName(name)
else:
lut = self._comboBoxColormap.getCurrentColors()
colormap.setColormapLUT(lut)
self._ignoreColormapChange = False
def _updateNormalization(self, button):
if self._ignoreColormapChange is True:
return
if not button.isChecked():
return
if button is self._normButtonLinear:
norm = Colormap.LINEAR
scale = Axis.LINEAR
elif button is self._normButtonLog:
norm = Colormap.LOGARITHM
scale = Axis.LOGARITHMIC
else:
assert(False)
colormap = self.getColormap()
if colormap is not None:
self._ignoreColormapChange = True
colormap.setNormalization(norm)
axis = self._plot.getXAxis()
axis.setScale(scale)
self._ignoreColormapChange = False
self._invalidateHistogram()
self._updateMinMaxData()
def _minMaxTextEdited(self, text):
"""Handle _minValue and _maxValue textEdited signal"""
self._minMaxWasEdited = True
def _minEditingFinished(self):
"""Handle _minValue editingFinished signal
Together with :meth:`_minMaxTextEdited`, this avoids to notify
colormap change when the min and max value where not edited.
"""
if self._minMaxWasEdited:
self._minMaxWasEdited = False
# Fix start value
if (self._maxValue.getValue() is not None and
self._minValue.getValue() > self._maxValue.getValue()):
self._minValue.setValue(self._maxValue.getValue())
self._updateMinMax()
def _maxEditingFinished(self):
"""Handle _maxValue editingFinished signal
Together with :meth:`_minMaxTextEdited`, this avoids to notify
colormap change when the min and max value where not edited.
"""
if self._minMaxWasEdited:
self._minMaxWasEdited = False
# Fix end value
if (self._minValue.getValue() is not None and
self._minValue.getValue() > self._maxValue.getValue()):
self._maxValue.setValue(self._minValue.getValue())
self._updateMinMax()
[docs] def keyPressEvent(self, event):
"""Override key handling.
It disables leaving the dialog when editing a text field.
"""
if event.key() == qt.Qt.Key_Enter and (self._minValue.hasFocus() or
self._maxValue.hasFocus()):
# Bypass QDialog keyPressEvent
# To avoid leaving the dialog when pressing enter on a text field
super(qt.QDialog, self).keyPressEvent(event)
else:
# Use QDialog keyPressEvent
super(ColormapDialog, self).keyPressEvent(event)