From 6962777b9202497fc91d201dc4ec64d7243390dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:29:04 -0700 Subject: [PATCH 01/14] 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']) From 07d1a62bfc0d26d9242d348dee0dbb16c63a33f7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:31:28 -0700 Subject: [PATCH 02/14] ImageItem: add support for rgb handling by histogramlut --- pyqtgraph/graphicsItems/ImageItem.py | 52 +++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..2ae8b812 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -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,30 @@ 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]): + h = np.histogram(stepData[..., i], **kwds) + hist.append((h[1][:-1], h[0])) + return hist + else: + hist = np.histogram(stepData, **kwds) + return hist[1][:-1], hist[0] def setPxMode(self, b): """ From 4a4a7383bc3cf549a7e2fb54c8a7379bd6031168 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:33:34 -0700 Subject: [PATCH 03/14] ImageView: add support for RGB levels mode --- pyqtgraph/imageview/ImageView.py | 132 +++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..f6cacde0 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -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 ` to display image data and a :class:`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__() + ` ============= ========================================================= 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 `. + """ + 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): """ From bde358ffaf17d6ec401d06a51c9df123a2839a56 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:34:37 -0700 Subject: [PATCH 04/14] Fix colormapwidget.restorestate --- pyqtgraph/widgets/ColorMapWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index f6e28960..bd5668ae 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -152,7 +152,7 @@ class ColorMapParameter(ptree.types.GroupParameter): def restoreState(self, state): if 'fields' in state: self.setFields(state['fields']) - for itemState in state['items']: + for name, itemState in state['items'].items(): item = self.addNew(itemState['field']) item.restoreState(itemState) From 6e22524ac28f484ff74d12853b0a5bf6ba6b0fb2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:50:31 -0700 Subject: [PATCH 05/14] Update histogramlut example to allow rgb mode --- examples/HistogramLUT.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py index 4d89dd3f..082a963c 100644 --- a/examples/HistogramLUT.py +++ b/examples/HistogramLUT.py @@ -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() From fcf45036711c97c51b62f65e3ae5cba930b655bb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 2 Oct 2017 08:58:03 -0700 Subject: [PATCH 06/14] Fix: avoid division by 0 when image is single valued --- pyqtgraph/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..3a50eb9e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1079,7 +1079,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 From c3e52f15b0df0104455922512762887fdfc81e25 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:16:36 -0700 Subject: [PATCH 07/14] Fix ImageItem rgb histogram calculation --- pyqtgraph/graphicsItems/ImageItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 2ae8b812..34150282 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -489,14 +489,17 @@ class ImageItem(GraphicsObject): bins = [mn, mx] kwds['bins'] = bins - stepData = stepData[np.isfinite(stepData)] + if perChannel: hist = [] for i in range(stepData.shape[-1]): - h = np.histogram(stepData[..., i], **kwds) + 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] From faca369a8d38f00b9324051482f51e0bfe269b4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:17:22 -0700 Subject: [PATCH 08/14] code cleanup --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6919cfba..dc6286e3 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -76,7 +76,6 @@ class HistogramLUTItem(GraphicsWidget): 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) @@ -87,9 +86,6 @@ 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.vb.sigRangeChanged.connect(self.viewRangeChanged) add = QtGui.QPainter.CompositionMode_Plus @@ -114,7 +110,6 @@ class HistogramLUTItem(GraphicsWidget): 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)): colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] @@ -125,9 +120,6 @@ class HistogramLUTItem(GraphicsWidget): else: plot.setFillLevel(None) - #def sizeHint(self, *args): - #return QtCore.QSizeF(115, 200) - def paint(self, p, *args): if self.levelMode != 'mono': return @@ -143,8 +135,6 @@ class HistogramLUTItem(GraphicsWidget): 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.""" From 21bda49a294fe133a1a594d6a33dc7d683fd5df0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:36 -0700 Subject: [PATCH 09/14] Docstring updates --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index dc6286e3..85cbe9cf 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -25,11 +25,29 @@ __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) @@ -37,10 +55,6 @@ class HistogramLUTItem(GraphicsWidget): sigLevelChangeFinished = QtCore.Signal(object) 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. - """ GraphicsWidget.__init__(self) self.lut = None self.imageItem = lambda: None # fake a dead weakref @@ -241,6 +255,8 @@ class HistogramLUTItem(GraphicsWidget): def getLevels(self): """Return the min and max levels. + + For rgba mode, this returns a list of the levels for each channel. """ if self.levelMode == 'mono': return self.region.getRegion() From a04db637755ceafb20cc784343f2c134157928af Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:56 -0700 Subject: [PATCH 10/14] Include level mode in save/restore --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 85cbe9cf..68448c11 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -319,8 +319,10 @@ class HistogramLUTItem(GraphicsWidget): 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']) From f1de464c460e9cbdb0987a4cf85b76930e88ad18 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:30:38 -0700 Subject: [PATCH 11/14] Preserve levels when switching between mono and rgba modes --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 68448c11..019fa3a7 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -288,9 +288,21 @@ class HistogramLUTItem(GraphicsWidget): Options are 'mono' or 'rgba'. """ assert mode in ('mono', 'rgba') + + oldLevels = self.getLevels() + self.levelMode = mode self._showRegions() self.imageChanged() + + # 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) + self.update() def _showRegions(self): From ce15f4530ac8a343520ccb2923bbe9a8f04b3978 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:34:42 -0700 Subject: [PATCH 12/14] Fix: image levels reset to mono after drag release --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 019fa3a7..e6d692e6 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -199,7 +199,7 @@ 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) def regionChanging(self): From 2db502a9cce1f6ff6605805c12c5d6bb90da91cd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Nov 2017 09:25:27 -0800 Subject: [PATCH 13/14] Fix bug when switching level mode --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index e6d692e6..90e2e790 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -293,7 +293,6 @@ class HistogramLUTItem(GraphicsWidget): self.levelMode = mode self._showRegions() - self.imageChanged() # do our best to preserve old levels if mode == 'mono': @@ -302,7 +301,12 @@ class HistogramLUTItem(GraphicsWidget): 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): From 019c421ca102233ba2df27653c723b90a593e09e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 17:48:03 -0800 Subject: [PATCH 14/14] Don't attempt to set same level mode again --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 90e2e790..f85b64dd 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -289,8 +289,10 @@ class HistogramLUTItem(GraphicsWidget): """ assert mode in ('mono', 'rgba') - oldLevels = self.getLevels() + if mode == self.levelMode: + return + oldLevels = self.getLevels() self.levelMode = mode self._showRegions()