summaryrefslogtreecommitdiff
path: root/silx/gui/plot/tools
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2021-01-06 14:10:12 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2021-01-06 14:10:12 +0100
commitb3bea947efa55d2c0f198b6c6795b3177be27f45 (patch)
tree4116758aafe4483bf472c1d54b519e685737fd77 /silx/gui/plot/tools
parent5ad425ff4e62f5e003178813ebd073577679a00e (diff)
New upstream version 0.14.0+dfsg
Diffstat (limited to 'silx/gui/plot/tools')
-rw-r--r--silx/gui/plot/tools/profile/manager.py31
-rw-r--r--silx/gui/plot/tools/profile/rois.py14
-rw-r--r--silx/gui/plot/tools/roi.py239
-rw-r--r--silx/gui/plot/tools/test/testROI.py67
4 files changed, 324 insertions, 27 deletions
diff --git a/silx/gui/plot/tools/profile/manager.py b/silx/gui/plot/tools/profile/manager.py
index 4d467f0..757b741 100644
--- a/silx/gui/plot/tools/profile/manager.py
+++ b/silx/gui/plot/tools/profile/manager.py
@@ -76,6 +76,17 @@ class _RunnableComputeProfile(qt.QRunnable):
self._signals.moveToThread(threadPool.thread())
self._item = item
self._roi = roi
+ self._cancelled = False
+
+ def _lazyCancel(self):
+ """Cancel the runner if it is not yet started.
+
+ The threadpool will still execute the runner, but this will process
+ nothing.
+
+ This is only used with Qt<5.9 where QThreadPool.tryTake is not available.
+ """
+ self._cancelled = True
def autoDelete(self):
return False
@@ -106,12 +117,13 @@ class _RunnableComputeProfile(qt.QRunnable):
def run(self):
"""Process the profile computation.
"""
- try:
- profileData = self._roi.computeProfile(self._item)
- except Exception:
- _logger.error("Error while computing profile", exc_info=True)
- else:
- self.resultReady.emit(self._roi, profileData)
+ if not self._cancelled:
+ try:
+ profileData = self._roi.computeProfile(self._item)
+ except Exception:
+ _logger.error("Error while computing profile", exc_info=True)
+ else:
+ self.resultReady.emit(self._roi, profileData)
self.runnerFinished.emit(self)
@@ -815,8 +827,11 @@ class ProfileManager(qt.QObject):
self._pendingRunners.remove(runner)
continue
if runner.getRoi() is profileRoi:
- if threadPool.tryTake(runner):
- self._pendingRunners.remove(runner)
+ if hasattr(threadPool, "tryTake"):
+ if threadPool.tryTake(runner):
+ self._pendingRunners.remove(runner)
+ else: # Support Qt<5.9
+ runner._lazyCancel()
item = self.getPlotItem()
if item is None or not isinstance(item, profileRoi.ITEM_KIND):
diff --git a/silx/gui/plot/tools/profile/rois.py b/silx/gui/plot/tools/profile/rois.py
index b49679c..9e651a7 100644
--- a/silx/gui/plot/tools/profile/rois.py
+++ b/silx/gui/plot/tools/profile/rois.py
@@ -137,11 +137,11 @@ class _ImageProfileArea(items.Shape):
if not isinstance(item, items.ImageBase):
raise TypeError("Unexpected class %s" % type(item))
- if isinstance(item, items.ImageData):
- currentData = item.getData(copy=False)
- elif isinstance(item, items.ImageRgba):
+ if isinstance(item, items.ImageRgba):
rgba = item.getData(copy=False)
currentData = rgba[..., 0]
+ else:
+ currentData = item.getData(copy=False)
roi = self.getParentRoi()
origin = item.getOrigin()
@@ -310,15 +310,15 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
method=method)
return coords, profile, profileName, xLabel
- if isinstance(item, items.ImageData):
- currentData = item.getData(copy=False)
- elif isinstance(item, items.ImageRgba):
+ if isinstance(item, items.ImageRgba):
rgba = item.getData(copy=False)
is_uint8 = rgba.dtype.type == numpy.uint8
# luminosity
if is_uint8:
- rgba = rgba.astype(numpy.float)
+ rgba = rgba.astype(numpy.float64)
currentData = 0.21 * rgba[..., 0] + 0.72 * rgba[..., 1] + 0.07 * rgba[..., 2]
+ else:
+ currentData = item.getData(copy=False)
yLabel = "%s" % str(method).capitalize()
coords, profile, title, xLabel = createProfile2(currentData)
diff --git a/silx/gui/plot/tools/roi.py b/silx/gui/plot/tools/roi.py
index 431ecb2..4e2d6db 100644
--- a/silx/gui/plot/tools/roi.py
+++ b/silx/gui/plot/tools/roi.py
@@ -34,10 +34,13 @@ import enum
import logging
import time
import weakref
+import functools
import numpy
from ... import qt, icons
+from ...utils import blockSignals
+from ...utils import LockReentrant
from .. import PlotWidget
from ..items import roi as roi_items
@@ -163,6 +166,155 @@ class CreateRoiModeAction(qt.QAction):
pass
+class RoiModeSelector(qt.QWidget):
+ def __init__(self, parent=None):
+ super(RoiModeSelector, self).__init__(parent=parent)
+ self.__roi = None
+ self.__reentrant = LockReentrant()
+
+ layout = qt.QHBoxLayout(self)
+ if isinstance(parent, qt.QMenu):
+ margins = layout.contentsMargins()
+ layout.setContentsMargins(margins.left(), 0, margins.right(), 0)
+ else:
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ self._label = qt.QLabel(self)
+ self._label.setText("Mode:")
+ self._label.setToolTip("Select a specific interaction to edit the ROI")
+ self._combo = qt.QComboBox(self)
+ self._combo.currentIndexChanged.connect(self._modeSelected)
+ layout.addWidget(self._label)
+ layout.addWidget(self._combo)
+ self._updateAvailableModes()
+
+ def getRoi(self):
+ """Returns the edited ROI.
+
+ :rtype: roi_items.RegionOfInterest
+ """
+ return self.__roi
+
+ def setRoi(self, roi):
+ """Returns the edited ROI.
+
+ :rtype: roi_items.RegionOfInterest
+ """
+ if self.__roi is roi:
+ return
+ if not isinstance(roi, roi_items.InteractionModeMixIn):
+ self.__roi = None
+ self._updateAvailableModes()
+ return
+
+ if self.__roi is not None:
+ self.__roi.sigInteractionModeChanged.disconnect(self._modeChanged)
+ self.__roi = roi
+ if self.__roi is not None:
+ self.__roi.sigInteractionModeChanged.connect(self._modeChanged)
+ self._updateAvailableModes()
+
+ def isEmpty(self):
+ return not self._label.isVisibleTo(self)
+
+ def _updateAvailableModes(self):
+ roi = self.getRoi()
+ if isinstance(roi, roi_items.InteractionModeMixIn):
+ modes = roi.availableInteractionModes()
+ else:
+ modes = []
+ if len(modes) <= 1:
+ self._label.setVisible(False)
+ self._combo.setVisible(False)
+ else:
+ self._label.setVisible(True)
+ self._combo.setVisible(True)
+ with blockSignals(self._combo):
+ self._combo.clear()
+ for im, m in enumerate(modes):
+ self._combo.addItem(m.label, m)
+ self._combo.setItemData(im, m.description, qt.Qt.ToolTipRole)
+ mode = roi.getInteractionMode()
+ self._modeChanged(mode)
+ index = modes.index(mode)
+ self._combo.setCurrentIndex(index)
+
+ def _modeChanged(self, mode):
+ """Triggered when the ROI interaction mode was changed externally"""
+ if self.__reentrant.locked():
+ # This event was initialised by the widget
+ return
+ roi = self.__roi
+ modes = roi.availableInteractionModes()
+ index = modes.index(mode)
+ with blockSignals(self._combo):
+ self._combo.setCurrentIndex(index)
+
+ def _modeSelected(self):
+ """Triggered when the ROI interaction mode was selected in the widget"""
+ index = self._combo.currentIndex()
+ if index == -1:
+ return
+ roi = self.getRoi()
+ if roi is not None:
+ mode = self._combo.itemData(index, qt.Qt.UserRole)
+ with self.__reentrant:
+ roi.setInteractionMode(mode)
+
+
+class RoiModeSelectorAction(qt.QWidgetAction):
+ """Display the selected mode of a ROI and allow to change it"""
+
+ def __init__(self, parent=None):
+ super(RoiModeSelectorAction, self).__init__(parent)
+ self.__roiManager = None
+
+ def createWidget(self, parent):
+ """Inherit the method to create a new widget"""
+ widget = RoiModeSelector(parent)
+ manager = self.__roiManager
+ if manager is not None:
+ roi = manager.getCurrentRoi()
+ widget.setRoi(roi)
+ self.setVisible(not widget.isEmpty())
+ return widget
+
+ def deleteWidget(self, widget):
+ """Inherit the method to delete a widget"""
+ widget.setRoi(None)
+ return qt.QWidgetAction.deleteWidget(self, widget)
+
+ def setRoiManager(self, roiManager):
+ """
+ Connect this action to a ROI manager.
+
+ :param RegionOfInterestManager roiManager: A ROI manager
+ """
+ if self.__roiManager is roiManager:
+ return
+ if self.__roiManager is not None:
+ self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged)
+ self.__roiManager = roiManager
+ if self.__roiManager is not None:
+ self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged)
+ self.__currentRoiChanged(roiManager.getCurrentRoi())
+
+ def __currentRoiChanged(self, roi):
+ """Handle changes of the selected ROI"""
+ self.setRoi(roi)
+
+ def setRoi(self, roi):
+ """Set a profile ROI to edit.
+
+ :param ProfileRoiMixIn roi: A profile ROI
+ """
+ widget = None
+ for widget in self.createdWidgets():
+ widget.setRoi(roi)
+ if widget is not None:
+ self.setVisible(not widget.isEmpty())
+
+
class RegionOfInterestManager(qt.QObject):
"""Class handling ROI interaction on a PlotWidget.
@@ -257,6 +409,8 @@ class RegionOfInterestManager(qt.QObject):
parent.sigItemRemoved.connect(self._itemRemoved)
+ parent._sigDefaultContextMenu.connect(self._feedContextMenu)
+
@classmethod
def getSupportedRoiClasses(cls):
"""Returns the default available ROI classes
@@ -400,25 +554,87 @@ class RegionOfInterestManager(qt.QObject):
def _plotSignals(self, event):
"""Handle mouse interaction for ROI addition"""
- if event['event'] in ('markerClicked', 'markerMoving'):
+ clicked = False
+ roi = None
+ if event["event"] in ("markerClicked", "markerMoving"):
plot = self.parent()
- legend = event['label']
+ legend = event["label"]
marker = plot._getMarker(legend=legend)
roi = self.__getRoiFromMarker(marker)
- if roi is not None and roi.isSelectable():
- self.setCurrentRoi(roi)
- else:
- self.setCurrentRoi(None)
- elif event['event'] == 'mouseClicked' and event['button'] == 'left':
+ elif event["event"] == "mouseClicked" and event["button"] == "left":
# Marker click is only for dnd
# This also can click on a marker
+ clicked = True
plot = self.parent()
- marker = plot._getMarkerAt(event['xpixel'], event['ypixel'])
+ marker = plot._getMarkerAt(event["xpixel"], event["ypixel"])
roi = self.__getRoiFromMarker(marker)
- if roi is not None and roi.isSelectable():
+ else:
+ return
+
+ if roi not in self._rois:
+ # The ROI is not own by this manager
+ return
+
+ if roi is not None:
+ currentRoi = self.getCurrentRoi()
+ if currentRoi is roi:
+ if clicked:
+ self.__updateMode(roi)
+ elif roi.isSelectable():
self.setCurrentRoi(roi)
+ else:
+ self.setCurrentRoi(None)
+
+ def __updateMode(self, roi):
+ if isinstance(roi, roi_items.InteractionModeMixIn):
+ available = roi.availableInteractionModes()
+ mode = roi.getInteractionMode()
+ imode = available.index(mode)
+ mode = available[(imode + 1) % len(available)]
+ roi.setInteractionMode(mode)
+
+ def _feedContextMenu(self, menu):
+ """Called wen the default plot context menu is about to be displayed"""
+ roi = self.getCurrentRoi()
+ if roi is not None:
+ if roi.isEditable():
+ # Filter by data position
+ # FIXME: It would be better to use GUI coords for it
+ plot = self.parent()
+ pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
+ data = plot.pixelToData(pos.x(), pos.y())
+ if roi.contains(data):
+ if isinstance(roi, roi_items.InteractionModeMixIn):
+ self._contextMenuForInteractionMode(menu, roi)
+
+ removeAction = qt.QAction(menu)
+ removeAction.setText("Remove %s" % roi.getName())
+ callback = functools.partial(self.removeRoi, roi)
+ removeAction.triggered.connect(callback)
+ menu.addAction(removeAction)
+
+ def _contextMenuForInteractionMode(self, menu, roi):
+ availableModes = roi.availableInteractionModes()
+ currentMode = roi.getInteractionMode()
+ submenu = qt.QMenu(menu)
+ modeGroup = qt.QActionGroup(menu)
+ modeGroup.setExclusive(True)
+ for mode in availableModes:
+ action = qt.QAction(menu)
+ action.setText(mode.label)
+ action.setToolTip(mode.description)
+ action.setCheckable(True)
+ if mode is currentMode:
+ action.setChecked(True)
else:
- self.setCurrentRoi(None)
+ callback = functools.partial(roi.setInteractionMode, mode)
+ action.triggered.connect(callback)
+ modeGroup.addAction(action)
+ submenu.addAction(action)
+ action = qt.QAction(menu)
+ action.setMenu(submenu)
+ action.setText("%s interaction mode" % roi.getName())
+ menu.addAction(action)
# RegionOfInterest API
@@ -666,8 +882,9 @@ class RegionOfInterestManager(qt.QObject):
if self._drawnROI is not None:
# Cancel ROI create
- self.removeRoi(self._drawnROI)
+ roi = self._drawnROI
self._drawnROI = None
+ self.removeRoi(roi)
plot = self.parent()
if plot is not None:
diff --git a/silx/gui/plot/tools/test/testROI.py b/silx/gui/plot/tools/test/testROI.py
index 33a0000..8a00073 100644
--- a/silx/gui/plot/tools/test/testROI.py
+++ b/silx/gui/plot/tools/test/testROI.py
@@ -136,6 +136,31 @@ class TestRoiItems(TestCaseQt):
numpy.testing.assert_allclose(item.getCenter(), center)
numpy.testing.assert_allclose(item.getRadius(), newRadius)
+ def testCircle_contains(self):
+ center = numpy.array([2, -1])
+ radius = 1.
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ self.assertTrue(item.contains([1, -1]))
+ self.assertFalse(item.contains([0, 0]))
+ self.assertTrue(item.contains([2, 0]))
+ self.assertFalse(item.contains([3.01, -1]))
+
+ def testEllipse_contains(self):
+ center = numpy.array([-2, 0])
+ item = roi_items.EllipseROI()
+ item.setCenter(center)
+ item.setOrientation(numpy.pi / 4.0)
+ item.setMajorRadius(2)
+ item.setMinorRadius(1)
+ print(item.getMinorRadius(), item.getMajorRadius())
+ self.assertFalse(item.contains([0, 0]))
+ self.assertTrue(item.contains([-1, 1]))
+ self.assertTrue(item.contains([-3, 0]))
+ self.assertTrue(item.contains([-2, 0]))
+ self.assertTrue(item.contains([-2, 1]))
+ self.assertFalse(item.contains([-4, 1]))
+
def testRectangle_isIn(self):
origin = numpy.array([0, 0])
size = numpy.array([10, 20])
@@ -557,8 +582,9 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
mx, my = self.plot.dataToPixel(*center)
self.mouseMove(widget, pos=(mx, my))
self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.mouseMove(widget, pos=(mx, my+25))
self.mouseMove(widget, pos=(mx, my+50))
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my+50))
result = numpy.array(item.getEndPoints())
# x location is still the same
@@ -615,6 +641,45 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
# Clean up
manager.clear()
+ def testArcRoiSwitchMode(self):
+ """Make sure we can switch mode by clicking on the ROI"""
+ xlimit = self.plot.getXAxis().getLimits()
+ ylimit = self.plot.getYAxis().getLimits()
+ points = numpy.array([xlimit, ylimit]).T
+ center = numpy.mean(points, axis=0)
+ size = numpy.abs(points[1] - points[0])
+
+ # Create the line
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.ArcROI()
+ item.setGeometry(center, size[1] / 10, size[1] / 2, 0, 3)
+ item.setEditable(True)
+ item.setSelectable(True)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+
+ # Initial state
+ self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
+ self.qWait(500)
+
+ # Click on the center
+ widget = self.plot.getWidgetHandle()
+ mx, my = self.plot.dataToPixel(*center)
+
+ # Select the ROI
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
+
+ # Change the mode
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ self.assertIs(item.getInteractionMode(), roi_items.ArcROI.PolarMode)
+
+ manager.clear()
+ self.qapp.processEvents()
def suite():