# Copyright (C) 2017 DESY, Notkestr. 85, D-22607 Hamburg
#
# lavue is an image viewing program for photon science imaging detectors.
# Its usual application is as a live viewer using hidra as data source.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation in version 2
# of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
# Authors:
# Jan Kotanski <jan.kotanski@desy.de>
# Christoph Rosemann <christoph.rosemann@desy.de>
#
""" Horizontal HistogramWidget """
import pyqtgraph as _pg
from pyqtgraph import QtCore, QtGui
import numpy as np
import logging
from .external.pyqtgraph_0_12 import (
histogram__init__, histogram_paint, histogram_setHistogramRange,
# histogram_setLevels, histogram_fillHistogram
)
#: ( (:obj:`str`,:obj:`str`,:obj:`str`) )
#: pg major version, pg minor verion, pg patch version
_VMAJOR, _VMINOR, _VPATCH = _pg.__version__.split(".")[:3] \
if _pg.__version__ else ("0", "9", "0")
try:
_NPATCH = int(_VPATCH)
except Exception:
_NPATCH = 0
_PQGVER = int(_VMAJOR) * 1000 + int(_VMINOR) * 100 + _NPATCH
_pg.graphicsItems.GradientEditorItem.Gradients['reversegrey'] = {
'ticks': [(0.0, (255, 255, 255, 255)),
(1.0, (0, 0, 0, 255)), ], 'mode': 'rgb'}
_pg.graphicsItems.GradientEditorItem.Gradients['highcontrast'] = {
'ticks': [(0.0, (0, 0, 0, 255)),
(1.0, (255, 255, 0, 255)), ], 'mode': 'rgb'}
_pg.graphicsItems.GradientEditorItem.Gradients['spectrum'] = {
'ticks': [(0.0, (255, 0, 255, 255)),
(1.0, (255, 0, 0, 255))], 'mode': 'hsv'}
_pg.graphicsItems.GradientEditorItem.Gradients['spectrumclip'] = {
'ticks': [(0.0, (255, 0, 255, 255)),
(.99, (255, 0, 0, 255)),
(1.0, (255, 255, 255, 255))], 'mode': 'hsv'}
# define two new gradients of choice
_pg.graphicsItems.GradientEditorItem.Gradients['inverted'] = {
'ticks': [(0.0, (255, 255, 255, 255)),
(1.0, (0, 0, 0, 255)), ], 'mode': 'rgb'}
_pg.graphicsItems.GradientEditorItem.Gradients['highcontrastclip'] = {
'ticks': [(0.0, (255, 255, 255, 255)),
(0.15, (0, 0, 0, 255)),
(0.4, (255, 0, 0, 255)),
(0.7, (255, 255, 0, 255)),
(0.99, (255, 255, 255, 255)),
(1.0, (0, 0, 255, 255)), ], 'mode': 'rgb'}
__all__ = ['HistogramHLUTWidget']
logger = logging.getLogger("lavue")
class GradientEditorItemWS(
_pg.graphicsItems.GradientEditorItem.GradientEditorItem):
""" gradient editor item with a signal on loadPreset """
#: (:class:`pyqtgraph.QtCore.pyqtSignal`) minimum level changed signal
sigNameChanged = QtCore.pyqtSignal(str)
def __init__(self, *args, **kargs):
""" constructor
:param args: GradientEditorItem parameters list
:type args: :obj:`list` < :obj:`any`>
:param kargs: GradientEditorItem parameter dictionary
:type kargs: :obj:`dict` < :obj:`str`, :obj:`any`>
"""
self.__skipupdate = True
_pg.graphicsItems.GradientEditorItem.GradientEditorItem.__init__(
self, *args, **kargs)
self.__skipupdate = False
#: (:obj:`str`) color gradient name
self.name = "highcontrast"
#: (:class:`pyqtgrath.QtGui.QAction`) save gradient action
self.saveAction = QtGui.QAction('Save ...', self)
#: (:obj:`pyqtgrath.QtGui.QAction`) remove gradient action
self.removeAction = QtGui.QAction('Remove', self)
def addMenuActions(self):
""" add save/remove actions
"""
self.menu.addAction(self.saveAction)
self.menu.addAction(self.removeAction)
def removeTick(self, tick, finish=True):
""" removes ticks with hook
:param tick: ticks to remove
:type tick: :obj:`list` < :obj:`any`>
:param finish: finish remove flag
:type finish: :obj:`bool`
"""
if not self.__skipupdate:
_pg.graphicsItems.GradientEditorItem.GradientEditorItem.\
removeTick(
self, tick=tick, finish=finish)
else:
_pg.graphicsItems.GradientEditorItem.TickSliderItem.\
removeTick(self, tick)
if finish:
# hook for a pyqtgraph bug
# self.updateGradient()
self.sigGradientChangeFinished.emit(self)
def getCurrentGradient(self):
""" provides dictionary with the current gradient
:returns: gradient dictionary with:
{"mode": <colorMode>,
"ticks": [
(<pos>, (<r>, <g>, <b>, <a>)),
(<pos>, (<r>, <g>, <b>, <a>)),
...
(<pos>, (<r>, <g>, <b>, <a>)),
]
}
:rtype: :obj:`dict` < :obj:`str`:, :obj:`str`: or :obj:`list`:>
"""
tlist = self.listTicks()
tdct = {}
tdct['ticks'] = []
tdct['mode'] = self.colorMode
for tck, pos in tlist:
clr = tck.color
tdct['ticks'].append(
(pos,
(clr.red(), clr.green(), clr.blue(), clr.alpha()))
)
return tdct
def loadPreset(self, name):
""" loads a predefined gradient and emits sigNameChanged
:param name: gradient name
:type name: :obj:`str`
"""
_pg.graphicsItems.GradientEditorItem.GradientEditorItem.loadPreset(
self, name)
self.name = name
self.sigNameChanged.emit(name)
class HistogramHLUTItem(_pg.HistogramLUTItem):
#: (:class:`pyqtgraph.QtCore.pyqtSignal`) automatic levels changed signal
autoLevelsChanged = QtCore.pyqtSignal(int) # bool does not work...
#: (:class:`pyqtgraph.QtCore.pyqtSignal`) minimum level changed signal
sigNameChanged = QtCore.pyqtSignal(str)
#: (:class:`pyqtgraph.QtCore.pyqtSignal`) save gradient requested
saveGradientRequested = QtCore.pyqtSignal()
#: (:class:`pyqtgraph.QtCore.pyqtSignal`) remove gradient requested
removeGradientRequested = QtCore.pyqtSignal()
""" Horizontal HistogramItem """
def __init__(self, bins=None, step=None, image=None, fillHistogram=True,
expertmode=False):
""" constructor
:param bins: bins edges algorithm for histogram, default: 'auto'
:type bins: :obj:`str`
:param step: data step for calculation of histogram levels,
default: 'auto'
:type step: :obj:`str` or :obj:`int`
:param image: 2d image
:type image: :class:`pyqtgraph.ImageItem`
:param fillHistogram: histogram will be filled in
:type fillHistogram: :obj:`bool`
:param expertmode: expert mode flag
:type expertmode: :obj:`bool`
"""
if _PQGVER >= 1100:
self.__init_1100(bins, step, image, fillHistogram, expertmode)
else:
self.__init_old(bins, step, image, fillHistogram, expertmode)
self.vb.enableAutoRange(self.vb.YAxis, 0.99)
def __init_1100(self, bins=None, step=None, image=None, fillHistogram=True,
expertmode=False):
""" constructor for old pyqtgraph
:param bins: bins edges algorithm for histogram, default: 'auto'
:type bins: :obj:`str`
:param step: data step for calculation of histogram levels,
default: 'auto'
:type step: :obj:`str` or :obj:`int`
:param image: 2d image
:type image: :class:`pyqtgraph.ImageItem`
:param fillHistogram: histogram will be filled in
:type fillHistogram: :obj:`bool`
:param expertmode: expert mode flag
:type expertmode: :obj:`bool`
"""
if _PQGVER >= 1202:
_pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.__init__(
self, image, fillHistogram, levelMode='mono',
gradientPosition='bottom', orientation='horizontal')
else:
histogram__init__(
self, image, fillHistogram, levelMode='mono',
gradientPosition='bottom', orientation='horizontal')
#: (:obj:`bool`) expert mode
self.__expertmode = expertmode
#: (:obj: `bool`) rgb flag
self.__rgb = False
# self.vb.setMaximumHeight(15200)
#: (:obj:`list`) buffer for removed gradients
self.__oldgradient = []
self.resetGradient(False)
self.autolevelfactor = None
self.__step = step or 'auto'
self.__bins = bins or 'auto'
def __init_old(self, bins=None, step=None, image=None, fillHistogram=True,
expertmode=False):
""" constructor for old pyqtgraph
:param bins: bins edges algorithm for histogram, default: 'auto'
:type bins: :obj:`str`
:param step: data step for calculation of histogram levels,
default: 'auto'
:type step: :obj:`str` or :obj:`int`
:param image: 2d image
:type image: :class:`pyqtgraph.ImageItem`
:param fillHistogram: histogram will be filled in
:type fillHistogram: :obj:`bool`
:param expertmode: expert mode flag
:type expertmode: :obj:`bool`
"""
_pg.graphicsItems.GraphicsWidget.GraphicsWidget.__init__(self)
#: (:obj:`bool`) expert mode
self.__expertmode = expertmode
#: (:class:`numpy.ndarray`) look up table
self.lut = None
if _VMAJOR == '0' and int(_VMINOR) < 10 and int(_VPATCH) < 9:
#: (:class:`weakref.ref` or :class:`pyqtgraph.ImageItem`)
#: weakref to image item or image item itself (for < 0.9.8)
self.imageItem = None
else:
self.imageItem = lambda: None
#: (:obj: `bool`) rgb flag
self.__rgb = False
#: (:class:`PyQt5.QtGui.QGraphicsGridLayout`) grid layout
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
self.layout.setContentsMargins(1, 1, 1, 1)
self.layout.setSpacing(0)
#: (:class:`pyqtgraph.graphicsItems.ViewBox.ViewBox`) view box
self.vb = _pg.graphicsItems.ViewBox.ViewBox(parent=self)
# self.vb.setMaximumHeight(152)
self.vb.setMinimumHeight(45)
self.vb.setMouseEnabled(x=True, y=False)
# self.vb.setMouseEnabled(x=False, y=True)
#: (:class:`GradientEditorItemWS`) gradient editor item with a signal
self.gradient = GradientEditorItemWS()
if self.__expertmode:
self.gradient.addMenuActions()
self.gradient.setOrientation('bottom')
self.gradient.loadPreset('grey')
#: (:obj:`list`) buffer for removed gradients
self.__oldgradient = []
#: (:class:`pyqtgraph.graphicsItems.LinearRegionItem.LinearRegionItem`)
#: linear region item
self.region = _pg.graphicsItems.LinearRegionItem.LinearRegionItem(
[0, 1],
_pg.graphicsItems.LinearRegionItem.LinearRegionItem.Vertical)
self.region.setZValue(1000)
self.vb.addItem(self.region)
#: (:class:`pyqtgraph.graphicsItems.AxisItem.AxisItem`) axis item
self.axis = _pg.graphicsItems.AxisItem.AxisItem(
'top', linkView=self.vb, maxTickLength=-10, showValues=False,
parent=self)
self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 1, 0)
self.layout.addItem(self.gradient, 2, 0)
# self.range = None
self.autolevelfactor = None
self.__step = step or 'auto'
self.__bins = bins or 'auto'
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
self.gradient.sigGradientChanged.connect(self.gradientChanged)
self.gradient.sigNameChanged.connect(self._emitSigNameChanged)
self.gradient.saveAction.triggered.connect(
self._emitSaveGradientRequested)
self.gradient.removeAction.triggered.connect(
self._emitRemoveGradientRequested)
self.region.sigRegionChanged.connect(self.regionChanging)
self.region.sigRegionChangeFinished.connect(self.regionChanged)
self.vb.sigRangeChanged.connect(self.viewRangeChanged)
self.plot = _pg.graphicsItems.PlotDataItem.PlotDataItem()
self.plots = [self.plot]
# self.plot.dataBounds(1, 0.9)
# self.plot.dataBounds(0, 0.9)
self.levelMode = 'mono'
self.fillHistogram(fillHistogram)
self.vb.addItem(self.plot)
self.autoHistogramRange()
if image is not None:
self.setImageItem(image)
# self.background = None
def setRGB(self, rgb):
""" sets rgb flag
:param rgb: rgb lag
:type rgb: :obj:`bool`
"""
self.__rgb = rgb
def gradientChanged(self):
""" gradient changed with rgb
"""
if self.__rgb:
self.lut = None
self.sigLookupTableChanged.emit(self)
else:
_pg.HistogramLUTItem.gradientChanged(self)
def resetGradient(self, signal=True):
""" resets gradient widget
"""
self.gradient.sigGradientChanged.disconnect(self.gradientChanged)
if signal:
self.gradient.sigNameChanged.disconnect(self._emitSigNameChanged)
self.gradient.saveAction.triggered.disconnect(
self._emitSaveGradientRequested)
self.gradient.removeAction.triggered.disconnect(
self._emitRemoveGradientRequested)
self.gradient.hide()
if hasattr(self.gradient, "prepareGeometryChange"):
self.gradient.prepareGeometryChange()
self.layout.removeItem(self.gradient)
self.gradient.close()
self.__oldgradient.append(self.gradient)
self.gradient = GradientEditorItemWS()
if self.__expertmode:
self.gradient.addMenuActions()
self.gradient.setOrientation('bottom')
self.gradient.loadPreset('grey')
self.layout.addItem(self.gradient, 2, 0)
self.gradient.sigGradientChanged.connect(self.gradientChanged)
self.gradient.sigNameChanged.connect(self._emitSigNameChanged)
self.gradient.saveAction.triggered.connect(
self._emitSaveGradientRequested)
self.gradient.removeAction.triggered.connect(
self._emitRemoveGradientRequested)
@QtCore.pyqtSlot(str)
def _emitSigNameChanged(self, name):
""" emits SigNameChanged
:param name: gradient name
:type name: :obj:`str`
"""
self.sigNameChanged.emit(name)
@QtCore.pyqtSlot()
def _emitSaveGradientRequested(self):
""" emits saveGradientRequested
"""
self.saveGradientRequested.emit()
@QtCore.pyqtSlot()
def _emitRemoveGradientRequested(self):
""" emits removeGradientRequested
"""
self.removeGradientRequested.emit()
def setBins(self, bins):
""" sets bins edges algorithm for histogram
:param channel: bins edges algorithm for histogram
:type channel: :obj:`str`
"""
self.__bins = bins
def setStep(self, step):
""" sets image step data for algorithm of histogram
:param channel: image step data for algorithm of histogram
:type channel: :obj:`int`
"""
try:
if self.__step:
self.__step = int(step)
else:
self.__step = "auto"
except Exception:
self.__step = "auto"
def setGradientByName(self, name):
""" sets gradient by name
:param name: gradient name
:type name: :obj:`str`
"""
try:
self.gradient.loadPreset(str(name))
except Exception:
self.gradient.loadPreset("highcontrast")
def paint(self, p, *args):
""" paints the histogram item
:param p: QPainter painter
:type p: :class:`PyQt5.QtGui.QPainter`
:param args: paint argument
:type args: :obj:`list` < :obj:`any`>
"""
if _PQGVER >= 1202:
self.__paint_1202(p, *args)
elif _PQGVER >= 1100:
histogram_paint(self, p, *args)
else:
self.__paint_old(p, *args)
def __paint_1202(self, p, *args):
_pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.paint(
self, p, *args)
def __paint_old(self, p, *args):
""" paints the histogram item
:param p: QPainter painter
:type p: :class:`PyQt5.QtGui.QPainter`
:param args: paint argument
:type args: :obj:`list` < :obj:`any`>
"""
pen = self.region.lines[0].pen
rgn = self.getLevels()
p1 = self.vb.mapFromViewToItem(
self, _pg.Point(rgn[0], self.vb.viewRect().center().y()))
p2 = self.vb.mapFromViewToItem(
self, _pg.Point(rgn[1], self.vb.viewRect().center().y()))
gradRect = self.gradient.mapRectToParent(
self.gradient.gradRect.rect())
for pen in [_pg.functions.mkPen('k', width=3), pen]:
p.setPen(pen)
p.drawLine(p1, gradRect.topLeft())
p.drawLine(p2, gradRect.topRight())
p.drawLine(gradRect.topLeft(), gradRect.bottomLeft())
p.drawLine(gradRect.topRight(), gradRect.bottomRight())
def setHistogramRange(self, mn, mx, padding=0.1):
"""sets the Y range on the histogram plot. This disables auto-scaling.
:param mn: minimum range level
:type mn: :obj:`float`
:param mx: maximum range level
:type mx: :obj:`float`
:param padding: histogram padding
:type padding: :obj:`float`
"""
if _PQGVER >= 1202:
self.__setHistogramRange_1202(self, mn, mx, padding)
if _PQGVER >= 1100:
histogram_setHistogramRange(self, mn, mx, padding)
else:
self.__setHistogramRange_old(self, mn, mx, padding)
def __setHistogramRange_old(self, mn, mx, padding=0.1):
"""sets the Y range on the histogram plot. This disables auto-scaling.
:param mn: minimum range level
:type mn: :obj:`float`
:param mx: maximum range level
:type mx: :obj:`float`
:param padding: histogram padding
:type padding: :obj:`float`
"""
self.vb.enableAutoRange(self.vb.XAxis, False)
self.vb.setYRange(mn, mx, padding)
def __setHistogramRange_1202(self, mn, mx, padding=0.1):
"""sets the Y range on the histogram plot. This disables auto-scaling.
:param mn: minimum range level
:type mn: :obj:`float`
:param mx: maximum range level
:type mx: :obj:`float`
:param padding: histogram padding
:type padding: :obj:`float`
"""
_pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.setHistogramRange(
self, mn, mx, padding)
def __imageItem(self):
""" provides imageItem independent of the pyqtgraph version
:returns: image item
:rtype: :class:`pyqtgraph.ImageItem`
"""
if _VMAJOR == '0' and int(_VMINOR) < 10 and int(_VPATCH) < 9:
#: (:class:`weakref.ref` or :class:`pyqtgraph.ImageItem`)
#: weakref to image item or image item itself (for < 0.9.8)
return self.imageItem
else:
return self.imageItem()
def getFactorRegion(self):
""" provides mono auto levels calculated from autofactor
:returns: minlevel, maxlevel
:rtype: (float, float)
"""
hx = None
hy = None
if self.autolevelfactor is not None:
try:
hx, hy = self.__imageItem().getHistogram(
step=self.__step, bins=self.__bins)
except Exception as e:
logger.warning(str(e))
# print(str(e))
if hy is not None and hx is not None and hx.any() and hy.any():
if abs(hx[0]) < 1.e-3 or abs(hx[0]+2.) < 1.e-3:
hx = hx[1:]
hy = hy[1:]
if hx.any() and hy.any():
hmax = max(hy)
hmin = self.autolevelfactor*hmax/100.
# mn, mx = self.__imageItem().levels[:2]
indexes = np.where(hy >= hmin)
ind1 = indexes[0][0]
ind2 = indexes[-1][-1]
return hx[ind1], hx[ind2]
return None, None
def getChannelFactorRegion(self, ch=None):
""" provides mono auto levels calculated from autofactor
:param ch: histogram channels
:type ch: :obj:
:returns: minlevel, maxlevel for channels
:rtype: :obj:`list` < (float, float) >
"""
channels = []
if ch is None:
channels = self.__imageItem().getHistogram(perChannel=True)
if ch[0] is None:
return
autofactor = False
if self.autolevelfactor is not None:
for i in range(1, 5):
chs = None
if len(ch) >= i:
h = ch[i-1]
hx = h[0]
hy = h[1]
if hy is not None and hx is not None and \
hx.any() and hy.any():
if abs(hx[0]) < 1.e-3 or abs(hx[0]+2.) < 1.e-3:
hhx = hx[1:]
hhy = hy[1:]
else:
hhx = hx
hhy = hy
if hhx.any() and hhy.any():
hmax = max(hhy)
hmin = self.autolevelfactor * hmax / 100.
# mn, mx = self.__imageItem().levels[:2]
indexes = np.where(hhy >= hmin)
ind1 = indexes[0][0]
ind2 = indexes[-1][-1]
chs = (hhx[ind1], hhx[ind2])
autofactor = True
channels.append(chs)
if autofactor:
return channels
def imageChanged(self, autoLevel=False, autoRange=False):
""" overload imageChange method
:param autoLevel: auto level flag
:type autoLevel: :obj:`bool`
:param autoRange: auto range flag
:type autoRange: :obj:`bool`
"""
if self.levelMode == 'mono':
for plt in self.plots[1:]:
plt.setVisible(False)
self.plots[0].setVisible(True)
hx1, hx2 = self.getFactorRegion()
if hx1 is not None:
self.region.setRegion([hx1, hx2])
_pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.\
imageChanged(
self, autoLevel=False, autoRange=autoRange)
return
try:
# _pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.\
# imageChanged(
# self, autoLevel=autoLevel, autoRange=autoRange)
h = self.__imageItem().getHistogram(
step=self.__step, bins=self.__bins)
if h[0] is None:
return
self.plot.setData(*h)
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region.setRegion([mn, mx])
except Exception as e:
logger.warning(str(e))
# print(str(e))
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)
else:
# hide channels not present in image data
self.plots[i].setVisible(False)
autofactor = False
channels = self.getChannelFactorRegion(ch)
if channels is not None:
for i, hxx in enumerate(channels):
if hxx is not None:
self.regions[i + 1].setRegion([hxx[0], hxx[1]])
autofactor = True
if not autofactor and autoLevel:
for i in range(1, 5):
if len(ch) >= i:
h = ch[i-1]
mn = h[0][0]
mx = h[0][-1]
self.regions[i].setRegion([mn, mx])
if autofactor:
_pg.graphicsItems.HistogramLUTItem.HistogramLUTItem.\
imageChanged(
self, autoLevel=False, autoRange=autoRange)
# make sure we are displaying the correct number of channels
self._showRegions()
def switchLevelMode(self, mode):
""" switch rgba mode
:param mode: rgba mode i.e. 'mono' or 'rgba'
:type mode: :obj:`str`
"""
if mode == self.levelMode or mode not in {'mono', 'rgba'}:
return
self.levelMode = mode
self._showRegions()
if mode == 'mono':
levels = self.region.getRegion()
self.setLevels(*levels)
else:
if hasattr(self, "regions"):
levels = [self.regions[i].getRegion() for i in range(1, 5)]
else:
oldLevels = self.region.getRegion()
levels = [oldLevels] * 4
self.setLevels(rgba=levels)
self.__imageItem().setLevels(self.getLevels())
self.imageChanged()
self.update()