fixed bug in graphicsItems/ImageItem.py: degenerate images (max==min) would raise exception in getHistogram()

This commit is contained in:
Jim Crowell 2018-10-10 10:29:16 -04:00
parent 574c5f3a47
commit d261c2f0f2

View File

@ -16,23 +16,23 @@ __all__ = ['ImageItem']
class ImageItem(GraphicsObject): class ImageItem(GraphicsObject):
""" """
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
GraphicsObject displaying an image. Optimized for rapid update (ie video display). GraphicsObject displaying an image. Optimized for rapid update (ie video display).
This item displays either a 2D numpy array (height, width) or This item displays either a 2D numpy array (height, width) or
a 3D array (height, width, RGBa). This array is optionally scaled (see a 3D array (height, width, RGBa). This array is optionally scaled (see
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored :func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`) with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
before being displayed. before being displayed.
ImageItem is frequently used in conjunction with ImageItem is frequently used in conjunction with
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI :class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
for controlling the levels and lookup table used to display the image. for controlling the levels and lookup table used to display the image.
""" """
sigImageChanged = QtCore.Signal() sigImageChanged = QtCore.Signal()
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
def __init__(self, image=None, **kargs): def __init__(self, image=None, **kargs):
""" """
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments. See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
@ -41,23 +41,23 @@ class ImageItem(GraphicsObject):
self.menu = None self.menu = None
self.image = None ## original image data self.image = None ## original image data
self.qimage = None ## rendered image for display self.qimage = None ## rendered image for display
self.paintMode = None self.paintMode = None
self.levels = None ## [min, max] or [[redMin, redMax], ...] self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None self.lut = None
self.autoDownsample = False self.autoDownsample = False
self.axisOrder = getConfigOption('imageAxisOrder') self.axisOrder = getConfigOption('imageAxisOrder')
# In some cases, we use a modified lookup table to handle both rescaling # In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently # and LUT more efficiently
self._effectiveLut = None self._effectiveLut = None
self.drawKernel = None self.drawKernel = None
self.border = None self.border = None
self.removable = False self.removable = False
if image is not None: if image is not None:
self.setImage(image, **kargs) self.setImage(image, **kargs)
else: else:
@ -66,32 +66,32 @@ class ImageItem(GraphicsObject):
def setCompositionMode(self, mode): def setCompositionMode(self, mode):
"""Change the composition mode of the item (see QPainter::CompositionMode """Change the composition mode of the item (see QPainter::CompositionMode
in the Qt documentation). This is useful when overlaying multiple ImageItems. in the Qt documentation). This is useful when overlaying multiple ImageItems.
============================================ ============================================================ ============================================ ============================================================
**Most common arguments:** **Most common arguments:**
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
is opaque. Otherwise, it uses the alpha channel to blend is opaque. Otherwise, it uses the alpha channel to blend
the image with the background. the image with the background.
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
reflect the lightness or darkness of the background. reflect the lightness or darkness of the background.
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
are added together. are added together.
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
============================================ ============================================================ ============================================ ============================================================
""" """
self.paintMode = mode self.paintMode = mode
self.update() self.update()
def setBorder(self, b): def setBorder(self, b):
self.border = fn.mkPen(b) self.border = fn.mkPen(b)
self.update() self.update()
def width(self): def width(self):
if self.image is None: if self.image is None:
return None return None
axis = 0 if self.axisOrder == 'col-major' else 1 axis = 0 if self.axisOrder == 'col-major' else 1
return self.image.shape[axis] return self.image.shape[axis]
def height(self): def height(self):
if self.image is None: if self.image is None:
return None return None
@ -111,10 +111,10 @@ class ImageItem(GraphicsObject):
def setLevels(self, levels, update=True): def setLevels(self, levels, update=True):
""" """
Set image scaling levels. Can be one of: Set image scaling levels. Can be one of:
* [blackLevel, whiteLevel] * [blackLevel, whiteLevel]
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] * [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>` Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
for more details on how levels are applied. for more details on how levels are applied.
""" """
@ -125,18 +125,18 @@ class ImageItem(GraphicsObject):
self._effectiveLut = None self._effectiveLut = None
if update: if update:
self.updateImage() self.updateImage()
def getLevels(self): def getLevels(self):
return self.levels return self.levels
#return self.whiteLevel, self.blackLevel #return self.whiteLevel, self.blackLevel
def setLookupTable(self, lut, update=True): def setLookupTable(self, lut, update=True):
""" """
Set the lookup table (numpy array) to use for this image. (see Set the lookup table (numpy array) to use for this image. (see
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used). :func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
Optionally, lut can be a callable that accepts the current image as an Optionally, lut can be a callable that accepts the current image as an
argument and returns the lookup table to use. argument and returns the lookup table to use.
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`. or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
""" """
@ -149,7 +149,7 @@ class ImageItem(GraphicsObject):
def setAutoDownsample(self, ads): def setAutoDownsample(self, ads):
""" """
Set the automatic downsampling mode for this ImageItem. Set the automatic downsampling mode for this ImageItem.
Added in version 0.9.9 Added in version 0.9.9
""" """
self.autoDownsample = ads self.autoDownsample = ads
@ -198,44 +198,44 @@ class ImageItem(GraphicsObject):
""" """
Update the image displayed by this item. For more information on how the image Update the image displayed by this item. For more information on how the image
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>` is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
================= ========================================================================= ================= =========================================================================
**Arguments:** **Arguments:**
image (numpy array) Specifies the image data. May be 2D (width, height) or image (numpy array) Specifies the image data. May be 2D (width, height) or
3D (width, height, RGBa). The array dtype must be integer or floating 3D (width, height, RGBa). The array dtype must be integer or floating
point of any bit depth. For 3D arrays, the third dimension must point of any bit depth. For 3D arrays, the third dimension must
be of length 3 (RGB) or 4 (RGBA). See *notes* below. be of length 3 (RGB) or 4 (RGBA). See *notes* below.
autoLevels (bool) If True, this forces the image to automatically select autoLevels (bool) If True, this forces the image to automatically select
levels based on the maximum and minimum values in the data. levels based on the maximum and minimum values in the data.
By default, this argument is true unless the levels argument is By default, this argument is true unless the levels argument is
given. given.
lut (numpy array) The color lookup table to use when displaying the image. lut (numpy array) The color lookup table to use when displaying the image.
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`. See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
levels (min, max) The minimum and maximum values to use when rescaling the image levels (min, max) The minimum and maximum values to use when rescaling the image
data. By default, this will be set to the minimum and maximum values data. By default, this will be set to the minimum and maximum values
in the image. If the image array has dtype uint8, no rescaling is necessary. in the image. If the image array has dtype uint8, no rescaling is necessary.
opacity (float 0.0-1.0) opacity (float 0.0-1.0)
compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>` compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
border Sets the pen used when drawing the image border. Default is None. border Sets the pen used when drawing the image border. Default is None.
autoDownsample (bool) If True, the image is automatically downsampled to match the autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and screen resolution. This improves performance for large images and
reduces aliasing. If autoDownsample is not specified, then ImageItem will reduces aliasing. If autoDownsample is not specified, then ImageItem will
choose whether to downsample the image based on its size. choose whether to downsample the image based on its size.
================= ========================================================================= ================= =========================================================================
**Notes:** **Notes:**
For backward compatibility, image data is assumed to be in column-major order (column, row). For backward compatibility, image data is assumed to be in column-major order (column, row).
However, most image data is stored in row-major order (row, column) and will need to be However, most image data is stored in row-major order (row, column) and will need to be
transposed before calling setImage():: transposed before calling setImage()::
imageitem.setImage(imagedata.T) imageitem.setImage(imagedata.T)
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`. by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
""" """
profile = debug.Profiler() profile = debug.Profiler()
@ -292,7 +292,7 @@ class ImageItem(GraphicsObject):
def dataTransform(self): def dataTransform(self):
"""Return the transform that maps from this image's input array to its """Return the transform that maps from this image's input array to its
local coordinate system. local coordinate system.
This transform corrects for the transposition that occurs when image data This transform corrects for the transposition that occurs when image data
is interpreted in row-major order. is interpreted in row-major order.
""" """
@ -307,7 +307,7 @@ class ImageItem(GraphicsObject):
def inverseDataTransform(self): def inverseDataTransform(self):
"""Return the transform that maps from this image's local coordinate """Return the transform that maps from this image's local coordinate
system to its input array. system to its input array.
See dataTransform() for more information. See dataTransform() for more information.
""" """
tr = QtGui.QTransform() tr = QtGui.QTransform()
@ -339,7 +339,7 @@ class ImageItem(GraphicsObject):
def updateImage(self, *args, **kargs): def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image. ## used for re-rendering qimage from self.image.
## can we make any assumptions here that speed things up? ## can we make any assumptions here that speed things up?
## dtype, range, size are all the same? ## dtype, range, size are all the same?
defaults = { defaults = {
@ -350,11 +350,11 @@ class ImageItem(GraphicsObject):
def render(self): def render(self):
# Convert data to QImage for display. # Convert data to QImage for display.
profile = debug.Profiler() profile = debug.Profiler()
if self.image is None or self.image.size == 0: if self.image is None or self.image.size == 0:
return return
# Request a lookup table if this image has only one channel # Request a lookup table if this image has only one channel
if self.image.ndim == 2 or self.image.shape[2] == 1: if self.image.ndim == 2 or self.image.shape[2] == 1:
if isinstance(self.lut, collections.Callable): if isinstance(self.lut, collections.Callable):
@ -385,7 +385,7 @@ class ImageItem(GraphicsObject):
image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1]) image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds) self._lastDownsample = (xds, yds)
# Check if downsampling reduced the image size to zero due to inf values. # Check if downsampling reduced the image size to zero due to inf values.
if image.size == 0: if image.size == 0:
return return
@ -403,27 +403,27 @@ class ImageItem(GraphicsObject):
levdiff = maxlev - minlev levdiff = maxlev - minlev
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
if lut is None: if lut is None:
efflut = fn.rescaleData(ind, scale=255./levdiff, efflut = fn.rescaleData(ind, scale=255./levdiff,
offset=minlev, dtype=np.ubyte) offset=minlev, dtype=np.ubyte)
else: else:
lutdtype = np.min_scalar_type(lut.shape[0]-1) lutdtype = np.min_scalar_type(lut.shape[0]-1)
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff, efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1)) offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
efflut = lut[efflut] efflut = lut[efflut]
self._effectiveLut = efflut self._effectiveLut = efflut
lut = self._effectiveLut lut = self._effectiveLut
levels = None levels = None
# Convert single-channel image to 2D array # Convert single-channel image to 2D array
if image.ndim == 3 and image.shape[-1] == 1: if image.ndim == 3 and image.shape[-1] == 1:
image = image[..., 0] image = image[..., 0]
# Assume images are in column-major order for backward compatibility # Assume images are in column-major order for backward compatibility
# (most images are in row-major order) # (most images are in row-major order)
if self.axisOrder == 'col-major': if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim]) image = image.transpose((1, 0, 2)[:image.ndim])
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False) self.qimage = fn.makeQImage(argb, alpha, transpose=False)
@ -453,26 +453,26 @@ class ImageItem(GraphicsObject):
self.render() self.render()
self.qimage.save(fileName, *args) self.qimage.save(fileName, *args)
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
targetHistogramSize=500, **kwds): targetHistogramSize=500, **kwds):
"""Returns x and y arrays containing the histogram values for the current image. """Returns x and y arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram(). For an explanation of the return format, see numpy.histogram().
The *step* argument causes pixels to be skipped when computing the histogram to save time. The *step* argument causes pixels to be skipped when computing the histogram to save time.
If *step* is 'auto', then a step is chosen such that the analyzed data has If *step* is 'auto', then a step is chosen such that the analyzed data has
dimensions roughly *targetImageSize* for each axis. dimensions roughly *targetImageSize* for each axis.
The *bins* argument and any extra keyword arguments are passed to The *bins* argument and any extra keyword arguments are passed to
np.histogram(). If *bins* is 'auto', then a bin number is automatically np.histogram(). If *bins* is 'auto', then a bin number is automatically
chosen based on the image characteristics: chosen based on the image characteristics:
* Integer images will have approximately *targetHistogramSize* bins, * Integer images will have approximately *targetHistogramSize* bins,
with each bin having an integer width. with each bin having an integer width.
* All other types will have *targetHistogramSize* bins. * All other types will have *targetHistogramSize* bins.
If *perChannel* is True, then the histogram is computed once per channel If *perChannel* is True, then the histogram is computed once per channel
and the output is a list of the results. and the output is a list of the results.
This method is also used when automatically computing levels. This method is also used when automatically computing levels.
""" """
if self.image is None or self.image.size == 0: if self.image is None or self.image.size == 0:
@ -483,10 +483,13 @@ class ImageItem(GraphicsObject):
if np.isscalar(step): if np.isscalar(step):
step = (step, step) step = (step, step)
stepData = self.image[::step[0], ::step[1]] stepData = self.image[::step[0], ::step[1]]
if bins == 'auto': if bins == 'auto':
mn = np.nanmin(stepData) mn = np.nanmin(stepData)
mx = np.nanmax(stepData) mx = np.nanmax(stepData)
if mx == mn:
# degenerate image, arange will fail
mx += 1
if np.isnan(mn) or np.isnan(mx): if np.isnan(mn) or np.isnan(mx):
# the data are all-nan # the data are all-nan
return None, None return None, None
@ -497,7 +500,7 @@ class ImageItem(GraphicsObject):
else: else:
# for float data, let numpy select the bins. # for float data, let numpy select the bins.
bins = np.linspace(mn, mx, 500) bins = np.linspace(mn, mx, 500)
if len(bins) == 0: if len(bins) == 0:
bins = [mn, mx] bins = [mn, mx]
@ -524,7 +527,7 @@ class ImageItem(GraphicsObject):
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation) (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
""" """
self.setFlag(self.ItemIgnoresTransformations, b) self.setFlag(self.ItemIgnoresTransformations, b)
def setScaledMode(self): def setScaledMode(self):
self.setPxMode(False) self.setPxMode(False)
@ -534,14 +537,14 @@ class ImageItem(GraphicsObject):
if self.qimage is None: if self.qimage is None:
return None return None
return QtGui.QPixmap.fromImage(self.qimage) return QtGui.QPixmap.fromImage(self.qimage)
def pixelSize(self): def pixelSize(self):
"""return scene-size of a single pixel in the image""" """return scene-size of a single pixel in the image"""
br = self.sceneBoundingRect() br = self.sceneBoundingRect()
if self.image is None: if self.image is None:
return 1,1 return 1,1
return br.width()/self.width(), br.height()/self.height() return br.width()/self.width(), br.height()/self.height()
def viewTransformChanged(self): def viewTransformChanged(self):
if self.autoDownsample: if self.autoDownsample:
self.qimage = None self.qimage = None
@ -582,7 +585,7 @@ class ImageItem(GraphicsObject):
self.menu.addAction(remAct) self.menu.addAction(remAct)
self.menu.remAct = remAct self.menu.remAct = remAct
return self.menu return self.menu
def hoverEvent(self, ev): def hoverEvent(self, ev):
if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
@ -595,7 +598,7 @@ class ImageItem(GraphicsObject):
#print(ev.device()) #print(ev.device())
#print(ev.pointerType()) #print(ev.pointerType())
#print(ev.pressure()) #print(ev.pressure())
def drawAt(self, pos, ev=None): def drawAt(self, pos, ev=None):
pos = [int(pos.x()), int(pos.y())] pos = [int(pos.x()), int(pos.y())]
dk = self.drawKernel dk = self.drawKernel
@ -604,7 +607,7 @@ class ImageItem(GraphicsObject):
sy = [0,dk.shape[1]] sy = [0,dk.shape[1]]
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
for i in [0,1]: for i in [0,1]:
dx1 = -min(0, tx[i]) dx1 = -min(0, tx[i])
dx2 = min(0, self.image.shape[0]-tx[i]) dx2 = min(0, self.image.shape[0]-tx[i])
@ -620,7 +623,7 @@ class ImageItem(GraphicsObject):
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
mask = self.drawMask mask = self.drawMask
src = dk src = dk
if isinstance(self.drawMode, collections.Callable): if isinstance(self.drawMode, collections.Callable):
self.drawMode(dk, self.image, mask, ss, ts, ev) self.drawMode(dk, self.image, mask, ss, ts, ev)
else: else:
@ -636,7 +639,7 @@ class ImageItem(GraphicsObject):
else: else:
raise Exception("Unknown draw mode '%s'" % self.drawMode) raise Exception("Unknown draw mode '%s'" % self.drawMode)
self.updateImage() self.updateImage()
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
self.drawKernel = kernel self.drawKernel = kernel
self.drawKernelCenter = center self.drawKernelCenter = center