summaryrefslogtreecommitdiff
path: root/silx/gui/plot/items/image.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/items/image.py')
-rw-r--r--silx/gui/plot/items/image.py617
1 files changed, 0 insertions, 617 deletions
diff --git a/silx/gui/plot/items/image.py b/silx/gui/plot/items/image.py
deleted file mode 100644
index 0d9c9a4..0000000
--- a/silx/gui/plot/items/image.py
+++ /dev/null
@@ -1,617 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 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 provides the :class:`ImageData` and :class:`ImageRgba` items
-of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import logging
-
-import numpy
-
-from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
-
-_logger = logging.getLogger(__name__)
-
-
-def _convertImageToRgba32(image, copy=True):
- """Convert an RGB or RGBA image to RGBA32.
-
- It converts from floats in [0, 1], bool, integer and uint in [0, 255]
-
- If the input image is already an RGBA32 image,
- the returned image shares the same data.
-
- :param image: Image to convert to
- :type image: numpy.ndarray with 3 dimensions: height, width, color channels
- :param bool copy: True (Default) to get a copy, False, avoid copy if possible
- :return: The image converted to RGBA32 with dimension: (height, width, 4)
- :rtype: numpy.ndarray of uint8
- """
- assert image.ndim == 3
- assert image.shape[-1] in (3, 4)
-
- # Convert type to uint8
- if image.dtype.name != 'uint8':
- if image.dtype.kind == 'f': # Float in [0, 1]
- image = (numpy.clip(image, 0., 1.) * 255).astype(numpy.uint8)
- elif image.dtype.kind == 'b': # boolean
- image = image.astype(numpy.uint8) * 255
- elif image.dtype.kind in ('i', 'u'): # int, uint
- image = numpy.clip(image, 0, 255).astype(numpy.uint8)
- else:
- raise ValueError('Unsupported image dtype: %s', image.dtype.name)
- copy = False # A copy as already been done, avoid next one
-
- # Convert RGB to RGBA
- if image.shape[-1] == 3:
- new_image = numpy.empty((image.shape[0], image.shape[1], 4),
- dtype=numpy.uint8)
- new_image[:,:,:3] = image
- new_image[:,:, 3] = 255
- return new_image # This is a copy anyway
- else:
- return numpy.array(image, copy=copy)
-
-
-class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
- """Description of an image
-
- :param numpy.ndarray data: Initial image data
- """
-
- def __init__(self, data=None, mask=None):
- DataItem.__init__(self)
- LabelsMixIn.__init__(self)
- DraggableMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- if data is None:
- data = numpy.zeros((0, 0, 4), dtype=numpy.uint8)
- self._data = data
- self._mask = mask
- self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if isinstance(item, slice):
- return [self[index] for index in range(*item.indices(5))]
- elif item == 0:
- return self.getData(copy=False)
- elif item == 1:
- return self.getName()
- elif item == 2:
- info = self.getInfo(copy=False)
- return {} if info is None else info
- elif item == 3:
- return None
- elif item == 4:
- params = {
- 'info': self.getInfo(),
- 'origin': self.getOrigin(),
- 'scale': self.getScale(),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'draggable': self.isDraggable(),
- 'colormap': None,
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- }
- return params
- else:
- raise IndexError("Index out of range: %s" % str(item))
-
- def _isPlotLinear(self, plot):
- """Return True if plot only uses linear scale for both of x and y
- axes."""
- linear = plot.getXAxis().LINEAR
- if plot.getXAxis().getScale() != linear:
- return False
- if plot.getYAxis().getScale() != linear:
- return False
- return True
-
- def _getBounds(self):
- if self.getData(copy=False).size == 0: # Empty data
- return None
-
- height, width = self.getData(copy=False).shape[:2]
- origin = self.getOrigin()
- scale = self.getScale()
- # Taking care of scale might be < 0
- xmin, xmax = origin[0], origin[0] + width * scale[0]
- if xmin > xmax:
- xmin, xmax = xmax, xmin
- # Taking care of scale might be < 0
- ymin, ymax = origin[1], origin[1] + height * scale[1]
- if ymin > ymax:
- ymin, ymax = ymax, ymin
-
- plot = self.getPlot()
- if plot is not None and not self._isPlotLinear(plot):
- return None
- else:
- return xmin, xmax, ymin, ymax
-
- @docstring(DraggableMixIn)
- def drag(self, from_, to):
- origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
-
- def getData(self, copy=True):
- """Returns the image data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._data, copy=copy)
-
- def setData(self, data):
- """Set the image data
-
- :param numpy.ndarray data:
- """
- previousShape = self._data.shape
- self._data = data
- self._valueDataChanged()
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
- # Data shape changed, so mask shape changes.
- # Send event, mask is lazily updated in getMaskData
- self._updated(ItemChangedType.MASK)
-
- def getMaskData(self, copy=True):
- """Returns the mask data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._mask is None:
- return None
-
- # Update mask if it does not match data shape
- shape = self.getData(copy=False).shape[:2]
- if self._mask.shape != shape:
- # Clip/extend mask to match data
- newMask = numpy.zeros(shape, dtype=self._mask.dtype)
- newMask[:self._mask.shape[0], :self._mask.shape[1]] = self._mask[:shape[0], :shape[1]]
- self._mask = newMask
-
- return numpy.array(self._mask, copy=copy)
-
- def setMaskData(self, mask, copy=True):
- """Set the image data
-
- :param numpy.ndarray data:
- :param bool copy: True (Default) to make a copy,
- False to use as is (do not modify!)
- """
- if mask is not None:
- mask = numpy.array(mask, copy=copy)
-
- shape = self.getData(copy=False).shape[:2]
- if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", mask.shape, shape)
- # Clip/extent is done lazily in getMaskData
- elif self._mask is None:
- return # No update
-
- self._mask = mask
- self._valueDataChanged()
- self._updated(ItemChangedType.MASK)
-
- def _valueDataChanged(self):
- """Clear cache of default data array"""
- self.__valueDataCache = None
-
- def _getValueData(self, copy=True):
- """Return data used by :meth:`getValueData`
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- return self.getData(copy=copy)
-
- def getValueData(self, copy=True):
- """Return data (converted to int or float) with mask applied.
-
- Masked values are set to Not-A-Number.
- It returns a 2D array of values (int or float).
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- if self.__valueDataCache is None:
- data = self._getValueData(copy=False)
- mask = self.getMaskData(copy=False)
- if mask is not None:
- if numpy.issubdtype(data.dtype, numpy.floating):
- dtype = data.dtype
- else:
- dtype = numpy.float64
- data = numpy.array(data, dtype=dtype, copy=True)
- data[mask != 0] = numpy.NaN
- self.__valueDataCache = data
- return numpy.array(self.__valueDataCache, copy=copy)
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- raise NotImplementedError('This MUST be implemented in sub-class')
-
- def getOrigin(self):
- """Returns the offset from origin at which to display the image.
-
- :rtype: 2-tuple of float
- """
- return self._origin
-
- def setOrigin(self, origin):
- """Set the offset from origin at which to display the image.
-
- :param origin: (ox, oy) Offset from origin
- :type origin: float or 2-tuple of float
- """
- if isinstance(origin, abc.Sequence):
- origin = float(origin[0]), float(origin[1])
- else: # single value origin
- origin = float(origin), float(origin)
- if origin != self._origin:
- self._origin = origin
- self._boundsChanged()
- self._updated(ItemChangedType.POSITION)
-
- def getScale(self):
- """Returns the scale of the image in data coordinates.
-
- :rtype: 2-tuple of float
- """
- return self._scale
-
- def setScale(self, scale):
- """Set the scale of the image
-
- :param scale: (sx, sy) Scale of the image
- :type scale: float or 2-tuple of float
- """
- if isinstance(scale, abc.Sequence):
- scale = float(scale[0]), float(scale[1])
- else: # single value scale
- scale = float(scale), float(scale)
-
- if scale != self._scale:
- self._scale = scale
- self._boundsChanged()
- self._updated(ItemChangedType.SCALE)
-
-
-class ImageData(ImageBase, ColormapMixIn):
- """Description of a data image with a colormap"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0), dtype=numpy.float32))
- ColormapMixIn.__init__(self)
- self._alternativeImage = None
- self.__alpha = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- if (self.getAlternativeImageData(copy=False) is not None or
- self.getAlphaData(copy=False) is not None):
- dataToUse = self.getRgbaImageData(copy=False)
- else:
- dataToUse = self.getData(copy=False)
-
- if dataToUse.size == 0:
- return None # No data to display
-
- colormap = self.getColormap()
- if colormap.isAutoscale():
- # Avoid backend to compute autoscale: use item cache
- colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
-
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if item == 3:
- return self.getAlternativeImageData(copy=False)
-
- params = ImageBase.__getitem__(self, item)
- if item == 4:
- params['colormap'] = self.getColormap()
-
- return params
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: Array of uint8 of shape (height, width, 4)
- :rtype: numpy.ndarray
- """
- alternative = self.getAlternativeImageData(copy=False)
- if alternative is not None:
- return _convertImageToRgba32(alternative, copy=copy)
- else:
- # Apply colormap, in this case an new array is always returned
- colormap = self.getColormap()
- image = colormap.applyToData(self)
- alphaImage = self.getAlphaData(copy=False)
- if alphaImage is not None:
- # Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
- return image
-
- def getAlternativeImageData(self, copy=True):
- """Get the optional RGBA image that is displayed instead of the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._alternativeImage is None:
- return None
- else:
- return numpy.array(self._alternativeImage, copy=copy)
-
- def getAlphaData(self, copy=True):
- """Get the optional transparency image applied on the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self.__alpha is None:
- return None
- else:
- return numpy.array(self.__alpha, copy=copy)
-
- def setData(self, data, alternative=None, alpha=None, copy=True):
- """"Set the image data and optionally an alternative RGB(A) representation
-
- :param numpy.ndarray data: Data array with 2 dimensions (h, w)
- :param alternative: RGB(A) image to display instead of data,
- shape: (h, w, 3 or 4)
- :type alternative: Union[None,numpy.ndarray]
- :param alpha: An array of transparency value in [0, 1] to use for
- display with shape: (h, w)
- :type alpha: Union[None,numpy.ndarray]
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
- if data.dtype.kind == 'b':
- _logger.warning(
- 'Converting boolean image to int8 to plot it.')
- data = numpy.array(data, copy=False, dtype=numpy.int8)
- elif numpy.iscomplexobj(data):
- _logger.warning(
- 'Converting complex image to absolute value to plot it.')
- data = numpy.absolute(data)
-
- if alternative is not None:
- alternative = numpy.array(alternative, copy=copy)
- assert alternative.ndim == 3
- assert alternative.shape[2] in (3, 4)
- assert alternative.shape[:2] == data.shape[:2]
- self._alternativeImage = alternative
-
- if alpha is not None:
- alpha = numpy.array(alpha, copy=copy)
- assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
- alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
- self.__alpha = alpha
-
- super().setData(data)
-
- def _updated(self, event=None, checkVisibility=True):
- # Synchronizes colormapped data if changed
- if event in (ItemChangedType.DATA, ItemChangedType.MASK):
- self._setColormappedData(
- self.getValueData(copy=False),
- copy=False)
- super()._updated(event=event, checkVisibility=checkVisibility)
-
-
-class ImageRgba(ImageBase):
- """Description of an RGB(A) image"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0, 4), dtype=numpy.uint8))
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=None,
- alpha=self.getAlpha())
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- return _convertImageToRgba32(self.getData(copy=False), copy=copy)
-
- def setData(self, data, copy=True):
- """Set the image data
-
- :param data: RGB(A) image data to set
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
- super().setData(data)
-
- def _getValueData(self, copy=True):
- """Compute the intensity of the RGBA image as default data.
-
- Conversion: https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
-
- :param bool copy:
- """
- rgba = self.getRgbaImageData(copy=False).astype(numpy.float32)
- intensity = (rgba[:, :, 0] * 0.299 +
- rgba[:, :, 1] * 0.587 +
- rgba[:, :, 2] * 0.114)
- intensity *= rgba[:, :, 3] / 255.
- return intensity
-
-
-class MaskImageData(ImageData):
- """Description of an image used as a mask.
-
- This class is used to flag mask items. This information is used to improve
- internal silx widgets.
- """
- pass
-
-
-class ImageStack(ImageData):
- """Item to store a stack of images and to show it in the plot as one
- of the images of the stack.
-
- The stack is a 3D array ordered this way: `frame id, y, x`.
- So the first image of the stack can be reached this way: `stack[0, :, :]`
- """
-
- def __init__(self):
- ImageData.__init__(self)
- self.__stack = None
- """A 3D numpy array (or a mimic one, see ListOfImages)"""
- self.__stackPosition = None
- """Displayed position in the cube"""
-
- def setStackData(self, stack, position=None, copy=True):
- """Set the stack data
-
- :param stack: A 3D numpy array like
- :param int position: The position of the displayed image in the stack
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if self.__stack is stack:
- return
- if copy:
- stack = numpy.array(stack)
- assert stack.ndim == 3
- self.__stack = stack
- if position is not None:
- self.__stackPosition = position
- if self.__stackPosition is None:
- self.__stackPosition = 0
- self.__updateDisplayedData()
-
- def getStackData(self, copy=True):
- """Get the stored stack array.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: A 3D numpy array, or numpy array like
- """
- if copy:
- return numpy.array(self.__stack)
- else:
- return self.__stack
-
- def setStackPosition(self, pos):
- """Set the displayed position on the stack.
-
- This function will clamp the stack position according to
- the real size of the first axis of the stack.
-
- :param int pos: A position on the first axis of the stack.
- """
- if self.__stackPosition == pos:
- return
- self.__stackPosition = pos
- self.__updateDisplayedData()
-
- def getStackPosition(self):
- """Get the displayed position of the stack.
-
- :rtype: int
- """
- return self.__stackPosition
-
- def __updateDisplayedData(self):
- """Update the displayed frame whenever the stack or the stack
- position are updated."""
- if self.__stack is None or self.__stackPosition is None:
- empty = numpy.array([]).reshape(0, 0)
- self.setData(empty, copy=False)
- return
- size = len(self.__stack)
- self.__stackPosition = numpy.clip(self.__stackPosition, 0, size)
- self.setData(self.__stack[self.__stackPosition], copy=False)