Merge pull request #621 from campagnola/rgb-histogram

Rgb histogram
This commit is contained in:
Luke Campagnola 2018-01-26 08:52:10 -08:00 committed by GitHub
commit 870c047d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 354 additions and 133 deletions

View File

@ -28,19 +28,27 @@ v = pg.GraphicsView()
vb = pg.ViewBox()
vb.setAspectLocked()
v.setCentralItem(vb)
l.addWidget(v, 0, 0)
l.addWidget(v, 0, 0, 3, 1)
w = pg.HistogramLUTWidget()
l.addWidget(w, 0, 1)
data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20))
monoRadio = QtGui.QRadioButton('mono')
rgbaRadio = QtGui.QRadioButton('rgba')
l.addWidget(monoRadio, 1, 1)
l.addWidget(rgbaRadio, 2, 1)
monoRadio.setChecked(True)
def setLevelMode():
mode = 'mono' if monoRadio.isChecked() else 'rgba'
w.setLevelMode(mode)
monoRadio.toggled.connect(setLevelMode)
data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0))
for i in range(32):
for j in range(32):
data[i*8, j*8] += .1
img = pg.ImageItem(data)
#data2 = np.zeros((2,) + data.shape + (2,))
#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes
#img = pg.ImageItem(data2[0,:,:,0])
vb.addItem(img)
vb.autoRange()

View File

@ -1093,7 +1093,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels[i]
if minVal == maxVal:
maxVal += 1e-16
newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype)
rng = maxVal-minVal
rng = 1 if rng == 0 else rng
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
data = newData
else:
# Apply level scaling unless it would have no effect on the data

View File

@ -146,7 +146,8 @@ class GraphicsItem(object):
return parents
def viewRect(self):
"""Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget"""
"""Return the visible bounds of this item's ViewBox or GraphicsWidget,
in the local coordinate system of the item."""
view = self.getViewBox()
if view is None:
return None

View File

