summaryrefslogtreecommitdiff
path: root/silx/gui/plot/matplotlib
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/matplotlib')
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py282
-rw-r--r--silx/gui/plot/matplotlib/ModestImage.py174
-rw-r--r--silx/gui/plot/matplotlib/__init__.py70
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