ImageView: add support for RGB levels mode

This commit is contained in:
Luke Campagnola 2017-09-26 08:33:34 -07:00
parent 07d1a62bfc
commit 4a4a7383bc

View File

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