@ -25,25 +25,41 @@ __all__ = ['HistogramLUTItem']
class HistogramLUTItem(GraphicsWidget):
"""
This is a graphicsWidget which provides controls for adjusting the display of an image.
Includes:
- Image histogram
- Movable region over histogram to select black/white levels
- Gradient editor to define color lookup table for single-channel images
Parameters
----------
image : ImageItem or None
If *image* is provided, then the control will be automatically linked to
the image and changes to the control will be immediately reflected in
the image's appearance.
fillHistogram : bool
By default, the histogram is rendered with a fill.
For performance, set *fillHistogram* = False.
rgbHistogram : bool
Sets whether the histogram is computed once over all channels of the
image, or once per channel.
levelMode : 'mono' or 'rgba'
If 'mono', then only a single set of black/whilte level lines is drawn,
and the levels apply to all channels in the image. If 'rgba', then one
set of levels is drawn for each channel.
"""
sigLookupTableChanged = QtCore.Signal(object)
sigLevelsChanged = QtCore.Signal(object)
sigLevelChangeFinished = QtCore.Signal(object)
def __init__(self, image=None, fillHistogram=True):
"""
If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance.
By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False.
"""
def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'):
GraphicsWidget.__init__(self)
self.lut = None
self.imageItem = lambda: None # fake a dead weakref
self.levelMode = levelMode
self.rgbHistogram = rgbHistogram
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
@ -56,9 +72,26 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient = GradientEditorItem()
self.gradient.setOrientation('right')
self.gradient.loadPreset('grey')
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
self.region.setZValue(1000)
self.vb.addItem(self.region)
self.regions = [
LinearRegionItem([0, 1], 'horizontal', swapMode='block'),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r',
brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g',
brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b',
brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w',
brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))]
for region in self.regions:
region.setZValue(1000)
self.vb.addItem(region)
region.lines[0].addMarker('<|', 0.5)
region.lines[1].addMarker('|>', 0.5)
region.sigRegionChanged.connect(self.regionChanging)
region.sigRegionChangeFinished.connect(self.regionChanged)
self.region = self.regions[0] # for backward compatibility.
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1)
@ -67,76 +100,64 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
#self.grid = GridItem()
#self.vb.addItem(self.grid)
self.gradient.sigGradientChanged.connect(self.gradientChanged)
self.region.sigRegionChanged.connect(self.regionChanging)
self.region.sigRegionChangeFinished.connect(self.regionChanged)
self.vb.sigRangeChanged.connect(self.viewRangeChanged)
self.plot = PlotDataItem()
self.plot.rotate(90)
add = QtGui.QPainter.CompositionMode_Plus
self.plots = [
PlotCurveItem(pen=(200, 200, 200, 100)), # mono
PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r
PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g
PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b
PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a
]
self.plot = self.plots[0] # for backward compatibility.
for plot in self.plots:
plot.rotate(90)
self.vb.addItem(plot)
self.fillHistogram(fillHistogram)
self._showRegions()
self.vb.addItem(self.plot)
self.autoHistogramRange()
if image is not None:
self.setImageItem(image)
#self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
if fill:
self.plot.setFillLevel(level)
self.plot.setFillBrush(color)
else:
self.plot.setFillLevel(None)
#def sizeHint(self, *args):
#return QtCore.QSizeF(115, 200)
colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)]
for i,plot in enumerate(self.plots):
if fill:
plot.setFillLevel(level)
plot.setBrush(colors[i])
else:
plot.setFillLevel(None)
def paint(self, p, *args):
if self.levelMode != 'mono':
return
pen = self.region.lines[0].pen
rgn = self.getLevels()
p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))
gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect())
for pen in [fn.mkPen('k', width=3), pen]:
for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]:
p.setPen(pen)
p.drawLine(p1, gradRect.bottomLeft())
p.drawLine(p2, gradRect.topLeft())
p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft())
p.drawLine(p2 - Point(0, 5), gradRect.topLeft())
p.drawLine(gradRect.topLeft(), gradRect.topRight())
p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
#p.drawRect(self.boundingRect())
def setHistogramRange(self, mn, mx, padding=0.1):
"""Set the Y range on the histogram plot. This disables auto-scaling."""
self.vb.enableAutoRange(self.vb.YAxis, False)
self.vb.setYRange(mn, mx, padding)
#d = mx-mn
#mn -= d*padding
#mx += d*padding
#self.range = [mn,mx]
#self.updateRange()
#self.vb.setMouseEnabled(False, True)
#self.region.setBounds([mn,mx])
def autoHistogramRange(self):
"""Enable auto-scaling on the histogram plot."""
self.vb.enableAutoRange(self.vb.XYAxes)
#self.range = None
#self.updateRange()
#self.vb.setMouseEnabled(False, False)
#def updateRange(self):
#self.vb.autoRange()
#if self.range is not None:
#self.vb.setYRange(*self.range)
#vr = self.vb.viewRect()
#self.region.setBounds([vr.top(), vr.bottom()])
def setImageItem(self, img):
"""Set an ImageItem to have its levels and LUT automatically controlled
@ -145,10 +166,8 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem = weakref.ref(img)
img.sigImageChanged.connect(self.imageChanged)
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
#self.gradientChanged()
self.regionChanged()
self.imageChanged(autoLevel=True)
#self.vb.autoRange()
def viewRangeChanged(self):
self.update()
@ -161,14 +180,14 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
self.lut = None
#if self.imageItem is not None:
#self.imageItem.setLookupTable(self.gradient.getLookupTable(512))
self.sigLookupTableChanged.emit(self)
def getLookupTable(self, img=None, n=None, alpha=None):
"""Return a lookup table from the color gradient defined by this
HistogramLUTItem.
"""
if self.levelMode is not 'mono':
return None
if n is None:
if img.dtype == np.uint8:
n = 256
@ -180,36 +199,148 @@ class HistogramLUTItem(GraphicsWidget):
def regionChanged(self):
if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion())
self.imageItem().setLevels(self.getLevels())
self.sigLevelChangeFinished.emit(self)
#self.update()
def regionChanging(self):
if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion())
self.imageItem().setLevels(self.getLevels())
self.sigLevelsChanged.emit(self)
self.update()
def imageChanged(self, autoLevel=False, autoRange=False):
profiler = debug.Profiler()
h = self.imageItem().getHistogram()
profiler('get histogram')
if h[0] is None:
if self.imageItem() is None:
return
self.plot.setData(*h)
profiler('set plot')
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region.setRegion([mn, mx])
profiler('set region')
if self.levelMode == 'mono':
for plt in self.plots[1:]:
plt.setVisible(False)
self.plots[0].setVisible(True)
# plot one histogram for all image data
profiler = debug.Profiler()
h = self.imageItem().getHistogram()
profiler('get histogram')
if h[0] is None:
return
self.plot.setData(*h)
profiler('set plot')
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region.setRegion([mn, mx])
profiler('set region')
else:
mn, mx = self.imageItem().levels
self.region.setRegion([mn, mx])
else:
# plot one histogram for each channel
self.plots[0].setVisible(False)
ch = self.imageItem().getHistogram(perChannel=True)
if ch[0] is None:
return
for i in range(1, 5):
if len(ch) >= i:
h = ch[i-1]
self.plots[i].setVisible(True)
self.plots[i].setData(*h)
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region[i].setRegion([mn, mx])
else:
# hide channels not present in image data
self.plots[i].setVisible(False)
# make sure we are displaying the correct number of channels
self._showRegions()
def getLevels(self):
"""Return the min and max levels.
"""
return self.region.getRegion()
def setLevels(self, mn, mx):
"""Set the min and max levels.
For rgba mode, this returns a list of the levels for each channel.
"""
self.region.setRegion([mn, mx])
if self.levelMode == 'mono':
return self.region.getRegion()
else:
nch = self.imageItem().channels()
if nch is None:
nch = 3
return [r.getRegion() for r in self.regions[1:nch+1]]
def setLevels(self, min=None, max=None, rgba=None):
"""Set the min/max (bright and dark) levels.
Arguments may be *min* and *max* for single-channel data, or
*rgba* = [(rmin, rmax), ...] for multi-channel data.
"""
if self.levelMode == 'mono':
if min is None:
min, max = rgba[0]
assert None not in (min, max)
self.region.setRegion((min, max))
else:
if rgba is None:
raise TypeError("Must specify rgba argument when levelMode != 'mono'.")
for i, levels in enumerate(rgba):
self.regions[i+1].setRegion(levels)
def setLevelMode(self, mode):
""" Set the method of controlling the image levels offered to the user.
Options are 'mono' or 'rgba'.
"""
assert mode in ('mono', 'rgba')
if mode == self.levelMode:
return
oldLevels = self.getLevels()
self.levelMode = mode
self._showRegions()
# do our best to preserve old levels
if mode == 'mono':
levels = np.array(oldLevels).mean(axis=0)
self.setLevels(*levels)
else:
levels = [oldLevels] * 4
self.setLevels(rgba=levels)
# force this because calling self.setLevels might not set the imageItem
# levels if there was no change to the region item
self.imageItem().setLevels(self.getLevels())
self.imageChanged()
self.update()
def _showRegions(self):
for i in range(len(self.regions)):
self.regions[i].setVisible(False)
if self.levelMode == 'rgba':
imax = 4
if self.imageItem() is not None:
# Only show rgb channels if connected image lacks alpha.
nch = self.imageItem().channels()
if nch is None:
nch = 3
xdif = 1.0 / nch
for i in range(1, nch+1):
self.regions[i].setVisible(True)
self.regions[i].setSpan((i-1) * xdif, i * xdif)
self.gradient.hide()
elif self.levelMode == 'mono':
self.regions[0].setVisible(True)
self.gradient.show()
else:
raise ValueError("Unknown level mode %r" % self.levelMode)
def saveState(self):
return {
'gradient': self.gradient.saveState(),
'levels': self.getLevels(),
'mode': self.levelMode,
}
def restoreState(self, state):
self.setLevelMode(state['mode'])
self.gradient.restoreState(state['gradient'])
self.setLevels(*state['levels'])

View File

@ -98,6 +98,11 @@ class ImageItem(GraphicsObject):
axis = 1 if self.axisOrder == 'col-major' else 0
return self.image.shape[axis]
def channels(self):
if self.image is None:
return None
return self.image.shape[2] if self.image.ndim == 3 else 1
def boundingRect(self):
if self.image is None:
return QtCore.QRectF(0., 0., 0., 0.)
@ -348,10 +353,15 @@ class ImageItem(GraphicsObject):
profile = debug.Profiler()
if self.image is None or self.image.size == 0:
return
if isinstance(self.lut, collections.Callable):
lut = self.lut(self.image)
# 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
else:
lut = self.lut
lut = None
if self.autoDownsample:
# reduce dimensions of image based on screen resolution
@ -395,9 +405,12 @@ class ImageItem(GraphicsObject):
lut = self._effectiveLut
levels = None
# Convert single-channel image to 2D array
if image.ndim == 3 and image.shape[-1] == 1:
image = image[..., 0]
# Assume images are in column-major order for backward compatibility
# (most images are in row-major order)
if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim])
@ -430,7 +443,8 @@ class ImageItem(GraphicsObject):
self.render()
self.qimage.save(fileName, *args)
def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
targetHistogramSize=500, **kwds):
"""Returns x and y arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram().
@ -446,6 +460,9 @@ class ImageItem(GraphicsObject):
with each bin having an integer width.
* All other types will have *targetHistogramSize* bins.
If *perChannel* is True, then the histogram is computed once per channel
and the output is a list of the results.
This method is also used when automatically computing levels.
"""
if self.image is None:
@ -458,21 +475,33 @@ class ImageItem(GraphicsObject):
stepData = self.image[::step[0], ::step[1]]
if bins == 'auto':
mn = stepData.min()
mx = stepData.max()
if stepData.dtype.kind in "ui":
mn = stepData.min()
mx = stepData.max()
# For integer data, we select the bins carefully to avoid aliasing
step = np.ceil((mx-mn) / 500.)
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
if len(bins) == 0:
bins = [mn, mx]
else:
bins = 500
# for float data, let numpy select the bins.
bins = np.linspace(mn, mx, 500)
if len(bins) == 0:
bins = [mn, mx]
kwds['bins'] = bins
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0]
if perChannel:
hist = []
for i in range(stepData.shape[-1]):
stepChan = stepData[..., i]
stepChan = stepChan[np.isfinite(stepChan)]
h = np.histogram(stepChan, **kwds)
hist.append((h[1][:-1], h[0]))
return hist
else:
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0]
def setPxMode(self, b):
"""

