summaryrefslogtreecommitdiff
path: root/silx/gui/plot/CurvesROIWidget.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2019-05-28 08:16:16 +0200
commita763e5d1b3921b3194f3d4e94ab9de3fbe08bbdd (patch)
tree45d462ed36a5522e9f3b9fde6c4ec4918c2ae8e3 /silx/gui/plot/CurvesROIWidget.py
parentcebdc9244c019224846cb8d2668080fe386a6adc (diff)
New upstream version 0.10.1+dfsg
Diffstat (limited to 'silx/gui/plot/CurvesROIWidget.py')
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1834
1 files changed, 1178 insertions, 656 deletions
diff --git a/silx/gui/plot/CurvesROIWidget.py b/silx/gui/plot/CurvesROIWidget.py
index 81e684e..b426a23 100644
--- a/silx/gui/plot/CurvesROIWidget.py
+++ b/silx/gui/plot/CurvesROIWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,50 +22,43 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""Widget to handle regions of interest (ROI) on curves displayed in a PlotWindow.
+"""
+Widget to handle regions of interest (:class:`ROI`) on curves displayed in a
+:class:`PlotWindow`.
This widget is meant to work with :class:`PlotWindow`.
-
-ROI are defined by :
-
-- A name (`ROI` column)
-- A type. The type is the label of the x axis.
- This can be used to apply or not some ROI to a curve and do some post processing.
-- The x coordinate of the left limit (`from` column)
-- The x coordinate of the right limit (`to` column)
-- Raw counts: Sum of the curve's values in the defined Region Of Intereset.
-
- .. image:: img/rawCounts.png
-
-- Net counts: Raw counts minus background
-
- .. image:: img/netCounts.png
"""
-__authors__ = ["V.A. Sole", "T. Vincent"]
+__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
-__date__ = "13/11/2017"
+__date__ = "13/03/2018"
from collections import OrderedDict
-
import logging
import os
import sys
-import weakref
-
+import functools
import numpy
-
from silx.io import dictdump
from silx.utils import deprecation
-
+from silx.utils.weakref import WeakMethodProxy
from .. import icons, qt
+from silx.gui.plot.items.curve import Curve
+from silx.math.combo import min_max
+import weakref
+from silx.gui.widgets.TableWidget import TableWidget
_logger = logging.getLogger(__name__)
class CurvesROIWidget(qt.QWidget):
- """Widget displaying a table of ROI information.
+ """
+ Widget displaying a table of ROI information.
+
+ Implements also the following behavior:
+
+ * if the roiTable has no ROI when showing create the default ICR one
:param parent: See :class:`QWidget`
:param str name: The title of this widget
@@ -73,19 +66,18 @@ class CurvesROIWidget(qt.QWidget):
sigROIWidgetSignal = qt.Signal(object)
"""Signal of ROIs modifications.
-
- Modification information if given as a dict with an 'event' key
- providing the type of events.
-
- Type of events:
-
- - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
-
- - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
- 'rowheader'
+ Modification information if given as a dict with an 'event' key
+ providing the type of events.
+ Type of events:
+ - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
+ - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
+ 'rowheader'
"""
sigROISignal = qt.Signal(object)
+ """Deprecated signal for backward compatibility with silx < 0.7.
+ Prefer connecting directly to :attr:`CurvesRoiWidget.sigRoiSignal`
+ """
def __init__(self, parent=None, name=None, plot=None):
super(CurvesROIWidget, self).__init__(parent)
@@ -93,6 +85,8 @@ class CurvesROIWidget(qt.QWidget):
self.setWindowTitle(name)
assert plot is not None
self._plotRef = weakref.ref(plot)
+ self._showAllMarkers = False
+ self.currentROI = None
layout = qt.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@@ -103,13 +97,22 @@ class CurvesROIWidget(qt.QWidget):
self.setHeader()
layout.addWidget(self.headerLabel)
##############
- self.roiTable = ROITable(self)
+ widgetAllCheckbox = qt.QWidget(parent=self)
+ self._showAllCheckBox = qt.QCheckBox("show all ROI",
+ parent=widgetAllCheckbox)
+ widgetAllCheckbox.setLayout(qt.QHBoxLayout())
+ spacer = qt.QWidget(parent=widgetAllCheckbox)
+ spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ widgetAllCheckbox.layout().addWidget(spacer)
+ widgetAllCheckbox.layout().addWidget(self._showAllCheckBox)
+ layout.addWidget(widgetAllCheckbox)
+ ##############
+ self.roiTable = ROITable(self, plot=plot)
rheight = self.roiTable.horizontalHeader().sizeHint().height()
self.roiTable.setMinimumHeight(4 * rheight)
- self.fillFromROIDict = self.roiTable.fillFromROIDict
- self.getROIListAndDict = self.roiTable.getROIListAndDict
layout.addWidget(self.roiTable)
self._roiFileDir = qt.QDir.home().absolutePath()
+ self._showAllCheckBox.toggled.connect(self.roiTable.showAllMarkers)
#################
hbox = qt.QWidget(self)
@@ -127,7 +130,8 @@ class CurvesROIWidget(qt.QWidget):
self.addButton.setToolTip('Remove the selected ROI')
self.resetButton = qt.QPushButton(hbox)
self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the default ROI')
+ self.addButton.setToolTip('Clear all created ROIs. We only let the '
+ 'default ROI')
hboxlayout.addWidget(self.addButton)
hboxlayout.addWidget(self.delButton)
@@ -149,19 +153,22 @@ class CurvesROIWidget(qt.QWidget):
layout.addWidget(hbox)
+ # Signal / Slot connections
self.addButton.clicked.connect(self._add)
self.delButton.clicked.connect(self._del)
self.resetButton.clicked.connect(self._reset)
self.loadButton.clicked.connect(self._load)
self.saveButton.clicked.connect(self._save)
- self.roiTable.sigROITableSignal.connect(self._forward)
- self.currentROI = None
- self._middleROIMarkerFlag = False
+ self.roiTable.activeROIChanged.connect(self._emitCurrentROISignal)
+
self._isConnected = False # True if connected to plot signals
self._isInit = False
+ # expose API
+ self.getROIListAndDict = self.roiTable.getROIListAndDict
+
def getPlotWidget(self):
"""Returns the associated PlotWidget or None
@@ -173,10 +180,6 @@ class CurvesROIWidget(qt.QWidget):
self._visibilityChangedHandler(visible=True)
qt.QWidget.showEvent(self, event)
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(self, event)
-
@property
def roiFileDir(self):
"""The directory from which to load/save ROI from/to files."""
@@ -188,135 +191,81 @@ class CurvesROIWidget(qt.QWidget):
def roiFileDir(self, roiFileDir):
self._roiFileDir = str(roiFileDir)
- def setRois(self, roidict, order=None):
- """Set the ROIs by providing a dictionary of ROI information.
-
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
-
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
-
-
- :param roidict: Dictionary of ROIs
- :param str order: Field used for ordering the ROIs.
- One of "from", "to", "type".
- None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
- """
- if order is None or order.lower() == "none":
- roilist = list(roidict.keys())
- else:
- assert order in ["from", "to", "type"]
- roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return self.roiTable.fillFromROIDict(roilist, roidict)
+ def setRois(self, rois, order=None):
+ return self.roiTable.setRois(rois, order)
def getRois(self, order=None):
- """Return the currently defined ROIs, as an ordered dict.
+ return self.roiTable.getRois(order)
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
+ def setMiddleROIMarkerFlag(self, flag=True):
+ return self.roiTable.setMiddleROIMarkerFlag(flag)
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ def _add(self):
+ """Add button clicked handler"""
+ def getNextRoiName():
+ rois = self.roiTable.getRois(order=None)
+ roisNames = []
+ [roisNames.append(roiName) for roiName in rois]
+ nrois = len(rois)
+ if nrois == 0:
+ return "ICR"
+ else:
+ i = 1
+ newroi = "newroi %d" % i
+ while newroi in roisNames:
+ i += 1
+ newroi = "newroi %d" % i
+ return newroi
+ roi = ROI(name=getNextRoiName())
- :param order: Field used for ordering the ROIs.
- One of "from", "to", "type", "netcounts", "rawcounts".
- None (default) to get the same order as displayed in the widget.
- :return: Ordered dictionary of ROI information
- """
- roilist, roidict = self.roiTable.getROIListAndDict()
- if order is None or order.lower() == "none":
- ordered_roilist = roilist
+ if roi.getName() == "ICR":
+ roi.setType("Default")
else:
- assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return OrderedDict([(name, roidict[name]) for name in ordered_roilist])
+ roi.setType(self.getPlotWidget().getXAxis().getLabel())
- def setMiddleROIMarkerFlag(self, flag=True):
- """Activate or deactivate middle marker.
+ xmin, xmax = self.getPlotWidget().getXAxis().getLimits()
+ fromdata = xmin + 0.25 * (xmax - xmin)
+ todata = xmin + 0.75 * (xmax - xmin)
+ if roi.isICR():
+ fromdata, dummy0, todata, dummy1 = self._getAllLimits()
+ roi.setFrom(fromdata)
+ roi.setTo(todata)
- This allows shifting both min and max limits at once, by dragging
- a marker located in the middle.
-
- :param bool flag: True to activate middle ROI marker
- """
- if flag:
- self._middleROIMarkerFlag = True
- else:
- self._middleROIMarkerFlag = False
+ self.roiTable.addRoi(roi)
- def _add(self):
- """Add button clicked handler"""
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "AddROI"
- roilist, roidict = self.roiTable.getROIListAndDict()
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _del(self):
"""Delete button clicked handler"""
- row = self.roiTable.currentRow()
- if row >= 0:
- index = self.roiTable.labels.index('Type')
- text = str(self.roiTable.item(row, index).text())
- if text.upper() != 'DEFAULT':
- index = self.roiTable.labels.index('ROI')
- key = str(self.roiTable.item(row, index).text())
- else:
- # This is to prevent deleting ICR ROI, that is
- # usually initialized as "Default" type.
- return
- roilist, roidict = self.roiTable.getROIListAndDict()
- row = roilist.index(key)
- del roilist[row]
- del roidict[key]
- if len(roilist) > 0:
- currentroi = roilist[0]
- else:
- currentroi = None
-
- self.roiTable.fillFromROIDict(roilist=roilist,
- roidict=roidict,
- currentroi=currentroi)
- ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- def _forward(self, ddict):
- """Broadcast events from ROITable signal"""
+ self.roiTable.deleteActiveRoi()
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "DelROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _reset(self):
"""Reset button clicked handler"""
+ self.roiTable.clear()
+ self._add()
+
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "ResetROI"
- roilist0, roidict0 = self.roiTable.getROIListAndDict()
- index = 0
- for key in roilist0:
- if roidict0[key]['type'].upper() == 'DEFAULT':
- index = roilist0.index(key)
- break
- roilist = []
- roidict = {}
- if len(roilist0):
- roilist.append(roilist0[index])
- roidict[roilist[0]] = {}
- roidict[roilist[0]].update(roidict0[roilist[0]])
- self.roiTable.fillFromROIDict(roilist=roilist, roidict=roidict)
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _load(self):
"""Load button clicked handler"""
@@ -334,32 +283,22 @@ class CurvesROIWidget(qt.QWidget):
dialog.close()
self.roiFileDir = os.path.dirname(outputFile)
- self.load(outputFile)
+ self.roiTable.load(outputFile)
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "LoadROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
+ self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def load(self, filename):
"""Load ROI widget information from a file storing a dict of ROI.
:param str filename: The file from which to load ROI
"""
- rois = dictdump.load(filename)
- currentROI = None
- if self.roiTable.rowCount():
- item = self.roiTable.item(self.roiTable.currentRow(), 0)
- if item is not None:
- currentROI = str(item.text())
-
- # Remove rawcounts and netcounts from ROIs
- for roi in rois['ROI']['roidict'].values():
- roi.pop('rawcounts', None)
- roi.pop('netcounts', None)
-
- self.roiTable.fillFromROIDict(roilist=rois['ROI']['roilist'],
- roidict=rois['ROI']['roidict'],
- currentroi=currentROI)
-
- roilist, roidict = self.roiTable.getROIListAndDict()
- event = {'event': 'LoadROI', 'roilist': roilist, 'roidict': roidict}
- self.sigROIWidgetSignal.emit(event)
+ self.roiTable.load(filename)
def _save(self):
"""Save button clicked handler"""
@@ -396,142 +335,24 @@ class CurvesROIWidget(qt.QWidget):
:param str filename: The file to which to save the ROIs
"""
- roilist, roidict = self.roiTable.getROIListAndDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
- dictdump.dump(datadict, filename)
+ self.roiTable.save(filename)
def setHeader(self, text='ROIs'):
"""Set the header text of this widget"""
self.headerLabel.setText("<b>%s<\b>" % text)
- def _roiSignal(self, ddict):
- """Handle ROI widget signal"""
- _logger.debug("CurvesROIWidget._roiSignal %s", str(ddict))
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- if ddict['event'] == "AddROI":
- xmin, xmax = plot.getXAxis().getLimits()
- fromdata = xmin + 0.25 * (xmax - xmin)
- todata = xmin + 0.75 * (xmax - xmin)
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- nrois = len(roiList)
- if nrois == 0:
- newroi = "ICR"
- fromdata, dummy0, todata, dummy1 = self._getAllLimits()
- draggable = False
- color = 'black'
- else:
- # find the next index free for newroi.
- for i in range(nrois):
- i += 1
- newroi = "newroi %d" % i
- if newroi not in roiList:
- break
- color = 'blue'
- draggable = True
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=draggable)
- roiList.append(newroi)
- roiDict[newroi] = {}
- if newroi == "ICR":
- roiDict[newroi]['type'] = "Default"
- else:
- roiDict[newroi]['type'] = plot.getXAxis().getLabel()
- roiDict[newroi]['from'] = fromdata
- roiDict[newroi]['to'] = todata
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=newroi)
- self.currentROI = newroi
- self.calculateRois()
- elif ddict['event'] in ['DelROI', "ResetROI"]:
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- roiDictKeys = list(roiDict.keys())
- if len(roiDictKeys):
- currentroi = roiDictKeys[0]
- else:
- # create again the ICR
- ddict = {"event": "AddROI"}
- return self._roiSignal(ddict)
-
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=currentroi)
- self.currentROI = currentroi
-
- elif ddict['event'] == 'LoadROI':
- self.calculateRois()
+ @deprecation.deprecated(replacement="calculateRois",
+ reason="CamelCase convention",
+ since_version="0.7")
+ def calculateROIs(self, *args, **kw):
+ self.calculateRois(*args, **kw)
- elif ddict['event'] == 'selectionChanged':
- _logger.debug("Selection changed")
- self.roilist, self.roidict = self.roiTable.getROIListAndDict()
- fromdata = ddict['roi']['from']
- todata = ddict['roi']['to']
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if ddict['key'] == 'ICR':
- draggable = False
- color = 'black'
- else:
- draggable = True
- color = 'blue'
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=True)
- self.currentROI = ddict['key']
- if ddict['colheader'] in ['From', 'To']:
- dict0 = {}
- dict0['event'] = "SetActiveCurveEvent"
- dict0['legend'] = plot.getActiveCurve(just_legend=1)
- plot.setActiveCurve(dict0['legend'])
- elif ddict['colheader'] == 'Raw Counts':
- pass
- elif ddict['colheader'] == 'Net Counts':
- pass
- else:
- self._emitCurrentROISignal()
+ def calculateRois(self, roiList=None, roiDict=None):
+ """Compute ROI information"""
+ return self.roiTable.calculateRois()
- else:
- _logger.debug("Unknown or ignored event %s", ddict['event'])
+ def showAllMarkers(self, _show=True):
+ self.roiTable.showAllMarkers(_show)
def _getAllLimits(self):
"""Retrieve the limits based on the curves."""
@@ -565,429 +386,1121 @@ class CurvesROIWidget(qt.QWidget):
return xmin, ymin, xmax, ymax
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
+ def showEvent(self, event):
+ self._visibilityChangedHandler(visible=True)
+ qt.QWidget.showEvent(self, event)
- def calculateRois(self, roiList=None, roiDict=None):
- """Compute ROI information"""
- if roiList is None or roiDict is None:
- roiList, roiDict = self.roiTable.getROIListAndDict()
+ def hideEvent(self, event):
+ self._visibilityChangedHandler(visible=False)
+ qt.QWidget.hideEvent(self, event)
- plot = self.getPlotWidget()
- if plot is None:
- activeCurve = None
- else:
- activeCurve = plot.getActiveCurve(just_legend=False)
+ def _visibilityChangedHandler(self, visible):
+ """Handle widget's visibility updates.
- if activeCurve is None:
- xproc = None
- yproc = None
- self.setHeader()
- else:
- x = activeCurve.getXData(copy=False)
- y = activeCurve.getYData(copy=False)
- legend = activeCurve.getLegend()
- idx = numpy.argsort(x, kind='mergesort')
- xproc = numpy.take(x, idx)
- yproc = numpy.take(y, idx)
- self.setHeader('ROIs of %s' % legend)
-
- for key in roiList:
- if key == 'ICR':
- if xproc is not None:
- roiDict[key]['from'] = xproc.min()
- roiDict[key]['to'] = xproc.max()
- else:
- roiDict[key]['from'] = 0
- roiDict[key]['to'] = -1
- fromData = roiDict[key]['from']
- toData = roiDict[key]['to']
- if xproc is not None:
- idx = numpy.nonzero((fromData <= xproc) &
- (xproc <= toData))[0]
- if len(idx):
- xw = xproc[idx]
- yw = yproc[idx]
- rawCounts = yw.sum(dtype=numpy.float)
- deltaX = xw[-1] - xw[0]
- deltaY = yw[-1] - yw[0]
- if deltaX > 0.0:
- slope = (deltaY / deltaX)
- background = yw[0] + slope * (xw - xw[0])
- netCounts = (rawCounts -
- background.sum(dtype=numpy.float))
- else:
- netCounts = 0.0
- else:
- rawCounts = 0.0
- netCounts = 0.0
- roiDict[key]['rawcounts'] = rawCounts
- roiDict[key]['netcounts'] = netCounts
- else:
- roiDict[key].pop('rawcounts', None)
- roiDict[key].pop('netcounts', None)
+ It is connected to plot signals only when visible.
+ """
+ if visible:
+ # if no ROI existing yet, add the default one
+ if self.roiTable.rowCount() is 0:
+ self._add()
+ self.calculateRois()
- self.roiTable.fillFromROIDict(
- roilist=roiList,
- roidict=roiDict,
- currentroi=self.currentROI if self.currentROI in roiList else None)
+ def fillFromROIDict(self, *args, **kwargs):
+ self.roiTable.fillFromROIDict(*args, **kwargs)
def _emitCurrentROISignal(self):
ddict = {}
ddict['event'] = "currentROISignal"
- _roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI in roiDict:
- ddict['ROI'] = roiDict[self.currentROI]
+ if self.roiTable.activeRoi is not None:
+ ddict['ROI'] = self.roiTable.activeRoi.toDict()
+ ddict['current'] = self.roiTable.activeRoi.getName()
else:
- self.currentROI = None
- ddict['current'] = self.currentROI
+ ddict['current'] = None
self.sigROISignal.emit(ddict)
- def _handleROIMarkerEvent(self, ddict):
- """Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
+ @property
+ def currentRoi(self):
+ return self.roiTable.activeRoi
- label = ddict['label']
- if label not in ['ROI min', 'ROI max', 'ROI middle']:
- return
- roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI is None:
- return
- if self.currentROI not in roiDict:
- return
+class _FloatItem(qt.QTableWidgetItem):
+ """
+ Simple QTableWidgetItem overloading the < operator to deal with ordering
+ """
+ def __init__(self):
+ qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
- plot = self.getPlotWidget()
- if plot is None:
- return
+ def __lt__(self, other):
+ if self.text() in ('', ROITable.INFO_NOT_FOUND):
+ return False
+ if other.text() in ('', ROITable.INFO_NOT_FOUND):
+ return True
+ return float(self.text()) < float(other.text())
+
+
+class ROITable(TableWidget):
+ """Table widget displaying ROI information.
+
+ See :class:`QTableWidget` for constructor arguments.
- x = ddict['x']
-
- if label == 'ROI min':
- roiDict[self.currentROI]['from'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI max':
- roiDict[self.currentROI]['to'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI middle':
- delta = x - 0.5 * (roiDict[self.currentROI]['from'] +
- roiDict[self.currentROI]['to'])
- roiDict[self.currentROI]['from'] += delta
- roiDict[self.currentROI]['to'] += delta
- plot.addXMarker(roiDict[self.currentROI]['from'],
- legend='ROI min',
- text='ROI min',
- color='blue',
- draggable=True)
- plot.addXMarker(roiDict[self.currentROI]['to'],
- legend='ROI max',
- text='ROI max',
- color='blue',
- draggable=True)
+ Behavior: listen at the active curve changed only when the widget is
+ visible. Otherwise won't compute the row and net counts...
+ """
+
+ activeROIChanged = qt.Signal()
+ """Signal emitted when the active roi changed or when the value of the
+ active roi are changing"""
+
+ COLUMNS_INDEX = OrderedDict([
+ ('ID', 0),
+ ('ROI', 1),
+ ('Type', 2),
+ ('From', 3),
+ ('To', 4),
+ ('Raw Counts', 5),
+ ('Net Counts', 6),
+ ('Raw Area', 7),
+ ('Net Area', 8),
+ ])
+
+ COLUMNS = list(COLUMNS_INDEX.keys())
+
+ INFO_NOT_FOUND = '????????'
+
+ def __init__(self, parent=None, plot=None, rois=None):
+ super(ROITable, self).__init__(parent)
+ self._showAllMarkers = False
+ self._userIsEditingRoi = False
+ """bool used to avoid conflict when editing the ROI object"""
+ self._isConnected = False
+ self._roiToItems = {}
+ self._roiDict = {}
+ """dict of ROI object. Key is ROi id, value is the ROI object"""
+ self._markersHandler = _RoiMarkerManager()
+
+ """
+ Associate for each marker legend used when the `_showAllMarkers` option
+ is active a roi.
+ """
+ self.setColumnCount(len(self.COLUMNS))
+ self.setPlot(plot)
+ self.__setTooltip()
+ self.setSortingEnabled(True)
+ self.itemChanged.connect(self._itemChanged)
+
+ @property
+ def roidict(self):
+ return self._getRoiDict()
+
+ @property
+ def activeRoi(self):
+ return self._markersHandler._activeRoi
+
+ def _getRoiDict(self):
+ ddict = {}
+ for id in self._roiDict:
+ ddict[self._roiDict[id].getName()] = self._roiDict[id]
+ return ddict
+
+ def clear(self):
+ """
+ .. note:: clear the interface only. keep the roidict...
+ """
+ self._markersHandler.clear()
+ self._roiToItems = {}
+ self._roiDict = {}
+
+ qt.QTableWidget.clear(self)
+ self.setRowCount(0)
+ self.setHorizontalHeaderLabels(self.COLUMNS)
+ header = self.horizontalHeader()
+ if hasattr(header, 'setSectionResizeMode'): # Qt5
+ header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
+ else: # Qt4
+ header.setResizeMode(qt.QHeaderView.ResizeToContents)
+ self.sortByColumn(0, qt.Qt.AscendingOrder)
+ self.hideColumn(self.COLUMNS_INDEX['ID'])
+
+ def setPlot(self, plot):
+ self.clear()
+ self.plot = plot
+
+ def __setTooltip(self):
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
+ 'Region of interest identifier')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
+ 'Type of the ROI')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
+ 'X-value of the min point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
+ 'X-value of the max point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
+ 'Estimation of the integral between y=0 and the selected curve')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Net Counts']).setToolTip(
+ 'Estimation of the integral between the segment [maxPt, minPt] '
+ 'and the selected curve')
+
+ def setRois(self, rois, order=None):
+ """Set the ROIs by providing a dictionary of ROI information.
+
+ The dictionary keys are the ROI names.
+ Each value is a sub-dictionary of ROI info with the following fields:
+
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+
+
+ :param roidict: Dictionary of ROIs
+ :param str order: Field used for ordering the ROIs.
+ One of "from", "to", "type".
+ None (default) for no ordering, or same order as specified
+ in parameter ``roidict`` if provided as an OrderedDict.
+ """
+ assert order in [None, "from", "to", "type"]
+ self.clear()
+
+ # backward compatibility since 0.10.0
+ if isinstance(rois, dict):
+ for roiName, roi in rois.items():
+ roi['name'] = roiName
+ _roi = ROI._fromDict(roi)
+ self.addRoi(_roi)
+ else:
+ for roi in rois:
+ assert isinstance(roi, ROI)
+ self.addRoi(roi)
+ self._updateMarkers()
+
+ def addRoi(self, roi):
+ """
+
+ :param :class:`ROI` roi: roi to add to the table
+ """
+ assert isinstance(roi, ROI)
+ self._getItem(name='ID', row=None, roi=roi)
+ self._roiDict[roi.getID()] = roi
+ self._markersHandler.add(roi, _RoiMarkerHandler(roi, self.plot))
+ self._updateRoiInfo(roi.getID())
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
+ roi.getID())
+ roi.sigChanged.connect(callback)
+ # set it as the active one
+ self.setActiveRoi(roi)
+
+ def _getItem(self, name, row, roi):
+ if row:
+ item = self.item(row, self.COLUMNS_INDEX[name])
+ else:
+ item = None
+ if item:
+ return item
+ else:
+ if name == 'ID':
+ assert roi
+ if roi.getID() in self._roiToItems:
+ return self._roiToItems[roi.getID()]
+ else:
+ # create a new row
+ row = self.rowCount()
+ self.setRowCount(self.rowCount() + 1)
+ item = qt.QTableWidgetItem(str(roi.getID()),
+ type=qt.QTableWidgetItem.Type)
+ self._roiToItems[roi.getID()] = item
+ elif name == 'ROI':
+ item = qt.QTableWidgetItem(roi.getName() if roi else '',
+ type=qt.QTableWidgetItem.Type)
+ if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
+ else:
+ item.setFlags(qt.Qt.ItemIsSelectable |
+ qt.Qt.ItemIsEnabled |
+ qt.Qt.ItemIsEditable)
+ elif name == 'Type':
+ item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
+ item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
+ elif name in ('To', 'From'):
+ item = _FloatItem()
+ if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
+ else:
+ item.setFlags(qt.Qt.ItemIsSelectable |
+ qt.Qt.ItemIsEnabled |
+ qt.Qt.ItemIsEditable)
+ elif name in ('Raw Counts', 'Net Counts', 'Raw Area', 'Net Area'):
+ item = _FloatItem()
+ item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
else:
- return
- self.calculateRois(roiList, roiDict)
- self._emitCurrentROISignal()
+ raise ValueError('item type not recognized')
+
+ self.setItem(row, self.COLUMNS_INDEX[name], item)
+ return item
+
+ def _itemChanged(self, item):
+ def getRoi():
+ IDItem = self.item(item.row(), self.COLUMNS_INDEX['ID'])
+ assert IDItem
+ id = int(IDItem.text())
+ assert id in self._roiDict
+ roi = self._roiDict[id]
+ return roi
+
+ def signalChanged(roi):
+ if self.activeRoi and roi.getID() == self.activeRoi.getID():
+ self.activeROIChanged.emit()
+
+ self._userIsEditingRoi = True
+ if item.column() in (self.COLUMNS_INDEX['To'], self.COLUMNS_INDEX['From']):
+ roi = getRoi()
+
+ if item.text() not in ('', self.INFO_NOT_FOUND):
+ try:
+ value = float(item.text())
+ except ValueError:
+ value = 0
+ changed = False
+ if item.column() == self.COLUMNS_INDEX['To']:
+ if value != roi.getTo():
+ roi.setTo(value)
+ changed = True
+ else:
+ assert(item.column() == self.COLUMNS_INDEX['From'])
+ if value != roi.getFrom():
+ roi.setFrom(value)
+ changed = True
+ if changed:
+ self._updateMarker(roi.getName())
+ signalChanged(roi)
+
+ if item.column() is self.COLUMNS_INDEX['ROI']:
+ roi = getRoi()
+ if roi.getName() != item.text():
+ roi.setName(item.text())
+ self._markersHandler.getMarkerHandler(roi.getID()).updateTexts()
+ signalChanged(roi)
+
+ self._userIsEditingRoi = False
+
+ def deleteActiveRoi(self):
+ """
+ remove the current active roi
+ """
+ activeItems = self.selectedItems()
+ if len(activeItems) is 0:
+ return
+ roiToRm = set()
+ for item in activeItems:
+ row = item.row()
+ itemID = self.item(row, self.COLUMNS_INDEX['ID'])
+ roiToRm.add(self._roiDict[int(itemID.text())])
+ [self.removeROI(roi) for roi in roiToRm]
+ self.setActiveRoi(None)
+
+ def removeROI(self, roi):
+ """
+ remove the requested roi
- def _visibilityChangedHandler(self, visible):
- """Handle widget's visibility updates.
+ :param str name: the name of the roi to remove from the table
+ """
+ if roi and roi.getID() in self._roiToItems:
+ item = self._roiToItems[roi.getID()]
+ self.removeRow(item.row())
+ del self._roiToItems[roi.getID()]
- It is connected to plot signals only when visible.
+ assert roi.getID() in self._roiDict
+ del self._roiDict[roi.getID()]
+ self._markersHandler.remove(roi)
+
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
+ roi.getID())
+ roi.sigChanged.connect(callback)
+
+ def setActiveRoi(self, roi):
"""
- plot = self.getPlotWidget()
+ Define the given roi as the active one.
- if visible:
- if not self._isInit:
- # Deferred ROI widget init finalization
- self._finalizeInit()
-
- if not self._isConnected and plot is not None:
- plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.connect(
- self._activeCurveChanged)
- self._isConnected = True
+ .. warning:: this roi should already be registred / added to the table
- self.calculateRois()
+ :param :class:`ROI` roi: the roi to defined as active
+ """
+ if roi is None:
+ self.clearSelection()
+ self._markersHandler.setActiveRoi(None)
+ self.activeROIChanged.emit()
else:
- if self._isConnected:
- if plot is not None:
- plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.disconnect(
- self._activeCurveChanged)
- self._isConnected = False
+ assert isinstance(roi, ROI)
+ if roi and roi.getID() in self._roiToItems.keys():
+ self.selectRow(self._roiToItems[roi.getID()].row())
+ self._markersHandler.setActiveRoi(roi)
+ self.activeROIChanged.emit()
+
+ def _updateRoiInfo(self, roiID):
+ if self._userIsEditingRoi is True:
+ return
+ if roiID not in self._roiDict:
+ return
+ roi = self._roiDict[roiID]
+ if roi.isICR():
+ activeCurve = self.plot.getActiveCurve()
+ if activeCurve:
+ xData = activeCurve.getXData()
+ if len(xData) > 0:
+ min, max = min_max(xData)
+ roi.blockSignals(True)
+ roi.setFrom(min)
+ roi.setTo(max)
+ roi.blockSignals(False)
+
+ itemID = self._getItem(name='ID', roi=roi, row=None)
+ itemName = self._getItem(name='ROI', row=itemID.row(), roi=roi)
+ itemName.setText(roi.getName())
+
+ itemType = self._getItem(name='Type', row=itemID.row(), roi=roi)
+ itemType.setText(roi.getType() or self.INFO_NOT_FOUND)
+
+ itemFrom = self._getItem(name='From', row=itemID.row(), roi=roi)
+ fromdata = str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ itemFrom.setText(fromdata)
+
+ itemTo = self._getItem(name='To', row=itemID.row(), roi=roi)
+ todata = str(roi.getTo()) if roi.getTo() is not None else self.INFO_NOT_FOUND
+ itemTo.setText(todata)
+
+ rawCounts, netCounts = roi.computeRawAndNetCounts(
+ curve=self.plot.getActiveCurve(just_legend=False))
+ itemRawCounts = self._getItem(name='Raw Counts', row=itemID.row(),
+ roi=roi)
+ rawCounts = str(rawCounts) if rawCounts is not None else self.INFO_NOT_FOUND
+ itemRawCounts.setText(rawCounts)
+
+ itemNetCounts = self._getItem(name='Net Counts', row=itemID.row(),
+ roi=roi)
+ netCounts = str(netCounts) if netCounts is not None else self.INFO_NOT_FOUND
+ itemNetCounts.setText(netCounts)
+
+ rawArea, netArea = roi.computeRawAndNetArea(
+ curve=self.plot.getActiveCurve(just_legend=False))
+ itemRawArea = self._getItem(name='Raw Area', row=itemID.row(),
+ roi=roi)
+ rawArea = str(rawArea) if rawArea is not None else self.INFO_NOT_FOUND
+ itemRawArea.setText(rawArea)
+
+ itemNetArea = self._getItem(name='Net Area', row=itemID.row(),
+ roi=roi)
+ netArea = str(netArea) if netArea is not None else self.INFO_NOT_FOUND
+ itemNetArea.setText(netArea)
+
+ if self.activeRoi and roi.getID() == self.activeRoi.getID():
+ self.activeROIChanged.emit()
+
+ def currentChanged(self, current, previous):
+ if previous and current.row() != previous.row() and current.row() >= 0:
+ roiItem = self.item(current.row(),
+ self.COLUMNS_INDEX['ID'])
+
+ assert roiItem
+ self.setActiveRoi(self._roiDict[int(roiItem.text())])
+ self._markersHandler.updateAllMarkers()
+ qt.QTableWidget.currentChanged(self, current, previous)
+
+ @deprecation.deprecated(reason="Removed",
+ replacement="roidict and roidict.values()",
+ since_version="0.10.0")
+ def getROIListAndDict(self):
+ """
- def _activeCurveChanged(self, *args):
- """Recompute ROIs when active curve changed."""
- self.calculateRois()
+ :return: the list of roi objects and the dictionary of roi name to roi
+ object.
+ """
+ roidict = self._roiDict
+ return list(roidict.values()), roidict
- def _finalizeInit(self):
- self._isInit = True
- self.sigROIWidgetSignal.connect(self._roiSignal)
- # initialize with the ICR if no ROi existing yet
- if len(self.getRois()) is 0:
- self._roiSignal({'event': "AddROI"})
+ def calculateRois(self, roiList=None, roiDict=None):
+ """
+ Update values of all registred rois (raw and net counts in particular)
+ :param roiList: deprecated parameter
+ :param roiDict: deprecated parameter
+ """
+ if roiDict:
+ deprecation.deprecated_warning(name='roiDict', type_='Parameter',
+ reason='Unused parameter',
+ since_version="0.10.0")
+ if roiList:
+ deprecation.deprecated_warning(name='roiList', type_='Parameter',
+ reason='Unused parameter',
+ since_version="0.10.0")
+
+ for roiID in self._roiDict:
+ self._updateRoiInfo(roiID)
+
+ def _updateMarker(self, roiID):
+ """Make sure the marker of the given roi name is updated"""
+ if self._showAllMarkers or (self.activeRoi
+ and self.activeRoi.getName() == roiID):
+ self._updateMarkers()
+
+ def _updateMarkers(self):
+ if self._showAllMarkers is True:
+ self._markersHandler.updateMarkers()
+ else:
+ if not self.activeRoi or not self.plot:
+ return
+ assert isinstance(self.activeRoi, ROI)
+ markerHandler = self._markersHandler.getMarkerHandler(self.activeRoi.getID())
+ if markerHandler is not None:
+ markerHandler.updateMarkers()
-class ROITable(qt.QTableWidget):
- """Table widget displaying ROI information.
+ def getRois(self, order):
+ """
+ Return the currently defined ROIs, as an ordered dict.
- See :class:`QTableWidget` for constructor arguments.
- """
+ The dictionary keys are the ROI names.
+ Each value is a :class:`ROI` object..
- sigROITableSignal = qt.Signal(object)
- """Signal of ROI table modifications.
- """
+ :param order: Field used for ordering the ROIs.
+ One of "from", "to", "type", "netcounts", "rawcounts".
+ None (default) to get the same order as displayed in the widget.
+ :return: Ordered dictionary of ROI information
+ """
- def __init__(self, *args, **kwargs):
- super(ROITable, self).__init__(*args, **kwargs)
- self.setRowCount(1)
- self.labels = 'ROI', 'Type', 'From', 'To', 'Raw Counts', 'Net Counts'
- self.setColumnCount(len(self.labels))
- self.setSortingEnabled(False)
+ if order is None or order.lower() == "none":
+ ordered_roilist = list(self._roiDict.values())
+ res = OrderedDict([(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist])
+ else:
+ assert order in ["from", "to", "type", "netcounts", "rawcounts"]
+ ordered_roilist = sorted(self._roiDict.keys(),
+ key=lambda roi_id: self._roiDict[roi_id].get(order))
+ res = OrderedDict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
+
+ return res
+
+ def save(self, filename):
+ """
+ Save current ROIs of the widget as a dict of ROI to a file.
+
+ :param str filename: The file to which to save the ROIs
+ """
+ roilist = []
+ roidict = {}
+ for roiID, roi in self._roiDict.items():
+ roilist.append(roi.toDict())
+ roidict[roi.getName()] = roi.toDict()
+ datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
+ dictdump.dump(datadict, filename)
- for index, label in enumerate(self.labels):
- item = self.horizontalHeaderItem(index)
- if item is None:
- item = qt.QTableWidgetItem(label,
- qt.QTableWidgetItem.Type)
- item.setText(label)
- self.setHorizontalHeaderItem(index, item)
+ def load(self, filename):
+ """
+ Load ROI widget information from a file storing a dict of ROI.
- self.roidict = {}
- self.roilist = []
+ :param str filename: The file from which to load ROI
+ """
+ roisDict = dictdump.load(filename)
+ rois = []
- self.building = False
- self.fillFromROIDict(roilist=self.roilist, roidict=self.roidict)
+ # Remove rawcounts and netcounts from ROIs
+ for roiDict in roisDict['ROI']['roidict'].values():
+ roiDict.pop('rawcounts', None)
+ roiDict.pop('netcounts', None)
+ rois.append(ROI._fromDict(roiDict))
- self.cellClicked[(int, int)].connect(self._cellClickedSlot)
- self.cellChanged[(int, int)].connect(self._cellChangedSlot)
- verticalHeader = self.verticalHeader()
- verticalHeader.sectionClicked[int].connect(self._rowChangedSlot)
+ self.setRois(rois)
- self.__setTooltip()
+ def showAllMarkers(self, _show=True):
+ """
- def __setTooltip(self):
- assert(self.labels[0] == 'ROI')
- self.horizontalHeaderItem(0).setToolTip('Region of interest identifier')
- assert(self.labels[1] == 'Type')
- self.horizontalHeaderItem(1).setToolTip('Type of the ROI')
- assert(self.labels[2] == 'From')
- self.horizontalHeaderItem(2).setToolTip('X-value of the min point')
- assert(self.labels[3] == 'To')
- self.horizontalHeaderItem(3).setToolTip('X-value of the max point')
- assert(self.labels[4] == 'Raw Counts')
- self.horizontalHeaderItem(4).setToolTip('Estimation of the integral \
- between y=0 and the selected curve')
- assert(self.labels[5] == 'Net Counts')
- self.horizontalHeaderItem(5).setToolTip('Estimation of the integral \
- between the segment [maxPt, minPt] and the selected curve')
+ :param bool _show: if true show all the markers of all the ROIs
+ boundaries otherwise will only show the one of
+ the active ROI.
+ """
+ self._markersHandler.setShowAllMarkers(_show)
+
+ def setMiddleROIMarkerFlag(self, flag=True):
+ """
+ Activate or deactivate middle marker.
+
+ This allows shifting both min and max limits at once, by dragging
+ a marker located in the middle.
+
+ :param bool flag: True to activate middle ROI marker
+ """
+ self._markersHandler._middleROIMarkerFlag = flag
+
+ def _handleROIMarkerEvent(self, ddict):
+ """Handle plot signals related to marker events."""
+ if ddict['event'] == 'markerMoved':
+ label = ddict['label']
+ roiID = self._markersHandler.getRoiID(markerID=label)
+ if roiID:
+ self._markersHandler.changePosition(markerID=label,
+ x=ddict['x'])
+ self._updateRoiInfo(roiID)
+
+ def showEvent(self, event):
+ self._visibilityChangedHandler(visible=True)
+ qt.QWidget.showEvent(self, event)
+
+ def hideEvent(self, event):
+ self._visibilityChangedHandler(visible=False)
+ qt.QWidget.hideEvent(self, event)
+
+ def _visibilityChangedHandler(self, visible):
+ """Handle widget's visibility updates.
+
+ It is connected to plot signals only when visible.
+ """
+ if visible:
+ assert self.plot
+ if self._isConnected is False:
+ self.plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
+ self.plot.sigActiveCurveChanged.connect(self._activeCurveChanged)
+ self._isConnected = True
+ self.calculateRois()
+ else:
+ if self._isConnected:
+ self.plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
+ self.plot.sigActiveCurveChanged.disconnect(self._activeCurveChanged)
+ self._isConnected = False
+
+ def _activeCurveChanged(self, curve):
+ self.calculateRois()
+
+ def setCountsVisible(self, visible):
+ """
+ Display the columns relative to areas or not
+
+ :param bool visible: True if the columns 'Raw Area' and 'Net Area'
+ should be visible.
+ """
+ if visible is True:
+ self.showColumn(self.COLUMNS_INDEX['Raw Counts'])
+ self.showColumn(self.COLUMNS_INDEX['Net Counts'])
+ else:
+ self.hideColumn(self.COLUMNS_INDEX['Raw Counts'])
+ self.hideColumn(self.COLUMNS_INDEX['Net Counts'])
+
+ def setAreaVisible(self, visible):
+ """
+ Display the columns relative to areas or not
+
+ :param bool visible: True if the columns 'Raw Area' and 'Net Area'
+ should be visible.
+ """
+ if visible is True:
+ self.showColumn(self.COLUMNS_INDEX['Raw Area'])
+ self.showColumn(self.COLUMNS_INDEX['Net Area'])
+ else:
+ self.hideColumn(self.COLUMNS_INDEX['Raw Area'])
+ self.hideColumn(self.COLUMNS_INDEX['Net Area'])
def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
- """Set the ROIs by providing a list of ROI names and a dictionary
- of ROI information for each ROI.
+ """
+ This function API is kept for compatibility.
+ But `setRois` should be preferred.
+ Set the ROIs by providing a list of ROI names and a dictionary
+ of ROI information for each ROI.
The ROI names must match an existing dictionary key.
The name list is used to provide an order for the ROIs.
-
The dictionary's values are sub-dictionaries containing 3
mandatory fields:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
:param roilist: List of ROI names (keys of roidict)
:type roilist: List
:param dict roidict: Dict of ROI information
:param currentroi: Name of the selected ROI or None (no selection)
"""
- if roidict is None:
- roidict = {}
-
- self.building = True
- line0 = 0
- self.roilist = []
- self.roidict = {}
- for key in roilist:
- if key in roidict.keys():
- roi = roidict[key]
- self.roilist.append(key)
- self.roidict[key] = {}
- self.roidict[key].update(roi)
- line0 = line0 + 1
- nlines = self.rowCount()
- if (line0 > nlines):
- self.setRowCount(line0)
- line = line0 - 1
- self.roidict[key]['line'] = line
- ROI = key
- roitype = "%s" % roi['type']
- fromdata = "%6g" % (roi['from'])
- todata = "%6g" % (roi['to'])
- if 'rawcounts' in roi:
- rawcounts = "%6g" % (roi['rawcounts'])
- else:
- rawcounts = " ?????? "
- if 'netcounts' in roi:
- netcounts = "%6g" % (roi['netcounts'])
- else:
- netcounts = " ?????? "
- fields = [ROI, roitype, fromdata, todata, rawcounts, netcounts]
- col = 0
- for field in fields:
- key2 = self.item(line, col)
- if key2 is None:
- key2 = qt.QTableWidgetItem(field,
- qt.QTableWidgetItem.Type)
- self.setItem(line, col, key2)
- else:
- key2.setText(field)
- if (ROI.upper() == 'ICR') or (ROI.upper() == 'DEFAULT'):
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- else:
- if col in [0, 2, 3]:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- else:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- col = col + 1
- self.setRowCount(line0)
- i = 0
- for _label in self.labels:
- self.resizeColumnToContents(i)
- i = i + 1
- self.sortByColumn(2, qt.Qt.AscendingOrder)
- for i in range(len(self.roilist)):
- key = str(self.item(i, 0).text())
- self.roilist[i] = key
- self.roidict[key]['line'] = i
- if len(self.roilist) == 1:
- self.selectRow(0)
+ if roidict is not None:
+ self.setRois(roidict)
else:
- if currentroi in self.roidict.keys():
- self.selectRow(self.roidict[currentroi]['line'])
- _logger.debug("Qt4 ensureCellVisible to be implemented")
- self.building = False
+ self.setRois(roilist)
+ if currentroi:
+ self.setActiveRoi(currentroi)
- def getROIListAndDict(self):
- """Return the currently defined ROIs, as a 2-tuple
- ``(roiList, roiDict)``
- ``roiList`` is a list of ROI names.
- ``roiDict`` is a dictionary of ROI info.
+_indexNextROI = 0
- The ROI names must match an existing dictionary key.
- The name list is used to provide an order for the ROIs.
- The dictionary's values are sub-dictionaries containing 3
- fields:
+class ROI(qt.QObject):
+ """The Region Of Interest is defined by:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - A name
+ - A type. The type is the label of the x axis. This can be used to apply or
+ not some ROI to a curve and do some post processing.
+ - The x coordinate of the left limit (fromdata)
+ - The x coordinate of the right limit (todata)
+ :param str: name of the ROI
+ :param fromdata: left limit of the roi
+ :param todata: right limit of the roi
+ :param type: type of the ROI
+ """
+
+ sigChanged = qt.Signal()
+ """Signal emitted when the ROI is edited"""
+
+ def __init__(self, name, fromdata=None, todata=None, type_=None):
+ qt.QObject.__init__(self)
+ assert type(name) is str
+ global _indexNextROI
+ self._id = _indexNextROI
+ _indexNextROI += 1
+
+ self._name = name
+ self._fromdata = fromdata
+ self._todata = todata
+ self._type = type_ or 'Default'
- :return: ordered dict as a tuple of (list of ROI names, dict of info)
+ def getID(self):
"""
- return self.roilist, self.roidict
- def _cellClickedSlot(self, *var, **kw):
- # selection changed event, get the current selection
- row = self.currentRow()
- col = self.currentColumn()
- if row >= 0 and row < len(self.roilist):
- item = self.item(row, 0)
- text = '' if item is None else str(item.text())
- self.roilist[row] = text
- self._emitSelectionChangedSignal(row, col)
+ :return int: the unique ID of the ROI
+ """
+ return self._id
- def _rowChangedSlot(self, row):
- self._emitSelectionChangedSignal(row, 0)
+ def setType(self, type_):
+ """
- def _cellChangedSlot(self, row, col):
- _logger.debug("_cellChangedSlot(%d, %d)", row, col)
- if self.building:
- return
- if col == 0:
- self.nameSlot(row, col)
+ :param str type_:
+ """
+ if self._type != type_:
+ self._type = type_
+ self.sigChanged.emit()
+
+ def getType(self):
+ """
+
+ :return str: the type of the ROI.
+ """
+ return self._type
+
+ def setName(self, name):
+ """
+ Set the name of the :class:`ROI`
+
+ :param str name:
+ """
+ if self._name != name:
+ self._name = name
+ self.sigChanged.emit()
+
+ def getName(self):
+ """
+
+ :return str: name of the :class:`ROI`
+ """
+ return self._name
+
+ def setFrom(self, frm):
+ """
+
+ :param frm: set x coordinate of the left limit
+ """
+ if self._fromdata != frm:
+ self._fromdata = frm
+ self.sigChanged.emit()
+
+ def getFrom(self):
+ """
+
+ :return: x coordinate of the left limit
+ """
+ return self._fromdata
+
+ def setTo(self, to):
+ """
+
+ :param to: x coordinate of the right limit
+ """
+ if self._todata != to:
+ self._todata = to
+ self.sigChanged.emit()
+
+ def getTo(self):
+ """
+
+ :return: x coordinate of the right limit
+ """
+ return self._todata
+
+ def getMiddle(self):
+ """
+
+ :return: middle position between 'from' and 'to' values
+ """
+ return 0.5 * (self.getFrom() + self.getTo())
+
+ def toDict(self):
+ """
+
+ :return: dict containing the roi parameters
+ """
+ ddict = {
+ 'type': self._type,
+ 'name': self._name,
+ 'from': self._fromdata,
+ 'to': self._todata,
+ }
+ if hasattr(self, '_extraInfo'):
+ ddict.update(self._extraInfo)
+ return ddict
+
+ @staticmethod
+ def _fromDict(dic):
+ assert 'name' in dic
+ roi = ROI(name=dic['name'])
+ roi._extraInfo = {}
+ for key in dic:
+ if key == 'from':
+ roi.setFrom(dic['from'])
+ elif key == 'to':
+ roi.setTo(dic['to'])
+ elif key == 'type':
+ roi.setType(dic['type'])
+ else:
+ roi._extraInfo[key] = dic[key]
+
+ return roi
+
+ def isICR(self):
+ """
+
+ :return: True if the ROI is the `ICR`
+ """
+ return self._name == 'ICR'
+
+ def computeRawAndNetCounts(self, curve):
+ """Compute the Raw and net counts in the ROI for the given curve.
+
+ - Raw count: Points values sum of the curve in the defined Region Of
+ Interest.
+
+ .. image:: img/rawCounts.png
+
+ - Net count: Raw counts minus background
+
+ .. image:: img/netCounts.png
+
+ :param CurveItem curve:
+ :return tuple: rawCount, netCount
+ """
+ assert isinstance(curve, Curve) or curve is None
+
+ if curve is None:
+ return None, None
+
+ x = curve.getXData(copy=False)
+ y = curve.getYData(copy=False)
+
+ idx = numpy.nonzero((self._fromdata <= x) &
+ (x <= self._todata))[0]
+ if len(idx):
+ xw = x[idx]
+ yw = y[idx]
+ rawCounts = yw.sum(dtype=numpy.float)
+ deltaX = xw[-1] - xw[0]
+ deltaY = yw[-1] - yw[0]
+ if deltaX > 0.0:
+ slope = (deltaY / deltaX)
+ background = yw[0] + slope * (xw - xw[0])
+ netCounts = (rawCounts -
+ background.sum(dtype=numpy.float))
+ else:
+ netCounts = 0.0
else:
- self._valueChanged(row, col)
+ rawCounts = 0.0
+ netCounts = 0.0
+ return rawCounts, netCounts
+
+ def computeRawAndNetArea(self, curve):
+ """Compute the Raw and net counts in the ROI for the given curve.
+
+ - Raw area: integral of the curve between the min ROI point and the
+ max ROI point to the y = 0 line.
- def _valueChanged(self, row, col):
- if col not in [2, 3]:
+ .. image:: img/rawArea.png
+
+ - Net area: Raw counts minus background
+
+ .. image:: img/netArea.png
+
+ :param CurveItem curve:
+ :return tuple: rawArea, netArea
+ """
+ assert isinstance(curve, Curve) or curve is None
+
+ if curve is None:
+ return None, None
+
+ x = curve.getXData(copy=False)
+ y = curve.getYData(copy=False)
+
+ y = y[(x >= self._fromdata) & (x <= self._todata)]
+ x = x[(x >= self._fromdata) & (x <= self._todata)]
+
+ if x.size is 0:
+ return 0.0, 0.0
+
+ rawArea = numpy.trapz(y, x=x)
+ # to speed up and avoid an intersection calculation we are taking the
+ # closest index to the ROI
+ closestXLeftIndex = (numpy.abs(x - self.getFrom())).argmin()
+ closestXRightIndex = (numpy.abs(x - self.getTo())).argmin()
+ yBackground = y[closestXLeftIndex], y[closestXRightIndex]
+ background = numpy.trapz(yBackground, x=x)
+ netArea = rawArea - background
+ return rawArea, netArea
+
+
+class _RoiMarkerManager(object):
+ """
+ Deal with all the ROI markers
+ """
+ def __init__(self):
+ self._roiMarkerHandlers = {}
+ self._middleROIMarkerFlag = False
+ self._showAllMarkers = False
+ self._activeRoi = None
+
+ def setActiveRoi(self, roi):
+ self._activeRoi = roi
+ self.updateAllMarkers()
+
+ def setShowAllMarkers(self, show):
+ if show != self._showAllMarkers:
+ self._showAllMarkers = show
+ self.updateAllMarkers()
+
+ def add(self, roi, markersHandler):
+ assert isinstance(roi, ROI)
+ assert isinstance(markersHandler, _RoiMarkerHandler)
+ if roi.getID() in self._roiMarkerHandlers:
+ raise ValueError('roi with the same ID already existing')
+ else:
+ self._roiMarkerHandlers[roi.getID()] = markersHandler
+
+ def getMarkerHandler(self, roiID):
+ if roiID in self._roiMarkerHandlers:
+ return self._roiMarkerHandlers[roiID]
+ else:
+ return None
+
+ def clear(self):
+ roisHandler = list(self._roiMarkerHandlers.values())
+ for roiHandler in roisHandler:
+ self.remove(roiHandler.roi)
+
+ def remove(self, roi):
+ if roi is None:
return
- item = self.item(row, col)
- if item is None:
+ assert isinstance(roi, ROI)
+ if roi.getID() in self._roiMarkerHandlers:
+ self._roiMarkerHandlers[roi.getID()].clear()
+ del self._roiMarkerHandlers[roi.getID()]
+
+ def hasMarker(self, markerID):
+ assert type(markerID) is str
+ return self.getMarker(markerID) is not None
+
+ def changePosition(self, markerID, x):
+ markerHandler = self.getMarker(markerID)
+ if markerHandler is None:
+ raise ValueError('Marker %s not register' % markerID)
+ markerHandler.changePosition(markerID=markerID, x=x)
+
+ def updateMarker(self, markerID):
+ markerHandler = self.getMarker(markerID)
+ if markerHandler is None:
+ raise ValueError('Marker %s not register' % markerID)
+ roiID = self.getRoiID(markerID)
+ visible = (self._activeRoi and self._activeRoi.getID() == roiID) or self._showAllMarkers is True
+ markerHandler.setVisible(visible)
+ markerHandler.updateAllMarkers()
+
+ def updateRoiMarkers(self, roiID):
+ if roiID in self._roiMarkerHandlers:
+ visible = ((self._activeRoi and self._activeRoi.getID() == roiID)
+ or self._showAllMarkers is True)
+ _roi = self._roiMarkerHandlers[roiID]._roi()
+ if _roi and not _roi.isICR():
+ self._roiMarkerHandlers[roiID].showMiddleMarker(self._middleROIMarkerFlag)
+ self._roiMarkerHandlers[roiID].setVisible(visible)
+ self._roiMarkerHandlers[roiID].updateMarkers()
+
+ def getMarker(self, markerID):
+ assert type(markerID) is str
+ for marker in list(self._roiMarkerHandlers.values()):
+ if marker.hasMarker(markerID):
+ return marker
+
+ def updateMarkers(self):
+ for markerHandler in list(self._roiMarkerHandlers.values()):
+ markerHandler.updateMarkers()
+
+ def getRoiID(self, markerID):
+ for roiID, markerHandler in self._roiMarkerHandlers.items():
+ if markerHandler.hasMarker(markerID):
+ return roiID
+ return None
+
+ def setShowMiddleMarkers(self, show):
+ self._middleROIMarkerFlag = show
+ self._roiMarkerHandlers.updateAllMarkers()
+
+ def updateAllMarkers(self):
+ for roiID in self._roiMarkerHandlers:
+ self.updateRoiMarkers(roiID)
+
+ def getVisibleRois(self):
+ res = {}
+ for roiID, roiHandler in self._roiMarkerHandlers.items():
+ markers = (roiHandler.getMarker('min'), roiHandler.getMarker('max'),
+ roiHandler.getMarker('middle'))
+ for marker in markers:
+ if marker.isVisible():
+ if roiID not in res:
+ res[roiID] = []
+ res[roiID].append(marker)
+ return res
+
+
+class _RoiMarkerHandler(object):
+ """Used to deal with ROI markers used in ROITable"""
+ def __init__(self, roi, plot):
+ assert roi and isinstance(roi, ROI)
+ assert plot
+
+ self._roi = weakref.ref(roi)
+ self._plot = weakref.ref(plot)
+ self._draggable = False if roi.isICR() else True
+ self._color = 'black' if roi.isICR() else 'blue'
+ self._displayMidMarker = False
+ self._visible = True
+
+ @property
+ def draggable(self):
+ return self._draggable
+
+ @property
+ def plot(self):
+ return self._plot()
+
+ def clear(self):
+ if self.plot and self.roi:
+ self.plot.removeMarker(self._markerID('min'))
+ self.plot.removeMarker(self._markerID('max'))
+ self.plot.removeMarker(self._markerID('middle'))
+
+ @property
+ def roi(self):
+ return self._roi()
+
+ def setVisible(self, visible):
+ if visible != self._visible:
+ self._visible = visible
+ self.updateMarkers()
+
+ def showMiddleMarker(self, visible):
+ if self.draggable is False and visible is True:
+ _logger.warning("ROI is not draggable. Won't display middle marker")
return
- text = str(item.text())
- try:
- value = float(text)
- except:
+ self._displayMidMarker = visible
+ self.getMarker('middle').setVisible(self._displayMidMarker)
+
+ def updateMarkers(self):
+ if self.roi is None:
return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
+ self._updateMinMarkerPos()
+ self._updateMaxMarkerPos()
+ self._updateMiddleMarkerPos()
+
+ def _updateMinMarkerPos(self):
+ self.getMarker('min').setPosition(x=self.roi.getFrom(), y=None)
+ self.getMarker('min').setVisible(self._visible)
+
+ def _updateMaxMarkerPos(self):
+ self.getMarker('max').setPosition(x=self.roi.getTo(), y=None)
+ self.getMarker('max').setVisible(self._visible)
+
+ def _updateMiddleMarkerPos(self):
+ self.getMarker('middle').setPosition(x=self.roi.getMiddle(), y=None)
+ self.getMarker('middle').setVisible(self._displayMidMarker and self._visible)
+
+ def getMarker(self, markerType):
+ if self.plot is None:
+ return None
+ assert markerType in ('min', 'max', 'middle')
+ if self.plot._getMarker(self._markerID(markerType)) is None:
+ assert self.roi
+ if markerType == 'min':
+ val = self.roi.getFrom()
+ elif markerType == 'max':
+ val = self.roi.getTo()
+ else:
+ val = self.roi.getMiddle()
+
+ _color = self._color
+ if markerType == 'middle':
+ _color = 'yellow'
+ self.plot.addXMarker(val,
+ legend=self._markerID(markerType),
+ text=self.getMarkerName(markerType),
+ color=_color,
+ draggable=self.draggable)
+ return self.plot._getMarker(self._markerID(markerType))
+
+ def _markerID(self, markerType):
+ assert markerType in ('min', 'max', 'middle')
+ assert self.roi
+ return '_'.join((str(self.roi.getID()), markerType))
+
+ def getMarkerName(self, markerType):
+ assert markerType in ('min', 'max', 'middle')
+ assert self.roi
+ return ' '.join((self.roi.getName(), markerType))
+
+ def updateTexts(self):
+ self.getMarker('min').setText(self.getMarkerName('min'))
+ self.getMarker('max').setText(self.getMarkerName('max'))
+ self.getMarker('middle').setText(self.getMarkerName('middle'))
+
+ def changePosition(self, markerID, x):
+ assert self.hasMarker(markerID)
+ markerType = self._getMarkerType(markerID)
+ assert markerType is not None
+ if self.roi is None:
return
- item = self.item(row, 0)
- if item is None:
- text = ""
+ if markerType == 'min':
+ self.roi.setFrom(x)
+ self._updateMiddleMarkerPos()
+ elif markerType == 'max':
+ self.roi.setTo(x)
+ self._updateMiddleMarkerPos()
else:
- text = str(item.text())
- if not len(text):
- return
- if col == 2:
- self.roidict[text]['from'] = value
- elif col == 3:
- self.roidict[text]['to'] = value
- self._emitSelectionChangedSignal(row, col)
-
- def nameSlot(self, row, col):
- if col != 0:
- return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
- return
- item = self.item(row, col)
- if item is None:
- text = ""
+ delta = x - 0.5 * (self.roi.getFrom() + self.roi.getTo())
+ self.roi.setFrom(self.roi.getFrom() + delta)
+ self.roi.setTo(self.roi.getTo() + delta)
+ self._updateMinMarkerPos()
+ self._updateMaxMarkerPos()
+
+ def hasMarker(self, marker):
+ return marker in (self._markerID('min'),
+ self._markerID('max'),
+ self._markerID('middle'))
+
+ def _getMarkerType(self, markerID):
+ if markerID.endswith('_min'):
+ return 'min'
+ elif markerID.endswith('_max'):
+ return 'max'
+ elif markerID.endswith('_middle'):
+ return 'middle'
else:
- text = str(item.text())
- if len(text) and (text not in self.roilist):
- old = self.roilist[row]
- self.roilist[row] = text
- self.roidict[text] = {}
- self.roidict[text].update(self.roidict[old])
- del self.roidict[old]
- self._emitSelectionChangedSignal(row, col)
-
- def _emitSelectionChangedSignal(self, row, col):
- ddict = {}
- ddict['event'] = "selectionChanged"
- ddict['row'] = row
- ddict['col'] = col
- ddict['roi'] = self.roidict[self.roilist[row]]
- ddict['key'] = self.roilist[row]
- ddict['colheader'] = self.labels[col]
- ddict['rowheader'] = "%d" % row
- self.sigROITableSignal.emit(ddict)
+ return None
class CurvesROIDockWidget(qt.QDockWidget):
@@ -1007,6 +1520,8 @@ class CurvesROIDockWidget(qt.QDockWidget):
def __init__(self, parent=None, plot=None, name=None):
super(CurvesROIDockWidget, self).__init__(name, parent)
+ assert plot is not None
+ self.plot = plot
self.roiWidget = CurvesROIWidget(self, name, plot=plot)
"""Main widget of type :class:`CurvesROIWidget`"""
@@ -1016,12 +1531,15 @@ class CurvesROIDockWidget(qt.QDockWidget):
self.calculateROIs = self.calculateRois = self.roiWidget.calculateRois
self.setRois = self.roiWidget.setRois
self.getRois = self.roiWidget.getRois
+
self.roiWidget.sigROISignal.connect(self._forwardSigROISignal)
- self.currentROI = self.roiWidget.currentROI
self.layout().setContentsMargins(0, 0, 0, 0)
self.setWidget(self.roiWidget)
+ self.setAreaVisible = self.roiWidget.roiTable.setAreaVisible
+ self.setCountsVisible = self.roiWidget.roiTable.setCountsVisible
+
def _forwardSigROISignal(self, ddict):
# emit deprecated signal for backward compatibility (silx < 0.7)
self.sigROISignal.emit(ddict)
@@ -1042,3 +1560,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
"""
self.raise_()
qt.QDockWidget.showEvent(self, event)
+
+ @property
+ def currentROI(self):
+ return self.roiWidget.currentRoi