commit
870c047d30
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user