HistogramLUTItem: add rgb level mode, save/restore methods

This commit is contained in:
Luke Campagnola 2017-09-26 08:29:04 -07:00
parent f627a6a447
commit 6962777b92
2 changed files with 163 additions and 57 deletions

View File

@ -146,7 +146,8 @@ class GraphicsItem(object):
return parents return parents
def viewRect(self): 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() view = self.getViewBox()
if view is None: if view is None:
return None return None

View File

@ -36,7 +36,7 @@ class HistogramLUTItem(GraphicsWidget):
sigLevelsChanged = QtCore.Signal(object) sigLevelsChanged = QtCore.Signal(object)
sigLevelChangeFinished = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object)
def __init__(self, image=None, fillHistogram=True): def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'):
""" """
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. 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. By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False.
@ -44,6 +44,8 @@ class HistogramLUTItem(GraphicsWidget):
GraphicsWidget.__init__(self) GraphicsWidget.__init__(self)
self.lut = None self.lut = None
self.imageItem = lambda: None # fake a dead weakref self.imageItem = lambda: None # fake a dead weakref
self.levelMode = levelMode
self.rgbHistogram = rgbHistogram
self.layout = QtGui.QGraphicsGridLayout() self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout) self.setLayout(self.layout)
@ -56,9 +58,27 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient = GradientEditorItem() self.gradient = GradientEditorItem()
self.gradient.setOrientation('right') self.gradient.setOrientation('right')
self.gradient.loadPreset('grey') self.gradient.loadPreset('grey')
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.regions = [
self.region.setZValue(1000) LinearRegionItem([0, 1], 'horizontal', swapMode='block'),
self.vb.addItem(self.region) 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.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.vb, 0, 1)
@ -71,12 +91,23 @@ class HistogramLUTItem(GraphicsWidget):
#self.vb.addItem(self.grid) #self.vb.addItem(self.grid)
self.gradient.sigGradientChanged.connect(self.gradientChanged) 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.vb.sigRangeChanged.connect(self.viewRangeChanged)
self.plot = PlotDataItem() add = QtGui.QPainter.CompositionMode_Plus
self.plot.rotate(90) 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.fillHistogram(fillHistogram)
self._showRegions()
self.vb.addItem(self.plot) self.vb.addItem(self.plot)
self.autoHistogramRange() self.autoHistogramRange()
@ -86,25 +117,30 @@ class HistogramLUTItem(GraphicsWidget):
#self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
if fill: colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)]
self.plot.setFillLevel(level) for i,plot in enumerate(self.plots):
self.plot.setFillBrush(color) if fill:
else: plot.setFillLevel(level)
self.plot.setFillLevel(None) plot.setBrush(colors[i])
else:
plot.setFillLevel(None)
#def sizeHint(self, *args): #def sizeHint(self, *args):
#return QtCore.QSizeF(115, 200) #return QtCore.QSizeF(115, 200)
def paint(self, p, *args): def paint(self, p, *args):
if self.levelMode != 'mono':
return
pen = self.region.lines[0].pen pen = self.region.lines[0].pen
rgn = self.getLevels() rgn = self.getLevels()
p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) 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])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))
gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) 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.setPen(pen)
p.drawLine(p1, gradRect.bottomLeft()) p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft())
p.drawLine(p2, gradRect.topLeft()) p.drawLine(p2 - Point(0, 5), gradRect.topLeft())
p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.topLeft(), gradRect.topRight())
p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
#p.drawRect(self.boundingRect()) #p.drawRect(self.boundingRect())
@ -115,28 +151,9 @@ class HistogramLUTItem(GraphicsWidget):
self.vb.enableAutoRange(self.vb.YAxis, False) self.vb.enableAutoRange(self.vb.YAxis, False)
self.vb.setYRange(mn, mx, padding) 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): def autoHistogramRange(self):
"""Enable auto-scaling on the histogram plot.""" """Enable auto-scaling on the histogram plot."""
self.vb.enableAutoRange(self.vb.XYAxes) 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): def setImageItem(self, img):
"""Set an ImageItem to have its levels and LUT automatically controlled """Set an ImageItem to have its levels and LUT automatically controlled
@ -145,10 +162,8 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem = weakref.ref(img) self.imageItem = weakref.ref(img)
img.sigImageChanged.connect(self.imageChanged) img.sigImageChanged.connect(self.imageChanged)
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
#self.gradientChanged()
self.regionChanged() self.regionChanged()
self.imageChanged(autoLevel=True) self.imageChanged(autoLevel=True)
#self.vb.autoRange()
def viewRangeChanged(self): def viewRangeChanged(self):
self.update() self.update()
@ -161,14 +176,14 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
self.lut = None self.lut = None
#if self.imageItem is not None:
#self.imageItem.setLookupTable(self.gradient.getLookupTable(512))
self.sigLookupTableChanged.emit(self) self.sigLookupTableChanged.emit(self)
def getLookupTable(self, img=None, n=None, alpha=None): def getLookupTable(self, img=None, n=None, alpha=None):
"""Return a lookup table from the color gradient defined by this """Return a lookup table from the color gradient defined by this
HistogramLUTItem. HistogramLUTItem.
""" """
if self.levelMode is not 'mono':
return None
if n is None: if n is None:
if img.dtype == np.uint8: if img.dtype == np.uint8:
n = 256 n = 256
@ -182,34 +197,124 @@ class HistogramLUTItem(GraphicsWidget):
if self.imageItem() is not None: if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion()) self.imageItem().setLevels(self.region.getRegion())
self.sigLevelChangeFinished.emit(self) self.sigLevelChangeFinished.emit(self)
#self.update()
def regionChanging(self): def regionChanging(self):
if self.imageItem() is not None: if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion()) self.imageItem().setLevels(self.getLevels())
self.sigLevelsChanged.emit(self) self.sigLevelsChanged.emit(self)
self.update() self.update()
def imageChanged(self, autoLevel=False, autoRange=False): def imageChanged(self, autoLevel=False, autoRange=False):
profiler = debug.Profiler() if self.imageItem() is None:
h = self.imageItem().getHistogram()
profiler('get histogram')
if h[0] is None:
return return
self.plot.setData(*h)
profiler('set plot') if self.levelMode == 'mono':
if autoLevel: for plt in self.plots[1:]:
mn = h[0][0] plt.setVisible(False)
mx = h[0][-1] self.plots[0].setVisible(True)
self.region.setRegion([mn, mx]) # plot one histogram for all image data
profiler('set region') 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): def getLevels(self):
"""Return the min and max levels. """Return the min and max levels.
""" """
return self.region.getRegion() 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, mn, mx): def setLevels(self, min=None, max=None, rgba=None):
"""Set the min and max levels. """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.
""" """
self.region.setRegion([mn, mx]) 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')
self.levelMode = mode
self._showRegions()
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(),
}
def restoreState(self, state):
self.gradient.restoreState(state['gradient'])
self.setLevels(*state['levels'])