summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot/tools/test/testRoiCore.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot/tools/test/testRoiCore.py')
-rw-r--r--src/silx/gui/plot/tools/test/testRoiCore.py535
1 files changed, 535 insertions, 0 deletions
diff --git a/src/silx/gui/plot/tools/test/testRoiCore.py b/src/silx/gui/plot/tools/test/testRoiCore.py
new file mode 100644
index 0000000..e7f6d8a
--- /dev/null
+++ b/src/silx/gui/plot/tools/test/testRoiCore.py
@@ -0,0 +1,535 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2020 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "28/06/2018"
+
+
+import numpy.testing
+
+from silx.gui import qt
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt, SignalListener
+from silx.gui.plot import PlotWindow
+import silx.gui.plot.items.roi as roi_items
+from silx.gui.plot.tools import roi
+
+
+class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
+ """Tests for RegionOfInterestManager class"""
+
+ def setUp(self):
+ super(TestRegionOfInterestManager, self).setUp()
+ self.plot = PlotWindow()
+
+ self.roiTableWidget = roi.RegionOfInterestTableWidget()
+ dock = qt.QDockWidget()
+ dock.setWidget(self.roiTableWidget)
+ self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
+
+ self.plot.show()
+ self.qWaitForWindowExposed(self.plot)
+
+ def tearDown(self):
+ del self.roiTableWidget
+ self.qapp.processEvents()
+ self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.plot.close()
+ del self.plot
+ super(TestRegionOfInterestManager, self).tearDown()
+
+ def test(self):
+ """Test ROI of different shapes"""
+ tests = ( # shape, points=[list of (x, y), list of (x, y)]
+ (roi_items.PointROI, numpy.array(([(10.0, 15.0)], [(20.0, 25.0)]))),
+ (
+ roi_items.RectangleROI,
+ numpy.array((((1.0, 10.0), (11.0, 20.0)), ((2.0, 3.0), (12.0, 13.0)))),
+ ),
+ (
+ roi_items.PolygonROI,
+ numpy.array(
+ (
+ ((0.0, 1.0), (0.0, 10.0), (10.0, 0.0)),
+ ((5.0, 6.0), (5.0, 16.0), (15.0, 6.0)),
+ )
+ ),
+ ),
+ (
+ roi_items.LineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.VerticalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ )
+
+ for roiClass, points in tests:
+ with self.subTest(roiClass=roiClass):
+ manager = roi.RegionOfInterestManager(self.plot)
+ self.roiTableWidget.setRegionOfInterestManager(manager)
+ manager.start(roiClass)
+
+ self.assertEqual(manager.getRois(), ())
+
+ finishListener = SignalListener()
+ manager.sigInteractiveModeFinished.connect(finishListener)
+
+ changedListener = SignalListener()
+ manager.sigRoiChanged.connect(changedListener)
+
+ # Add a point
+ r = roiClass()
+ r.setFirstShapePoints(points[0])
+ manager.addRoi(r)
+ self.qapp.processEvents()
+ self.assertTrue(len(manager.getRois()), 1)
+ self.assertEqual(changedListener.callCount(), 1)
+
+ # Remove it
+ manager.removeRoi(manager.getRois()[0])
+ self.assertEqual(manager.getRois(), ())
+ self.assertEqual(changedListener.callCount(), 2)
+
+ # Add two point
+ r = roiClass()
+ r.setFirstShapePoints(points[0])
+ manager.addRoi(r)
+ self.qapp.processEvents()
+ r = roiClass()
+ r.setFirstShapePoints(points[1])
+ manager.addRoi(r)
+ self.qapp.processEvents()
+ self.assertTrue(len(manager.getRois()), 2)
+ self.assertEqual(changedListener.callCount(), 4)
+
+ # Reset it
+ result = manager.clear()
+ self.assertTrue(result)
+ self.assertEqual(manager.getRois(), ())
+ self.assertEqual(changedListener.callCount(), 5)
+
+ changedListener.clear()
+
+ # Add two point
+ r = roiClass()
+ r.setFirstShapePoints(points[0])
+ manager.addRoi(r)
+ self.qapp.processEvents()
+ r = roiClass()
+ r.setFirstShapePoints(points[1])
+ manager.addRoi(r)
+ self.qapp.processEvents()
+ self.assertTrue(len(manager.getRois()), 2)
+ self.assertEqual(changedListener.callCount(), 2)
+
+ # stop
+ result = manager.stop()
+ self.assertTrue(result)
+ self.assertTrue(len(manager.getRois()), 1)
+ self.qapp.processEvents()
+ self.assertEqual(finishListener.callCount(), 1)
+
+ manager.clear()
+
+ def testRoiDisplay(self):
+ rois = []
+
+ # Line
+ item = roi_items.LineROI()
+ startPoint = numpy.array([1, 2])
+ endPoint = numpy.array([3, 4])
+ item.setEndPoints(startPoint, endPoint)
+ rois.append(item)
+ # Horizontal line
+ item = roi_items.HorizontalLineROI()
+ item.setPosition(15)
+ rois.append(item)
+ # Vertical line
+ item = roi_items.VerticalLineROI()
+ item.setPosition(15)
+ rois.append(item)
+ # Point
+ item = roi_items.PointROI()
+ point = numpy.array([1, 2])
+ item.setPosition(point)
+ rois.append(item)
+ # Rectangle
+ item = roi_items.RectangleROI()
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item.setGeometry(origin=origin, size=size)
+ rois.append(item)
+ # Polygon
+ item = roi_items.PolygonROI()
+ points = numpy.array([[10, 10], [12, 10], [50, 1]])
+ item.setPoints(points)
+ rois.append(item)
+ # Degenerated polygon: No points
+ item = roi_items.PolygonROI()
+ points = numpy.empty((0, 2))
+ item.setPoints(points)
+ rois.append(item)
+ # Degenerated polygon: A single point
+ item = roi_items.PolygonROI()
+ points = numpy.array([[5, 10]])
+ item.setPoints(points)
+ rois.append(item)
+ # Degenerated arc: it's a point
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ rois.append(item)
+ # Degenerated arc: it's a line
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ rois.append(item)
+ # Special arc: it's a donut
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ rois.append(item)
+ # Arc
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = (
+ 1,
+ 100,
+ numpy.pi * 0.5,
+ numpy.pi,
+ )
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ rois.append(item)
+ # Horizontal Range
+ item = roi_items.HorizontalRangeROI()
+ item.setRange(-1, 3)
+ rois.append(item)
+
+ manager = roi.RegionOfInterestManager(self.plot)
+ self.roiTableWidget.setRegionOfInterestManager(manager)
+ for item in rois:
+ with self.subTest(roi=str(item)):
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ item.setEditable(True)
+ self.qapp.processEvents()
+ item.setEditable(False)
+ self.qapp.processEvents()
+ manager.removeRoi(item)
+ self.qapp.processEvents()
+
+ def testSelectionProxy(self):
+ item1 = roi_items.PointROI()
+ item1.setSelectable(True)
+ item2 = roi_items.PointROI()
+ item2.setSelectable(True)
+ item1.setFocusProxy(item2)
+ manager = roi.RegionOfInterestManager(self.plot)
+ manager.setCurrentRoi(item1)
+ self.assertIs(manager.getCurrentRoi(), item2)
+
+ def testRemovedSelection(self):
+ item1 = roi_items.PointROI()
+ item1.setSelectable(True)
+ manager = roi.RegionOfInterestManager(self.plot)
+ manager.addRoi(item1)
+ manager.setCurrentRoi(item1)
+ manager.removeRoi(item1)
+ self.assertIs(manager.getCurrentRoi(), None)
+
+ def testInitROIWithParent(self):
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.PointROI(manager)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ manager.removeRoi(item)
+ self.qapp.processEvents()
+
+ def testMaxROI(self):
+ """Test Max ROI"""
+ origin1 = numpy.array([1.0, 10.0])
+ size1 = numpy.array([10.0, 10.0])
+ origin2 = numpy.array([2.0, 3.0])
+ size2 = numpy.array([10.0, 10.0])
+
+ manager = roi.InteractiveRegionOfInterestManager(self.plot)
+ self.roiTableWidget.setRegionOfInterestManager(manager)
+ self.assertEqual(manager.getRois(), ())
+
+ changedListener = SignalListener()
+ manager.sigRoiChanged.connect(changedListener)
+
+ # Add two point
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin1, size=size1)
+ manager.addRoi(item)
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin2, size=size2)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ self.assertEqual(changedListener.callCount(), 2)
+ self.assertEqual(len(manager.getRois()), 2)
+
+ # Try to set max ROI to 1 while there is 2 ROIs
+ with self.assertRaises(ValueError):
+ manager.setMaxRois(1)
+
+ manager.clear()
+ self.assertEqual(len(manager.getRois()), 0)
+ self.assertEqual(changedListener.callCount(), 3)
+
+ # Set max limit to 1
+ manager.setMaxRois(1)
+
+ # Add a point
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin1, size=size1)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ self.assertEqual(changedListener.callCount(), 4)
+
+ # Add a 2nd point while max ROI is 1
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin1, size=size1)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ self.assertEqual(changedListener.callCount(), 6)
+ self.assertEqual(len(manager.getRois()), 1)
+
+ def testChangeInteractionMode(self):
+ """Test change of interaction mode"""
+ manager = roi.RegionOfInterestManager(self.plot)
+ self.roiTableWidget.setRegionOfInterestManager(manager)
+ manager.start(roi_items.PointROI)
+
+ interactiveModeToolBar = self.plot.getInteractiveModeToolBar()
+ panAction = interactiveModeToolBar.getPanModeAction()
+
+ for roiClass in manager.getSupportedRoiClasses():
+ with self.subTest(roiClass=roiClass):
+ # Change to pan mode
+ panAction.trigger()
+
+ # Change to interactive ROI mode
+ action = manager.getInteractionModeAction(roiClass)
+ action.trigger()
+
+ self.assertEqual(roiClass, manager.getCurrentInteractionModeRoiClass())
+
+ manager.clear()
+
+ def testLineInteraction(self):
+ """This test make sure that a ROI based on handles can be edited with
+ the mouse."""
+ xlimit = self.plot.getXAxis().getLimits()
+ ylimit = self.plot.getYAxis().getLimits()
+ points = numpy.array([xlimit, ylimit]).T
+ center = numpy.mean(points, axis=0)
+
+ # Create the line
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.LineROI()
+ item.setEndPoints(points[0], points[1])
+ item.setEditable(True)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+
+ # Drag the center
+ widget = self.plot.getWidgetHandle()
+ 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 + 50))
+
+ result = numpy.array(item.getEndPoints())
+ # x location is still the same
+ numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5)
+ # size is still the same
+ numpy.testing.assert_allclose(
+ points[1] - points[0], result[1] - result[0], atol=0.5
+ )
+ # But Y is not the same
+ self.assertNotEqual(points[0, 1], result[0, 1])
+ self.assertNotEqual(points[1, 1], result[1, 1])
+ item = None
+ manager.clear()
+ self.qapp.processEvents()
+
+ def testPlotWhenCleared(self):
+ """PlotWidget.clear should clean up the available ROIs"""
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.LineROI()
+ item.setEndPoints((0, 0), (1, 1))
+ item.setEditable(True)
+ manager.addRoi(item)
+ self.qWait()
+ try:
+ # Make sure the test setup is fine
+ self.assertNotEqual(len(manager.getRois()), 0)
+ self.assertNotEqual(len(self.plot.getItems()), 0)
+
+ # Call clear and test the expected state
+ self.plot.clear()
+ self.assertEqual(len(manager.getRois()), 0)
+ self.assertEqual(len(self.plot.getItems()), 0)
+ finally:
+ # Clean up
+ manager.clear()
+
+ def testPlotWhenRoiRemoved(self):
+ """Make sure there is no remaining items in the plot when a ROI is removed"""
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.LineROI()
+ item.setEndPoints((0, 0), (1, 1))
+ item.setEditable(True)
+ manager.addRoi(item)
+ self.qWait()
+ try:
+ # Make sure the test setup is fine
+ self.assertNotEqual(len(manager.getRois()), 0)
+ self.assertNotEqual(len(self.plot.getItems()), 0)
+
+ # Call clear and test the expected state
+ manager.removeRoi(item)
+ self.assertEqual(len(manager.getRois()), 0)
+ self.assertEqual(len(self.plot.getItems()), 0)
+ finally:
+ # 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 testBandRoiSwitchMode(self):
+ """Make sure we can switch mode by clicking on the ROI"""
+ xlimit = self.plot.getXAxis().getLimits()
+ ylimit = self.plot.getYAxis().getLimits()
+ xcenter = 0.5 * (xlimit[0] + xlimit[1])
+ ycenter = 0.5 * (ylimit[0] + ylimit[1])
+
+ # Create the line
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.BandROI()
+ item.setGeometry(
+ (xlimit[0], ycenter),
+ (xlimit[1], ycenter),
+ 20,
+ )
+ item.setEditable(True)
+ item.setSelectable(True)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+
+ # Initial state
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+ self.qWait(500)
+
+ # Click on the center
+ widget = self.plot.getWidgetHandle()
+ mx, my = self.plot.dataToPixel(xcenter, ycenter)
+
+ # Select the ROI
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ # Change the mode
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.UnboundedMode
+
+ # Set available modes that exclude the current one
+ item.setAvailableInteractionModes([roi_items.BandROI.BoundedMode])
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ # Clicking does not change the mode since there is only one
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ manager.clear()
+ self.qapp.processEvents()