From b3bea947efa55d2c0f198b6c6795b3177be27f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Wed, 6 Jan 2021 14:10:12 +0100 Subject: New upstream version 0.14.0+dfsg --- silx/gui/plot/tools/profile/manager.py | 31 +++-- silx/gui/plot/tools/profile/rois.py | 14 +- silx/gui/plot/tools/roi.py | 239 +++++++++++++++++++++++++++++++-- silx/gui/plot/tools/test/testROI.py | 67 ++++++++- 4 files changed, 324 insertions(+), 27 deletions(-) (limited to 'silx/gui/plot/tools') 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(): -- cgit v1.2.3