c52382c3b9
Pull request #907 addressed a specific case where a signal was emitted before a state update. If an application's slot then calls back into the instance, the instance was in an inconsistent state. This commit audits and fixes similar issues throughout the pyqtgraph library. This commit fixes several latent issues: * SignalProxy: flush -> sigDelayed -> signalReceived would have incorrectly resulted in timer.stop(). * ViewBox: resizeEvent -> sigStateChange -> background state * ViewBox: setRange -> sigStateChange -> autoranging not updated correctly * ViewBox: updateMatrix -> sigTransformChanged -> any _matrixNeedsUpdate = True -> ignored * Parameter: Child may have missed state tree messages on insert or received extra on remove * GraphicsView: updateMatrix -> sigDeviceRangeChanged/sigDeviceTransformChange -> before propagated to locked viewports.
347 lines
13 KiB
Python
347 lines
13 KiB
Python
"""
|
|
GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images.
|
|
"""
|
|
|
|
|
|
from ..Qt import QtGui, QtCore
|
|
from .. import functions as fn
|
|
from .GraphicsWidget import GraphicsWidget
|
|
from .ViewBox import *
|
|
from .GradientEditorItem import *
|
|
from .LinearRegionItem import *
|
|
from .PlotDataItem import *
|
|
from .AxisItem import *
|
|
from .GridItem import *
|
|
from ..Point import Point
|
|
from .. import functions as fn
|
|
import numpy as np
|
|
from .. import debug as debug
|
|
|
|
import weakref
|
|
|
|
__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, 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)
|
|
self.layout.setContentsMargins(1,1,1,1)
|
|
self.layout.setSpacing(0)
|
|
self.vb = ViewBox(parent=self)
|
|
self.vb.setMaximumWidth(152)
|
|
self.vb.setMinimumWidth(45)
|
|
self.vb.setMouseEnabled(x=False, y=True)
|
|
self.gradient = GradientEditorItem()
|
|
self.gradient.setOrientation('right')
|
|
self.gradient.loadPreset('grey')
|
|
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)
|
|
self.layout.addItem(self.gradient, 0, 2)
|
|
self.range = None
|
|
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
|
|
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
|
|
|
|
self.gradient.sigGradientChanged.connect(self.gradientChanged)
|
|
self.vb.sigRangeChanged.connect(self.viewRangeChanged)
|
|
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)
|
|
|
|
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)]
|
|
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((0, 0, 0, 100), width=3), pen]:
|
|
p.setPen(pen)
|
|
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())
|
|
|
|
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)
|
|
|
|
def autoHistogramRange(self):
|
|
"""Enable auto-scaling on the histogram plot."""
|
|
self.vb.enableAutoRange(self.vb.XYAxes)
|
|
|
|
def setImageItem(self, img):
|
|
"""Set an ImageItem to have its levels and LUT automatically controlled
|
|
by this HistogramLUTItem.
|
|
"""
|
|
self.imageItem = weakref.ref(img)
|
|
img.sigImageChanged.connect(self.imageChanged)
|
|
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
|
self.regionChanged()
|
|
self.imageChanged(autoLevel=True)
|
|
|
|
def viewRangeChanged(self):
|
|
self.update()
|
|
|
|
def gradientChanged(self):
|
|
if self.imageItem() is not None:
|
|
if self.gradient.isLookupTrivial():
|
|
self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8))
|
|
else:
|
|
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
|
|
|
|
self.lut = None
|
|
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 != 'mono':
|
|
return None
|
|
if n is None:
|
|
if img.dtype == np.uint8:
|
|
n = 256
|
|
else:
|
|
n = 512
|
|
if self.lut is None:
|
|
self.lut = self.gradient.getLookupTable(n, alpha=alpha)
|
|
return self.lut
|
|
|
|
def regionChanged(self):
|
|
if self.imageItem() is not None:
|
|
self.imageItem().setLevels(self.getLevels())
|
|
self.sigLevelChangeFinished.emit(self)
|
|
|
|
def regionChanging(self):
|
|
if self.imageItem() is not None:
|
|
self.imageItem().setLevels(self.getLevels())
|
|
self.update()
|
|
self.sigLevelsChanged.emit(self)
|
|
|
|
def imageChanged(self, autoLevel=False, autoRange=False):
|
|
if self.imageItem() is None:
|
|
return
|
|
|
|
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.
|
|
|
|
For rgba mode, this returns a list of the levels for each channel.
|
|
"""
|
|
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'])
|