2013-12-26 06:03:01 +00:00
|
|
|
from __future__ import division
|
|
|
|
|
2013-12-22 07:08:39 +00:00
|
|
|
from ..Qt import QtGui, QtCore
|
2012-03-02 02:55:32 +00:00
|
|
|
import numpy as np
|
2012-05-11 22:05:41 +00:00
|
|
|
import collections
|
2013-12-22 07:08:39 +00:00
|
|
|
from .. import functions as fn
|
|
|
|
from .. import debug as debug
|
2012-05-11 22:05:41 +00:00
|
|
|
from .GraphicsObject import GraphicsObject
|
2014-02-18 01:48:22 +00:00
|
|
|
from ..Point import Point
|
2016-04-28 05:33:51 +00:00
|
|
|
from .. import getConfigOption
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
__all__ = ['ImageItem']
|
2014-08-07 13:12:31 +00:00
|
|
|
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
class ImageItem(GraphicsObject):
|
|
|
|
"""
|
2012-04-16 20:45:55 +00:00
|
|
|
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
|
|
|
|
This item displays either a 2D numpy array (height, width) or
|
2018-10-10 14:29:16 +00:00
|
|
|
a 3D array (height, width, RGBa). This array is optionally scaled (see
|
2012-04-16 20:45:55 +00:00
|
|
|
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
|
|
|
|
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
|
|
|
|
before being displayed.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
|
|
|
ImageItem is frequently used in conjunction with
|
|
|
|
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
|
2012-04-16 20:45:55 +00:00
|
|
|
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
|
|
|
|
for controlling the levels and lookup table used to display the image.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
sigImageChanged = QtCore.Signal()
|
2012-05-30 03:18:34 +00:00
|
|
|
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def __init__(self, image=None, **kargs):
|
|
|
|
"""
|
2012-04-16 20:45:55 +00:00
|
|
|
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
|
|
|
GraphicsObject.__init__(self)
|
2012-09-11 14:26:34 +00:00
|
|
|
self.menu = None
|
2012-03-02 02:55:32 +00:00
|
|
|
self.image = None ## original image data
|
|
|
|
self.qimage = None ## rendered image for display
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
self.paintMode = None
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
|
|
|
self.lut = None
|
2013-11-20 18:51:39 +00:00
|
|
|
self.autoDownsample = False
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-06-16 15:54:52 +00:00
|
|
|
self.axisOrder = getConfigOption('imageAxisOrder')
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-01-30 08:10:25 +00:00
|
|
|
# In some cases, we use a modified lookup table to handle both rescaling
|
|
|
|
# and LUT more efficiently
|
|
|
|
self._effectiveLut = None
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
self.drawKernel = None
|
|
|
|
self.border = None
|
2012-05-30 03:18:34 +00:00
|
|
|
self.removable = False
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
if image is not None:
|
|
|
|
self.setImage(image, **kargs)
|
|
|
|
else:
|
|
|
|
self.setOpts(**kargs)
|
|
|
|
|
|
|
|
def setCompositionMode(self, mode):
|
2012-04-16 20:45:55 +00:00
|
|
|
"""Change the composition mode of the item (see QPainter::CompositionMode
|
|
|
|
in the Qt documentation). This is useful when overlaying multiple ImageItems.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
============================================ ============================================================
|
|
|
|
**Most common arguments:**
|
|
|
|
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
|
|
|
is opaque. Otherwise, it uses the alpha channel to blend
|
|
|
|
the image with the background.
|
2018-10-10 14:29:16 +00:00
|
|
|
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
2012-04-16 20:45:55 +00:00
|
|
|
reflect the lightness or darkness of the background.
|
2018-10-10 14:29:16 +00:00
|
|
|
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
2012-04-16 20:45:55 +00:00
|
|
|
are added together.
|
|
|
|
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
|
|
|
============================================ ============================================================
|
|
|
|
"""
|
2012-03-02 02:55:32 +00:00
|
|
|
self.paintMode = mode
|
|
|
|
self.update()
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def setBorder(self, b):
|
|
|
|
self.border = fn.mkPen(b)
|
|
|
|
self.update()
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def width(self):
|
|
|
|
if self.image is None:
|
|
|
|
return None
|
2016-06-16 15:54:52 +00:00
|
|
|
axis = 0 if self.axisOrder == 'col-major' else 1
|
2016-04-28 05:33:51 +00:00
|
|
|
return self.image.shape[axis]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def height(self):
|
|
|
|
if self.image is None:
|
|
|
|
return None
|
2016-06-16 15:54:52 +00:00
|
|
|
axis = 1 if self.axisOrder == 'col-major' else 0
|
2016-04-28 05:33:51 +00:00
|
|
|
return self.image.shape[axis]
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
def channels(self):
|
|
|
|
if self.image is None:
|
|
|
|
return None
|
|
|
|
return self.image.shape[2] if self.image.ndim == 3 else 1
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def boundingRect(self):
|
|
|
|
if self.image is None:
|
|
|
|
return QtCore.QRectF(0., 0., 0., 0.)
|
|
|
|
return QtCore.QRectF(0., 0., float(self.width()), float(self.height()))
|
|
|
|
|
|
|
|
def setLevels(self, levels, update=True):
|
|
|
|
"""
|
2012-04-16 20:45:55 +00:00
|
|
|
Set image scaling levels. Can be one of:
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
* [blackLevel, whiteLevel]
|
|
|
|
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
|
|
|
|
for more details on how levels are applied.
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2016-01-30 08:10:25 +00:00
|
|
|
if levels is not None:
|
|
|
|
levels = np.asarray(levels)
|
|
|
|
if not fn.eq(levels, self.levels):
|
|
|
|
self.levels = levels
|
|
|
|
self._effectiveLut = None
|
|
|
|
if update:
|
|
|
|
self.updateImage()
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def getLevels(self):
|
|
|
|
return self.levels
|
|
|
|
#return self.whiteLevel, self.blackLevel
|
|
|
|
|
|
|
|
def setLookupTable(self, lut, update=True):
|
|
|
|
"""
|
2018-10-10 14:29:16 +00:00
|
|
|
Set the lookup table (numpy array) to use for this image. (see
|
2012-04-16 20:45:55 +00:00
|
|
|
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
|
2018-10-10 14:29:16 +00:00
|
|
|
Optionally, lut can be a callable that accepts the current image as an
|
2012-04-16 20:45:55 +00:00
|
|
|
argument and returns the lookup table to use.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
|
|
|
|
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
|
|
|
"""
|
2016-01-30 08:10:25 +00:00
|
|
|
if lut is not self.lut:
|
|
|
|
self.lut = lut
|
|
|
|
self._effectiveLut = None
|
|
|
|
if update:
|
|
|
|
self.updateImage()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2013-11-20 18:51:39 +00:00
|
|
|
def setAutoDownsample(self, ads):
|
2014-04-11 14:54:21 +00:00
|
|
|
"""
|
|
|
|
Set the automatic downsampling mode for this ImageItem.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2014-04-11 14:54:21 +00:00
|
|
|
Added in version 0.9.9
|
|
|
|
"""
|
2013-11-20 18:51:39 +00:00
|
|
|
self.autoDownsample = ads
|
|
|
|
self.qimage = None
|
|
|
|
self.update()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def setOpts(self, update=True, **kargs):
|
2016-06-16 15:54:52 +00:00
|
|
|
if 'axisOrder' in kargs:
|
2016-08-31 22:15:44 +00:00
|
|
|
val = kargs['axisOrder']
|
|
|
|
if val not in ('row-major', 'col-major'):
|
|
|
|
raise ValueError('axisOrder must be either "row-major" or "col-major"')
|
|
|
|
self.axisOrder = val
|
2012-03-02 02:55:32 +00:00
|
|
|
if 'lut' in kargs:
|
|
|
|
self.setLookupTable(kargs['lut'], update=update)
|
|
|
|
if 'levels' in kargs:
|
|
|
|
self.setLevels(kargs['levels'], update=update)
|
|
|
|
#if 'clipLevel' in kargs:
|
|
|
|
#self.setClipLevel(kargs['clipLevel'])
|
|
|
|
if 'opacity' in kargs:
|
|
|
|
self.setOpacity(kargs['opacity'])
|
|
|
|
if 'compositionMode' in kargs:
|
|
|
|
self.setCompositionMode(kargs['compositionMode'])
|
|
|
|
if 'border' in kargs:
|
|
|
|
self.setBorder(kargs['border'])
|
2012-05-30 03:18:34 +00:00
|
|
|
if 'removable' in kargs:
|
|
|
|
self.removable = kargs['removable']
|
|
|
|
self.menu = None
|
2013-11-20 18:51:39 +00:00
|
|
|
if 'autoDownsample' in kargs:
|
|
|
|
self.setAutoDownsample(kargs['autoDownsample'])
|
|
|
|
if update:
|
|
|
|
self.update()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def setRect(self, rect):
|
2012-04-16 20:45:55 +00:00
|
|
|
"""Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
|
2012-03-02 02:55:32 +00:00
|
|
|
self.resetTransform()
|
|
|
|
self.translate(rect.left(), rect.top())
|
2012-03-17 16:10:51 +00:00
|
|
|
self.scale(rect.width() / self.width(), rect.height() / self.height())
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2014-08-07 13:03:26 +00:00
|
|
|
def clear(self):
|
|
|
|
self.image = None
|
|
|
|
self.prepareGeometryChange()
|
|
|
|
self.informViewBoundsChanged()
|
|
|
|
self.update()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def setImage(self, image=None, autoLevels=None, **kargs):
|
|
|
|
"""
|
2012-04-16 20:45:55 +00:00
|
|
|
Update the image displayed by this item. For more information on how the image
|
|
|
|
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
================= =========================================================================
|
|
|
|
**Arguments:**
|
2018-10-10 14:29:16 +00:00
|
|
|
image (numpy array) Specifies the image data. May be 2D (width, height) or
|
2012-04-16 20:45:55 +00:00
|
|
|
3D (width, height, RGBa). The array dtype must be integer or floating
|
|
|
|
point of any bit depth. For 3D arrays, the third dimension must
|
2016-04-28 05:33:51 +00:00
|
|
|
be of length 3 (RGB) or 4 (RGBA). See *notes* below.
|
2018-10-10 14:29:16 +00:00
|
|
|
autoLevels (bool) If True, this forces the image to automatically select
|
2012-04-16 20:45:55 +00:00
|
|
|
levels based on the maximum and minimum values in the data.
|
|
|
|
By default, this argument is true unless the levels argument is
|
|
|
|
given.
|
|
|
|
lut (numpy array) The color lookup table to use when displaying the image.
|
|
|
|
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
|
|
|
|
levels (min, max) The minimum and maximum values to use when rescaling the image
|
2018-10-10 14:29:16 +00:00
|
|
|
data. By default, this will be set to the minimum and maximum values
|
2012-04-16 20:45:55 +00:00
|
|
|
in the image. If the image array has dtype uint8, no rescaling is necessary.
|
|
|
|
opacity (float 0.0-1.0)
|
2016-04-28 05:33:51 +00:00
|
|
|
compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
2012-04-16 20:45:55 +00:00
|
|
|
border Sets the pen used when drawing the image border. Default is None.
|
2014-02-18 01:48:22 +00:00
|
|
|
autoDownsample (bool) If True, the image is automatically downsampled to match the
|
2018-10-10 14:29:16 +00:00
|
|
|
screen resolution. This improves performance for large images and
|
2017-07-14 22:10:04 +00:00
|
|
|
reduces aliasing. If autoDownsample is not specified, then ImageItem will
|
|
|
|
choose whether to downsample the image based on its size.
|
2012-04-16 20:45:55 +00:00
|
|
|
================= =========================================================================
|
2018-10-10 14:29:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
**Notes:**
|
|
|
|
|
2016-04-28 05:33:51 +00:00
|
|
|
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
|
|
|
|
transposed before calling setImage()::
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-04-28 05:33:51 +00:00
|
|
|
imageitem.setImage(imagedata.T)
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-06-16 15:54:52 +00:00
|
|
|
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
|
|
|
|
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2013-11-27 06:16:13 +00:00
|
|
|
profile = debug.Profiler()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
gotNewData = False
|
|
|
|
if image is None:
|
|
|
|
if self.image is None:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
gotNewData = True
|
2013-06-19 23:32:55 +00:00
|
|
|
shapeChanged = (self.image is None or image.shape != self.image.shape)
|
2016-01-30 08:10:25 +00:00
|
|
|
image = image.view(np.ndarray)
|
|
|
|
if self.image is None or image.dtype != self.image.dtype:
|
|
|
|
self._effectiveLut = None
|
|
|
|
self.image = image
|
2013-11-20 18:51:39 +00:00
|
|
|
if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
|
|
|
|
if 'autoDownsample' not in kargs:
|
|
|
|
kargs['autoDownsample'] = True
|
2013-06-19 23:32:55 +00:00
|
|
|
if shapeChanged:
|
|
|
|
self.prepareGeometryChange()
|
|
|
|
self.informViewBoundsChanged()
|
2013-11-27 06:16:13 +00:00
|
|
|
|
|
|
|
profile()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
if autoLevels is None:
|
|
|
|
if 'levels' in kargs:
|
|
|
|
autoLevels = False
|
|
|
|
else:
|
|
|
|
autoLevels = True
|
|
|
|
if autoLevels:
|
|
|
|
img = self.image
|
|
|
|
while img.size > 2**16:
|
|
|
|
img = img[::2, ::2]
|
2018-06-01 10:30:33 +00:00
|
|
|
mn, mx = np.nanmin(img), np.nanmax(img)
|
|
|
|
# mn and mx can still be NaN if the data is all-NaN
|
|
|
|
if mn == mx or np.isnan(mn) or np.isnan(mx):
|
2012-03-02 02:55:32 +00:00
|
|
|
mn = 0
|
|
|
|
mx = 255
|
|
|
|
kargs['levels'] = [mn,mx]
|
2013-11-27 06:16:13 +00:00
|
|
|
|
|
|
|
profile()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
self.setOpts(update=False, **kargs)
|
2013-11-27 06:16:13 +00:00
|
|
|
|
|
|
|
profile()
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
self.qimage = None
|
|
|
|
self.update()
|
2013-11-27 06:16:13 +00:00
|
|
|
|
|
|
|
profile()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
if gotNewData:
|
|
|
|
self.sigImageChanged.emit()
|
|
|
|
|
2016-08-26 01:18:15 +00:00
|
|
|
def dataTransform(self):
|
|
|
|
"""Return the transform that maps from this image's input array to its
|
|
|
|
local coordinate system.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-08-26 01:18:15 +00:00
|
|
|
This transform corrects for the transposition that occurs when image data
|
|
|
|
is interpreted in row-major order.
|
|
|
|
"""
|
|
|
|
# Might eventually need to account for downsampling / clipping here
|
|
|
|
tr = QtGui.QTransform()
|
|
|
|
if self.axisOrder == 'row-major':
|
|
|
|
# transpose
|
|
|
|
tr.scale(1, -1)
|
|
|
|
tr.rotate(-90)
|
|
|
|
return tr
|
|
|
|
|
|
|
|
def inverseDataTransform(self):
|
|
|
|
"""Return the transform that maps from this image's local coordinate
|
|
|
|
system to its input array.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-08-26 01:18:15 +00:00
|
|
|
See dataTransform() for more information.
|
|
|
|
"""
|
|
|
|
tr = QtGui.QTransform()
|
|
|
|
if self.axisOrder == 'row-major':
|
|
|
|
# transpose
|
|
|
|
tr.scale(1, -1)
|
|
|
|
tr.rotate(-90)
|
|
|
|
return tr
|
|
|
|
|
|
|
|
def mapToData(self, obj):
|
|
|
|
tr = self.inverseDataTransform()
|
|
|
|
return tr.map(obj)
|
|
|
|
|
|
|
|
def mapFromData(self, obj):
|
|
|
|
tr = self.dataTransform()
|
|
|
|
return tr.map(obj)
|
|
|
|
|
2016-01-30 08:10:25 +00:00
|
|
|
def quickMinMax(self, targetSize=1e6):
|
|
|
|
"""
|
|
|
|
Estimate the min/max values of the image data by subsampling.
|
|
|
|
"""
|
|
|
|
data = self.image
|
|
|
|
while data.size > targetSize:
|
|
|
|
ax = np.argmax(data.shape)
|
|
|
|
sl = [slice(None)] * data.ndim
|
|
|
|
sl[ax] = slice(None, None, 2)
|
|
|
|
data = data[sl]
|
2017-10-22 23:45:52 +00:00
|
|
|
return np.nanmin(data), np.nanmax(data)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def updateImage(self, *args, **kargs):
|
|
|
|
## used for re-rendering qimage from self.image.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
## can we make any assumptions here that speed things up?
|
|
|
|
## dtype, range, size are all the same?
|
|
|
|
defaults = {
|
|
|
|
'autoLevels': False,
|
|
|
|
}
|
|
|
|
defaults.update(kargs)
|
|
|
|
return self.setImage(*args, **defaults)
|
|
|
|
|
|
|
|
def render(self):
|
2014-02-18 01:48:22 +00:00
|
|
|
# Convert data to QImage for display.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2013-11-27 06:16:13 +00:00
|
|
|
profile = debug.Profiler()
|
2013-03-13 21:17:39 +00:00
|
|
|
if self.image is None or self.image.size == 0:
|
2012-03-02 02:55:32 +00:00
|
|
|
return
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
# Request a lookup table if this image has only one channel
|
|
|
|
if self.image.ndim == 2 or self.image.shape[2] == 1:
|
|
|
|
if isinstance(self.lut, collections.Callable):
|
|
|
|
lut = self.lut(self.image)
|
|
|
|
else:
|
|
|
|
lut = self.lut
|
2012-03-02 02:55:32 +00:00
|
|
|
else:
|
2017-09-26 15:31:28 +00:00
|
|
|
lut = None
|
2014-02-18 01:48:22 +00:00
|
|
|
|
2013-11-20 18:51:39 +00:00
|
|
|
if self.autoDownsample:
|
|
|
|
# reduce dimensions of image based on screen resolution
|
|
|
|
o = self.mapToDevice(QtCore.QPointF(0,0))
|
|
|
|
x = self.mapToDevice(QtCore.QPointF(1,0))
|
|
|
|
y = self.mapToDevice(QtCore.QPointF(0,1))
|
2017-08-31 12:55:58 +00:00
|
|
|
|
|
|
|
# Check if graphics view is too small to render anything
|
|
|
|
if o is None or x is None or y is None:
|
|
|
|
return
|
|
|
|
|
2014-02-18 01:48:22 +00:00
|
|
|
w = Point(x-o).length()
|
|
|
|
h = Point(y-o).length()
|
2016-06-03 00:40:35 +00:00
|
|
|
if w == 0 or h == 0:
|
|
|
|
self.qimage = None
|
|
|
|
return
|
2016-06-16 15:54:52 +00:00
|
|
|
xds = max(1, int(1.0 / w))
|
|
|
|
yds = max(1, int(1.0 / h))
|
|
|
|
axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1]
|
|
|
|
image = fn.downsample(self.image, xds, axis=axes[0])
|
|
|
|
image = fn.downsample(image, yds, axis=axes[1])
|
|
|
|
self._lastDownsample = (xds, yds)
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2017-08-08 19:35:54 +00:00
|
|
|
# Check if downsampling reduced the image size to zero due to inf values.
|
|
|
|
if image.size == 0:
|
|
|
|
return
|
2013-11-20 18:51:39 +00:00
|
|
|
else:
|
|
|
|
image = self.image
|
2016-01-30 08:10:25 +00:00
|
|
|
|
|
|
|
# if the image data is a small int, then we can combine levels + lut
|
|
|
|
# into a single lut for better performance
|
2016-01-30 17:52:37 +00:00
|
|
|
levels = self.levels
|
|
|
|
if levels is not None and levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16):
|
2016-01-30 08:10:25 +00:00
|
|
|
if self._effectiveLut is None:
|
|
|
|
eflsize = 2**(image.itemsize*8)
|
|
|
|
ind = np.arange(eflsize)
|
2016-01-30 17:52:37 +00:00
|
|
|
minlev, maxlev = levels
|
2016-09-08 00:56:00 +00:00
|
|
|
levdiff = maxlev - minlev
|
|
|
|
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
|
2016-01-30 08:10:25 +00:00
|
|
|
if lut is None:
|
2018-10-10 14:29:16 +00:00
|
|
|
efflut = fn.rescaleData(ind, scale=255./levdiff,
|
2016-01-30 08:10:25 +00:00
|
|
|
offset=minlev, dtype=np.ubyte)
|
|
|
|
else:
|
|
|
|
lutdtype = np.min_scalar_type(lut.shape[0]-1)
|
2016-09-08 00:56:00 +00:00
|
|
|
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
|
2016-01-30 08:10:25 +00:00
|
|
|
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
|
|
|
|
efflut = lut[efflut]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-01-30 08:10:25 +00:00
|
|
|
self._effectiveLut = efflut
|
|
|
|
lut = self._effectiveLut
|
|
|
|
levels = None
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
# Convert single-channel image to 2D array
|
|
|
|
if image.ndim == 3 and image.shape[-1] == 1:
|
|
|
|
image = image[..., 0]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-04-28 05:33:51 +00:00
|
|
|
# Assume images are in column-major order for backward compatibility
|
|
|
|
# (most images are in row-major order)
|
2016-06-16 15:54:52 +00:00
|
|
|
if self.axisOrder == 'col-major':
|
2016-04-28 05:33:51 +00:00
|
|
|
image = image.transpose((1, 0, 2)[:image.ndim])
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2016-04-28 05:33:51 +00:00
|
|
|
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
|
2013-11-25 01:45:10 +00:00
|
|
|
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def paint(self, p, *args):
|
2013-11-27 06:16:13 +00:00
|
|
|
profile = debug.Profiler()
|
2012-03-02 02:55:32 +00:00
|
|
|
if self.image is None:
|
|
|
|
return
|
|
|
|
if self.qimage is None:
|
|
|
|
self.render()
|
2013-03-13 21:17:39 +00:00
|
|
|
if self.qimage is None:
|
|
|
|
return
|
2013-11-27 06:16:13 +00:00
|
|
|
profile('render QImage')
|
2012-03-02 02:55:32 +00:00
|
|
|
if self.paintMode is not None:
|
|
|
|
p.setCompositionMode(self.paintMode)
|
2013-11-27 06:16:13 +00:00
|
|
|
profile('set comp mode')
|
|
|
|
|
2016-06-16 15:54:52 +00:00
|
|
|
shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1]
|
2016-04-28 05:33:51 +00:00
|
|
|
p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage)
|
2013-11-27 06:16:13 +00:00
|
|
|
profile('p.drawImage')
|
2012-03-02 02:55:32 +00:00
|
|
|
if self.border is not None:
|
|
|
|
p.setPen(self.border)
|
|
|
|
p.drawRect(self.boundingRect())
|
|
|
|
|
2012-12-05 02:02:05 +00:00
|
|
|
def save(self, fileName, *args):
|
|
|
|
"""Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
|
|
|
|
if self.qimage is None:
|
|
|
|
self.render()
|
|
|
|
self.qimage.save(fileName, *args)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2018-10-10 14:29:16 +00:00
|
|
|
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
|
2017-09-26 15:31:28 +00:00
|
|
|
targetHistogramSize=500, **kwds):
|
2012-04-16 20:45:55 +00:00
|
|
|
"""Returns x and y arrays containing the histogram values for the current image.
|
2014-01-15 03:22:50 +00:00
|
|
|
For an explanation of the return format, see numpy.histogram().
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2014-01-15 03:22:50 +00:00
|
|
|
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
|
|
|
|
dimensions roughly *targetImageSize* for each axis.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
|
|
|
The *bins* argument and any extra keyword arguments are passed to
|
2014-01-15 03:22:50 +00:00
|
|
|
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
|
|
|
chosen based on the image characteristics:
|
2018-10-10 14:29:16 +00:00
|
|
|
|
|
|
|
* Integer images will have approximately *targetHistogramSize* bins,
|
2014-01-15 03:22:50 +00:00
|
|
|
with each bin having an integer width.
|
|
|
|
* All other types will have *targetHistogramSize* bins.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
If *perChannel* is True, then the histogram is computed once per channel
|
|
|
|
and the output is a list of the results.
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-04-16 20:45:55 +00:00
|
|
|
This method is also used when automatically computing levels.
|
|
|
|
"""
|
2018-04-25 23:11:25 +00:00
|
|
|
if self.image is None or self.image.size == 0:
|
2018-06-01 10:30:33 +00:00
|
|
|
return None, None
|
2014-01-15 03:22:50 +00:00
|
|
|
if step == 'auto':
|
2018-04-25 23:11:25 +00:00
|
|
|
step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))),
|
|
|
|
max(1, int(np.ceil(self.image.shape[1] / targetImageSize))))
|
2014-01-15 03:22:50 +00:00
|
|
|
if np.isscalar(step):
|
|
|
|
step = (step, step)
|
|
|
|
stepData = self.image[::step[0], ::step[1]]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2014-01-15 03:22:50 +00:00
|
|
|
if bins == 'auto':
|
2018-06-01 10:30:33 +00:00
|
|
|
mn = np.nanmin(stepData)
|
|
|
|
mx = np.nanmax(stepData)
|
2018-10-10 14:29:16 +00:00
|
|
|
if mx == mn:
|
|
|
|
# degenerate image, arange will fail
|
|
|
|
mx += 1
|
2018-06-01 10:30:33 +00:00
|
|
|
if np.isnan(mn) or np.isnan(mx):
|
|
|
|
# the data are all-nan
|
|
|
|
return None, None
|
2014-01-15 03:22:50 +00:00
|
|
|
if stepData.dtype.kind in "ui":
|
2017-09-26 15:31:28 +00:00
|
|
|
# For integer data, we select the bins carefully to avoid aliasing
|
2014-01-15 03:22:50 +00:00
|
|
|
step = np.ceil((mx-mn) / 500.)
|
|
|
|
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
2013-12-26 06:03:01 +00:00
|
|
|
else:
|
2017-09-26 15:31:28 +00:00
|
|
|
# for float data, let numpy select the bins.
|
|
|
|
bins = np.linspace(mn, mx, 500)
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
if len(bins) == 0:
|
|
|
|
bins = [mn, mx]
|
2014-01-15 03:22:50 +00:00
|
|
|
|
|
|
|
kwds['bins'] = bins
|
2017-10-03 15:16:36 +00:00
|
|
|
|
2017-09-26 15:31:28 +00:00
|
|
|
if perChannel:
|
|
|
|
hist = []
|
|
|
|
for i in range(stepData.shape[-1]):
|
2017-10-03 15:16:36 +00:00
|
|
|
stepChan = stepData[..., i]
|
|
|
|
stepChan = stepChan[np.isfinite(stepChan)]
|
|
|
|
h = np.histogram(stepChan, **kwds)
|
2017-09-26 15:31:28 +00:00
|
|
|
hist.append((h[1][:-1], h[0]))
|
|
|
|
return hist
|
|
|
|
else:
|
2017-10-03 15:16:36 +00:00
|
|
|
stepData = stepData[np.isfinite(stepData)]
|
2017-09-26 15:31:28 +00:00
|
|
|
hist = np.histogram(stepData, **kwds)
|
|
|
|
return hist[1][:-1], hist[0]
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
def setPxMode(self, b):
|
2012-04-16 20:45:55 +00:00
|
|
|
"""
|
|
|
|
Set whether the item ignores transformations and draws directly to screen pixels.
|
|
|
|
If True, the item will not inherit any scale or rotation transformations from its
|
|
|
|
parent items, but its position will be transformed as usual.
|
|
|
|
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
|
|
|
|
"""
|
2012-03-02 02:55:32 +00:00
|
|
|
self.setFlag(self.ItemIgnoresTransformations, b)
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def setScaledMode(self):
|
|
|
|
self.setPxMode(False)
|
|
|
|
|
|
|
|
def getPixmap(self):
|
|
|
|
if self.qimage is None:
|
|
|
|
self.render()
|
|
|
|
if self.qimage is None:
|
|
|
|
return None
|
|
|
|
return QtGui.QPixmap.fromImage(self.qimage)
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def pixelSize(self):
|
|
|
|
"""return scene-size of a single pixel in the image"""
|
|
|
|
br = self.sceneBoundingRect()
|
|
|
|
if self.image is None:
|
|
|
|
return 1,1
|
|
|
|
return br.width()/self.width(), br.height()/self.height()
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2013-11-20 18:51:39 +00:00
|
|
|
def viewTransformChanged(self):
|
|
|
|
if self.autoDownsample:
|
|
|
|
self.qimage = None
|
|
|
|
self.update()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-30 03:18:34 +00:00
|
|
|
def mouseDragEvent(self, ev):
|
|
|
|
if ev.button() != QtCore.Qt.LeftButton:
|
|
|
|
ev.ignore()
|
|
|
|
return
|
2012-05-30 05:02:03 +00:00
|
|
|
elif self.drawKernel is not None:
|
|
|
|
ev.accept()
|
|
|
|
self.drawAt(ev.pos(), ev)
|
2012-05-30 03:18:34 +00:00
|
|
|
|
|
|
|
def mouseClickEvent(self, ev):
|
|
|
|
if ev.button() == QtCore.Qt.RightButton:
|
|
|
|
if self.raiseContextMenu(ev):
|
|
|
|
ev.accept()
|
2012-03-02 02:55:32 +00:00
|
|
|
if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
|
|
|
self.drawAt(ev.pos(), ev)
|
2012-05-30 03:18:34 +00:00
|
|
|
|
|
|
|
def raiseContextMenu(self, ev):
|
|
|
|
menu = self.getMenu()
|
|
|
|
if menu is None:
|
|
|
|
return False
|
|
|
|
menu = self.scene().addParentContextMenus(self, menu, ev)
|
|
|
|
pos = ev.screenPos()
|
|
|
|
menu.popup(QtCore.QPoint(pos.x(), pos.y()))
|
|
|
|
return True
|
|
|
|
|
|
|
|
def getMenu(self):
|
|
|
|
if self.menu is None:
|
|
|
|
if not self.removable:
|
|
|
|
return None
|
|
|
|
self.menu = QtGui.QMenu()
|
|
|
|
self.menu.setTitle("Image")
|
|
|
|
remAct = QtGui.QAction("Remove image", self.menu)
|
|
|
|
remAct.triggered.connect(self.removeClicked)
|
|
|
|
self.menu.addAction(remAct)
|
|
|
|
self.menu.remAct = remAct
|
|
|
|
return self.menu
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-05-30 03:18:34 +00:00
|
|
|
def hoverEvent(self, ev):
|
|
|
|
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.RightButton)
|
|
|
|
elif not ev.isExit() and self.removable:
|
|
|
|
ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def tabletEvent(self, ev):
|
2016-09-07 00:46:52 +00:00
|
|
|
pass
|
|
|
|
#print(ev.device())
|
|
|
|
#print(ev.pointerType())
|
|
|
|
#print(ev.pressure())
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def drawAt(self, pos, ev=None):
|
|
|
|
pos = [int(pos.x()), int(pos.y())]
|
|
|
|
dk = self.drawKernel
|
|
|
|
kc = self.drawKernelCenter
|
|
|
|
sx = [0,dk.shape[0]]
|
|
|
|
sy = [0,dk.shape[1]]
|
|
|
|
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
|
|
|
|
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
for i in [0,1]:
|
|
|
|
dx1 = -min(0, tx[i])
|
|
|
|
dx2 = min(0, self.image.shape[0]-tx[i])
|
|
|
|
tx[i] += dx1+dx2
|
|
|
|
sx[i] += dx1+dx2
|
|
|
|
|
|
|
|
dy1 = -min(0, ty[i])
|
|
|
|
dy2 = min(0, self.image.shape[1]-ty[i])
|
|
|
|
ty[i] += dy1+dy2
|
|
|
|
sy[i] += dy1+dy2
|
|
|
|
|
|
|
|
ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1]))
|
|
|
|
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
|
|
|
|
mask = self.drawMask
|
|
|
|
src = dk
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-05-11 22:05:41 +00:00
|
|
|
if isinstance(self.drawMode, collections.Callable):
|
2012-03-02 02:55:32 +00:00
|
|
|
self.drawMode(dk, self.image, mask, ss, ts, ev)
|
|
|
|
else:
|
|
|
|
src = src[ss]
|
|
|
|
if self.drawMode == 'set':
|
|
|
|
if mask is not None:
|
|
|
|
mask = mask[ss]
|
|
|
|
self.image[ts] = self.image[ts] * (1-mask) + src * mask
|
|
|
|
else:
|
|
|
|
self.image[ts] = src
|
|
|
|
elif self.drawMode == 'add':
|
|
|
|
self.image[ts] += src
|
|
|
|
else:
|
|
|
|
raise Exception("Unknown draw mode '%s'" % self.drawMode)
|
|
|
|
self.updateImage()
|
2018-10-10 14:29:16 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
|
|
|
|
self.drawKernel = kernel
|
|
|
|
self.drawKernelCenter = center
|
|
|
|
self.drawMode = mode
|
|
|
|
self.drawMask = mask
|
|
|
|
|
2012-05-30 03:18:34 +00:00
|
|
|
def removeClicked(self):
|
2012-05-31 20:22:50 +00:00
|
|
|
## Send remove event only after we have exited the menu event handler
|
|
|
|
self.removeTimer = QtCore.QTimer()
|
2014-08-07 13:03:26 +00:00
|
|
|
self.removeTimer.timeout.connect(self.emitRemoveRequested)
|
2012-05-31 20:22:50 +00:00
|
|
|
self.removeTimer.start(0)
|
|
|
|
|
2014-08-07 13:03:26 +00:00
|
|
|
def emitRemoveRequested(self):
|
|
|
|
self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
|
|
|
|
self.sigRemoveRequested.emit(self)
|