View File

@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features:
- ROI plotting
- Image normalization through a variety of methods
"""
import os
import os, sys
import numpy as np
from ..Qt import QtCore, QtGui, USE_PYSIDE
@ -26,6 +26,7 @@ from ..graphicsItems.ROI import *
from ..graphicsItems.LinearRegionItem import *
from ..graphicsItems.InfiniteLine import *
from ..graphicsItems.ViewBox import *
from ..graphicsItems.VTickGroup import VTickGroup
from ..graphicsItems.GradientEditorItem import addGradientListToDocstring
from .. import ptime as ptime
from .. import debug as debug
@ -79,7 +80,8 @@ class ImageView(QtGui.QWidget):
sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object)
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None,
levelMode='mono', *args):
"""
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem.
@ -101,6 +103,9 @@ class ImageView(QtGui.QWidget):
imageItem (ImageItem) If specified, this object will be used to
display the image. Must be an instance of ImageItem
or other compatible object.
levelMode See the *levelMode* argument to
:func:`HistogramLUTItem.__init__()
<pyqtgraph.HistogramLUTItem.__init__>`
============= =========================================================
Note: to display axis ticks inside the ImageView, instantiate it
@ -109,8 +114,10 @@ class ImageView(QtGui.QWidget):
pg.ImageView(view=pg.PlotItem())
"""
QtGui.QWidget.__init__(self, parent, *args)
self.levelMax = 4096
self.levelMin = 0
self._imageLevels = None # [(min, max), ...] per channel image metrics
self.levelMin = None # min / max levels across all channels
self.levelMax = None
self.name = name
self.image = None
self.axes = {}
@ -118,6 +125,7 @@ class ImageView(QtGui.QWidget):
self.ui = Ui_Form()
self.ui.setupUi(self)
self.scene = self.ui.graphicsView.scene()
self.ui.histogram.setLevelMode(levelMode)
self.ignoreTimeLine = False
@ -151,13 +159,15 @@ class ImageView(QtGui.QWidget):
self.normRoi.setZValue(20)
self.view.addItem(self.normRoi)
self.normRoi.hide()
self.roiCurve = self.ui.roiPlot.plot()
self.timeLine = InfiniteLine(0, movable=True)
self.roiCurves = []
self.timeLine = InfiniteLine(0, movable=True, markers=[('^', 0), ('v', 1)])
self.timeLine.setPen((255, 255, 0, 200))
self.timeLine.setZValue(1)
self.ui.roiPlot.addItem(self.timeLine)
self.ui.splitter.setSizes([self.height()-35, 35])
self.ui.roiPlot.hideAxis('left')
self.frameTicks = VTickGroup(yrange=[0.8, 1], pen=0.4)
self.ui.roiPlot.addItem(self.frameTicks, ignoreBounds=True)
self.keysPressed = {}
self.playTimer = QtCore.QTimer()
@ -200,7 +210,7 @@ class ImageView(QtGui.QWidget):
self.roiClicked() ## initialize roi plot to correct shape / visibility
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True):
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None):
"""
Set the image to be displayed in the widget.
@ -208,8 +218,9 @@ class ImageView(QtGui.QWidget):
**Arguments:**
img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and
*notes* below.
xvals (numpy array) 1D array of z-axis values corresponding to the third axis
in a 3D image. For video, this array should contain the time of each frame.
xvals (numpy array) 1D array of z-axis values corresponding to the first axis
in a 3D image. For video, this array should contain the time of each
frame.
autoRange (bool) whether to scale/pan the view to fit the image.
autoLevels (bool) whether to update the white/black levels to fit the image.
levels (min, max); the white and black level values to use.
@ -224,7 +235,11 @@ class ImageView(QtGui.QWidget):
and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
image data.
================== ===========================================================================
levelMode If specified, this sets the user interaction mode for setting image
levels. Options are 'mono', which provides a single level control for
all image channels, and 'rgb' or 'rgba', which provide individual
controls for each channel.
================== =======================================================================
**Notes:**
@ -252,6 +267,8 @@ class ImageView(QtGui.QWidget):
self.image = img
self.imageDisp = None
if levelMode is not None:
self.ui.histogram.setLevelMode(levelMode)
profiler()
@ -310,10 +327,9 @@ class ImageView(QtGui.QWidget):
profiler()
if self.axes['t'] is not None:
#self.ui.roiPlot.show()
self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max())
self.frameTicks.setXVals(self.tVals)
self.timeLine.setValue(0)
#self.ui.roiPlot.setMouseEnabled(False, False)
if len(self.tVals) > 1:
start = self.tVals.min()
stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02
@ -325,8 +341,7 @@ class ImageView(QtGui.QWidget):
stop = 1
for s in [self.timeLine, self.normRgn]:
s.setBounds([start, stop])
#else:
#self.ui.roiPlot.hide()
profiler()
self.imageItem.resetTransform()
@ -364,11 +379,14 @@ class ImageView(QtGui.QWidget):
def autoLevels(self):
"""Set the min/max intensity levels automatically to match the image data."""
self.setLevels(self.levelMin, self.levelMax)
self.setLevels(rgba=self._imageLevels)
def setLevels(self, min, max):
"""Set the min/max (bright and dark) levels."""
self.ui.histogram.setLevels(min, max)
def setLevels(self, *args, **kwds):
"""Set the min/max (bright and dark) levels.
See :func:`HistogramLUTItem.setLevels <pyqtgraph.HistogramLUTItem.setLevels>`.
"""
self.ui.histogram.setLevels(*args, **kwds)
def autoRange(self):
"""Auto scale and pan the view around the image such that the image fills the view."""
@ -377,12 +395,13 @@ class ImageView(QtGui.QWidget):
def getProcessedImage(self):
"""Returns the image data after it has been processed by any normalization options in use.
This method also sets the attributes self.levelMin and self.levelMax
to indicate the range of data in the image."""
"""
if self.imageDisp is None:
image = self.normalize(self.image)
self.imageDisp = image
self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp)))
self._imageLevels = self.quickMinMax(self.imageDisp)
self.levelMin = min([level[0] for level in self._imageLevels])
self.levelMax = max([level[1] for level in self._imageLevels])
return self.imageDisp
@ -527,13 +546,15 @@ class ImageView(QtGui.QWidget):
#self.ui.roiPlot.show()
self.ui.roiPlot.setMouseEnabled(True, True)
self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4])
self.roiCurve.show()
for c in self.roiCurves:
c.show()
self.roiChanged()
self.ui.roiPlot.showAxis('left')
else:
self.roi.hide()
self.ui.roiPlot.setMouseEnabled(False, False)
self.roiCurve.hide()
for c in self.roiCurves:
c.hide()
self.ui.roiPlot.hideAxis('left')
if self.hasTimeAxis():
@ -557,36 +578,65 @@ class ImageView(QtGui.QWidget):
return
image = self.getProcessedImage()
if image.ndim == 2:
axes = (0, 1)
elif image.ndim == 3:
axes = (1, 2)
else:
return
# Extract image data from ROI
axes = (self.axes['x'], self.axes['y'])
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
if data is not None:
while data.ndim > 1:
data = data.mean(axis=1)
if image.ndim == 3:
self.roiCurve.setData(y=data, x=self.tVals)
if data is None:
return
# Convert extracted data into 1D plot data
if self.axes['t'] is None:
# Average across y-axis of ROI
data = data.mean(axis=axes[1])
coords = coords[:,:,0] - coords[:,0:1,0]
xvals = (coords**2).sum(axis=0) ** 0.5
else:
# Average data within entire ROI for each frame
data = data.mean(axis=max(axes)).mean(axis=min(axes))
xvals = self.tVals
# Handle multi-channel data
if data.ndim == 1:
plots = [(xvals, data, 'w')]
if data.ndim == 2:
if data.shape[1] == 1:
colors = 'w'
else:
while coords.ndim > 2:
coords = coords[:,:,0]
coords = coords - coords[:,0,np.newaxis]
xvals = (coords**2).sum(axis=0) ** 0.5
self.roiCurve.setData(y=data, x=xvals)
colors = 'rgbw'
plots = []
for i in range(data.shape[1]):
d = data[:,i]
plots.append((xvals, d, colors[i]))
# Update plot line(s)
while len(plots) < len(self.roiCurves):
c = self.roiCurves.pop()
c.scene().removeItem(c)
while len(plots) > len(self.roiCurves):
self.roiCurves.append(self.ui.roiPlot.plot())
for i in range(len(plots)):
x, y, p = plots[i]
self.roiCurves[i].setData(x, y, pen=p)
def quickMinMax(self, data):
"""
Estimate the min/max values of *data* by subsampling.
Returns [(min, max), ...] with one item per channel
"""
while data.size > 1e6:
ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
return nanmin(data), nanmax(data)
cax = self.axes['c']
if cax is None:
return [(float(nanmin(data)), float(nanmax(data)))]
else:
return [(float(nanmin(data.take(i, axis=cax))),
float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])]
def normalize(self, image):
"""