From 6962777b9202497fc91d201dc4ec64d7243390dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:29:04 -0700 Subject: [PATCH] HistogramLUTItem: add rgb level mode, save/restore methods --- pyqtgraph/graphicsItems/GraphicsItem.py | 3 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 217 +++++++++++++++----- 2 files changed, 163 insertions(+), 57 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index d45818dc..f88069bc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -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 diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 31764250..6919cfba 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -36,7 +36,7 @@ class HistogramLUTItem(GraphicsWidget): sigLevelsChanged = 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. By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. @@ -44,6 +44,8 @@ class HistogramLUTItem(GraphicsWidget): 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 +58,27 @@ 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) @@ -71,12 +91,23 @@ class HistogramLUTItem(GraphicsWidget): #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() @@ -86,25 +117,30 @@ class HistogramLUTItem(GraphicsWidget): #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) + 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 sizeHint(self, *args): #return QtCore.QSizeF(115, 200) 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()) @@ -115,28 +151,9 @@ class HistogramLUTItem(GraphicsWidget): 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 +162,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 +176,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 @@ -182,34 +197,124 @@ class HistogramLUTItem(GraphicsWidget): if self.imageItem() is not None: self.imageItem().setLevels(self.region.getRegion()) 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() + 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): - """Set the min and max levels. + 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. """ - 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'])