summaryrefslogtreecommitdiff
path: root/silx/gui/dialog/ColormapDialog.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/dialog/ColormapDialog.py')
-rw-r--r--silx/gui/dialog/ColormapDialog.py1125
1 files changed, 0 insertions, 1125 deletions
diff --git a/silx/gui/dialog/ColormapDialog.py b/silx/gui/dialog/ColormapDialog.py
deleted file mode 100644
index 9c956f8..0000000
--- a/silx/gui/dialog/ColormapDialog.py
+++ /dev/null
@@ -1,1125 +0,0 @@
-# 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'
-
-
-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)
-
- @staticmethod
- def computeDataRange(data):
- """Compute the data range as used by :meth:`setDataRange`.
-
- :param data: The data to process
- :rtype: List[Union[None,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:
- dataRange = dataRange.minimum, dataRange.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
-
- @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()
-
- 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)
-
- 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)
-
- 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()
-
- def getColormap(self):
- """Return the colormap description.
-
- :rtype: ~silx.gui.colors.Colormap
- """
- if self._colormap is None:
- return None
- return self._colormap()
-
- 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()
-
- 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)
-
- 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)
-
- 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()
-
- 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)