diff options
Diffstat (limited to 'silx/sx')
-rw-r--r-- | silx/sx/__init__.py | 38 | ||||
-rw-r--r-- | silx/sx/_plot.py | 338 | ||||
-rw-r--r-- | silx/sx/_plot3d.py | 246 |
3 files changed, 585 insertions, 37 deletions
diff --git a/silx/sx/__init__.py b/silx/sx/__init__.py index 87bfb9e..bdec6e6 100644 --- a/silx/sx/__init__.py +++ b/silx/sx/__init__.py @@ -1,7 +1,7 @@ # 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 @@ -22,31 +22,35 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""Convenient module to use main features of silx from the console. - -Usage from (I)Python console or notebook: +"""This is a convenient package to use from Python or IPython interpreter. +It loads the main features of silx and provides high-level functions. >>> from silx import sx -With IPython/jupyter, this also runs %pylab. -From the console, it sets-up Qt in order to allow using GUI widgets. +When used in an interpreter is sets-up Qt and loads some silx widgets. +When used in a `jupyter <https://jupyter.org/>`_ / +`IPython <https://ipython.org/>`_ notebook, neither Qt nor silx widgets are loaded. + +When used in `IPython <https://ipython.org/>`_, it also runs ``%pylab``, +thus importing `numpy <http://www.numpy.org/>`_ and `matplotlib <https://matplotlib.org/>`_. """ + __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "16/01/2017" -import logging +import logging as _logging import sys as _sys -_logger = logging.getLogger(__name__) +_logger = _logging.getLogger(__name__) # Init logging when used from the console if hasattr(_sys, 'ps1'): - logging.basicConfig() + _logging.basicConfig() # Probe ipython try: @@ -81,12 +85,22 @@ else: del _icons # clean-up namespace from silx.gui.plot import * # noqa - from ._plot import plot, imshow # noqa + from ._plot import plot, imshow, ginput # noqa + + try: + import OpenGL as _OpenGL + except ImportError: + _logger.warning( + 'Not loading silx.gui.plot3d features: PyOpenGL is not installed') + else: + del _OpenGL # clean-up namespace + from ._plot3d import contour3d, points3d # noqa # %pylab if _get_ipython is not None and _get_ipython() is not None: - _get_ipython().enable_pylab(gui='inline' if _IS_NOTEBOOK else 'qt') + _get_ipython().enable_pylab(gui='inline' if _IS_NOTEBOOK else 'qt', + import_all=False) # Clean-up @@ -96,7 +110,7 @@ del _IS_NOTEBOOK # Load some silx stuff in namespace -from silx import * # noqa +from silx import version # noqa from silx.io import open # noqa from silx.io import * # noqa from silx.math import Histogramnd, HistogramndLut # noqa diff --git a/silx/sx/_plot.py b/silx/sx/_plot.py index e81e57e..dfc24d9 100644 --- a/silx/sx/_plot.py +++ b/silx/sx/_plot.py @@ -1,7 +1,7 @@ # 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 @@ -27,13 +27,19 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" -__date__ = "27/06/2017" +__date__ = "26/02/2018" +import collections import logging +import time +import weakref + import numpy -from ..gui.plot import Plot1D, Plot2D +from ..utils.weakref import WeakList +from ..gui import qt +from ..gui.plot import Plot1D, Plot2D, PlotWidget from ..gui.plot.Colors import COLORDICT from ..gui.plot.Colormap import Colormap from silx.third_party import six @@ -41,19 +47,15 @@ from silx.third_party import six _logger = logging.getLogger(__name__) +_plots = WeakList() +"""List of widgets created through plot and imshow""" + def plot(*args, **kwargs): """ - Plot curves in a dedicated widget. - - This function supports a subset of matplotlib.pyplot.plot arguments. - See: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot - - It opens a silx PlotWindow with its associated tools. + Plot curves in a :class:`~silx.gui.plot.PlotWindow.Plot1D` widget. - Examples: - - First import :mod:`sx` function: + How to use: >>> from silx import sx >>> import numpy @@ -67,8 +69,7 @@ def plot(*args, **kwargs): >>> angles = numpy.linspace(0, numpy.pi, 100) >>> sin_a = numpy.sin(angles) - >>> plot_sinus = sx.plot(angles, sin_a, - ... xlabel='angle (radian)', ylabel='sin(a)') + >>> plot_sinus = sx.plot(angles, sin_a, xlabel='angle (radian)', ylabel='sin(a)') Plot many curves by giving a 2D array, provided xn, yn arrays: @@ -96,13 +97,13 @@ def plot(*args, **kwargs): - '-.' dash-dot line - ':' dotted line - Remark: The first curve will always be displayed in black no matter the - given color. This is because it is selected by default and this is shown - by using the black color. - If provided, the names arguments color, linestyle, linewidth and marker override any style provided to a curve. + This function supports a subset of `matplotlib.pyplot.plot + <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot>`_ + arguments. + :param str color: Color to use for all curves (default: None) :param str linestyle: Type of line to use for all curves (default: None) :param float linewidth: With of all the curves (default: 1) @@ -194,6 +195,7 @@ def plot(*args, **kwargs): color=color or curve_color) plt.show() + _plots.insert(0, plt) return plt @@ -202,14 +204,10 @@ def imshow(data=None, cmap=None, norm=Colormap.LINEAR, aspect=False, origin=(0., 0.), scale=(1., 1.), title='', xlabel='X', ylabel='Y'): - """Plot an image in a dedicated widget. - - This function supports a subset of matplotlib.pyplot.imshow arguments. - See: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.imshow - - It opens a silx PlotWindow with its associated tools. + """ + Plot an image in a :class:`~silx.gui.plot.PlotWindow.Plot2D` widget. - Example to plot an image: + How to use: >>> from silx import sx >>> import numpy @@ -217,6 +215,10 @@ def imshow(data=None, cmap=None, norm=Colormap.LINEAR, >>> data = numpy.random.random(1024 * 1024).reshape(1024, 1024) >>> plt = sx.imshow(data, title='Random data') + This function supports a subset of `matplotlib.pyplot.imshow + <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.imshow>`_ + arguments. + :param data: data to plot as an image :type data: numpy.ndarray-like with 2 dimensions :param str cmap: The name of the colormap to use for the plot. @@ -268,4 +270,290 @@ def imshow(data=None, cmap=None, norm=Colormap.LINEAR, plt.addImage(data, origin=origin, scale=scale) plt.show() + _plots.insert(0, plt) return plt + + +class _GInputResult(tuple): + """Object storing :func:`ginput` result + + :param position: Selected point coordinates in the plot (x, y) + :param Item item: Plot item under the selected position + :param indices: Selected indices in the data of the item. + For a curve it is a list of indices, for an image it is (row, column) + :param data: Value of data at selected indices. + For a curve it is an array of values, for an image it is a single value + """ + + def __new__(cls, position, item, indices, data): + return super(_GInputResult, cls).__new__(cls, position) + + def __init__(self, position, item, indices, data): + self._itemRef = weakref.ref(item) if item is not None else None + self._indices = numpy.array(indices, copy=True) + if isinstance(data, collections.Iterable): + self._data = numpy.array(data, copy=True) + else: + self._data = data + + def getItem(self): + """Returns the item at the selected position if any. + + :return: plot item under the selected postion. + It is None if there was no item at that position or if + it is no more in the plot. + :rtype: silx.gui.plot.items.Item""" + return None if self._itemRef is None else self._itemRef() + + def getIndices(self): + """Returns indices in data array at the select position + + :return: 1D array of indices for curve and (row, column) for images + :rtype: numpy.ndarray + """ + return numpy.array(self._indices, copy=True) + + def getData(self): + """Returns data value at the selected position. + + For curves, an array of (x, y) values close to the point is returned. + For images, either a single value or a RGB(A) array is returned. + + :return: 2D array of (x, y) data values for curves (Nx2), + a single value for data images and RGB(A) array for images. + """ + if isinstance(self._data, numpy.ndarray): + return numpy.array(self._data, copy=True) + else: + return self._data + + +class _GInputHandler(qt.QEventLoop): + """Implements :func:`ginput` + + :param PlotWidget plot: + :param int n: + :param float timeout: + """ + + def __init__(self, plot, n, timeout): + super(_GInputHandler, self).__init__() + + if not isinstance(plot, PlotWidget): + raise ValueError('plot is not a PlotWidget: %s', plot) + + self._plot = plot + self._timeout = timeout + self._markersAndResult = [] + self._totalPoints = n + self._endTime = 0. + + def eventFilter(self, obj, event): + """Event filter for plot hide event""" + if event.type() == qt.QEvent.Hide: + self.quit() + + 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._markersAndResult) > 0: + legend, _ = self._markersAndResult.pop() + self._plot.remove(legend=legend, kind='marker') + + self._updateStatusBar() + return True # Stop further handling of those keys + + elif event.key() == qt.Qt.Key_Return: + self.quit() + return True # Stop further handling of those keys + + return super(_GInputHandler, self).eventFilter(obj, event) + + def exec_(self): + """Run blocking ginput handler + + :returns: List of selected points + """ + # Bootstrap + self._plot.setInteractiveMode(mode='zoom') + self._handleInteractiveModeChanged(None) + self._plot.sigInteractiveModeChanged.connect( + self._handleInteractiveModeChanged) + + self._plot.installEventFilter(self) + + # Run + if self._timeout: + timeoutTimer = qt.QTimer() + timeoutTimer.timeout.connect(self._updateStatusBar) + timeoutTimer.start(1000) + + self._endTime = time.time() + self._timeout + self._updateStatusBar() + + returnCode = super(_GInputHandler, self).exec_() + + timeoutTimer.stop() + else: + returnCode = super(_GInputHandler, self).exec_() + + # Clean-up + self._plot.removeEventFilter(self) + + self._plot.sigInteractiveModeChanged.disconnect( + self._handleInteractiveModeChanged) + + currentMode = self._plot.getInteractiveMode() + if currentMode['mode'] == 'zoom': # Stop handling mouse click + self._plot.sigPlotSignal.disconnect(self._handleSelect) + + self._plot.statusBar().clearMessage() + + points = tuple(result for _, result in self._markersAndResult) + + for legend, _ in self._markersAndResult: + self._plot.remove(legend=legend, kind='marker') + self._markersAndResult = [] + + return points if returnCode == 0 else () + + def _updateStatusBar(self): + """Update status bar message""" + msg = 'ginput: %d/%d input points' % (len(self._markersAndResult), + self._totalPoints) + if self._timeout: + remaining = self._endTime - time.time() + if remaining < 0: + self.quit() + return + msg += ', %d seconds remaining' % max(1, int(remaining)) + + currentMode = self._plot.getInteractiveMode() + if currentMode['mode'] != 'zoom': + msg += ' (Use zoom mode to add/remove points)' + + self._plot.statusBar().showMessage(msg) + + def _handleSelect(self, event): + """Handle mouse events""" + if event['event'] == 'mouseClicked' and event['button'] == 'left': + x, y = event['x'], event['y'] + xPixel, yPixel = event['xpixel'], event['ypixel'] + + # Add marker + legend = "sx.ginput %d" % len(self._markersAndResult) + self._plot.addMarker( + x, y, + legend=legend, + text='%d' % len(self._markersAndResult), + color='red', + draggable=False) + + # Pick item at selected position + picked = self._plot._pickImageOrCurve(xPixel, yPixel) + + if picked is None: + result = _GInputResult((x, y), + item=None, + indices=numpy.array((), dtype=int), + data=None) + + elif picked[0] == 'curve': + curve = picked[1] + indices = picked[2] + xData = curve.getXData(copy=False)[indices] + yData = curve.getYData(copy=False)[indices] + result = _GInputResult((x, y), + item=curve, + indices=indices, + data=numpy.array((xData, yData)).T) + + elif picked[0] == 'image': + image = picked[1] + # Get corresponding coordinate in image + origin = image.getOrigin() + scale = image.getScale() + column = int((x - origin[0]) / float(scale[0])) + row = int((y - origin[1]) / float(scale[1])) + data = image.getData(copy=False)[row, column] + result = _GInputResult((x, y), + item=image, + indices=(row, column), + data=data) + + self._markersAndResult.append((legend, result)) + self._updateStatusBar() + if len(self._markersAndResult) == self._totalPoints: + self.quit() + + def _handleInteractiveModeChanged(self, source): + """Handle change of interactive mode in the plot + + :param source: Objects that triggered the mode change + """ + mode = self._plot.getInteractiveMode() + if mode['mode'] == 'zoom': # Handle click events + self._plot.sigPlotSignal.connect(self._handleSelect) + else: # Do not handle click event + self._plot.sigPlotSignal.disconnect(self._handleSelect) + self._updateStatusBar() + + +def ginput(n=1, timeout=30, plot=None): + """Get input points on a plot. + + If no plot is provided, it uses a plot widget created with + either :func:`silx.sx.plot` or :func:`silx.sx.imshow`. + + How to use: + + >>> from silx import sx + + >>> sx.imshow(image) # Plot the image + >>> sx.ginput(1) # Request selection on the image plot + ((0.598, 1.234)) + + How to get more information about the selected positions: + + >>> positions = sx.ginput(1) + + >>> positions[0].getData() # Returns value(s) at selected position + + >>> positions[0].getIndices() # Returns data indices at selected position + + >>> positions[0].getItem() # Returns plot item at selected position + + :param int n: Number of points the user need to select + :param float timeout: Timeout in seconds before ginput returns + event if selection is not completed + :param silx.gui.plot.PlotWidget.PlotWidget plot: An optional PlotWidget + from which to get input + :return: List of clicked points coordinates (x, y) in plot + :raise ValueError: If provided plot is not a PlotWidget + """ + if plot is None: + # Select most recent visible plot widget + for widget in _plots: + if widget.isVisible(): + plot = widget + break + else: # If no plot widgets are visible, take most recent one + try: + plot = _plots[0] + except IndexError: + pass + else: + plot.show() + + if plot is None: + _logger.warning('No plot available to perform ginput, create one') + plot = Plot1D() + plot.show() + + plot.raise_() # So window becomes the top level one + + _logger.info('Performing ginput with plot widget %s', str(plot)) + handler = _GInputHandler(plot, n, timeout) + points = handler.exec_() + + return points diff --git a/silx/sx/_plot3d.py b/silx/sx/_plot3d.py new file mode 100644 index 0000000..3e67fe0 --- /dev/null +++ b/silx/sx/_plot3d.py @@ -0,0 +1,246 @@ +# 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 module adds convenient functions to use plot3d widgets from the console. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "07/02/2018" + + +from collections import Iterable +import logging +import numpy + +from ..gui import qt +from ..gui.plot3d.SceneWindow import SceneWindow +from ..gui.plot3d.ScalarFieldView import ScalarFieldView +from ..gui.plot3d import SFViewParamTree +from ..gui.plot.Colormap import Colormap +from ..gui.plot.Colors import rgba + + +_logger = logging.getLogger(__name__) + + +def contour3d(scalars, + contours=1, + copy=True, + color=None, + colormap='viridis', + vmin=None, + vmax=None, + opacity=1.): + """ + Plot isosurfaces of a 3D scalar field in a :class:`~silx.gui.plot3d.ScalarFieldView.ScalarFieldView` widget. + + How to use: + + >>> from silx import sx + + Provided data, a 3D scalar field as a numpy array of float32: + + >>> plot3d_window = sx.contour3d(data) + + Alternatively you can provide the level of the isosurfaces: + + >>> plot3d_window = sx.contour3d(data, contours=[0.2, 0.4]) + + This function provides a subset of `mayavi.mlab.contour3d + <http://docs.enthought.com/mayavi/mayavi/auto/mlab_helper_functions.html#contour3d>`_ + arguments. + + :param scalars: The 3D scalar field to visualize + :type scalars: numpy.ndarray of float32 with 3 dimensions + :param contours: + Either the number of isosurfaces to draw (as an int) or + the isosurface level (as a float) or a list of isosurface levels + (as a list of float) + :type contours: Union[int, float, List[float]] + :param bool copy: + True (default) to make a copy of scalars. + False to avoid this copy (do not modify provided data afterwards) + :param color: + Color.s to use for isosurfaces. + Either a single color or a list of colors (one for each isosurface). + A color can be defined by its name (as a str) or + as RGB(A) as float or uint8. + :param str colormap: + If color is not provided, this colormap is used + for coloring isosurfaces. + :param vmin: Minimum value of the colormap + :type vmin: Union[float, None] + :param vmax: Maximum value of the colormap + :type vmax: Union[float, None] + :param float opacity: + Transparency of the isosurfaces as a float in [0., 1.] + :return: The widget used to visualize the data + :rtype: ~silx.gui.plot3d.ScalarFieldView.ScalarFieldView + """ + # Prepare isolevel values + if isinstance(contours, int): + # Compute contours number of isovalues + mean = numpy.mean(scalars) + std = numpy.std(scalars) + + start = mean - std * ((contours - 1) // 2) + contours = [start + std * index for index in range(contours)] + + elif isinstance(contours, float): + contours = [contours] + + assert isinstance(contours, Iterable) + + # Prepare colors + if color is not None: + if isinstance(color, str) or isinstance(color[0], (int, float)): + # Single color provided, use it for all isosurfaces + colors = [rgba(color)] * len(contours) + else: + # As many colors as contours + colors = [rgba(c) for c in color] + + # convert colors from float to uint8 + colors = (numpy.array(colors) * 255).astype(numpy.uint8) + + else: # Use colormap + colormap = Colormap(name=colormap, vmin=vmin, vmax=vmax) + colors = colormap.applyToData(contours) + + assert len(colors) == len(contours) + + # Prepare and apply opacity + assert isinstance(opacity, float) + opacity = min(max(0., opacity), 1.) # Clip opacity + colors[:, -1] = (colors[:, -1] * opacity).astype(numpy.uint8) + + # Prepare widget + scalarField = ScalarFieldView() + + scalarField.setBackgroundColor((0.9, 0.9, 0.9)) + scalarField.setForegroundColor((0.1, 0.1, 0.1)) + scalarField.setData(scalars, copy=copy) + + # Create a parameter tree for the scalar field view + treeView = SFViewParamTree.TreeView(scalarField) + treeView.setSfView(scalarField) # Attach the parameter tree to the view + + # Add the parameter tree to the main window in a dock widget + dock = qt.QDockWidget() + dock.setWindowTitle('Parameters') + dock.setWidget(treeView) + scalarField.addDockWidget(qt.Qt.RightDockWidgetArea, dock) + + for level, color in zip(contours, colors): + scalarField.addIsosurface(level, color) + + scalarField.show() + + return scalarField + + +_POINTS3D_MODE_CONVERSION = { + '2dcircle': 'o', + '2dcross': 'x', + '2ddash': '_', + '2ddiamond': 'd', + '2dsquare': 's', + 'point': ',' +} + + +def points3d(x, y, z=None, + values=0., + copy=True, + colormap='viridis', + vmin=None, + vmax=None, + mode='o'): + """ + Plot a 3D scatter plot in a :class:`~silx.gui.plot3d.SceneWindow.SceneWindow` widget. + + How to use: + + >>> from silx import sx + + Provided x, y, z, values, 4 numpy array of float32: + + >>> plot3d_window = sx.points3d(x, y, z) + + >>> plot3d_window = sx.points3d(x, y, z, values) + + This function provides a subset of `mayavi.mlab.points3d + <http://docs.enthought.com/mayavi/mayavi/auto/mlab_helper_functions.html#points3d>`_ + arguments. + + :param numpy.ndarray x: X coordinates of the points + :param numpy.ndarray y: Y coordinates of the points + :param numpy.ndarray z: Z coordinates of the points (optional) + :param numpy.ndarray values: Values at each point (optional) + :param bool copy: + True (default) to make a copy of scalars. + False to avoid this copy (do not modify provided data afterwards) + :param str colormap: + Colormap to use for coding points as colors. + :param vmin: Minimum value of the colormap + :type vmin: Union[float, None] + :param vmax: Maximum value of the colormap + :type vmax: Union[float, None] + :param str mode: The type of marker to use + + - Circle: 'o', '2dcircle' + - Diamond: 'd', '2ddiamond' + - Square: 's', '2dsquare' + - Plus: '+' + - Cross: 'x', '2dcross' + - Star: '*' + - Vertical line: '|' + - Horizontal line: '_', '2ddash' + - Point: '.' + - Pixel: ',' + :return: The widget used to visualize the data + :rtype: ~silx.gui.plot3d.SceneWindow.SceneWindow + """ + # Prepare widget + window = SceneWindow() + sceneWidget = window.getSceneWidget() + sceneWidget.setBackgroundColor((0.9, 0.9, 0.9)) + sceneWidget.setForegroundColor((0.5, 0.5, 0.5)) + sceneWidget.setTextColor((0.1, 0.1, 0.1)) + + mode = _POINTS3D_MODE_CONVERSION.get(mode, mode) + + if z is None: # 2D scatter plot + scatter = sceneWidget.add2DScatter(x, y, values, copy=copy) + else: # 3D scatter plot + scatter = sceneWidget.add3DScatter(x, y, z, values, copy=copy) + + colormap = Colormap(name=colormap, vmin=vmin, vmax=vmax) + scatter.setColormap(colormap) + scatter.setSymbol(mode) + + window.show() + + return window |