diff options
Diffstat (limited to 'silx/gui/plot/matplotlib')
-rw-r--r-- | silx/gui/plot/matplotlib/Colormap.py | 282 | ||||
-rw-r--r-- | silx/gui/plot/matplotlib/ModestImage.py | 174 | ||||
-rw-r--r-- | silx/gui/plot/matplotlib/__init__.py | 70 |
3 files changed, 526 insertions, 0 deletions
diff --git a/silx/gui/plot/matplotlib/Colormap.py b/silx/gui/plot/matplotlib/Colormap.py new file mode 100644 index 0000000..a86d76e --- /dev/null +++ b/silx/gui/plot/matplotlib/Colormap.py @@ -0,0 +1,282 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################*/ +"""Matplotlib's new colormaps""" + +import numpy +import logging +from matplotlib.colors import ListedColormap +import matplotlib.colors +import matplotlib.cm +import silx.resources + +_logger = logging.getLogger(__name__) + +_AVAILABLE_AS_RESOURCE = ('magma', 'inferno', 'plasma', 'viridis') +"""List available colormap name as resources""" + +_AVAILABLE_AS_BUILTINS = ('gray', 'reversed gray', + 'temperature', 'red', 'green', 'blue') +"""List of colormaps available through built-in declarations""" + +_CMAPS = {} +"""Cache colormaps""" + + +@property +def magma(): + return getColormap('magma') + + +@property +def inferno(): + return getColormap('inferno') + + +@property +def plasma(): + return getColormap('plasma') + + +@property +def viridis(): + return getColormap('viridis') + + +def getColormap(name): + """Returns matplotlib colormap corresponding to given name + + :param str name: The name of the colormap + :return: The corresponding colormap + :rtype: matplolib.colors.Colormap + """ + if not _CMAPS: # Lazy initialization of own colormaps + cdict = {'red': ((0.0, 0.0, 0.0), + (1.0, 1.0, 1.0)), + 'green': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0)), + 'blue': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0))} + _CMAPS['red'] = matplotlib.colors.LinearSegmentedColormap( + 'red', cdict, 256) + + cdict = {'red': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0)), + 'green': ((0.0, 0.0, 0.0), + (1.0, 1.0, 1.0)), + 'blue': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0))} + _CMAPS['green'] = matplotlib.colors.LinearSegmentedColormap( + 'green', cdict, 256) + + cdict = {'red': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0)), + 'green': ((0.0, 0.0, 0.0), + (1.0, 0.0, 0.0)), + 'blue': ((0.0, 0.0, 0.0), + (1.0, 1.0, 1.0))} + _CMAPS['blue'] = matplotlib.colors.LinearSegmentedColormap( + 'blue', cdict, 256) + + # Temperature as defined in spslut + cdict = {'red': ((0.0, 0.0, 0.0), + (0.5, 0.0, 0.0), + (0.75, 1.0, 1.0), + (1.0, 1.0, 1.0)), + 'green': ((0.0, 0.0, 0.0), + (0.25, 1.0, 1.0), + (0.75, 1.0, 1.0), + (1.0, 0.0, 0.0)), + 'blue': ((0.0, 1.0, 1.0), + (0.25, 1.0, 1.0), + (0.5, 0.0, 0.0), + (1.0, 0.0, 0.0))} + # but limited to 256 colors for a faster display (of the colorbar) + _CMAPS['temperature'] = \ + matplotlib.colors.LinearSegmentedColormap( + 'temperature', cdict, 256) + + # reversed gray + cdict = {'red': ((0.0, 1.0, 1.0), + (1.0, 0.0, 0.0)), + 'green': ((0.0, 1.0, 1.0), + (1.0, 0.0, 0.0)), + 'blue': ((0.0, 1.0, 1.0), + (1.0, 0.0, 0.0))} + + _CMAPS['reversed gray'] = \ + matplotlib.colors.LinearSegmentedColormap( + 'yerg', cdict, 256) + + if name in _CMAPS: + return _CMAPS[name] + elif name in _AVAILABLE_AS_RESOURCE: + filename = silx.resources.resource_filename("gui/colormaps/%s.npy" % name) + data = numpy.load(filename) + lut = ListedColormap(data, name=name) + _CMAPS[name] = lut + return lut + else: + # matplotlib built-in + return matplotlib.cm.get_cmap(name) + + +def getScalarMappable(colormap, data=None): + """Returns matplotlib ScalarMappable corresponding to colormap + + :param :class:`.Colormap` colormap: The colormap to convert + :param numpy.ndarray data: + The data on which the colormap is applied. + If provided, it is used to compute autoscale. + :return: matplotlib object corresponding to colormap + :rtype: matplotlib.cm.ScalarMappable + """ + assert colormap is not None + + if colormap.getName() is not None: + cmap = getColormap(colormap.getName()) + + else: # No name, use custom colors + if colormap.getColormapLUT() is None: + raise ValueError( + 'addImage: colormap no name nor list of colors.') + colors = colormap.getColormapLUT() + assert len(colors.shape) == 2 + assert colors.shape[-1] in (3, 4) + if colors.dtype == numpy.uint8: + # Convert to float in [0., 1.] + colors = colors.astype(numpy.float32) / 255. + cmap = matplotlib.colors.ListedColormap(colors) + + if colormap.getNormalization().startswith('log'): + vmin, vmax = None, None + if not colormap.isAutoscale(): + if colormap.getVMin() > 0.: + vmin = colormap.getVMin() + if colormap.getVMax() > 0.: + vmax = colormap.getVMax() + + if vmin is None or vmax is None: + _logger.warning('Log colormap with negative bounds, ' + + 'changing bounds to positive ones.') + elif vmin > vmax: + _logger.warning('Colormap bounds are inverted.') + vmin, vmax = vmax, vmin + + # Set unset/negative bounds to positive bounds + if vmin is None or vmax is None: + # Convert to numpy array + data = numpy.array(data if data is not None else [], copy=False) + + if data.size > 0: + finiteData = data[numpy.isfinite(data)] + posData = finiteData[finiteData > 0] + if vmax is None: + # 1. as an ultimate fallback + vmax = posData.max() if posData.size > 0 else 1. + if vmin is None: + vmin = posData.min() if posData.size > 0 else vmax + if vmin > vmax: + vmin = vmax + else: + vmin, vmax = 1., 1. + + norm = matplotlib.colors.LogNorm(vmin, vmax) + + else: # Linear normalization + if colormap.isAutoscale(): + # Convert to numpy array + data = numpy.array(data if data is not None else [], copy=False) + + if data.size == 0: + vmin, vmax = 1., 1. + else: + finiteData = data[numpy.isfinite(data)] + if finiteData.size > 0: + vmin = finiteData.min() + vmax = finiteData.max() + else: + vmin, vmax = 1., 1. + + else: + vmin = colormap.getVMin() + vmax = colormap.getVMax() + if vmin > vmax: + _logger.warning('Colormap bounds are inverted.') + vmin, vmax = vmax, vmin + + norm = matplotlib.colors.Normalize(vmin, vmax) + + return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) + + +def applyColormapToData(data, + colormap): + """Apply a colormap to the data and returns the RGBA image + + This supports data of any dimensions (not only of dimension 2). + The returned array will have one more dimension (with 4 entries) + than the input data to store the RGBA channels + corresponding to each bin in the array. + + :param numpy.ndarray data: The data to convert. + :param :class:`.Colormap`: The colormap to apply + """ + # Debian 7 specific support + # No transparent colormap with matplotlib < 1.2.0 + # Add support for transparent colormap for uint8 data with + # colormap with 256 colors, linear norm, [0, 255] range + if matplotlib.__version__ < '1.2.0': + if (colormap.getName() is None and + colormap.getColormapLUT() is not None): + colors = colormap.getColormapLUT() + if (colors.shape[-1] == 4 and + not numpy.all(numpy.equal(colors[3], 255))): + # This is a transparent colormap + if (colors.shape == (256, 4) and + colormap.getNormalization() == 'linear' and + not colormap.isAutoscale() and + colormap.getVMin() == 0 and + colormap.getVMax() == 255 and + data.dtype == numpy.uint8): + # Supported case, convert data to RGBA + return colors[data.reshape(-1)].reshape( + data.shape + (4,)) + else: + _logger.warning( + 'matplotlib %s does not support transparent ' + 'colormap.', matplotlib.__version__) + + scalarMappable = getScalarMappable(colormap, data) + rgbaImage = scalarMappable.to_rgba(data, bytes=True) + + return rgbaImage + + +def getSupportedColormaps(): + """Get the supported colormap names as a tuple of str. + """ + colormaps = set(matplotlib.cm.datad.keys()) + colormaps.update(_AVAILABLE_AS_BUILTINS) + colormaps.update(_AVAILABLE_AS_RESOURCE) + return tuple(sorted(colormaps)) diff --git a/silx/gui/plot/matplotlib/ModestImage.py b/silx/gui/plot/matplotlib/ModestImage.py new file mode 100644 index 0000000..e4a72d5 --- /dev/null +++ b/silx/gui/plot/matplotlib/ModestImage.py @@ -0,0 +1,174 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################*/ +"""Matplotlib computationally modest image class.""" + +__authors__ = ["V.A. Sole", "T. Vincent"] +__license__ = "MIT" +__date__ = "03/05/2017" + + +import numpy + +from matplotlib import cbook +from matplotlib.image import AxesImage + + +class ModestImage(AxesImage): + """Computationally modest image class. + +Customization of https://github.com/ChrisBeaumont/ModestImage to allow +extent support. + +ModestImage is an extension of the Matplotlib AxesImage class +better suited for the interactive display of larger images. Before +drawing, ModestImage resamples the data array based on the screen +resolution and view window. This has very little affect on the +appearance of the image, but can substantially cut down on +computation since calculations of unresolved or clipped pixels +are skipped. + +The interface of ModestImage is the same as AxesImage. However, it +does not currently support setting the 'extent' property. There +may also be weird coordinate warping operations for images that +I'm not aware of. Don't expect those to work either. +""" + def __init__(self, *args, **kwargs): + self._full_res = None + self._sx, self._sy = None, None + self._bounds = (None, None, None, None) + self._origExtent = None + super(ModestImage, self).__init__(*args, **kwargs) + if 'extent' in kwargs and kwargs['extent'] is not None: + self.set_extent(kwargs['extent']) + + def set_extent(self, extent): + super(ModestImage, self).set_extent(extent) + if self._origExtent is None: + self._origExtent = self.get_extent() + + def get_image_extent(self): + """Returns the extent of the whole image. + + get_extent returns the extent of the drawn area and not of the full + image. + + :return: Bounds of the image (x0, x1, y0, y1). + :rtype: Tuple of 4 floats. + """ + if self._origExtent is not None: + return self._origExtent + else: + return self.get_extent() + + def set_data(self, A): + """ + Set the image array + + ACCEPTS: numpy/PIL Image A + """ + + self._full_res = A + self._A = A + + if (self._A.dtype != numpy.uint8 and + not numpy.can_cast(self._A.dtype, numpy.float)): + raise TypeError("Image data can not convert to float") + + if (self._A.ndim not in (2, 3) or + (self._A.ndim == 3 and self._A.shape[-1] not in (3, 4))): + raise TypeError("Invalid dimensions for image data") + + self._imcache = None + self._rgbacache = None + self._oldxslice = None + self._oldyslice = None + self._sx, self._sy = None, None + + def get_array(self): + """Override to return the full-resolution array""" + return self._full_res + + def _scale_to_res(self): + """ Change self._A and _extent to render an image whose +resolution is matched to the eventual rendering.""" + # extent has to be set BEFORE set_data + if self._origExtent is None: + if self.origin == "upper": + self._origExtent = (0, self._full_res.shape[1], + self._full_res.shape[0], 0) + else: + self._origExtent = (0, self._full_res.shape[1], + 0, self._full_res.shape[0]) + + if self.origin == "upper": + origXMin, origXMax, origYMax, origYMin = self._origExtent[0:4] + else: + origXMin, origXMax, origYMin, origYMax = self._origExtent[0:4] + ax = self.axes + ext = ax.transAxes.transform([1, 1]) - ax.transAxes.transform([0, 0]) + xlim, ylim = ax.get_xlim(), ax.get_ylim() + xlim = max(xlim[0], origXMin), min(xlim[1], origXMax) + if ylim[0] > ylim[1]: + ylim = max(ylim[1], origYMin), min(ylim[0], origYMax) + else: + ylim = max(ylim[0], origYMin), min(ylim[1], origYMax) + # print("THOSE LIMITS ARE TO BE COMPARED WITH THE EXTENT") + # print("IN ORDER TO KNOW WHAT IT IS LIMITING THE DISPLAY") + # print("IF THE AXES OR THE EXTENT") + dx, dy = xlim[1] - xlim[0], ylim[1] - ylim[0] + + y0 = max(0, ylim[0] - 5) + y1 = min(self._full_res.shape[0], ylim[1] + 5) + x0 = max(0, xlim[0] - 5) + x1 = min(self._full_res.shape[1], xlim[1] + 5) + y0, y1, x0, x1 = [int(a) for a in [y0, y1, x0, x1]] + + sy = int(max(1, min((y1 - y0) / 5., numpy.ceil(dy / ext[1])))) + sx = int(max(1, min((x1 - x0) / 5., numpy.ceil(dx / ext[0])))) + + # have we already calculated what we need? + if (self._sx is not None) and (self._sy is not None): + if (sx >= self._sx and sy >= self._sy and + x0 >= self._bounds[0] and x1 <= self._bounds[1] and + y0 >= self._bounds[2] and y1 <= self._bounds[3]): + return + + self._A = self._full_res[y0:y1:sy, x0:x1:sx] + self._A = cbook.safe_masked_invalid(self._A) + x1 = x0 + self._A.shape[1] * sx + y1 = y0 + self._A.shape[0] * sy + + if self.origin == "upper": + self.set_extent([x0, x1, y1, y0]) + else: + self.set_extent([x0, x1, y0, y1]) + self._sx = sx + self._sy = sy + self._bounds = (x0, x1, y0, y1) + self.changed() + + def draw(self, renderer, *args, **kwargs): + self._scale_to_res() + super(ModestImage, self).draw(renderer, *args, **kwargs) diff --git a/silx/gui/plot/matplotlib/__init__.py b/silx/gui/plot/matplotlib/__init__.py new file mode 100644 index 0000000..be9cb9a --- /dev/null +++ b/silx/gui/plot/matplotlib/__init__.py @@ -0,0 +1,70 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This module inits matplotlib and setups the backend to use. + +It MUST be imported prior to any other import of matplotlib. + +It provides the matplotlib :class:`FigureCanvasQTAgg` class corresponding +to the used backend. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "04/05/2017" + + +import sys +import logging + + +_logger = logging.getLogger(__name__) + +if 'matplotlib' in sys.modules: + _logger.warning( + 'matplotlib already loaded, setting its backend may not work') + + +from ... import qt + +import matplotlib + +if qt.BINDING == 'PySide': + matplotlib.rcParams['backend'] = 'Qt4Agg' + matplotlib.rcParams['backend.qt4'] = 'PySide' + import matplotlib.backends.backend_qt4agg as backend + +elif qt.BINDING == 'PyQt4': + matplotlib.rcParams['backend'] = 'Qt4Agg' + import matplotlib.backends.backend_qt4agg as backend + +elif qt.BINDING == 'PyQt5': + matplotlib.rcParams['backend'] = 'Qt5Agg' + import matplotlib.backends.backend_qt5agg as backend + +else: + backend = None + +if backend is not None: + FigureCanvasQTAgg = backend.FigureCanvasQTAgg # noqa |