diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
commit | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch) | |
tree | 9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/plot/ImageView.py |
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/plot/ImageView.py')
-rw-r--r-- | silx/gui/plot/ImageView.py | 860 |
1 files changed, 860 insertions, 0 deletions
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py new file mode 100644 index 0000000..780215e --- /dev/null +++ b/silx/gui/plot/ImageView.py @@ -0,0 +1,860 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-2017 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. +# +# ###########################################################################*/ +"""QWidget displaying a 2D image with histograms on its sides. + +The :class:`ImageView` implements this widget, and +:class:`ImageViewMainWindow` provides a main window with additional toolbar +and status bar. + +Basic usage of :class:`ImageView` is through the following methods: + +- :meth:`ImageView.getColormap`, :meth:`ImageView.setColormap` to update the + default colormap to use and update the currently displayed image. +- :meth:`ImageView.setImage` to update the displayed image. + +The :class:`ImageView` uses :class:`PlotWindow` and also +exposes :class:`silx.gui.plot.Plot` API for further control +(plot title, axes labels, adding other images, ...). + +For an example of use, see the implementation of :class:`ImageViewMainWindow`, +and `example/imageview.py`. +""" + +from __future__ import division + + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "13/10/2016" + + +import logging +import numpy + +from .. import qt + +from . import items, PlotWindow, PlotWidget, PlotActions +from .Colors import cursorColorForColormap +from .PlotTools import LimitsToolBar +from .Profile import ProfileToolBar + + +_logger = logging.getLogger(__name__) + + +# RadarView ################################################################### + +class RadarView(qt.QGraphicsView): + """Widget presenting a synthetic view of a 2D area and + the current visible area. + + Coordinates are as in QGraphicsView: + x goes from left to right and y goes from top to bottom. + This widget preserves the aspect ratio of the areas. + + The 2D area and the visible area can be set with :meth:`setDataRect` + and :meth:`setVisibleRect`. + When the visible area has been dragged by the user, its new position + is signaled by the *visibleRectDragged* signal. + + It is possible to invert the direction of the axes by using the + :meth:`scale` method of QGraphicsView. + """ + + visibleRectDragged = qt.Signal(float, float, float, float) + """Signals that the visible rectangle has been dragged. + + It provides: left, top, width, height in data coordinates. + """ + + _DATA_PEN = qt.QPen(qt.QColor('white')) + _DATA_BRUSH = qt.QBrush(qt.QColor('light gray')) + _VISIBLE_PEN = qt.QPen(qt.QColor('red')) + _VISIBLE_PEN.setWidth(2) + _VISIBLE_PEN.setCosmetic(True) + _VISIBLE_BRUSH = qt.QBrush(qt.QColor(0, 0, 0, 0)) + _TOOLTIP = 'Radar View:\nRed contour: Visible area\nGray area: The image' + + _PIXMAP_SIZE = 256 + + class _DraggableRectItem(qt.QGraphicsRectItem): + """RectItem which signals its change through visibleRectDragged.""" + def __init__(self, *args, **kwargs): + super(RadarView._DraggableRectItem, self).__init__( + *args, **kwargs) + + self._previousCursor = None + self.setFlag(qt.QGraphicsItem.ItemIsMovable) + self.setFlag(qt.QGraphicsItem.ItemSendsGeometryChanges) + self.setAcceptHoverEvents(True) + self._ignoreChange = False + self._constraint = 0, 0, 0, 0 + + def setConstraintRect(self, left, top, width, height): + """Set the constraint rectangle for dragging. + + The coordinates are in the _DraggableRectItem coordinate system. + + This constraint only applies to modification through interaction + (i.e., this constraint is not applied to change through API). + + If the _DraggableRectItem is smaller than the constraint rectangle, + the _DraggableRectItem remains within the constraint rectangle. + If the _DraggableRectItem is wider than the constraint rectangle, + the constraint rectangle remains within the _DraggableRectItem. + """ + self._constraint = left, left + width, top, top + height + + def setPos(self, *args, **kwargs): + """Overridden to ignore changes from API in itemChange.""" + self._ignoreChange = True + super(RadarView._DraggableRectItem, self).setPos(*args, **kwargs) + self._ignoreChange = False + + def moveBy(self, *args, **kwargs): + """Overridden to ignore changes from API in itemChange.""" + self._ignoreChange = True + super(RadarView._DraggableRectItem, self).moveBy(*args, **kwargs) + self._ignoreChange = False + + def itemChange(self, change, value): + """Callback called before applying changes to the item.""" + if (change == qt.QGraphicsItem.ItemPositionChange and + not self._ignoreChange): + # Makes sure that the visible area is in the data + # or that data is in the visible area if area is too wide + x, y = value.x(), value.y() + xMin, xMax, yMin, yMax = self._constraint + + if self.rect().width() <= (xMax - xMin): + if x < xMin: + value.setX(xMin) + elif x > xMax - self.rect().width(): + value.setX(xMax - self.rect().width()) + else: + if x > xMin: + value.setX(xMin) + elif x < xMax - self.rect().width(): + value.setX(xMax - self.rect().width()) + + if self.rect().height() <= (yMax - yMin): + if y < yMin: + value.setY(yMin) + elif y > yMax - self.rect().height(): + value.setY(yMax - self.rect().height()) + else: + if y > yMin: + value.setY(yMin) + elif y < yMax - self.rect().height(): + value.setY(yMax - self.rect().height()) + + if self.pos() != value: + # Notify change through signal + views = self.scene().views() + assert len(views) == 1 + views[0].visibleRectDragged.emit( + value.x() + self.rect().left(), + value.y() + self.rect().top(), + self.rect().width(), + self.rect().height()) + + return value + + return super(RadarView._DraggableRectItem, self).itemChange( + change, value) + + def hoverEnterEvent(self, event): + """Called when the mouse enters the rectangle area""" + self._previousCursor = self.cursor() + self.setCursor(qt.Qt.OpenHandCursor) + + def hoverLeaveEvent(self, event): + """Called when the mouse leaves the rectangle area""" + if self._previousCursor is not None: + self.setCursor(self._previousCursor) + self._previousCursor = None + + def __init__(self, parent=None): + self._scene = qt.QGraphicsScene() + self._dataRect = self._scene.addRect(0, 0, 1, 1, + self._DATA_PEN, + self._DATA_BRUSH) + self._visibleRect = self._DraggableRectItem(0, 0, 1, 1) + self._visibleRect.setPen(self._VISIBLE_PEN) + self._visibleRect.setBrush(self._VISIBLE_BRUSH) + self._scene.addItem(self._visibleRect) + + super(RadarView, self).__init__(self._scene, parent) + self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff) + self.setFocusPolicy(qt.Qt.NoFocus) + self.setStyleSheet('border: 0px') + self.setToolTip(self._TOOLTIP) + + def sizeHint(self): + # """Overridden to avoid sizeHint to depend on content size.""" + return self.minimumSizeHint() + + def wheelEvent(self, event): + # """Overridden to disable vertical scrolling with wheel.""" + event.ignore() + + def resizeEvent(self, event): + # """Overridden to fit current content to new size.""" + self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) + super(RadarView, self).resizeEvent(event) + + def setDataRect(self, left, top, width, height): + """Set the bounds of the data rectangular area. + + This sets the coordinate system. + """ + self._dataRect.setRect(left, top, width, height) + self._visibleRect.setConstraintRect(left, top, width, height) + self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) + + def setVisibleRect(self, left, top, width, height): + """Set the visible rectangular area. + + The coordinates are relative to the data rect. + """ + self._visibleRect.setRect(0, 0, width, height) + self._visibleRect.setPos(left, top) + self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) + + +# ImageView ################################################################### + +class ImageView(PlotWindow): + """Display a single image with horizontal and vertical histograms. + + Use :meth:`setImage` to control the displayed image. + This class also provides the :class:`silx.gui.plot.Plot` API. + + :param parent: The parent of this widget or None. + :param backend: The backend to use for the plot (default: matplotlib). + See :class:`.Plot` for the list of supported backend. + :type backend: str or :class:`BackendBase.BackendBase` + """ + + HISTOGRAMS_COLOR = 'blue' + """Color to use for the side histograms.""" + + HISTOGRAMS_HEIGHT = 200 + """Height in pixels of the side histograms.""" + + IMAGE_MIN_SIZE = 200 + """Minimum size in pixels of the image area.""" + + # Qt signals + valueChanged = qt.Signal(float, float, float) + """Signals that the data value under the cursor has changed. + + It provides: row, column, data value. + + When the cursor is over an histogram, either row or column is Nan + and the provided data value is the histogram value + (i.e., the sum along the corresponding row/column). + Row and columns are either Nan or integer values. + """ + + def __init__(self, parent=None, backend=None): + self._imageLegend = '__ImageView__image' + str(id(self)) + self._cache = None # Store currently visible data information + self._updatingLimits = False + + super(ImageView, self).__init__(parent=parent, backend=backend, + resetzoom=True, autoScale=False, + logScale=False, grid=False, + curveStyle=False, colormap=True, + aspectRatio=True, yInverted=True, + copy=True, save=True, print_=True, + control=False, position=False, + roi=False, mask=True) + if parent is None: + self.setWindowTitle('ImageView') + + self._initWidgets(backend) + + self.profile = ProfileToolBar(plot=self) + """"Profile tools attached to this plot. + + See :class:`silx.gui.plot.PlotTools.ProfileToolBar` + """ + + self.addToolBar(self.profile) + + # Sync PlotBackend and ImageView + self._updateYAxisInverted() + + def _initWidgets(self, backend): + """Set-up layout and plots.""" + # Monkey-patch for histogram size + # alternative: create a layout that does not use widget size hints + def sizeHint(): + return qt.QSize(self.HISTOGRAMS_HEIGHT, self.HISTOGRAMS_HEIGHT) + + self._histoHPlot = PlotWidget(backend=backend) + self._histoHPlot.setInteractiveMode('zoom') + self._histoHPlot.setCallback(self._histoHPlotCB) + self._histoHPlot.getWidgetHandle().sizeHint = sizeHint + self._histoHPlot.getWidgetHandle().minimumSizeHint = sizeHint + + self.setPanWithArrowKeys(True) + + self.setInteractiveMode('zoom') # Color set in setColormap + self.sigPlotSignal.connect(self._imagePlotCB) + self.sigSetYAxisInverted.connect(self._updateYAxisInverted) + self.sigActiveImageChanged.connect(self._activeImageChangedSlot) + + self._histoVPlot = PlotWidget(backend=backend) + self._histoVPlot.setInteractiveMode('zoom') + self._histoVPlot.setCallback(self._histoVPlotCB) + self._histoVPlot.getWidgetHandle().sizeHint = sizeHint + self._histoVPlot.getWidgetHandle().minimumSizeHint = sizeHint + + self._radarView = RadarView() + self._radarView.visibleRectDragged.connect(self._radarViewCB) + + self._layout = qt.QGridLayout() + self._layout.addWidget(self.getWidgetHandle(), 0, 0) + self._layout.addWidget(self._histoVPlot.getWidgetHandle(), 0, 1) + self._layout.addWidget(self._histoHPlot.getWidgetHandle(), 1, 0) + self._layout.addWidget(self._radarView, 1, 1) + + self._layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE) + self._layout.setColumnStretch(0, 1) + self._layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT) + self._layout.setColumnStretch(1, 0) + + self._layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE) + self._layout.setRowStretch(0, 1) + self._layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT) + self._layout.setRowStretch(1, 0) + + self._layout.setSpacing(0) + self._layout.setContentsMargins(0, 0, 0, 0) + + centralWidget = qt.QWidget() + centralWidget.setLayout(self._layout) + self.setCentralWidget(centralWidget) + + def _dirtyCache(self): + self._cache = None + + def _updateHistograms(self): + """Update histograms content using current active image.""" + activeImage = self.getActiveImage() + if activeImage is not None: + wasUpdatingLimits = self._updatingLimits + self._updatingLimits = True + + data = activeImage.getData(copy=False) + origin = activeImage.getOrigin() + scale = activeImage.getScale() + height, width = data.shape + + xMin, xMax = self.getGraphXLimits() + yMin, yMax = self.getGraphYLimits() + + # Convert plot area limits to image coordinates + # and work in image coordinates (i.e., in pixels) + xMin = int((xMin - origin[0]) / scale[0]) + xMax = int((xMax - origin[0]) / scale[0]) + yMin = int((yMin - origin[1]) / scale[1]) + yMax = int((yMax - origin[1]) / scale[1]) + + if (xMin < width and xMax >= 0 and + yMin < height and yMax >= 0): + # The image is at least partly in the plot area + # Get the visible bounds in image coords (i.e., in pixels) + subsetXMin = 0 if xMin < 0 else xMin + subsetXMax = (width if xMax >= width else xMax) + 1 + subsetYMin = 0 if yMin < 0 else yMin + subsetYMax = (height if yMax >= height else yMax) + 1 + + if (self._cache is None or + subsetXMin != self._cache['dataXMin'] or + subsetXMax != self._cache['dataXMax'] or + subsetYMin != self._cache['dataYMin'] or + subsetYMax != self._cache['dataYMax']): + # The visible area of data has changed, update histograms + + # Rebuild histograms for visible area + visibleData = data[subsetYMin:subsetYMax, + subsetXMin:subsetXMax] + histoHVisibleData = numpy.sum(visibleData, axis=0) + histoVVisibleData = numpy.sum(visibleData, axis=1) + + self._cache = { + 'dataXMin': subsetXMin, + 'dataXMax': subsetXMax, + 'dataYMin': subsetYMin, + 'dataYMax': subsetYMax, + + 'histoH': histoHVisibleData, + 'histoHMin': numpy.min(histoHVisibleData), + 'histoHMax': numpy.max(histoHVisibleData), + + 'histoV': histoVVisibleData, + 'histoVMin': numpy.min(histoVVisibleData), + 'histoVMax': numpy.max(histoVVisibleData) + } + + # Convert to histogram curve and update plots + # Taking into account origin and scale + coords = numpy.arange(2 * histoHVisibleData.size) + xCoords = (coords + 1) // 2 + subsetXMin + xCoords = origin[0] + scale[0] * xCoords + xData = numpy.take(histoHVisibleData, coords // 2) + self._histoHPlot.addCurve(xCoords, xData, + xlabel='', ylabel='', + replace=False, + color=self.HISTOGRAMS_COLOR, + linestyle='-', + selectable=False) + vMin = self._cache['histoHMin'] + vMax = self._cache['histoHMax'] + vOffset = 0.1 * (vMax - vMin) + if vOffset == 0.: + vOffset = 1. + self._histoHPlot.setGraphYLimits(vMin - vOffset, + vMax + vOffset) + + coords = numpy.arange(2 * histoVVisibleData.size) + yCoords = (coords + 1) // 2 + subsetYMin + yCoords = origin[1] + scale[1] * yCoords + yData = numpy.take(histoVVisibleData, coords // 2) + self._histoVPlot.addCurve(yData, yCoords, + xlabel='', ylabel='', + replace=False, + color=self.HISTOGRAMS_COLOR, + linestyle='-', + selectable=False) + vMin = self._cache['histoVMin'] + vMax = self._cache['histoVMax'] + vOffset = 0.1 * (vMax - vMin) + if vOffset == 0.: + vOffset = 1. + self._histoVPlot.setGraphXLimits(vMin - vOffset, + vMax + vOffset) + else: + self._dirtyCache() + self._histoHPlot.remove(kind='curve') + self._histoVPlot.remove(kind='curve') + + self._updatingLimits = wasUpdatingLimits + + def _updateRadarView(self): + """Update radar view visible area. + + Takes care of y coordinate conversion. + """ + xMin, xMax = self.getGraphXLimits() + yMin, yMax = self.getGraphYLimits() + self._radarView.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin) + + # Plots event listeners + + def _imagePlotCB(self, eventDict): + """Callback for imageView plot events.""" + if eventDict['event'] == 'mouseMoved': + activeImage = self.getActiveImage() + if activeImage is not None: + data = activeImage.getData(copy=False) + height, width = data.shape + + # Get corresponding coordinate in image + origin = activeImage.getOrigin() + scale = activeImage.getScale() + if (eventDict['x'] >= origin[0] and + eventDict['y'] >= origin[1]): + x = int((eventDict['x'] - origin[0]) / scale[0]) + y = int((eventDict['y'] - origin[1]) / scale[1]) + + if x >= 0 and x < width and y >= 0 and y < height: + self.valueChanged.emit(float(x), float(y), + data[y][x]) + + elif eventDict['event'] == 'limitsChanged': + # Do not handle histograms limitsChanged while + # updating their limits from here. + self._updatingLimits = True + + # Refresh histograms + self._updateHistograms() + + # could use eventDict['xdata'], eventDict['ydata'] instead + xMin, xMax = self.getGraphXLimits() + yMin, yMax = self.getGraphYLimits() + + # Set horizontal histo limits + self._histoHPlot.setGraphXLimits(xMin, xMax) + + # Set vertical histo limits + self._histoVPlot.setGraphYLimits(yMin, yMax) + + self._updateRadarView() + + self._updatingLimits = False + + def _histoHPlotCB(self, eventDict): + """Callback for horizontal histogram plot events.""" + if eventDict['event'] == 'mouseMoved': + if self._cache is not None: + activeImage = self.getActiveImage() + if activeImage is not None: + xOrigin = activeImage.getOrigin()[0] + xScale = activeImage.getScale()[0] + + minValue = xOrigin + xScale * self._cache['dataXMin'] + + if eventDict['x'] >= minValue: + data = self._cache['histoH'] + column = int((eventDict['x'] - minValue) / xScale) + if column >= 0 and column < data.shape[0]: + self.valueChanged.emit( + float('nan'), + float(column + self._cache['dataXMin']), + data[column]) + + elif eventDict['event'] == 'limitsChanged': + if (not self._updatingLimits and + eventDict['xdata'] != self.getGraphXLimits()): + xMin, xMax = eventDict['xdata'] + self.setGraphXLimits(xMin, xMax) + + def _histoVPlotCB(self, eventDict): + """Callback for vertical histogram plot events.""" + if eventDict['event'] == 'mouseMoved': + if self._cache is not None: + activeImage = self.getActiveImage() + if activeImage is not None: + yOrigin = activeImage.getOrigin()[1] + yScale = activeImage.getScale()[1] + + minValue = yOrigin + yScale * self._cache['dataYMin'] + + if eventDict['y'] >= minValue: + data = self._cache['histoV'] + row = int((eventDict['y'] - minValue) / yScale) + if row >= 0 and row < data.shape[0]: + self.valueChanged.emit( + float(row + self._cache['dataYMin']), + float('nan'), + data[row]) + + elif eventDict['event'] == 'limitsChanged': + if (not self._updatingLimits and + eventDict['ydata'] != self.getGraphYLimits()): + yMin, yMax = eventDict['ydata'] + self.setGraphYLimits(yMin, yMax) + + def _radarViewCB(self, left, top, width, height): + """Slot for radar view visible rectangle changes.""" + if not self._updatingLimits: + # Takes care of Y axis conversion + self.setLimits(left, left + width, top, top + height) + + def _updateYAxisInverted(self, inverted=None): + """Sync image, vertical histogram and radar view axis orientation.""" + if inverted is None: + # Do not perform this when called from plot signal + inverted = self.isYAxisInverted() + + self._histoVPlot.setYAxisInverted(inverted) + + # Use scale to invert radarView + # RadarView default Y direction is from top to bottom + # As opposed to Plot. So invert RadarView when Plot is NOT inverted. + self._radarView.resetTransform() + if not inverted: + self._radarView.scale(1., -1.) + self._updateRadarView() + + self._radarView.update() + + def _activeImageChangedSlot(self, previous, legend): + """Handle Plot active image change. + + Resets side histograms cache + """ + self._dirtyCache() + self._updateHistograms() + + def getHistogram(self, axis): + """Return the histogram and corresponding row or column extent. + + The returned value when an histogram is available is a dict with keys: + + - 'data': numpy array of the histogram values. + - 'extent': (start, end) row or column index. + end index is not included in the histogram. + + :param str axis: 'x' for horizontal, 'y' for vertical + :return: The histogram and its extent as a dict or None. + :rtype: dict + """ + assert axis in ('x', 'y') + if self._cache is None: + return None + else: + if axis == 'x': + return dict( + data=numpy.array(self._cache['histoH'], copy=True), + extent=(self._cache['dataXMin'], self._cache['dataXMax'])) + else: + return dict( + data=numpy.array(self._cache['histoV'], copy=True), + extent=(self._cache['dataYMin'], self._cache['dataYMax'])) + + def radarView(self): + """Get the lower right radarView widget.""" + return self._radarView + + def setRadarView(self, radarView): + """Change the lower right radarView widget. + + :param RadarView radarView: Widget subclassing RadarView to replace + the lower right corner widget. + """ + self._radarView.visibleRectDragged.disconnect(self._radarViewCB) + self._radarView = radarView + self._radarView.visibleRectDragged.connect(self._radarViewCB) + self._layout.addWidget(self._radarView, 1, 1) + + self._updateYAxisInverted() + + # High-level API + + def getColormap(self): + """Get the default colormap description. + + :return: A description of the current colormap. + See :meth:`setColormap` for details. + :rtype: dict + """ + return self.getDefaultColormap() + + def setColormap(self, colormap=None, normalization=None, + autoscale=None, vmin=None, vmax=None, colors=None): + """Set the default colormap and update active image. + + Parameters that are not provided are taken from the current colormap. + + The colormap parameter can also be a dict with the following keys: + + - *name*: string. The colormap to use: + 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'. + - *normalization*: string. The mapping to use for the colormap: + either 'linear' or 'log'. + - *autoscale*: bool. Whether to use autoscale (True) + or range provided by keys 'vmin' and 'vmax' (False). + - *vmin*: float. The minimum value of the range to use if 'autoscale' + is False. + - *vmax*: float. The maximum value of the range to use if 'autoscale' + is False. + - *colors*: optional. Nx3 or Nx4 array of float in [0, 1] or uint8. + List of RGB or RGBA colors to use (only if name is None) + + :param colormap: Name of the colormap in + 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'. + Or the description of the colormap as a dict. + :type colormap: dict or str. + :param str normalization: Colormap mapping: 'linear' or 'log'. + :param bool autoscale: Whether to use autoscale (True) + or [vmin, vmax] range (False). + :param float vmin: The minimum value of the range to use if + 'autoscale' is False. + :param float vmax: The maximum value of the range to use if + 'autoscale' is False. + :param numpy.ndarray colors: Only used if name is None. + Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays + """ + cmapDict = self.getDefaultColormap() + + if isinstance(colormap, dict): + # Support colormap parameter as a dict + assert normalization is None + assert autoscale is None + assert vmin is None + assert vmax is None + assert colors is None + for key, value in colormap.items(): + cmapDict[key] = value + + else: + if colormap is not None: + cmapDict['name'] = colormap + if normalization is not None: + cmapDict['normalization'] = normalization + if autoscale is not None: + cmapDict['autoscale'] = autoscale + if vmin is not None: + cmapDict['vmin'] = vmin + if vmax is not None: + cmapDict['vmax'] = vmax + if colors is not None: + cmapDict['colors'] = colors + + cursorColor = cursorColorForColormap(cmapDict['name']) + self.setInteractiveMode('zoom', color=cursorColor) + + self.setDefaultColormap(cmapDict) + + # Update active image colormap + activeImage = self.getActiveImage() + if isinstance(activeImage, items.ColormapMixIn): + activeImage.setColormap(self.getColormap()) + + def setImage(self, image, origin=(0, 0), scale=(1., 1.), + copy=True, reset=True): + """Set the image to display. + + :param image: A 2D array representing the image or None to empty plot. + :type image: numpy.ndarray-like with 2 dimensions or None. + :param origin: The (x, y) position of the origin of the image. + Default: (0, 0). + The origin is the lower left corner of the image when + the Y axis is not inverted. + :type origin: Tuple of 2 floats: (origin x, origin y). + :param scale: The scale factor to apply to the image on X and Y axes. + Default: (1, 1). + It is the size of a pixel in the coordinates of the axes. + Scales must be positive numbers. + :type scale: Tuple of 2 floats: (scale x, scale y). + :param bool copy: Whether to copy image data (default) or not. + :param bool reset: Whether to reset zoom and ROI (default) or not. + """ + self._dirtyCache() + + assert len(origin) == 2 + assert len(scale) == 2 + assert scale[0] > 0 + assert scale[1] > 0 + + if image is None: + self.remove(self._imageLegend, kind='image') + return + + data = numpy.array(image, order='C', copy=copy) + assert data.size != 0 + assert len(data.shape) == 2 + height, width = data.shape + + self.addImage(data, + legend=self._imageLegend, + origin=origin, scale=scale, + colormap=self.getColormap(), + replace=False) + self.setActiveImage(self._imageLegend) + self._updateHistograms() + + self._radarView.setDataRect(origin[0], + origin[1], + width * scale[0], + height * scale[1]) + + if reset: + self.resetZoom() + + +# ImageViewMainWindow ######################################################### + +class ImageViewMainWindow(ImageView): + """:class:`ImageView` with additional toolbars + + Adds extra toolbar and a status bar to :class:`ImageView`. + """ + def __init__(self, parent=None, backend=None): + self._dataInfo = None + super(ImageViewMainWindow, self).__init__(parent, backend) + self.setWindowFlags(qt.Qt.Window) + + self.setGraphXLabel('X') + self.setGraphYLabel('Y') + self.setGraphTitle('Image') + + # Add toolbars and status bar + self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self)) + + self.statusBar() + + menu = self.menuBar().addMenu('File') + menu.addAction(self.saveAction) + menu.addAction(self.printAction) + menu.addSeparator() + action = menu.addAction('Quit') + action.triggered[bool].connect(qt.QApplication.instance().quit) + + menu = self.menuBar().addMenu('Edit') + menu.addAction(self.copyAction) + menu.addSeparator() + menu.addAction(self.resetZoomAction) + menu.addAction(self.colormapAction) + menu.addAction(PlotActions.KeepAspectRatioAction(self, self)) + menu.addAction(PlotActions.YAxisInvertedAction(self, self)) + + menu = self.menuBar().addMenu('Profile') + menu.addAction(self.profile.browseAction) + menu.addAction(self.profile.hLineAction) + menu.addAction(self.profile.vLineAction) + menu.addAction(self.profile.lineAction) + menu.addAction(self.profile.clearAction) + + # Connect to ImageView's signal + self.valueChanged.connect(self._statusBarSlot) + + def _statusBarSlot(self, row, column, value): + """Update status bar with coordinates/value from plots.""" + if numpy.isnan(row): + msg = 'Column: %d, Sum: %g' % (int(column), value) + elif numpy.isnan(column): + msg = 'Row: %d, Sum: %g' % (int(row), value) + else: + msg = 'Position: (%d, %d), Value: %g' % (int(row), int(column), + value) + if self._dataInfo is not None: + msg = self._dataInfo + ', ' + msg + + self.statusBar().showMessage(msg) + + def setImage(self, image, *args, **kwargs): + """Set the displayed image. + + See :meth:`ImageView.setImage` for details. + """ + if hasattr(image, 'dtype') and hasattr(image, 'shape'): + assert len(image.shape) == 2 + height, width = image.shape + self._dataInfo = 'Data: %dx%d (%s)' % (width, height, + str(image.dtype)) + self.statusBar().showMessage(self._dataInfo) + else: + self._dataInfo = None + + # Set the new image in ImageView widget + super(ImageViewMainWindow, self).setImage(image, *args, **kwargs) + self.setStatusBar(None) |