diff options
author | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2020-07-21 14:45:14 +0200 |
---|---|---|
committer | Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> | 2020-07-21 14:45:14 +0200 |
commit | 328032e2317e3ac4859196bbf12bdb71795302fe (patch) | |
tree | 8cd13462beab109e3cb53410c42335b6d1e00ee6 /silx/gui/plot/backends | |
parent | 33ed2a64c92b0311ae35456c016eb284e426afc2 (diff) |
New upstream version 0.13.0+dfsg
Diffstat (limited to 'silx/gui/plot/backends')
-rwxr-xr-x | silx/gui/plot/backends/BackendBase.py | 34 | ||||
-rwxr-xr-x | silx/gui/plot/backends/BackendMatplotlib.py | 252 | ||||
-rwxr-xr-x | silx/gui/plot/backends/BackendOpenGL.py | 78 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotCurve.py | 15 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/GLPlotImage.py | 122 | ||||
-rw-r--r-- | silx/gui/plot/backends/glutils/PlotImageFile.py | 6 |
6 files changed, 321 insertions, 186 deletions
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py index 75d999b..bcc93a5 100755 --- a/silx/gui/plot/backends/BackendBase.py +++ b/silx/gui/plot/backends/BackendBase.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2019 European Synchrotron Radiation Facility +# Copyright (c) 2004-2020 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 @@ -63,8 +63,6 @@ class BackendBase(object): # Store a weakref to get access to the plot state. self._setPlot(plot) - self.__zoomBackAction = None - @property def _plot(self): """The plot this backend is attached to.""" @@ -83,24 +81,12 @@ class BackendBase(object): """ self._plotRef = weakref.ref(plot) - # Default Qt context menu - - def contextMenuEvent(self, event): - """Override QWidget.contextMenuEvent to implement the context menu""" - if self.__zoomBackAction is None: - from ..actions.control import ZoomBackAction # Avoid cyclic import - self.__zoomBackAction = ZoomBackAction(plot=self._plot, - parent=self._plot) - menu = qt.QMenu(self) - menu.addAction(self.__zoomBackAction) - menu.exec_(event.globalPos()) - # Add methods def addCurve(self, x, y, color, symbol, linewidth, linestyle, yaxis, - xerror, yerror, z, + xerror, yerror, fill, alpha, symbolsize, baseline): """Add a 1D curve given by x an y to the graph. @@ -134,7 +120,6 @@ class BackendBase(object): :type xerror: numpy.ndarray or None :param yerror: Values with the uncertainties on the y values :type yerror: numpy.ndarray or None - :param int z: Layer on which to draw the cuve :param bool fill: True to fill the curve, False otherwise :param float alpha: Curve opacity, as a float in [0., 1.] :param float symbolsize: Size of the symbol (if any) drawn @@ -144,7 +129,7 @@ class BackendBase(object): return object() def addImage(self, data, - origin, scale, z, + origin, scale, colormap, alpha): """Add an image to the plot. @@ -156,7 +141,6 @@ class BackendBase(object): :param scale: (scale X, scale Y) of the data. Default: (1., 1.) :type scale: 2-tuple of float - :param int z: Layer on which to draw the image :param ~silx.gui.colors.Colormap colormap: Colormap object to use. Ignored if data is RGB(A). :param float alpha: Opacity of the image, as a float in range [0, 1]. @@ -165,7 +149,7 @@ class BackendBase(object): return object() def addTriangles(self, x, y, triangles, - color, z, alpha): + color, alpha): """Add a set of triangles. :param numpy.ndarray x: The data corresponding to the x axis @@ -173,14 +157,13 @@ class BackendBase(object): :param numpy.ndarray triangles: The indices to make triangles as a (Ntriangle, 3) array :param numpy.ndarray color: color(s) as (npoints, 4) array - :param int z: Layer on which to draw the cuve :param float alpha: Opacity as a float in [0., 1.] :returns: The triangles' unique identifier used by the backend """ return object() - def addItem(self, x, y, shape, color, fill, overlay, z, - linestyle, linewidth, linebgcolor): + def addShape(self, x, y, shape, color, fill, overlay, + linestyle, linewidth, linebgcolor): """Add an item (i.e. a shape) to the plot. :param numpy.ndarray x: The X coords of the points of the shape @@ -190,7 +173,6 @@ class BackendBase(object): :param str color: Color of the item :param bool fill: True to fill the shape :param bool overlay: True if item is an overlay, False otherwise - :param int z: Layer on which to draw the item :param str linestyle: Style of the line. Only relevant for line markers where X or Y is None. Value in: @@ -545,7 +527,7 @@ class BackendBase(object): """ raise NotImplementedError() - def pixelToData(self, x, y, axis, check): + def pixelToData(self, x, y, axis): """Convert a position in pixels in the widget to a position in the data space. @@ -553,8 +535,6 @@ class BackendBase(object): :param float y: The Y coordinate in pixels. :param str axis: The Y axis to use for the conversion ('left' or 'right'). - :param bool check: True to check if the coordinates are in the - plot area. :returns: The corresponding position in data space or None if the pixel position is not in the plot area. :rtype: A tuple of 2 floats: (xData, yData) or None. diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py index 2336494..036e630 100755 --- a/silx/gui/plot/backends/BackendMatplotlib.py +++ b/silx/gui/plot/backends/BackendMatplotlib.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2019 European Synchrotron Radiation Facility +# Copyright (c) 2004-2020 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 @@ -52,6 +52,7 @@ from matplotlib.patches import Rectangle, Polygon from matplotlib.image import AxesImage from matplotlib.backend_bases import MouseEvent from matplotlib.lines import Line2D +from matplotlib.text import Text from matplotlib.collections import PathCollection, LineCollection from matplotlib.ticker import Formatter, ScalarFormatter, Locator from matplotlib.tri import Triangulation @@ -252,6 +253,60 @@ class _PickableContainer(Container): return False, {} +class _TextWithOffset(Text): + """Text object which can be displayed at a specific position + of the plot, but with a pixel offset""" + + def __init__(self, *args, **kwargs): + Text.__init__(self, *args, **kwargs) + self.pixel_offset = (0, 0) + self.__cache = None + + def draw(self, renderer): + self.__cache = None + return Text.draw(self, renderer) + + def __get_xy(self): + if self.__cache is not None: + return self.__cache + + align = self.get_horizontalalignment() + if align == "left": + xoffset = self.pixel_offset[0] + elif align == "right": + xoffset = -self.pixel_offset[0] + else: + xoffset = 0 + + align = self.get_verticalalignment() + if align == "top": + yoffset = -self.pixel_offset[1] + elif align == "bottom": + yoffset = self.pixel_offset[1] + else: + yoffset = 0 + + trans = self.get_transform() + invtrans = self.get_transform().inverted() + + x = super(_TextWithOffset, self).convert_xunits(self._x) + y = super(_TextWithOffset, self).convert_xunits(self._y) + pos = x, y + proj = trans.transform_point(pos) + proj = proj + numpy.array((xoffset, yoffset)) + pos = invtrans.transform_point(proj) + self.__cache = pos + return pos + + def convert_xunits(self, x): + """Return the pixel position of the annotated point.""" + return self.__get_xy()[0] + + def convert_yunits(self, y): + """Return the pixel position of the annotated point.""" + return self.__get_xy()[1] + + class _MarkerContainer(_PickableContainer): """Marker artists container supporting draw/remove and text position update @@ -263,9 +318,10 @@ class _MarkerContainer(_PickableContainer): :param y: Y coordinate of the marker (None for vertical lines) """ - def __init__(self, artists, x, y, yAxis): + def __init__(self, artists, symbol, x, y, yAxis): self.line = artists[0] self.text = artists[1] if len(artists) > 1 else None + self.symbol = symbol self.x = x self.y = y self.yAxis = yAxis @@ -278,27 +334,39 @@ class _MarkerContainer(_PickableContainer): if self.text is not None: self.text.draw(*args, **kwargs) - def updateMarkerText(self, xmin, xmax, ymin, ymax): + def updateMarkerText(self, xmin, xmax, ymin, ymax, yinverted): """Update marker text position and visibility according to plot limits :param xmin: X axis lower limit :param xmax: X axis upper limit :param ymin: Y axis lower limit - :param ymax: Y axis upprt limit + :param ymax: Y axis upper limit + :param yinverted: True if the y axis is inverted """ if self.text is not None: visible = ((self.x is None or xmin <= self.x <= xmax) and (self.y is None or ymin <= self.y <= ymax)) self.text.set_visible(visible) - if self.x is not None and self.y is None: # vertical line - delta = abs(ymax - ymin) - if ymin > ymax: - ymax = ymin - ymax -= 0.005 * delta - self.text.set_y(ymax) - - if self.x is None and self.y is not None: # Horizontal line + if self.x is not None and self.y is not None: + if self.symbol is None: + valign = 'baseline' + else: + if yinverted: + valign = 'bottom' + else: + valign = 'top' + self.text.set_verticalalignment(valign) + + elif self.y is None: # vertical line + # Always display it on top + center = (ymax + ymin) * 0.5 + pos = (ymax - ymin) * 0.5 * 0.99 + if yinverted: + pos = -pos + self.text.set_y(center + pos) + + elif self.x is None: # Horizontal line delta = abs(xmax - xmin) if xmin > xmax: xmax = xmin @@ -354,9 +422,35 @@ class _DoubleColoredLinePatch(matplotlib.patches.Patch): class Image(AxesImage): - """An AxesImage with a fast path for uint8 RGBA images""" + """An AxesImage with a fast path for uint8 RGBA images. + + :param List[float] silx_origin: (ox, oy) Offset of the image. + :param List[float] silx_scale: (sx, sy) Scale of the image. + """ + + def __init__(self, *args, + silx_origin=(0., 0.), + silx_scale=(1., 1.), + **kwargs): + super().__init__(*args, **kwargs) + self.__silx_origin = silx_origin + self.__silx_scale = silx_scale + + def contains(self, mouseevent): + """Overridden to fill 'ind' with row and column""" + inside, info = super().contains(mouseevent) + if inside: + x, y = mouseevent.xdata, mouseevent.ydata + ox, oy = self.__silx_origin + sx, sy = self.__silx_scale + height, width = self.get_size() + column = numpy.clip(int((x - ox) / sx), 0, width - 1) + row = numpy.clip(int((y - oy) / sy), 0, height - 1) + info['ind'] = (row,), (column,) + return inside, info def set_data(self, A): + """Overridden to add a fast path for RGBA unit8 images""" A = numpy.array(A, copy=False) if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8: super(Image, self).set_data(A) @@ -402,10 +496,15 @@ class BackendMatplotlib(BackendBase.BackendBase): # disable the use of offsets try: - self.ax.get_yaxis().get_major_formatter().set_useOffset(False) - self.ax.get_xaxis().get_major_formatter().set_useOffset(False) - self.ax2.get_yaxis().get_major_formatter().set_useOffset(False) - self.ax2.get_xaxis().get_major_formatter().set_useOffset(False) + axes = [ + self.ax.get_yaxis().get_major_formatter(), + self.ax.get_xaxis().get_major_formatter(), + self.ax2.get_yaxis().get_major_formatter(), + self.ax2.get_xaxis().get_major_formatter(), + ] + for axis in axes: + axis.set_useOffset(False) + axis.set_scientific(False) except: _logger.warning('Cannot disabled axes offsets in %s ' % matplotlib.__version__) @@ -485,10 +584,10 @@ class BackendMatplotlib(BackendBase.BackendBase): def addCurve(self, x, y, color, symbol, linewidth, linestyle, yaxis, - xerror, yerror, z, + xerror, yerror, fill, alpha, symbolsize, baseline): for parameter in (x, y, color, symbol, linewidth, linestyle, - yaxis, z, fill, alpha, symbolsize): + yaxis, fill, alpha, symbolsize): assert parameter is not None assert yaxis in ('left', 'right') @@ -584,12 +683,12 @@ class BackendMatplotlib(BackendBase.BackendBase): return _PickableContainer(artists) - def addImage(self, data, origin, scale, z, colormap, alpha): + def addImage(self, data, origin, scale, colormap, alpha): # Non-uniform image # http://wiki.scipy.org/Cookbook/Histograms # Non-linear axes # http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib - for parameter in (data, origin, scale, z): + for parameter in (data, origin, scale): assert parameter is not None origin = float(origin[0]), float(origin[1]) @@ -600,7 +699,9 @@ class BackendMatplotlib(BackendBase.BackendBase): image = Image(self.ax, interpolation='nearest', picker=True, - origin='lower') + origin='lower', + silx_origin=origin, + silx_scale=scale) if alpha < 1: image.set_alpha(alpha) @@ -627,13 +728,17 @@ class BackendMatplotlib(BackendBase.BackendBase): if data.ndim == 2: # Data image, convert to RGBA image data = colormap.applyToData(data) - + elif data.dtype == numpy.uint16: + # Normalize uint16 data to have a similar behavior as opengl backend + data = data.astype(numpy.float32) + data /= 65535 + image.set_data(data) self.ax.add_artist(image) return image - def addTriangles(self, x, y, triangles, color, z, alpha): - for parameter in (x, y, triangles, color, z, alpha): + def addTriangles(self, x, y, triangles, color, alpha): + for parameter in (x, y, triangles, color, alpha): assert parameter is not None color = numpy.array(color, copy=False) @@ -651,8 +756,8 @@ class BackendMatplotlib(BackendBase.BackendBase): return collection - def addItem(self, x, y, shape, color, fill, overlay, z, - linestyle, linewidth, linebgcolor): + def addShape(self, x, y, shape, color, fill, overlay, + linestyle, linewidth, linebgcolor): if (linebgcolor is not None and shape not in ('rectangle', 'polygon', 'polylines')): _logger.warning( @@ -755,17 +860,11 @@ class BackendMatplotlib(BackendBase.BackendBase): markersize=10.)[-1] if text is not None: - if symbol is None: - valign = 'baseline' - else: - valign = 'top' - text = " " + text - - textArtist = ax.text(x, y, text, - color=color, - horizontalalignment='left', - verticalalignment=valign) - + textArtist = _TextWithOffset(x, y, text, + color=color, + horizontalalignment='left') + if symbol is not None: + textArtist.pixel_offset = 10, 3 elif x is not None: line = ax.axvline(x, color=color, @@ -773,11 +872,11 @@ class BackendMatplotlib(BackendBase.BackendBase): linestyle=linestyle) if text is not None: # Y position will be updated in updateMarkerText call - textArtist = ax.text(x, 1., " " + text, - color=color, - horizontalalignment='left', - verticalalignment='top') - + textArtist = _TextWithOffset(x, 1., text, + color=color, + horizontalalignment='left', + verticalalignment='top') + textArtist.pixel_offset = 5, 3 elif y is not None: line = ax.axhline(y, color=color, @@ -786,11 +885,11 @@ class BackendMatplotlib(BackendBase.BackendBase): if text is not None: # X position will be updated in updateMarkerText call - textArtist = ax.text(1., y, " " + text, - color=color, - horizontalalignment='right', - verticalalignment='top') - + textArtist = _TextWithOffset(1., y, text, + color=color, + horizontalalignment='right', + verticalalignment='top') + textArtist.pixel_offset = 5, 3 else: raise RuntimeError('A marker must at least have one coordinate') @@ -799,11 +898,12 @@ class BackendMatplotlib(BackendBase.BackendBase): # All markers are overlays line.set_animated(True) if textArtist is not None: + ax.add_artist(textArtist) textArtist.set_animated(True) artists = [line] if textArtist is None else [line, textArtist] - container = _MarkerContainer(artists, x, y, yaxis) - container.updateMarkerText(xmin, xmax, ymin, ymax) + container = _MarkerContainer(artists, symbol, x, y, yaxis) + container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted()) return container @@ -811,12 +911,13 @@ class BackendMatplotlib(BackendBase.BackendBase): xmin, xmax = self.ax.get_xbound() ymin1, ymax1 = self.ax.get_ybound() ymin2, ymax2 = self.ax2.get_ybound() + yinverted = self.isYAxisInverted() for item in self._overlayItems(): if isinstance(item, _MarkerContainer): if item.yAxis == 'left': - item.updateMarkerText(xmin, xmax, ymin1, ymax1) + item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted) else: - item.updateMarkerText(xmin, xmax, ymin2, ymax2) + item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted) # Remove methods @@ -1076,6 +1177,7 @@ class BackendMatplotlib(BackendBase.BackendBase): def setYAxisInverted(self, flag): if self.ax.yaxis_inverted() != bool(flag): self.ax.invert_yaxis() + self._updateMarkers() def isYAxisInverted(self): return self.ax.yaxis_inverted() @@ -1143,15 +1245,15 @@ class BackendMatplotlib(BackendBase.BackendBase): BackendBase.BackendBase.setAxesDisplayed(self, displayed) if displayed: # show axes and viewbox rect - self.ax.set_axis_on() - self.ax2.set_axis_on() + self.ax.set_frame_on(True) + self.ax2.set_frame_on(True) # set the default margins self.ax.set_position([.15, .15, .75, .75]) self.ax2.set_position([.15, .15, .75, .75]) else: # hide axes and viewbox rect - self.ax.set_axis_off() - self.ax2.set_axis_off() + self.ax.set_frame_on(False) + self.ax2.set_frame_on(False) # remove external margins self.ax.set_position([0, 0, 1, 1]) self.ax2.set_position([0, 0, 1, 1]) @@ -1168,7 +1270,7 @@ class BackendMatplotlib(BackendBase.BackendBase): else: dataBackgroundColor = backgroundColor - if self.ax.axison: + if self.ax.get_frame_on(): self.fig.patch.set_facecolor(backgroundColor) if self._matplotlibVersion < _parse_version('2'): self.ax.set_axis_bgcolor(dataBackgroundColor) @@ -1187,7 +1289,7 @@ class BackendMatplotlib(BackendBase.BackendBase): gridColor = foregroundColor for axes in (self.ax, self.ax2): - if axes.axison: + if axes.get_frame_on(): axes.spines['bottom'].set_color(foregroundColor) axes.spines['top'].set_color(foregroundColor) axes.spines['right'].set_color(foregroundColor) @@ -1244,11 +1346,6 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): self.mpl_connect('motion_notify_event', self._onMouseMove) self.mpl_connect('scroll_event', self._onMouseWheel) - def contextMenuEvent(self, event): - """Override QWidget.contextMenuEvent to implement the context menu""" - # Makes sure it is overridden (issue with PySide) - BackendBase.BackendBase.contextMenuEvent(self, event) - def postRedisplay(self): self._sigPostRedisplay.emit() @@ -1265,17 +1362,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): def _onMouseMove(self, event): if self._graphCursor: + position = self._plot.pixelToData( + event.x, + self._mplQtYAxisCoordConversion(event.y), + axis='left', + check=True) lineh, linev = self._graphCursor - if event.inaxes not in (self.ax, self.ax2) and lineh.get_visible(): - lineh.set_visible(False) - linev.set_visible(False) - self._plot._setDirtyPlot(overlayOnly=True) - else: + if position is not None: linev.set_visible(True) - linev.set_xdata((event.xdata, event.xdata)) + linev.set_xdata((position[0], position[0])) lineh.set_visible(True) - lineh.set_ydata((event.ydata, event.ydata)) + lineh.set_ydata((position[1], position[1])) self._plot._setDirtyPlot(overlayOnly=True) + elif lineh.get_visible(): + lineh.set_visible(False) + linev.set_visible(False) + self._plot._setDirtyPlot(overlayOnly=True) # onMouseMove must trigger replot if dirty flag is raised self._plot.onMouseMove( @@ -1294,14 +1396,22 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): def leaveEvent(self, event): """QWidget event handler""" - self._plot.onMouseLeaveWidget() + try: + plot = self._plot + except RuntimeError: + pass + else: + plot.onMouseLeaveWidget() # picking def pickItem(self, x, y, item): mouseEvent = MouseEvent( 'button_press_event', self, x, self._mplQtYAxisCoordConversion(y)) + # Override axes and data position with the axes mouseEvent.inaxes = item.axes + mouseEvent.xdata, mouseEvent.ydata = self.pixelToData( + x, y, axis='left' if item.axes is self.ax else 'right') picked, info = item.contains(mouseEvent) if not picked: diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py index 27f3894..cf1da31 100755 --- a/silx/gui/plot/backends/BackendOpenGL.py +++ b/silx/gui/plot/backends/BackendOpenGL.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 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 @@ -31,7 +31,6 @@ __license__ = "MIT" __date__ = "21/12/2018" import logging -import warnings import weakref import numpy @@ -62,7 +61,7 @@ _logger = logging.getLogger(__name__) # Content ##################################################################### class _ShapeItem(dict): - def __init__(self, x, y, shape, color, fill, overlay, z, + def __init__(self, x, y, shape, color, fill, overlay, linestyle, linewidth, linebgcolor): super(_ShapeItem, self).__init__() @@ -249,11 +248,6 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'} - def contextMenuEvent(self, event): - """Override QWidget.contextMenuEvent to implement the context menu""" - # Makes sure it is overridden (issue with PySide) - BackendBase.BackendBase.contextMenuEvent(self, event) - def sizeHint(self): return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend @@ -431,6 +425,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:] isXLog = self._plotFrame.xAxis.isLog isYLog = self._plotFrame.yAxis.isLog + isYInverted = self._plotFrame.isYAxisInverted # Used by marker rendering labels = [] @@ -572,13 +567,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # Do not render markers outside visible plot area continue + if isYInverted: + valign = BOTTOM + vPixelOffset = -pixelOffset + else: + valign = TOP + vPixelOffset = pixelOffset + if item['text'] is not None: x = pixelPos[0] + pixelOffset - y = pixelPos[1] + pixelOffset + y = pixelPos[1] + vPixelOffset label = Text2D(item['text'], x, y, color=item['color'], bgColor=(1., 1., 1., 0.5), - align=LEFT, valign=TOP) + align=LEFT, valign=valign) labels.append(label) # For now simple implementation: using a curve for each marker @@ -726,10 +728,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def addCurve(self, x, y, color, symbol, linewidth, linestyle, yaxis, - xerror, yerror, z, + xerror, yerror, fill, alpha, symbolsize, baseline): for parameter in (x, y, color, symbol, linewidth, linestyle, - yaxis, z, fill, symbolsize): + yaxis, fill, symbolsize): assert parameter is not None assert yaxis in ('left', 'right') @@ -767,8 +769,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): xErrorMinus, xErrorPlus = xerror[0], xerror[1] else: xErrorMinus, xErrorPlus = xerror, xerror - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(divide='ignore', invalid='ignore'): # Ignore divide by zero, invalid value encountered in log10 xErrorMinus = logX - numpy.log10(x - xErrorMinus) xErrorPlus = numpy.log10(x + xErrorPlus) - logX @@ -790,8 +791,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): yErrorMinus, yErrorPlus = yerror[0], yerror[1] else: yErrorMinus, yErrorPlus = yerror, yerror - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(divide='ignore', invalid='ignore'): # Ignore divide by zero, invalid value encountered in log10 yErrorMinus = logY - numpy.log10(y - yErrorMinus) yErrorPlus = numpy.log10(y + yErrorPlus) - logY @@ -846,9 +846,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): return curve def addImage(self, data, - origin, scale, z, + origin, scale, colormap, alpha): - for parameter in (data, origin, scale, z): + for parameter in (data, origin, scale): assert parameter is not None if data.ndim == 2: @@ -860,17 +860,25 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): 'addImage: Convert %s data to float32', str(data.dtype)) data = numpy.array(data, dtype=numpy.float32, order='C') - colormapIsLog = colormap.getNormalization() == 'log' - cmapRange = colormap.getColormapRange(data=data) - colormapLut = colormap.getNColors(nbColors=256) - - image = GLPlotColormap(data, - origin, - scale, - colormapLut, - colormapIsLog, - cmapRange, - alpha) + normalization = colormap.getNormalization() + if normalization in GLPlotColormap.SUPPORTED_NORMALIZATIONS: + # Fast path applying colormap on the GPU + cmapRange = colormap.getColormapRange(data=data) + colormapLut = colormap.getNColors(nbColors=256) + gamma = colormap.getGammaNormalizationParameter() + + image = GLPlotColormap(data, + origin, + scale, + colormapLut, + normalization, + gamma, + cmapRange, + alpha) + + else: # Fallback applying colormap on CPU + rgba = colormap.applyToData(data) + image = GLPlotRGBAImage(rgba, origin, scale, alpha) elif len(data.shape) == 3: # For RGB, RGBA data @@ -878,6 +886,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): if numpy.issubdtype(data.dtype, numpy.floating): data = numpy.array(data, dtype=numpy.float32, copy=False) + elif data.dtype in [numpy.uint8, numpy.uint16]: + pass elif numpy.issubdtype(data.dtype, numpy.integer): data = numpy.array(data, dtype=numpy.uint8, copy=False) else: @@ -899,7 +909,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): return image def addTriangles(self, x, y, triangles, - color, z, alpha): + color, alpha): # Handle axes log scale: convert data if self._plotFrame.xAxis.isLog: x = numpy.log10(x) @@ -910,8 +920,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): return triangles - def addItem(self, x, y, shape, color, fill, overlay, z, - linestyle, linewidth, linebgcolor): + def addShape(self, x, y, shape, color, fill, overlay, + linestyle, linewidth, linebgcolor): x = numpy.array(x, copy=False) y = numpy.array(y, copy=False) @@ -923,7 +933,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): raise RuntimeError( 'Cannot add item with Y <= 0 with Y axis log scale') - return _ShapeItem(x, y, shape, color, fill, overlay, z, + return _ShapeItem(x, y, shape, color, fill, overlay, linestyle, linewidth, linebgcolor) def addMarker(self, x, y, text, color, @@ -971,7 +981,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): super(BackendOpenGL, self).setCursor(qt.QCursor(cursor)) def setGraphCursor(self, flag, color, linewidth, linestyle): - if linestyle is not '-': + if linestyle != '-': _logger.warning( "BackendOpenGL.setGraphCursor linestyle parameter ignored") diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py index 3a0ebac..9ab85fd 100644 --- a/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ b/silx/gui/plot/backends/glutils/GLPlotCurve.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 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 @@ -35,7 +35,6 @@ __date__ = "03/04/2017" import math import logging -import warnings import numpy @@ -1129,11 +1128,9 @@ class GLPlotCurve2D(object): _baseline = numpy.repeat(_baseline, len(self.xData)) if isYLog is True: - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', - category=RuntimeWarning) + with numpy.errstate(divide='ignore', invalid='ignore'): log_val = numpy.log10(_baseline) - _baseline = numpy.where(_baseline>0.0, log_val, -38) + _baseline = numpy.where(_baseline>0.0, log_val, -38) return _baseline _baseline = deduce_baseline(baseline) @@ -1277,8 +1274,7 @@ class GLPlotCurve2D(object): if self.lineStyle is not None: # Using Cohen-Sutherland algorithm for line clipping - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings codes = ((self.yData > yPickMax) << 3) | \ ((self.yData < yPickMin) << 2) | \ ((self.xData > xPickMax) << 1) | \ @@ -1335,8 +1331,7 @@ class GLPlotCurve2D(object): indices.sort() else: - with warnings.catch_warnings(): # Ignore NaN comparison warnings - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings indices = numpy.nonzero((self.xData >= xPickMin) & (self.xData <= xPickMax) & (self.yData >= yPickMin) & diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py index 5d79023..e985a3d 100644 --- a/silx/gui/plot/backends/glutils/GLPlotImage.py +++ b/silx/gui/plot/backends/glutils/GLPlotImage.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 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 @@ -56,7 +56,7 @@ class _GLPlotData2D(object): sx, sy = self.scale col = int((x - ox) / sx) row = int((y - oy) / sy) - return ((row, col),) + return (row,), (col,) else: return None @@ -141,10 +141,8 @@ class GLPlotColormap(_GLPlotData2D): """, 'fragTransform': """ uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; + uniform vec2 bounds_oneOverRange; + uniform vec2 bounds_originOverRange; vec2 textureCoords(void) { vec2 pos = coords; @@ -154,7 +152,7 @@ class GLPlotColormap(_GLPlotData2D): if (isLog.y) { pos.y = pow(10., coords.y); } - return pos * bounds.oneOverRange - bounds.originOverRange; + return pos * bounds_oneOverRange - bounds_originOverRange; // TODO texture coords in range different from [0, 1] } """}, @@ -163,12 +161,11 @@ class GLPlotColormap(_GLPlotData2D): #version 120 uniform sampler2D data; - uniform struct { - sampler2D texture; - bool isLog; - float min; - float oneOverRange; - } cmap; + uniform sampler2D cmap_texture; + uniform int cmap_normalization; + uniform float cmap_parameter; + uniform float cmap_min; + uniform float cmap_oneOverRange; uniform float alpha; varying vec2 coords; @@ -179,19 +176,33 @@ class GLPlotColormap(_GLPlotData2D): void main(void) { float value = texture2D(data, textureCoords()).r; - if (cmap.isLog) { + if (cmap_normalization == 1) { /*Logarithm mapping*/ if (value > 0.) { - value = clamp(cmap.oneOverRange * - (oneOverLog10 * log(value) - cmap.min), + value = clamp(cmap_oneOverRange * + (oneOverLog10 * log(value) - cmap_min), 0., 1.); } else { value = 0.; } - } else { /*Linear mapping*/ - value = clamp(cmap.oneOverRange * (value - cmap.min), 0., 1.); + } else if (cmap_normalization == 2) { /*Square root mapping*/ + if (value >= 0.) { + value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min), + 0., 1.); + } else { + value = 0.; + } + } else if (cmap_normalization == 3) { /*Gamma correction mapping*/ + value = pow( + clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.), + cmap_parameter); + } else if (cmap_normalization == 4) { /* arcsinh mapping */ + /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */ + value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.); + } else { /*Linear mapping and fallback*/ + value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.); } - gl_FragColor = texture2D(cmap.texture, vec2(value, 0.5)); + gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5)); gl_FragColor.a *= alpha; } """ @@ -217,8 +228,10 @@ class GLPlotColormap(_GLPlotData2D): _SHADERS['log']['fragTransform'], attrib0='position') + SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh' + def __init__(self, data, origin, scale, - colormap, cmapIsLog=False, cmapRange=None, + colormap, normalization='linear', gamma=0., cmapRange=None, alpha=1.0): """Create a 2D colormap @@ -231,7 +244,9 @@ class GLPlotColormap(_GLPlotData2D): :type scale: 2-tuple of floats. :param str colormap: Name of the colormap to use TODO: Accept a 1D scalar array as the colormap - :param bool cmapIsLog: If True, uses log10 of the data value + :param str normalization: The colormap normalization. + One of: 'linear', 'log', 'sqrt', 'gamma' + ;param float gamma: The gamma parameter (for 'gamma' normalization) :param cmapRange: The range of colormap or None for autoscale colormap For logarithmic colormap, the range is in the untransformed data TODO: check consistency with matplotlib @@ -239,10 +254,12 @@ class GLPlotColormap(_GLPlotData2D): :param float alpha: Opacity from 0 (transparent) to 1 (opaque) """ assert data.dtype in self._INTERNAL_FORMATS + assert normalization in self.SUPPORTED_NORMALIZATIONS super(GLPlotColormap, self).__init__(data, origin, scale) self.colormap = numpy.array(colormap, copy=False) - self.cmapIsLog = cmapIsLog + self.normalization = normalization + self.gamma = gamma self._cmapRange = (1., 10.) # Colormap range self.cmapRange = cmapRange # Update _cmapRange self._alpha = numpy.clip(alpha, 0., 1.) @@ -263,8 +280,10 @@ class GLPlotColormap(_GLPlotData2D): @property def cmapRange(self): - if self.cmapIsLog: + if self.normalization == 'log': assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0. + elif self.normalization == 'sqrt': + assert self._cmapRange[0] >= 0. and self._cmapRange[1] > 0. return self._cmapRange @cmapRange.setter @@ -319,6 +338,7 @@ class GLPlotColormap(_GLPlotData2D): def _setCMap(self, prog): dataMin, dataMax = self.cmapRange # If log, it is stricly positive + param = 0. if self.data.dtype in (numpy.uint16, numpy.uint8): # Using unsigned int as normalized integer in OpenGL @@ -326,19 +346,35 @@ class GLPlotColormap(_GLPlotData2D): maxInt = float(numpy.iinfo(self.data.dtype).max) dataMin, dataMax = dataMin / maxInt, dataMax / maxInt - if self.cmapIsLog: + if self.normalization == 'log': dataMin = math.log10(dataMin) dataMax = math.log10(dataMax) - - gl.glUniform1i(prog.uniforms['cmap.texture'], + normID = 1 + elif self.normalization == 'sqrt': + dataMin = math.sqrt(dataMin) + dataMax = math.sqrt(dataMax) + normID = 2 + elif self.normalization == 'gamma': + # Keep dataMin, dataMax as is + param = self.gamma + normID = 3 + elif self.normalization == 'arcsinh': + dataMin = numpy.arcsinh(dataMin) + dataMax = numpy.arcsinh(dataMax) + normID = 4 + else: # Linear and fallback + normID = 0 + + gl.glUniform1i(prog.uniforms['cmap_texture'], self._cmap_texture.texUnit) - gl.glUniform1i(prog.uniforms['cmap.isLog'], self.cmapIsLog) - gl.glUniform1f(prog.uniforms['cmap.min'], dataMin) + gl.glUniform1i(prog.uniforms['cmap_normalization'], normID) + gl.glUniform1f(prog.uniforms['cmap_parameter'], param) + gl.glUniform1f(prog.uniforms['cmap_min'], dataMin) if dataMax > dataMin: oneOverRange = 1. / (dataMax - dataMin) else: oneOverRange = 0. # Fall-back - gl.glUniform1f(prog.uniforms['cmap.oneOverRange'], oneOverRange) + gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange) self._cmap_texture.bind() @@ -393,9 +429,9 @@ class GLPlotColormap(_GLPlotData2D): xOneOverRange = 1. / (ex - ox) yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], + gl.glUniform2f(prog.uniforms['bounds_originOverRange'], ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], + gl.glUniform2f(prog.uniforms['bounds_oneOverRange'], xOneOverRange, yOneOverRange) gl.glUniform1f(prog.uniforms['alpha'], self.alpha) @@ -500,10 +536,8 @@ class GLPlotRGBAImage(_GLPlotData2D): uniform sampler2D tex; uniform bvec2 isLog; - uniform struct { - vec2 oneOverRange; - vec2 originOverRange; - } bounds; + uniform vec2 bounds_oneOverRange; + uniform vec2 bounds_originOverRange; uniform float alpha; varying vec2 coords; @@ -516,7 +550,7 @@ class GLPlotRGBAImage(_GLPlotData2D): if (isLog.y) { pos.y = pow(10., coords.y); } - return pos * bounds.oneOverRange - bounds.originOverRange; + return pos * bounds_oneOverRange - bounds_originOverRange; // TODO texture coords in range different from [0, 1] } @@ -530,7 +564,8 @@ class GLPlotRGBAImage(_GLPlotData2D): _DATA_TEX_UNIT = 0 _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32), - numpy.dtype(numpy.uint8)) + numpy.dtype(numpy.uint8), + numpy.dtype(numpy.uint16)) _linearProgram = Program(_SHADERS['linear']['vertex'], _SHADERS['linear']['fragment'], @@ -582,9 +617,14 @@ class GLPlotRGBAImage(_GLPlotData2D): def prepare(self): if self._texture is None: - format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB + formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB' + format_ = getattr(gl, formatName) - self._texture = Image(format_, + if self.data.dtype == numpy.uint16: + formatName += '16' # Use sized internal format for uint16 + internalFormat = getattr(gl, formatName) + + self._texture = Image(internalFormat, self.data, format_=format_, texUnit=self._DATA_TEX_UNIT) @@ -639,9 +679,9 @@ class GLPlotRGBAImage(_GLPlotData2D): xOneOverRange = 1. / (ex - ox) yOneOverRange = 1. / (ey - oy) - gl.glUniform2f(prog.uniforms['bounds.originOverRange'], + gl.glUniform2f(prog.uniforms['bounds_originOverRange'], ox * xOneOverRange, oy * yOneOverRange) - gl.glUniform2f(prog.uniforms['bounds.oneOverRange'], + gl.glUniform2f(prog.uniforms['bounds_oneOverRange'], xOneOverRange, yOneOverRange) try: diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py index 83c7ae0..5fb6853 100644 --- a/silx/gui/plot/backends/glutils/PlotImageFile.py +++ b/silx/gui/plot/backends/glutils/PlotImageFile.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 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 @@ -59,7 +59,7 @@ def convertRGBDataToPNG(data): 0, 0, interlace) # Add filter 'None' before each scanline - preparedData = b'\x00' + b'\x00'.join(line.tostring() for line in data) + preparedData = b'\x00' + b'\x00'.join(line.tobytes() for line in data) compressedData = zlib.compress(preparedData, 8) IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T') @@ -134,7 +134,7 @@ def saveImageToFile(data, fileNameOrObj, fileFormat): fileObj.write(b'P6\n') fileObj.write(b'%d %d\n' % (width, height)) fileObj.write(b'255\n') - fileObj.write(data.tostring()) + fileObj.write(data.tobytes()) elif fileFormat == 'png': fileObj.write(convertRGBDataToPNG(data)) |