From 159ef14fb9e198bb0066ea14e6b980f065de63dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Tue, 31 Jul 2018 16:22:25 +0200 Subject: New upstream version 0.8.0+dfsg --- examples/blissPlot.py | 635 ++++++++++++++++++++++++++++++ examples/colormapDialog.py | 8 +- examples/findContours.py | 704 ++++++++++++++++++++++++++++++++++ examples/hdf5widget.py | 10 + examples/imageview.py | 1 + examples/plot3dSceneWindow.py | 5 + examples/plotGL32.py | 46 +++ examples/plotInteractiveImageROI.py | 110 ++++++ examples/plotStats.py | 123 ++++++ examples/plotTimeSeries.py | 33 ++ examples/plotUpdateCurveFromThread.py | 104 +++++ examples/plotUpdateFromThread.py | 128 ------- examples/plotUpdateImageFromThread.py | 133 +++++++ examples/plotWidget.py | 100 +++-- examples/scatterMask.py | 6 +- 15 files changed, 1972 insertions(+), 174 deletions(-) create mode 100644 examples/blissPlot.py create mode 100644 examples/findContours.py create mode 100644 examples/plotGL32.py create mode 100644 examples/plotInteractiveImageROI.py create mode 100644 examples/plotStats.py create mode 100644 examples/plotTimeSeries.py create mode 100644 examples/plotUpdateCurveFromThread.py delete mode 100644 examples/plotUpdateFromThread.py create mode 100644 examples/plotUpdateImageFromThread.py (limited to 'examples') diff --git a/examples/blissPlot.py b/examples/blissPlot.py new file mode 100644 index 0000000..71c1fd3 --- /dev/null +++ b/examples/blissPlot.py @@ -0,0 +1,635 @@ +# coding: utf-8 + + +from __future__ import division, absolute_import, print_function, unicode_literals + + +import six + +from silx.gui import qt, icons +from silx.gui.plot.actions import PlotAction, mode +from silx.gui.plot import PlotWindow, PlotWidget +from silx.gui.plot.Colors import rgba + + +class DrawModeAction(PlotAction): + """Action that control drawing mode""" + + _MODES = { # shape: (icon, text, tooltip + 'rectangle': ('shape-rectangle', 'Rectangle selection', 'Select a rectangular region'), + 'line': ('shape-diagonal', 'Line selection', 'Select a line'), + 'hline': ('shape-horizontal', 'H. line selection', 'Select a horizontal line'), + 'vline': ('shape-vertical', 'V. line selection', 'Select a vertical line'), + 'polygon': ('shape-polygon', 'Polygon selection', 'Select a polygon'), + } + + def __init__(self, plot, parent=None): + self._shape = 'polygon' + self._label = None + self._color = 'black' + self._width = None + icon, text, tooltip = self._MODES[self._shape] + + super(DrawModeAction, self).__init__( + plot, icon=icon, text=text, + tooltip=tooltip, + triggered=self._actionTriggered, + checkable=True, parent=parent) + + # Listen to mode change + self.plot.sigInteractiveModeChanged.connect(self._modeChanged) + # Init the state + self._modeChanged(None) + + def _update(self): + if self.isChecked(): + self._actionTriggered() + + def setShape(self, shape): + self._shape = shape + icon, text, tooltip = self._MODES[self._shape] + self.setIcon(icons.getQIcon(icon)) + self.setText(text) + self.setToolTip(tooltip) + self._update() + + def getShape(self): + return self._shape + + def setColor(self, color): + self._color = rgba(color) + self._update() + + def getColor(self): + return qt.QColor.fromRgbF(*self._color) + + def setLabel(self, label): + self._label = label + self._update() + + def getLabel(self): + return self._label + + def setWidth(self, width): + self._width = width + self._update() + + def getWidth(self): + return self._width + + def _modeChanged(self, source): + modeDict = self.plot.getInteractiveMode() + old = self.blockSignals(True) + self.setChecked(modeDict['mode'] == 'draw' and + modeDict['shape'] == self._shape and + modeDict['label'] == self._label) + self.blockSignals(old) + + def _actionTriggered(self, checked=False): + self.plot.setInteractiveMode('draw', + source=self, + shape=self._shape, + color=self._color, + label=self._label, + width=self._width) + + +class ShapeSelector(qt.QObject): + """Handles the selection of a single shape in a PlotWidget + + :param parent: QObject's parent + """ + + selectionChanged = qt.Signal(tuple) + """Signal emitted whenever the selection has changed. + + It provides the selection. + """ + + selectionFinished = qt.Signal(tuple) + """Signal emitted when selection is terminated. + + It provides the selection. + """ + + def __init__(self, parent=None): + assert isinstance(parent, PlotWidget) + super(ShapeSelector, self).__init__(parent) + self._isSelectionRunning = False + self._selection = () + self._itemId = "%s-%s" % (self.__class__.__name__, id(self)) + + # Add a toolbar to plot + self._toolbar = qt.QToolBar('Selection') + self._modeAction = DrawModeAction(plot=parent) + self._modeAction.setLabel(self._itemId) + self._modeAction.setColor(rgba('red')) + toolButton = qt.QToolButton() + toolButton.setDefaultAction(self._modeAction) + toolButton.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon) + self._toolbar.addWidget(toolButton) + + # Style + + def getColor(self): + """Returns the color used for the selection shape + + :rtype: QColor + """ + return self._modeAction.getColor() + + def setColor(self, color): + """Set the color used for the selection shape + + :param color: The color to use for selection shape as + either a color name, a QColor, a list of uint8 or float in [0, 1]. + """ + self._modeAction.setColor(color) + self._updateShape() + + # Control selection + + def getSelection(self): + """Returns selection control point coordinates + + Returns an empty tuple if there is no selection + + :return: Nx2 (x, y) coordinates or an empty tuple. + """ + return tuple(zip(*self._selection)) + + def _setSelection(self, x, y): + """Set the selection shape control points. + + Use :meth:`reset` to remove the selection. + + :param x: X coordinates of control points + :param y: Y coordinates of control points + """ + selection = x, y + if selection != self._selection: + self._selection = selection + self._updateShape() + self.selectionChanged.emit(self.getSelection()) + + def reset(self): + """Clear the rectangle selection""" + if self._selection: + self._selection = () + self._updateShape() + self.selectionChanged.emit(self.getSelection()) + + def start(self, shape): + """Start requiring user to select a rectangle + + :param str shape: The shape to select in: + 'rectangle', 'line', 'polygon', 'hline', 'vline' + """ + plot = self.parent() + if plot is None: + raise RuntimeError('No plot to perform selection') + + self.stop() + self.reset() + + assert shape in ('rectangle', 'line', 'polygon', 'hline', 'vline') + + self._modeAction.setShape(shape) + self._modeAction.trigger() # To set the interaction mode + + self._isSelectionRunning = True + + plot.sigPlotSignal.connect(self._handleDraw) + + self._toolbar.show() + plot.addToolBar(qt.Qt.BottomToolBarArea, self._toolbar) + + def stop(self): + """Stop shape selection""" + if not self._isSelectionRunning: + return + + plot = self.parent() + if plot is None: + return + + mode = plot.getInteractiveMode() + if mode['mode'] == 'draw' and mode['label'] == self._itemId: + plot.setInteractiveMode('zoom') # This disconnects draw handler + + plot.sigPlotSignal.disconnect(self._handleDraw) + + plot.removeToolBar(self._toolbar) + + self._isSelectionRunning = False + self.selectionFinished.emit(self.getSelection()) + + def _handleDraw(self, event): + """Handle shape drawing event""" + if (event['event'] == 'drawingFinished' and + event['parameters']['label'] == self._itemId): + self._setSelection(event['xdata'], event['ydata']) + self.stop() + + def _updateShape(self): + """Update shape on the plot""" + plot = self.parent() + if plot is not None: + if not self._selection: + plot.remove(legend=self._itemId, kind='item') + + else: + x, y = self._selection + shape = self._modeAction.getShape() + if shape == 'line': + shape = 'polylines' + + plot.addItem(x, y, + legend=self._itemId, + shape=shape, + color=rgba(self._modeAction.getColor()), + fill=False) + + + +class PointsSelector(qt.QObject): + """Handle selection of points in a PlotWidget""" + + selectionChanged = qt.Signal(tuple) + """Signal emitted whenever the selection has changed. + + It provides the selection. + """ + + selectionFinished = qt.Signal(tuple) + """Signal emitted when selection is terminated. + + It provides the selection. + """ + + + def __init__(self, parent): + assert isinstance(parent, PlotWidget) + super(PointsSelector, self).__init__(parent) + + self._isSelectionRunning = False + self._markersAndPos = [] + self._totalPoints = 0 + + def getSelection(self): + """Returns the selection""" + return tuple(pos for _, pos in self._markersAndPos) + + def eventFilter(self, obj, event): + """Event filter for plot hide and key event""" + if event.type() == qt.QEvent.Hide: + self.stop() + + elif event.type() == qt.QEvent.KeyPress: + if event.key() in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or ( + event.key() == qt.Qt.Key_Z and event.modifiers() & qt.Qt.ControlModifier): + if len(self._markersAndPos) > 0: + plot = self.parent() + if plot is not None: + legend, _ = self._markersAndPos.pop() + plot.remove(legend=legend, kind='marker') + + self._updateStatusBar() + self.selectionChanged.emit(self.getSelection()) + return True # Stop further handling of those keys + + elif event.key() == qt.Qt.Key_Return: + self.stop() + return True # Stop further handling of those keys + + return super(PointsSelector, self).eventFilter(obj, event) + + def start(self, nbPoints=1): + """Start interactive selection of points + + :param int nbPoints: Number of points to select + """ + self.stop() + self.reset() + + plot = self.parent() + if plot is None: + raise RuntimeError('No plot to perform selection') + + self._totalPoints = nbPoints + self._isSelectionRunning = True + + plot.setInteractiveMode(mode='zoom') + self._handleInteractiveModeChanged(None) + plot.sigInteractiveModeChanged.connect( + self._handleInteractiveModeChanged) + + plot.installEventFilter(self) + + self._updateStatusBar() + + def stop(self): + """Stop interactive point selection""" + if not self._isSelectionRunning: + return + + plot = self.parent() + if plot is None: + return + + plot.removeEventFilter(self) + + plot.sigInteractiveModeChanged.disconnect( + self._handleInteractiveModeChanged) + + currentMode = plot.getInteractiveMode() + if currentMode['mode'] == 'zoom': # Stop handling mouse click + plot.sigPlotSignal.disconnect(self._handleSelect) + + plot.statusBar().clearMessage() + self._isSelectionRunning = False + self.selectionFinished.emit(self.getSelection()) + + def reset(self): + """Reset selected points""" + plot = self.parent() + if plot is None: + return + + for legend, _ in self._markersAndPos: + plot.remove(legend=legend, kind='marker') + self._markersAndPos = [] + self.selectionChanged.emit(self.getSelection()) + + def _updateStatusBar(self): + """Update status bar message""" + plot = self.parent() + if plot is None: + return + + msg = 'Select %d/%d input points' % (len(self._markersAndPos), + self._totalPoints) + + currentMode = plot.getInteractiveMode() + if currentMode['mode'] != 'zoom': + msg += ' (Use zoom mode to add/remove points)' + + plot.statusBar().showMessage(msg) + + def _handleSelect(self, event): + """Handle mouse events""" + if event['event'] == 'mouseClicked' and event['button'] == 'left': + plot = self.parent() + if plot is None: + return + + x, y = event['x'], event['y'] + + # Add marker + legend = "sx.ginput %d" % len(self._markersAndPos) + plot.addMarker( + x, y, + legend=legend, + text='%d' % len(self._markersAndPos), + color='red', + draggable=False) + + self._markersAndPos.append((legend, (x, y))) + self._updateStatusBar() + if len(self._markersAndPos) >= self._totalPoints: + self.stop() + + def _handleInteractiveModeChanged(self, source): + """Handle change of interactive mode in the plot + + :param source: Objects that triggered the mode change + """ + plot = self.parent() + if plot is None: + return + + mode = plot.getInteractiveMode() + if mode['mode'] == 'zoom': # Handle click events + plot.sigPlotSignal.connect(self._handleSelect) + else: # Do not handle click event + plot.sigPlotSignal.disconnect(self._handleSelect) + self._updateStatusBar() + + +# TODO refactor to make a selection by composition rather than inheritance... +class BlissPlot(PlotWindow): + """Plot with selection methods""" + + sigSelectionDone = qt.Signal(object) + """Signal emitted when the selection is done + + It provides the list of selected points + """ + + def __init__(self, parent=None, **kwargs): + super(BlissPlot, self).__init__(parent=parent, **kwargs) + self._selectionColor = rgba('red') + self._selectionMode = None + self._markers = [] + self._pointNames = () + + # Style + + def getColor(self): + """Returns the color used for selection markers + + :rtype: QColor + """ + return qt.QColor.fromRgbF(*self._selectionColor) + + def setColor(self, color): + """Set the markers used for selection + + :param color: The color to use for selection markers as + either a color name, a QColor, a list of uint8 or float in [0, 1]. + """ + self._selectionColor = rgba(color) + self._updateMarkers() # To apply color change + + # Marker helpers + + def _setSelectedPointMarker(self, x, y, index=None): + """Add/Update a marker for a point + + :param float x: X coord in plot + :param float y: Y coord in plot + :param int index: Index of point in points names to set + :return: corresponding marker legend + :rtype: str + """ + if index is None: + index = len(self._markers) + + name = self._pointNames[index] + legend = "BlissPlotSelection-%d" % index + + self.addMarker( + x, y, + legend=legend, + text=name, + color=self._selectionColor, + draggable=self._selectionMode is not None) + return legend + + def _updateMarkers(self): + """Update all markers to sync color/draggable""" + for index, (x, y) in enumerate(self.getSelectedPoints()): + self._setSelectedPointMarker(x, y, index) + + # Selection mode control + + def startPointSelection(self, points=1): + """Request the user to select a number of points + + :param points: + The number of points the user need to select (default: 1) + or a list of point names or a single name. + :type points: Union[int, List[str], str] + :return: A future to access the result + :rtype: concurrent.futures.Future + """ + self.stopSelection() + self.resetSelection() + + if isinstance(points, six.string_types): + points = [points] + elif isinstance(points, int): + points = [str(i) for i in range(points)] + + self._pointNames = points + + self._markers = [] + self._selectionMode = 'points' + + self.setInteractiveMode(mode='zoom') + self._handleInteractiveModeChanged(None) + self.sigInteractiveModeChanged.connect( + self._handleInteractiveModeChanged) + + def stopSelection(self): + """Stop current selection. + + Calling this method emits the selection through sigSelectionDone + and does not clear the selection. + """ + if self._selectionMode is not None: + currentMode = self.getInteractiveMode() + if currentMode['mode'] == 'zoom': # Stop handling mouse click + self.sigPlotSignal.disconnect(self._handleSelect) + + self.sigInteractiveModeChanged.disconnect( + self._handleInteractiveModeChanged) + + self._selectionMode = None + self.statusBar().showMessage('Selection done') + + self._updateMarkers() # To make them not draggable + + self.sigSelectionDone.emit(self.getSelectedPoints()) + + def getSelectedPoints(self): + """Returns list of currently selected points + + :rtype: tuple + """ + return tuple(self._getItem(kind='marker', legend=legend).getPosition() + for legend in self._markers) + + def resetSelection(self): + """Clear current selection""" + for legend in self._markers: + self.remove(legend, kind='marker') + self._markers = [] + + if self._selectionMode is not None: + self._updateStatusBar() + else: + self.statusBar().clearMessage() + + def _handleInteractiveModeChanged(self, source): + """Handle change of interactive mode in the plot + + :param source: Objects that triggered the mode change + """ + mode = self.getInteractiveMode() + if mode['mode'] == 'zoom': # Handle click events + self.sigPlotSignal.connect(self._handleSelect) + else: # Do not handle click event + self.sigPlotSignal.disconnect(self._handleSelect) + self._updateStatusBar() + + def _handleSelect(self, event): + """Handle mouse events""" + if event['event'] == 'mouseClicked' and event['button'] == 'left': + if len(self._markers) == len(self._pointNames): + return + + x, y = event['x'], event['y'] + legend = self._setSelectedPointMarker(x, y, len(self._markers)) + self._markers.append(legend) + self._updateStatusBar() + + def keyPressEvent(self, event): + """Handle keys for undo/done actions""" + if self._selectionMode is not None: + if event.key() in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or ( + event.key() == qt.Qt.Key_Z and + event.modifiers() & qt.Qt.ControlModifier): + if len(self._markers) > 0: + legend = self._markers.pop() + self.remove(legend, kind='marker') + + self._updateStatusBar() + return # Stop processing the event + + elif event.key() == qt.Qt.Key_Return: + self.stopSelection() + return # Stop processing the event + + return super(BlissPlot, self).keyPressEvent(event) + + def _updateStatusBar(self): + """Update status bar message""" + if len(self._markers) < len(self._pointNames): + name = self._pointNames[len(self._markers)] + msg = 'Select point: %s (%d/%d)' % ( + name, len(self._markers), len(self._pointNames)) + else: + msg = 'Selection ready. Press Enter to validate' + + currentMode = self.getInteractiveMode() + if currentMode['mode'] != 'zoom': + msg += ' (Use zoom mode to add/edit points)' + + self.statusBar().showMessage(msg) + + +if __name__ == '__main__': + app = qt.QApplication([]) + + #plot = BlissPlot() + #plot.startPointSelection(('first', 'second', 'third')) + + def dumpChanged(selection): + print('selectionChanged', selection) + + def dumpFinished(selection): + print('selectionFinished', selection) + + plot = PlotWindow() + selector = ShapeSelector(plot) + #selector.start(shape='rectangle') + selector.selectionChanged.connect(dumpChanged) + selector.selectionFinished.connect(dumpFinished) + plot.show() + + points = PointsSelector(plot) + points.start(3) + points.selectionChanged.connect(dumpChanged) + points.selectionFinished.connect(dumpFinished) + #app.exec_() diff --git a/examples/colormapDialog.py b/examples/colormapDialog.py index c663591..9ef6508 100644 --- a/examples/colormapDialog.py +++ b/examples/colormapDialog.py @@ -22,12 +22,12 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This script shows the features of a :mod:`~silx.gui.plot.ColormapDialog`. +"""This script shows the features of a :mod:`~silx.gui.dialog.ColormapDialog`. """ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "19/01/2018" +__date__ = "14/06/2018" import functools import numpy @@ -38,8 +38,8 @@ except ImportError: scipy = None from silx.gui import qt -from silx.gui.plot.ColormapDialog import ColormapDialog -from silx.gui.plot.Colormap import Colormap +from silx.gui.dialog.ColormapDialog import ColormapDialog +from silx.gui.colors import Colormap from silx.gui.plot.ColorBar import ColorBarWidget diff --git a/examples/findContours.py b/examples/findContours.py new file mode 100644 index 0000000..a5bb663 --- /dev/null +++ b/examples/findContours.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-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. +# +# ###########################################################################*/ +"""Find contours examples + +.. note:: This module has an optional dependancy with sci-kit image library. + You might need to install it if you don't already have it. +""" + +import logging +import sys +import numpy +import time + +logging.basicConfig() +_logger = logging.getLogger("find_contours") + +from silx.gui import qt +import silx.gui.plot +from silx.gui.colors import Colormap +import silx.image.bilinear + + +try: + import skimage +except ImportError: + _logger.debug("Error while importing skimage", exc_info=True) + skimage = None + +if skimage is not None: + try: + from silx.image.marchingsquares._skimage import MarchingSquaresSciKitImage + except ImportError: + _logger.debug("Error while importing MarchingSquaresSciKitImage", exc_info=True) + MarchingSquaresSciKitImage = None +else: + MarchingSquaresSciKitImage = None + + +def rescale_image(image, shape): + y, x = numpy.ogrid[:shape[0], :shape[1]] + y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1) + b = silx.image.bilinear.BilinearImage(image) + # TODO: could be optimized using strides + x2d = numpy.zeros_like(y) + x + y2d = numpy.zeros_like(x) + y + result = b.map_coordinates((y2d, x2d)) + return result + + +def create_spiral(size, nb=1, freq=100): + half = size // 2 + y, x = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + y, x = y * coef, x * coef + 0.0001 + distance = numpy.sqrt(x * x + y * y) + angle = numpy.arctan(y / x) + data = numpy.sin(angle * nb * 2 + distance * freq * half / 100, dtype=numpy.float32) + return data + + +def create_magnetic_field(size, x1=0.0, y1=0.0, x2=0.0, y2=0.0): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + yy1, xx1 = (yy + half * y1) * coef, (xx + half * x1) * coef + distance1 = numpy.sqrt(xx1 * xx1 + yy1 * yy1) + yy2, xx2 = (yy + half * y2) * coef, (xx + half * x2) * coef + distance2 = numpy.sqrt(xx2 * xx2 + yy2 * yy2) + return (numpy.arctan2(distance1, distance2) - numpy.pi * 0.25) * 1000 + + +def create_gravity_field(size, objects): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + + def distance(x, y): + yy1, xx1 = (yy + half * y) * coef, (xx + half * x) * coef + return numpy.sqrt(xx1 ** 2 + yy1 ** 2) + result = numpy.zeros((size, size), dtype=numpy.float32) + for x, y, m in objects: + result += m / distance(x, y) + return numpy.log(result) * 1000 + + +def create_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + yy, xx = (yy - (dy * half)) * coef, (xx - (dx * half)) * coef + 0.0001 + distance = numpy.sqrt(xx * xx * sx + yy * yy * sy) + return distance + + +def create_composite_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0): + hole = (size - 4) // 4 + gap = 10 + base = create_gradient(size + hole + gap * 4, dx, dy, sx, sy) + result = numpy.zeros((size, size)) + width = (size - 2) // 2 + half_hole = hole // 2 + + def copy_module(x1, y1, x2, y2, width, height): + result[y1:y1 + height, x1:x1 + width] = base[y2:y2 + height, x2:x2 + width] + + y1 = 0 + y2 = 0 + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, 0, y2, width, hole) + copy_module(width + 1, y1, width + hole, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + return result + + +def create_value_noise(shape, octaves=8, weights=None, first_array=None): + data = numpy.zeros(shape, dtype=numpy.float32) + t = 2 + for i in range(octaves): + if t > shape[0] and t > shape[1]: + break + if i == 0 and first_array is not None: + d = first_array + else: + if weights is None: + w = (256 >> i) - 1 + else: + w = weights[i] + d = numpy.random.randint(w, size=(t, t)).astype(dtype=numpy.uint8) + d = rescale_image(d, shape) + data = data + d + t = t << 1 + return data + + +def create_island(shape, summit, under_water): + # Force a centric shape + first_array = numpy.zeros((4, 4), dtype=numpy.uint8) + first_array[1:3, 1:3] = 255 + weights = [255] + [(256 >> (i)) - 1 for i in range(8)] + data = create_value_noise(shape, octaves=7, first_array=first_array, weights=weights) + # more slops + data *= data + # normalize the height + data -= data.min() + data = data * ((summit + under_water) / data.max()) - under_water + return data + + +def createRgbaMaskImage(mask, color): + """Generate an RGBA image where a custom color is apply to the location of + the mask. Non masked part of the image is transparent.""" + image = numpy.zeros((mask.shape[0], mask.shape[1], 4), dtype=numpy.uint8) + color = numpy.array(color) + image[mask == True] = color + return image + + +class FindContours(qt.QMainWindow): + """ + This window show an example of use of a Hdf5TreeView. + + The tree is initialized with a list of filenames. A panel allow to play + with internal property configuration of the widget, and a text screen + allow to display events. + """ + + def __init__(self, filenames=None): + """ + :param files_: List of HDF5 or Spec files (pathes or + :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` + instances) + """ + qt.QMainWindow.__init__(self) + self.setWindowTitle("Silx HDF5 widget example") + + self.__plot = silx.gui.plot.Plot2D(parent=self) + dummy = numpy.array([[0]]) + self.__plot.addImage(dummy, legend="image", z=-10, replace=False) + dummy = numpy.array([[[0, 0, 0, 0]]]) + self.__plot.addImage(dummy, legend="iso-pixels", z=0, replace=False) + + self.__algo = None + self.__polygons = [] + self.__customPolygons = [] + self.__image = None + self.__mask = None + self.__customValue = None + + mainPanel = qt.QWidget(self) + layout = qt.QHBoxLayout() + layout.addWidget(self.__createConfigurationPanel(self)) + layout.addWidget(self.__plot) + mainPanel.setLayout(layout) + + self.setCentralWidget(mainPanel) + + def __createConfigurationPanel(self, parent): + panel = qt.QWidget(parent=parent) + layout = qt.QVBoxLayout() + panel.setLayout(layout) + + self.__kind = qt.QButtonGroup(self) + self.__kind.setExclusive(True) + + group = qt.QGroupBox(self) + group.setTitle("Image") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + button = qt.QRadioButton(parent=panel) + button.setText("Island") + button.clicked.connect(self.generateIsland) + button.setCheckable(True) + button.setChecked(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Gravity") + button.clicked.connect(self.generateGravityField) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Magnetic") + button.clicked.connect(self.generateMagneticField) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Spiral") + button.clicked.connect(self.generateSpiral) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Gradient") + button.clicked.connect(self.generateGradient) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Composite gradient") + button.clicked.connect(self.generateCompositeGradient) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QPushButton(parent=panel) + button.setText("Generate a new image") + button.clicked.connect(self.generate) + groupLayout.addWidget(button) + + # Contours + + group = qt.QGroupBox(self) + group.setTitle("Contours") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + button = qt.QCheckBox(parent=panel) + button.setText("Use the plot's mask") + button.setCheckable(True) + button.setChecked(True) + button.clicked.connect(self.updateContours) + groupLayout.addWidget(button) + self.__useMaskButton = button + + button = qt.QPushButton(parent=panel) + button.setText("Update contours") + button.clicked.connect(self.updateContours) + groupLayout.addWidget(button) + + # Implementations + + group = qt.QGroupBox(self) + group.setTitle("Implementation") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + self.__impl = qt.QButtonGroup(self) + self.__impl.setExclusive(True) + + button = qt.QRadioButton(parent=panel) + button.setText("silx") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + button.setChecked(True) + groupLayout.addWidget(button) + self.__implMerge = button + self.__impl.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("silx with cache") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__implMergeCache = button + self.__impl.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("skimage") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__implSkimage = button + self.__impl.addButton(button) + if MarchingSquaresSciKitImage is None: + button.setEnabled(False) + button.setToolTip("skimage is not installed or not compatible") + + # Processing + + group = qt.QGroupBox(self) + group.setTitle("Processing") + layout.addWidget(group) + group.setLayout(self.__createInfoLayout(group)) + + # Processing + + group = qt.QGroupBox(self) + group.setTitle("Custom level") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + label = qt.QLabel(parent=panel) + self.__value = qt.QSlider(panel) + self.__value.setOrientation(qt.Qt.Horizontal) + self.__value.sliderMoved.connect(self.__updateCustomContours) + self.__value.valueChanged.connect(self.__updateCustomContours) + groupLayout.addWidget(self.__value) + + return panel + + def __createInfoLayout(self, parent): + layout = qt.QGridLayout() + + header = qt.QLabel(parent=parent) + header.setText("Time: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 0, 0) + layout.addWidget(label, 0, 1) + self.__timeLabel = label + + header = qt.QLabel(parent=parent) + header.setText("Nb polygons: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 2, 0) + layout.addWidget(label, 2, 1) + self.__polygonsLabel = label + + header = qt.QLabel(parent=parent) + header.setText("Nb points: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 1, 0) + layout.addWidget(label, 1, 1) + self.__pointsLabel = label + + return layout + + def __cleanCustomContour(self): + for name in self.__customPolygons: + self.__plot.removeCurve(name) + self.__customPolygons = [] + dummy = numpy.array([[[0, 0, 0, 0]]]) + item = self.__plot.getImage(legend="iso-pixels") + item.setData([[[0, 0, 0, 0]]]) + + def __cleanPolygons(self): + for name in self.__polygons: + self.__plot.removeCurve(name) + + def clean(self): + self.__cleanCustomContour() + self.__cleanPolygons() + self.__polygons = [] + self.__image = None + self.__mask = None + + def updateContours(self): + self.__redrawContours() + self.updateCustomContours() + + def __updateCustomContours(self, value): + self.__customValue = value + self.updateCustomContours() + + def updateCustomContours(self): + if self.__algo is None: + return + value = self.__customValue + self.__cleanCustomContour() + if value is None: + return + + # iso pixels + iso_pixels = self.__algo.find_pixels(value) + if len(iso_pixels) != 0: + mask = numpy.zeros(self.__image.shape, dtype=numpy.int8) + indexes = iso_pixels[:, 0] * self.__image.shape[1] + iso_pixels[:, 1] + mask = mask.ravel() + mask[indexes] = 1 + mask.shape = self.__image.shape + mask = createRgbaMaskImage(mask, color=numpy.array([255, 0, 0, 128])) + item = self.__plot.getImage(legend="iso-pixels") + item.setData(mask) + + # iso contours + polygons = self.__algo.find_contours(value) + for ipolygon, polygon in enumerate(polygons): + if len(polygon) == 0: + continue + isClosed = numpy.allclose(polygon[0], polygon[-1]) + x = polygon[:, 1] + 0.5 + y = polygon[:, 0] + 0.5 + legend = "custom-polygon-%d" % ipolygon + self.__customPolygons.append(legend) + self.__plot.addCurve(x=x, y=y, linestyle="--", color="red", linewidth=2.0, legend=legend, resetzoom=False) + + def __updateAlgo(self, image, mask=None): + if mask is None: + if self.__useMaskButton.isChecked(): + mask = self.__plot.getMaskToolsDockWidget().getSelectionMask() + + self.__image = image + self.__mask = mask + + implButton = self.__impl.checkedButton() + if implButton == self.__implMerge: + from silx.image.marchingsquares import MarchingSquaresMergeImpl + self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask) + elif implButton == self.__implMergeCache: + from silx.image.marchingsquares import MarchingSquaresMergeImpl + self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask, use_minmax_cache=True) + elif implButton == self.__implSkimage and MarchingSquaresSciKitImage is not None: + self.__algo = MarchingSquaresSciKitImage(self.__image, self.__mask) + else: + _logger.error("No algorithm available") + self.__algo = None + + def setData(self, image, mask=None, value=0.0): + self.clean() + + self.__updateAlgo(image, mask=None) + + # image + item = self.__plot.getImage(legend="image") + item.setData(image) + item.setColormap(self.__colormap) + + self.__plot.resetZoom() + + def __redrawContours(self): + self.__updateAlgo(self.__image) + if self.__algo is None: + return + self.__cleanPolygons() + self.__drawContours(self.__values, self.__lineStyleCallback) + + def __drawContours(self, values, lineStyleCallback=None): + if self.__algo is None: + return + + self.__values = values + self.__lineStyleCallback = lineStyleCallback + if self.__values is None: + return + + nbTime = 0 + nbPolygons = 0 + nbPoints = 0 + + # iso contours + ipolygon = 0 + for ivalue, value in enumerate(values): + startTime = time.time() + polygons = self.__algo.find_contours(value) + nbTime += (time.time() - startTime) + nbPolygons += len(polygons) + for polygon in polygons: + if len(polygon) == 0: + continue + nbPoints += len(polygon) + isClosed = numpy.allclose(polygon[0], polygon[-1]) + x = polygon[:, 1] + 0.5 + y = polygon[:, 0] + 0.5 + legend = "polygon-%d" % ipolygon + if lineStyleCallback is not None: + extraStyle = lineStyleCallback(value, ivalue, ipolygon) + else: + extraStyle = {"linestyle": "-", "linewidth": 1.0, "color": "black"} + self.__polygons.append(legend) + self.__plot.addCurve(x=x, y=y, legend=legend, resetzoom=False, **extraStyle) + ipolygon += 1 + + self.__timeLabel.setText("%0.3fs" % nbTime) + self.__polygonsLabel.setText("%d" % nbPolygons) + self.__pointsLabel.setText("%d" % nbPoints) + + def __defineDefaultValues(self, value=None): + # Do not use min and max to avoid to create iso contours on small + # and many artefacts + if value is None: + value = self.__image.mean() + self.__customValue = value + div = 12 + delta = (self.__image.max() - self.__image.min()) / div + self.__value.setValue(value) + self.__value.setRange(self.__image.min() + delta, + self.__image.min() + delta * (div - 1)) + self.updateCustomContours() + + def generate(self): + self.__kind.checkedButton().click() + + def generateSpiral(self): + shape = 512 + nb_spiral = numpy.random.randint(1, 8) + freq = numpy.random.randint(2, 50) + image = create_spiral(shape, nb_spiral, freq) + image *= 1000.0 + self.__colormap = Colormap("cool") + self.setData(image=image, mask=None) + self.__defineDefaultValues() + + def generateIsland(self): + shape = (512, 512) + image = create_island(shape, summit=4808.72, under_water=1500) + self.__colormap = Colormap("terrain") + self.setData(image=image, mask=None) + + values = range(-800, 5000, 200) + + def styleCallback(value, ivalue, ipolygon): + if value == 0: + style = {"linestyle": "-", "linewidth": 1.0, "color": "black"} + elif value % 1000 == 0: + style = {"linestyle": "--", "linewidth": 0.5, "color": "black"} + else: + style = {"linestyle": "--", "linewidth": 0.1, "color": "black"} + return style + + self.__drawContours(values, styleCallback) + + self.__value.setValue(0) + self.__value.setRange(0, 5000) + self.__updateCustomContours(0) + + def generateMagneticField(self): + shape = 512 + x1 = numpy.random.random() * 2 - 1 + y1 = numpy.random.random() * 2 - 1 + x2 = numpy.random.random() * 2 - 1 + y2 = numpy.random.random() * 2 - 1 + image = create_magnetic_field(shape, x1, y1, x2, y2) + self.__colormap = Colormap("coolwarm") + self.setData(image=image, mask=None) + + maximum = abs(image.max()) + m = abs(image.min()) + if m > maximum: + maximum = m + maximum = int(maximum) + values = range(-maximum, maximum, maximum // 20) + + def styleCallback(value, ivalue, ipolygon): + if (ivalue % 2) == 0: + style = {"linestyle": "-", "linewidth": 0.5, "color": "black"} + else: + style = {"linestyle": "-", "linewidth": 0.5, "color": "white"} + return style + + self.__drawContours(values, styleCallback) + self.__defineDefaultValues(value=0) + + def generateGravityField(self): + shape = 512 + nb = numpy.random.randint(2, 10) + objects = [] + for _ in range(nb): + x = numpy.random.random() * 2 - 1 + y = numpy.random.random() * 2 - 1 + m = numpy.random.random() * 10 + 1.0 + objects.append((x, y, m)) + image = create_gravity_field(shape, objects) + self.__colormap = Colormap("inferno") + self.setData(image=image, mask=None) + + delta = (image.max() - image.min()) / 30.0 + values = numpy.arange(image.min(), image.max(), delta) + + def styleCallback(value, ivalue, ipolygon): + return {"linestyle": "-", "linewidth": 0.1, "color": "white"} + + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + def generateGradient(self): + shape = 512 + dx = numpy.random.random() * 2 - 1 + dy = numpy.random.random() * 2 - 1 + sx = numpy.random.randint(10, 5000) / 10.0 + sy = numpy.random.randint(10, 5000) / 10.0 + image = create_gradient(shape, dx=dx, dy=dy, sx=sx, sy=sy) + image *= 1000.0 + + def styleCallback(value, ivalue, ipolygon): + colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"] + color = colors[ivalue % len(colors)] + style = {"linestyle": "-", "linewidth": 2.0, "color": color} + return style + delta = (image.max() - image.min()) / 9.0 + values = numpy.arange(image.min(), image.max(), delta) + values = values[1:8] + + self.__colormap = Colormap("Greys") + self.setData(image=image, mask=None) + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + def generateCompositeGradient(self): + shape = 512 + hole = 1 / 4.0 + dx = numpy.random.random() * hole - hole / 2.0 + dy = numpy.random.random() * hole - hole * 2 + sx = numpy.random.random() * 10.0 + 1 + sy = numpy.random.random() * 10.0 + 1 + image = create_composite_gradient(shape, dx, dy, sx, sy) + image *= 1000.0 + + def styleCallback(value, ivalue, ipolygon): + colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"] + color = colors[ivalue % len(colors)] + style = {"linestyle": "-", "linewidth": 2.0, "color": color} + return style + delta = (image.max() - image.min()) / 9.0 + values = numpy.arange(image.min(), image.max(), delta) + values = values[1:8] + + self.__colormap = Colormap("Greys") + self.setData(image=image, mask=None) + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + +def main(): + app = qt.QApplication([]) + sys.excepthook = qt.exceptionHandler + window = FindContours() + window.generateIsland() + window.show() + result = app.exec_() + # remove ending warnings relative to QTimer + app.deleteLater() + return result + + +if __name__ == "__main__": + result = main() + sys.exit(result) diff --git a/examples/hdf5widget.py b/examples/hdf5widget.py index 7a8e7ae..bf92d4e 100755 --- a/examples/hdf5widget.py +++ b/examples/hdf5widget.py @@ -421,6 +421,16 @@ def get_hdf5_with_nxdata(): gd1.create_dataset("hypercube", data=numpy.arange(2*3*4*5*6).reshape((2, 3, 4, 5, 6))) + gd2 = g.create_group("3D_nonlinear_scaling") + gd2.attrs["NX_class"] = u"NXdata" + gd2.attrs["signal"] = u"cube" + gd2.attrs["axes"] = str_attrs(["img_idx", "rows_coordinates", "cols_coordinates"]) + gd2.create_dataset("cube", data=numpy.arange(4*5*6).reshape((4, 5, 6))) + gd2.create_dataset("img_idx", data=numpy.array([2., -0.1, 8, 3.14])) + gd2.create_dataset("rows_coordinates", data=0.1*numpy.arange(5)) + gd2.create_dataset("cols_coordinates", data=[0.1, 0.6, 0.7, 8., 8.1, 8.2]) + + # invalid NXdata g = h5.create_group("invalid") g0 = g.create_group("invalid NXdata") diff --git a/examples/imageview.py b/examples/imageview.py index c4f80bd..37d1857 100755 --- a/examples/imageview.py +++ b/examples/imageview.py @@ -49,6 +49,7 @@ from silx.gui.plot.ImageView import ImageViewMainWindow from silx.gui import qt import numpy +logging.basicConfig() logger = logging.getLogger(__name__) diff --git a/examples/plot3dSceneWindow.py b/examples/plot3dSceneWindow.py index efffcd3..3b78109 100644 --- a/examples/plot3dSceneWindow.py +++ b/examples/plot3dSceneWindow.py @@ -45,6 +45,8 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/11/2017" + +import sys import numpy from silx.gui import qt @@ -193,5 +195,8 @@ sceneWidget.addItem(group) # Add the group as an item of the scene # Show the SceneWidget widget window.show() +# Display exception in a pop-up message box +sys.excepthook = qt.exceptionHandler + # Run Qt event loop qapp.exec_() diff --git a/examples/plotGL32.py b/examples/plotGL32.py new file mode 100644 index 0000000..474fbd2 --- /dev/null +++ b/examples/plotGL32.py @@ -0,0 +1,46 @@ +import datetime as dt +import numpy as np + +from PyQt5.QtWidgets import QApplication +from silx.gui.plot import Plot1D + +if 1: + BACKEND = 'gl' # this gives incorrect results +else: + BACKEND = 'mpl' # this works + +def makePlot1D(): + + # Make large value with a relatively small range by creating POSIX time stamps. + base = dt.datetime.today() + dates = [base - dt.timedelta(seconds=x) for x in range(0, 2500, 20)] + + x = np.array([d.timestamp() for d in dates], dtype=np.float64) + np.random.seed(seed=1) + y = np.random.random(x.shape) * 12 - 3 + + print('x range', np.nanmin(x), np.nanmax(x)) + print('y range', np.nanmin(y), np.nanmax(y)) + + plot1D = Plot1D(backend=BACKEND) + xAxis = plot1D.getXAxis() + + curve = plot1D.addCurve(x=x, y=y, legend='curve', symbol='o', fill=True) + + plot1D.addMarker(x=x[0], y=y[0], legend='marker', text='the marker', draggable=True) + plot1D.addYMarker(y[0], legend='hmarker', text='the H marker', draggable=True) + plot1D.addXMarker(x[0], legend='vmarker', text='the V marker', draggable=True) + + plot1D.show() + + return plot1D + + +def main(): + global app, plot + app = QApplication([]) + plot = makePlot1D() + app.exec_() + +if __name__ == "__main__": + main() diff --git a/examples/plotInteractiveImageROI.py b/examples/plotInteractiveImageROI.py new file mode 100644 index 0000000..6c5bc8d --- /dev/null +++ b/examples/plotInteractiveImageROI.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +""" +This script illustrates image ROI selection in a :class:`~silx.gui.plot.PlotWidget` + +It uses :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` and +:class:`~silx.gui.plot.tools.roi.RegionOfInterestTableWidget` to handle the +interactive ROI selection and to display the list of ROIs. +""" + +import sys +import numpy + +from silx.gui import qt +from silx.gui.plot import Plot2D +from silx.gui.plot.tools.roi import RegionOfInterestManager +from silx.gui.plot.tools.roi import RegionOfInterestTableWidget +from silx.gui.plot.items.roi import RectangleROI + + +def dummy_image(): + """Create a dummy image""" + x = numpy.linspace(-1.5, 1.5, 1024) + xv, yv = numpy.meshgrid(x, x) + signal = numpy.exp(- (xv ** 2 / 0.15 ** 2 + + yv ** 2 / 0.25 ** 2)) + # add noise + signal += 0.3 * numpy.random.random(size=signal.shape) + return signal + + +app = qt.QApplication([]) # Start QApplication + +# Create the plot widget and add an image +plot = Plot2D() +plot.getDefaultColormap().setName('viridis') +plot.addImage(dummy_image()) + +# Create the object controlling the ROIs and set it up +roiManager = RegionOfInterestManager(plot) +roiManager.setColor('pink') # Set the color of ROI + + +# Set the name of each created region of interest +def updateAddedRegionOfInterest(roi): + """Called for each added region of interest: set the name""" + if roi.getLabel() == '': + roi.setLabel('ROI %d' % len(roiManager.getRois())) + + +roiManager.sigRoiAdded.connect(updateAddedRegionOfInterest) + +# Add a rectangular region of interest +roi = RectangleROI() +roi.setGeometry(origin=(50, 50), size=(200, 200)) +roi.setLabel('Initial ROI') +roiManager.addRoi(roi) + +# Create the table widget displaying +roiTable = RegionOfInterestTableWidget() +roiTable.setRegionOfInterestManager(roiManager) + +# Create a toolbar containing buttons for all ROI 'drawing' modes +roiToolbar = qt.QToolBar() # The layout to store the buttons +roiToolbar.setIconSize(qt.QSize(16, 16)) + +for roiClass in roiManager.getSupportedRoiClasses(): + # Create a tool button and associate it with the QAction of each mode + action = roiManager.getInteractionModeAction(roiClass) + roiToolbar.addAction(action) + +# Add the region of interest table and the buttons to a dock widget +widget = qt.QWidget() +layout = qt.QVBoxLayout() +widget.setLayout(layout) +layout.addWidget(roiToolbar) +layout.addWidget(roiTable) + +dock = qt.QDockWidget('Image ROI') +dock.setWidget(widget) +plot.addTabbedDockWidget(dock) + +# Show the widget and start the application +plot.show() +result = app.exec_() +app.deleteLater() +sys.exit(result) diff --git a/examples/plotStats.py b/examples/plotStats.py new file mode 100644 index 0000000..91a444e --- /dev/null +++ b/examples/plotStats.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This script is a simple example of how to add your own statistic to a +:class:`~silx.gui.plot.statsWidget.StatsWidget` from customs +:class:`~silx.gui.plot.stats.Stats` and display it. + +On this example we will: + + - show sum of values for each type + - compute curve integrals (only for 'curve'). + - compute center of mass for all possible items + +.. note:: for now the possible types manged by the Stats are ('curve', 'image', + 'scatter' and 'histogram') +""" + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "06/06/2018" + + +from silx.gui import qt +from silx.gui.plot import Plot1D +from silx.gui.plot.stats.stats import StatBase +import numpy + + +class Integral(StatBase): + """ + Simple calculation of the line integral + """ + def __init__(self): + StatBase.__init__(self, name='integral', compatibleKinds=('curve',)) + + def calculate(self, context): + xData, yData = context.data + return numpy.trapz(x=xData, y=yData) + + +class COM(StatBase): + """ + Compute data center of mass + """ + def __init__(self): + StatBase.__init__(self, name='COM', description="Center of mass") + + def calculate(self, context): + if context.kind in ('curve', 'histogram'): + xData, yData = context.data + deno = numpy.sum(yData).astype(numpy.float32) + if deno == 0.0: + return 0.0 + else: + return numpy.sum(xData * yData).astype(numpy.float32) / deno + elif context.kind == 'scatter': + xData, yData, values = context.data + values = values.astype(numpy.float64) + deno = numpy.sum(values) + if deno == 0.0: + return float('inf'), float('inf') + else: + comX = numpy.sum(xData * values) / deno + comY = numpy.sum(yData * values) / deno + return comX, comY + + +def main(): + app = qt.QApplication([]) + + plot = Plot1D() + + x = numpy.arange(21) + y = numpy.arange(21) + plot.addCurve(x=x, y=y, legend='myCurve') + plot.addCurve(x=x, y=(y + 5), legend='myCurve2') + + plot.setActiveCurve('myCurve') + + plot.addScatter(x=[0, 2, 5, 5, 12, 20], + y=[2, 3, 4, 20, 15, 6], + value=[5, 6, 7, 10, 90, 20], + legend='myScatter') + + stats = [ + ('sum', numpy.sum), + Integral(), + (COM(), '{0:.2f}'), + ] + + plot.getStatsWidget().setStats(stats) + plot.getStatsWidget().parent().setVisible(True) + # Update the checkedbox cause we arre playing with the visibility + plot.getStatsAction().setChecked(True) + + plot.show() + app.exec_() + + +if __name__ == '__main__': + main() diff --git a/examples/plotTimeSeries.py b/examples/plotTimeSeries.py new file mode 100644 index 0000000..28100fe --- /dev/null +++ b/examples/plotTimeSeries.py @@ -0,0 +1,33 @@ +import datetime as dt +import numpy as np + +from silx.gui import qt +from silx.gui.plot import Plot1D, TickMode + + +app = qt.QApplication([]) + +base = dt.datetime.today() +dates = [base - dt.timedelta(days=x) for x in range(0, 50)] + +x = np.array([d.timestamp() for d in dates], dtype=np.uint64) + +np.random.seed(seed=1) +y = np.random.random(x.shape) * 12 - 3 + +plot1D = Plot1D(backend='gl') +xAxis = plot1D.getXAxis() +xAxis.setTickMode(TickMode.TIME_SERIES) +xAxis.setTimeZone('UTC') + +def later(): + xAxis.setTimeZone(dt.timezone(dt.timedelta(hours=12))) + print('set time zone', xAxis.getTimeZone()) + +qt.QTimer.singleShot(4000, later) + +curve = plot1D.addCurve(x=x, y=y, legend='curve') +plot1D.show() + +app.exec_() + diff --git a/examples/plotUpdateCurveFromThread.py b/examples/plotUpdateCurveFromThread.py new file mode 100644 index 0000000..a36e5ee --- /dev/null +++ b/examples/plotUpdateCurveFromThread.py @@ -0,0 +1,104 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread. + +The problem is that plot and GUI methods should be called from the main thread. +To safely update the plot from another thread, one need to execute the update +asynchronously in the main thread. +In this example, this is achieved with +:func:`~silx.gui.utils.concurrent.submitToQtMainThread`. + +In this example a thread calls submitToQtMainThread to update the curve +of a plot. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/09/2017" + + +import threading +import time + +import numpy + +from silx.gui import qt +from silx.gui.utils import concurrent + +from silx.gui.plot import Plot1D + + +class UpdateThread(threading.Thread): + """Thread updating the curve of a :class:`~silx.gui.plot.Plot1D` + + :param plot1d: The Plot1D to update.""" + + def __init__(self, plot1d): + self.plot1d = plot1d + self.running = False + super(UpdateThread, self).__init__() + + def start(self): + """Start the update thread""" + self.running = True + super(UpdateThread, self).start() + + def run(self): + """Method implementing thread loop that updates the plot""" + while self.running: + time.sleep(1) + # Run plot update asynchronously + concurrent.submitToQtMainThread( + self.plot1d.addCurve, + numpy.arange(1000), + numpy.random.random(1000), + resetzoom=False) + + def stop(self): + """Stop the update thread""" + self.running = False + self.join(2) + + +def main(): + global app + app = qt.QApplication([]) + + # Create a Plot1D, set its limits and display it + plot1d = Plot1D() + plot1d.setLimits(0., 1000., 0., 1.) + plot1d.show() + + # Create the thread that calls submitToQtMainThread + updateThread = UpdateThread(plot1d) + updateThread.start() # Start updating the plot + + app.exec_() + + updateThread.stop() # Stop updating the plot + + +if __name__ == '__main__': + main() diff --git a/examples/plotUpdateFromThread.py b/examples/plotUpdateFromThread.py deleted file mode 100644 index 36df63f..0000000 --- a/examples/plotUpdateFromThread.py +++ /dev/null @@ -1,128 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread. - -The problem is that plot and GUI methods should be called from the main thread. -To safely update the plot from another thread, one need to make the update -asynchronously from the main thread. -In this example, this is achieved through a Qt signal. - -In this example we create a subclass of :class:`~silx.gui.plot.PlotWindow.Plot1D` -that adds a thread-safe method to add curves: -:meth:`ThreadSafePlot1D.addCurveThreadSafe`. -This thread-safe method is then called from a thread to update the plot. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "05/09/2017" - - -import threading -import time - -import numpy - -from silx.gui import qt -from silx.gui.plot import Plot1D - - -class ThreadSafePlot1D(Plot1D): - """Add a thread-safe :meth:`addCurveThreadSafe` method to Plot1D. - """ - - _sigAddCurve = qt.Signal(tuple, dict) - """Signal used to perform addCurve in the main thread. - - It takes args and kwargs as arguments. - """ - - def __init__(self, parent=None): - super(ThreadSafePlot1D, self).__init__(parent) - # Connect the signal to the method actually calling addCurve - self._sigAddCurve.connect(self.__addCurve) - - def __addCurve(self, args, kwargs): - """Private method calling addCurve from _sigAddCurve""" - self.addCurve(*args, **kwargs) - - def addCurveThreadSafe(self, *args, **kwargs): - """Thread-safe version of :meth:`silx.gui.plot.Plot.addCurve` - - This method takes the same arguments as Plot.addCurve. - - WARNING: This method does not return a value as opposed to Plot.addCurve - """ - self._sigAddCurve.emit(args, kwargs) - - -class UpdateThread(threading.Thread): - """Thread updating the curve of a :class:`ThreadSafePlot1D` - - :param plot1d: The ThreadSafePlot1D to update.""" - - def __init__(self, plot1d): - self.plot1d = plot1d - self.running = False - super(UpdateThread, self).__init__() - - def start(self): - """Start the update thread""" - self.running = True - super(UpdateThread, self).start() - - def run(self): - """Method implementing thread loop that updates the plot""" - while self.running: - time.sleep(1) - self.plot1d.addCurveThreadSafe( - numpy.arange(1000), numpy.random.random(1000), resetzoom=False) - - def stop(self): - """Stop the update thread""" - self.running = False - self.join(2) - - -def main(): - global app - app = qt.QApplication([]) - - # Create a ThreadSafePlot1D, set its limits and display it - plot1d = ThreadSafePlot1D() - plot1d.setLimits(0., 1000., 0., 1.) - plot1d.show() - - # Create the thread that calls ThreadSafePlot1D.addCurveThreadSafe - updateThread = UpdateThread(plot1d) - updateThread.start() # Start updating the plot - - app.exec_() - - updateThread.stop() # Stop updating the plot - - -if __name__ == '__main__': - main() diff --git a/examples/plotUpdateImageFromThread.py b/examples/plotUpdateImageFromThread.py new file mode 100644 index 0000000..5850263 --- /dev/null +++ b/examples/plotUpdateImageFromThread.py @@ -0,0 +1,133 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This script illustrates the update of a :class:`~silx.gui.plot.Plot2D` +widget from a thread. + +The problem is that plot and GUI methods should be called from the main thread. +To safely update the plot from another thread, one need to execute the update +asynchronously in the main thread. +In this example, this is achieved with +:func:`~silx.gui.utils.concurrent.submitToQtMainThread`. + +In this example a thread calls submitToQtMainThread to update the image +of a plot. + +Update from 1d to 2d example by Hans Fangohr, European XFEL GmbH, 26 Feb 2018. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/09/2017" + + +import threading +import time + +import numpy + +from silx.gui import qt +from silx.gui.utils import concurrent +from silx.gui.plot import Plot2D + + +Nx = 150 +Ny = 50 + + +class UpdateThread(threading.Thread): + """Thread updating the image of a :class:`~sil.gui.plot.Plot2D` + + :param plot2d: The Plot2D to update.""" + + def __init__(self, plot2d): + self.plot2d = plot2d + self.running = False + self.future_result = None + super(UpdateThread, self).__init__() + + def start(self): + """Start the update thread""" + self.running = True + super(UpdateThread, self).start() + + def run(self, pos={'x0': 0, 'y0': 0}): + """Method implementing thread loop that updates the plot + + It produces an image every 10 ms or so, and + either updates the plot or skip the image + """ + while self.running: + time.sleep(0.01) + + # Create image + # width of peak + sigma_x = 0.15 + sigma_y = 0.25 + # x and y positions + x = numpy.linspace(-1.5, 1.5, Nx) + y = numpy.linspace(-1.0, 1.0, Ny) + xv, yv = numpy.meshgrid(x, y) + signal = numpy.exp(- ((xv - pos['x0']) ** 2 / sigma_x ** 2 + + (yv - pos['y0']) ** 2 / sigma_y ** 2)) + # add noise + signal += 0.3 * numpy.random.random(size=signal.shape) + # random walk of center of peak ('drift') + pos['x0'] += 0.05 * (numpy.random.random() - 0.5) + pos['y0'] += 0.05 * (numpy.random.random() - 0.5) + + # If previous frame was not added to the plot yet, skip this one + if self.future_result is None or self.future_result.done(): + # plot the data asynchronously, and + # keep a reference to the `future` object + self.future_result = concurrent.submitToQtMainThread( + self.plot2d.addImage, signal, resetzoom=False) + + def stop(self): + """Stop the update thread""" + self.running = False + self.join(2) + + +def main(): + global app + app = qt.QApplication([]) + + # Create a Plot2D, set its limits and display it + plot2d = Plot2D() + plot2d.setLimits(0, Nx, 0, Ny) + plot2d.getDefaultColormap().setVRange(0., 1.5) + plot2d.show() + + # Create the thread that calls submitToQtMainThread + updateThread = UpdateThread(plot2d) + updateThread.start() # Start updating the plot + + app.exec_() + + updateThread.stop() # Stop updating the plot + + +if __name__ == '__main__': + main() diff --git a/examples/plotWidget.py b/examples/plotWidget.py index 24de519..c8a90a5 100644 --- a/examples/plotWidget.py +++ b/examples/plotWidget.py @@ -22,14 +22,15 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This script shows how to subclass :class:`~silx.gui.plot.PlotWidget` to tune its tools. +"""This script shows how to create a custom window around a PlotWidget. -It subclasses a :class:`~silx.gui.plot.PlotWidget` and adds toolbars and -a colorbar by using pluggable widgets: +It subclasses :class:`QMainWindow`, uses a :class:`~silx.gui.plot.PlotWidget` +as its central widget and adds toolbars and a colorbar by using pluggable widgets: +- :class:`~silx.gui.plot.PlotWidget` from :mod:`silx.gui.plot` +- QToolBar from :mod:`silx.gui.plot.tools` - QAction from :mod:`silx.gui.plot.actions` - QToolButton from :mod:`silx.gui.plot.PlotToolButtons` -- QToolBar from :mod:`silx.gui.plot.PlotTools` - :class:`silx.gui.plot.ColorBar.ColorBarWidget` """ @@ -40,43 +41,25 @@ __date__ = "05/09/2017" import numpy from silx.gui import qt -import silx.gui.plot +from silx.gui.plot import PlotWidget +from silx.gui.plot import tools # QToolbars to use with PlotWidget from silx.gui.plot import actions # QAction to use with PlotWidget from silx.gui.plot import PlotToolButtons # QToolButton to use with PlotWidget -from silx.gui.plot.PlotTools import LimitsToolBar from silx.gui.plot.ColorBar import ColorBarWidget -class MyPlotWidget(silx.gui.plot.PlotWidget): - """PlotWidget with an ad hoc toolbar and a colorbar""" - def __init__(self, parent=None): - super(MyPlotWidget, self).__init__(parent) - - # Add a tool bar to PlotWidget - toolBar = qt.QToolBar("Plot Tools", self) - self.addToolBar(toolBar) - - # Add actions from silx.gui.plot.action to the toolbar - resetZoomAction = actions.control.ResetZoomAction(self, self) - toolBar.addAction(resetZoomAction) - - # Add tool buttons from silx.gui.plot.PlotToolButtons - aspectRatioButton = PlotToolButtons.AspectToolButton( - parent=self, plot=self) - toolBar.addWidget(aspectRatioButton) - - # Add limits tool bar from silx.gui.plot.PlotTools - limitsToolBar = LimitsToolBar(parent=self, plot=self) - self.addToolBar(qt.Qt.BottomToolBarArea, limitsToolBar) +class MyPlotWindow(qt.QMainWindow): + """QMainWindow with selected tools""" - self._initColorBar() + def __init__(self, parent=None): + super(MyPlotWindow, self).__init__(parent) - def _initColorBar(self): - """Create the ColorBarWidget and add it to the PlotWidget""" + # Create a PlotWidget + self._plot = PlotWidget(parent=self) - # Add a colorbar on the right side - colorBar = ColorBarWidget(parent=self, plot=self) + # Create a colorbar linked with the PlotWidget + colorBar = ColorBarWidget(parent=self, plot=self._plot) # Make ColorBarWidget background white by changing its palette colorBar.setAutoFillBackground(True) @@ -85,28 +68,67 @@ class MyPlotWidget(silx.gui.plot.PlotWidget): palette.setColor(qt.QPalette.Window, qt.Qt.white) colorBar.setPalette(palette) - # Add the ColorBarWidget by changing PlotWidget's central widget + # Combine the ColorBarWidget and the PlotWidget as + # this QMainWindow's central widget gridLayout = qt.QGridLayout() gridLayout.setSpacing(0) gridLayout.setContentsMargins(0, 0, 0, 0) - plot = self.getWidgetHandle() # Get the widget rendering the plot - gridLayout.addWidget(plot, 0, 0) + gridLayout.addWidget(self._plot, 0, 0) gridLayout.addWidget(colorBar, 0, 1) gridLayout.setRowStretch(0, 1) gridLayout.setColumnStretch(0, 1) - centralWidget = qt.QWidget() + centralWidget = qt.QWidget(self) centralWidget.setLayout(gridLayout) self.setCentralWidget(centralWidget) + # Add ready to use toolbar with zoom and pan interaction mode buttons + interactionToolBar = tools.InteractiveModeToolBar( + parent=self, plot=self._plot) + self.addToolBar(interactionToolBar) + # Add toolbar actions to activate keyboard shortcuts + self.addActions(interactionToolBar.actions()) + + # Add a new toolbar + toolBar = qt.QToolBar("Plot Tools", self) + self.addToolBar(toolBar) + + # Add actions from silx.gui.plot.action to the toolbar + resetZoomAction = actions.control.ResetZoomAction( + parent=self, plot=self._plot) + toolBar.addAction(resetZoomAction) + + # Add tool buttons from silx.gui.plot.PlotToolButtons + aspectRatioButton = PlotToolButtons.AspectToolButton( + parent=self, plot=self._plot) + toolBar.addWidget(aspectRatioButton) + + # Add ready to use toolbar with copy, save and print buttons + outputToolBar = tools.OutputToolBar(parent=self, plot=self._plot) + self.addToolBar(outputToolBar) + # Add toolbar actions to activate keyboard shortcuts + self.addActions(outputToolBar.actions()) + + # Add limits tool bar from silx.gui.plot.PlotTools + limitsToolBar = tools.LimitsToolBar(parent=self, plot=self._plot) + self.addToolBar(qt.Qt.BottomToolBarArea, limitsToolBar) + + def getPlotWidget(self): + """Returns the PlotWidget contains in this window""" + return self._plot + def main(): global app app = qt.QApplication([]) - # Create the ad hoc plot widget and change its default colormap - plot = MyPlotWidget() + # Create the ad hoc window containing a PlotWidget and associated tools + window = MyPlotWindow() + window.setAttribute(qt.Qt.WA_DeleteOnClose) + window.show() + + # Change the default colormap + plot = window.getPlotWidget() plot.getDefaultColormap().setName('viridis') - plot.show() # Add an image to the plot x = numpy.outer( diff --git a/examples/scatterMask.py b/examples/scatterMask.py index 45b1bac..7a407ad 100644 --- a/examples/scatterMask.py +++ b/examples/scatterMask.py @@ -1,6 +1,6 @@ # coding: utf-8 # /*########################################################################## -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-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 @@ -91,7 +91,7 @@ class MaskScatterWidget(qt.QMainWindow): :param bool copy: True (default) to get a copy of the mask. If False, the returned array MUST not be modified. :return: The array of the mask with dimension of the scatter data. - If there is no scatter data, an empty array is returned. + If there is no scatter data, None is returned. :rtype: 1D numpy.ndarray of uint8 """ return self._maskToolsWidget.getSelectionMask(copy=copy) @@ -109,7 +109,7 @@ class MaskScatterWidget(qt.QMainWindow): self._plot.addImage(image, legend=self._bgImageLegend, origin=(xscale[0], yscale[0]), scale=(xscale[1], yscale[1]), - z=0, replace=False, + z=0, colormap=colormap) def setScatter(self, x, y, v=None, info=None, colormap=None): -- cgit v1.2.3