diff options
Diffstat (limited to 'src/silx/gui/plot3d/test')
-rw-r--r-- | src/silx/gui/plot3d/test/__init__.py | 25 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testGL.py | 73 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testScalarFieldView.py | 128 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testSceneWidget.py | 72 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testSceneWidgetPicking.py | 314 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testSceneWindow.py | 233 | ||||
-rw-r--r-- | src/silx/gui/plot3d/test/testStatsWidget.py | 201 |
7 files changed, 1046 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/test/__init__.py b/src/silx/gui/plot3d/test/__init__.py new file mode 100644 index 0000000..83491ad --- /dev/null +++ b/src/silx/gui/plot3d/test/__init__.py @@ -0,0 +1,25 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-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 +# 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. +# +# ###########################################################################*/ +"""plot3d test suite.""" diff --git a/src/silx/gui/plot3d/test/testGL.py b/src/silx/gui/plot3d/test/testGL.py new file mode 100644 index 0000000..a7309a9 --- /dev/null +++ b/src/silx/gui/plot3d/test/testGL.py @@ -0,0 +1,73 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 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. +# ###########################################################################*/ +"""Test OpenGL""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "10/08/2017" + + +import logging +import unittest + +from silx.gui._glutils import gl, OpenGLWidget +from silx.gui.utils.testutils import TestCaseQt +from silx.gui import qt + + +_logger = logging.getLogger(__name__) + + +class TestOpenGL(TestCaseQt): + """Tests of OpenGL widget.""" + + class OpenGLWidgetLogger(OpenGLWidget): + """Widget logging information of available OpenGL version""" + + def __init__(self): + self._dump = False + super(TestOpenGL.OpenGLWidgetLogger, self).__init__(version=(1, 0)) + + def paintOpenGL(self): + """Perform the rendering and logging""" + if not self._dump: + self._dump = True + _logger.info('OpenGL info:') + _logger.info('\tQt OpenGL context version: %d.%d', *self.getOpenGLVersion()) + _logger.info('\tGL_VERSION: %s' % gl.glGetString(gl.GL_VERSION)) + _logger.info('\tGL_SHADING_LANGUAGE_VERSION: %s' % + gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION)) + _logger.debug('\tGL_EXTENSIONS: %s' % gl.glGetString(gl.GL_EXTENSIONS)) + + gl.glClearColor(1., 1., 1., 1.) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + + def testOpenGL(self): + """Log OpenGL version using an OpenGLWidget""" + super(TestOpenGL, self).setUp() + widget = self.OpenGLWidgetLogger() + widget.show() + widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.qWaitForWindowExposed(widget) + widget.close() diff --git a/src/silx/gui/plot3d/test/testScalarFieldView.py b/src/silx/gui/plot3d/test/testScalarFieldView.py new file mode 100644 index 0000000..e6535fc --- /dev/null +++ b/src/silx/gui/plot3d/test/testScalarFieldView.py @@ -0,0 +1,128 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2018 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. +# ###########################################################################*/ +"""Test ScalarFieldView widget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "17/01/2018" + + +import logging +import unittest + +import numpy + +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui import qt + +from silx.gui.plot3d.ScalarFieldView import ScalarFieldView +from silx.gui.plot3d.SFViewParamTree import TreeView + + +_logger = logging.getLogger(__name__) + + +class TestScalarFieldView(TestCaseQt, ParametricTestCase): + """Tests of ScalarFieldView widget.""" + + def setUp(self): + super(TestScalarFieldView, self).setUp() + self.widget = ScalarFieldView() + self.widget.show() + + paramTreeWidget = TreeView() + paramTreeWidget.setSfView(self.widget) + + dock = qt.QDockWidget() + dock.setWidget(paramTreeWidget) + self.widget.addDockWidget(qt.Qt.BottomDockWidgetArea, dock) + + # Commented as it slows down the tests + # self.qWaitForWindowExposed(self.widget) + + def tearDown(self): + self.qapp.processEvents() + self.widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.widget.close() + del self.widget + super(TestScalarFieldView, self).tearDown() + + @staticmethod + def _buildData(size): + """Make a 3D dataset""" + coords = numpy.linspace(-10, 10, size) + z = coords.reshape(-1, 1, 1) + y = coords.reshape(1, -1, 1) + x = coords.reshape(1, 1, -1) + return numpy.sin(x * y * z) / (x * y * z) + + def testSimple(self): + """Set the data and an isosurface""" + data = self._buildData(size=32) + + self.widget.setData(data) + self.widget.addIsosurface(0.5, (1., 0., 0., 0.5)) + self.widget.addIsosurface(0.7, qt.QColor('green')) + self.qapp.processEvents() + + def testNotFinite(self): + """Test with NaN and inf in data set""" + + # Some NaNs and inf + data = self._buildData(size=32) + data[8, :, :] = numpy.nan + data[16, :, :] = numpy.inf + data[24, :, :] = - numpy.inf + + self.widget.addIsosurface(0.5, 'red') + self.widget.setData(data, copy=True) + self.qapp.processEvents() + self.widget.setData(None) + + # All NaNs or inf + data = numpy.empty((4, 4, 4), dtype=numpy.float32) + for value in (numpy.nan, numpy.inf): + with self.subTest(value=str(value)): + data[:] = value + self.widget.setData(data, copy=True) + self.qapp.processEvents() + + def testIsoSliderNormalization(self): + """Test set TreeView with a different isoslider normalization""" + data = self._buildData(size=32) + + self.widget.setData(data) + self.widget.addIsosurface(0.5, (1., 0., 0., 0.5)) + self.widget.addIsosurface(0.7, qt.QColor('green')) + self.qapp.processEvents() + + # Add a second TreeView + paramTreeWidget = TreeView(self.widget) + paramTreeWidget.setIsoLevelSliderNormalization('arcsinh') + paramTreeWidget.setSfView(self.widget) + + dock = qt.QDockWidget() + dock.setWidget(paramTreeWidget) + self.widget.addDockWidget(qt.Qt.BottomDockWidgetArea, dock) diff --git a/src/silx/gui/plot3d/test/testSceneWidget.py b/src/silx/gui/plot3d/test/testSceneWidget.py new file mode 100644 index 0000000..fc96781 --- /dev/null +++ b/src/silx/gui/plot3d/test/testSceneWidget.py @@ -0,0 +1,72 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 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 +# 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. +# ###########################################################################*/ +"""Test SceneWidget""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "06/03/2019" + + +import unittest + +import numpy + +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui import qt + +from silx.gui.plot3d.SceneWidget import SceneWidget + + +class TestSceneWidget(TestCaseQt, ParametricTestCase): + """Tests SceneWidget picking feature""" + + def setUp(self): + super(TestSceneWidget, self).setUp() + self.widget = SceneWidget() + self.widget.show() + self.qWaitForWindowExposed(self.widget) + + def tearDown(self): + self.qapp.processEvents() + self.widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.widget.close() + del self.widget + super(TestSceneWidget, self).tearDown() + + def testFogEffect(self): + """Test fog effect on scene primitive""" + image = self.widget.addImage(numpy.arange(100).reshape(10, 10)) + scatter = self.widget.add3DScatter(*numpy.random.random(4000).reshape(4, -1)) + scatter.setTranslation(10, 10) + scatter.setScale(10, 10, 10) + + self.widget.resetZoom('front') + self.qapp.processEvents() + + self.widget.setFogMode(self.widget.FogMode.LINEAR) + self.qapp.processEvents() + + self.widget.setFogMode(self.widget.FogMode.NONE) + self.qapp.processEvents() diff --git a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py new file mode 100644 index 0000000..d4d8db7 --- /dev/null +++ b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py @@ -0,0 +1,314 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2018-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 +# 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. +# ###########################################################################*/ +"""Test SceneWidget picking feature""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "03/10/2018" + + +import unittest + +import numpy + +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui import qt + +from silx.gui.plot3d.SceneWidget import SceneWidget, items + + +class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase): + """Tests SceneWidget picking feature""" + + def setUp(self): + super(TestSceneWidgetPicking, self).setUp() + self.widget = SceneWidget() + self.widget.resize(300, 300) + self.widget.show() + # self.qWaitForWindowExposed(self.widget) + + def tearDown(self): + self.qapp.processEvents() + self.widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.widget.close() + del self.widget + super(TestSceneWidgetPicking, self).tearDown() + + def _widgetCenter(self): + """Returns widget center""" + size = self.widget.size() + return size.width() // 2, size.height() // 2 + + def testPickImage(self): + """Test picking of ImageData and ImageRgba items""" + imageData = items.ImageData() + imageData.setData(numpy.arange(100).reshape(10, 10)) + + imageRgba = items.ImageRgba() + imageRgba.setData( + numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3)) + + for item in (imageData, imageRgba): + with self.subTest(item=item.__class__.__name__): + # Add item + self.widget.clearItems() + self.widget.addItem(item) + self.widget.resetZoom('front') + self.qapp.processEvents() + + # Picking on data (at widget center) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), item) + self.assertEqual(picking[0].getPositions('ndc').shape, (1, 3)) + data = picking[0].getData() + self.assertEqual(len(data), 1) + self.assertTrue(numpy.array_equal( + data, + item.getData()[picking[0].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) + + def testPickScatter(self): + """Test picking of Scatter2D and Scatter3D items""" + data = numpy.arange(100) + + scatter2d = items.Scatter2D() + scatter2d.setData(x=data, y=data, value=data) + + scatter3d = items.Scatter3D() + scatter3d.setData(x=data, y=data, z=data, value=data) + + for item in (scatter2d, scatter3d): + with self.subTest(item=item.__class__.__name__): + # Add item + self.widget.clearItems() + self.widget.addItem(item) + self.widget.resetZoom('front') + self.qapp.processEvents() + + # Picking on data (at widget center) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), item) + nbPos = len(picking[0].getPositions('ndc')) + data = picking[0].getData() + self.assertEqual(nbPos, len(data)) + self.assertTrue(numpy.array_equal( + data, + item.getValueData()[picking[0].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) + + def testPickVolume(self): + """Test picking of volume CutPlane and Isosurface items""" + for dtype in (numpy.float32, numpy.complex64): + with self.subTest(dtype=dtype): + refData = numpy.arange(10**3, dtype=dtype).reshape(10, 10, 10) + volume = self.widget.addVolume(refData) + if dtype == numpy.complex64: + volume.setComplexMode(volume.ComplexMode.REAL) + refData = numpy.real(refData) + self.widget.resetZoom('front') + + cutplane = volume.getCutPlanes()[0] + if dtype == numpy.complex64: + cutplane.setComplexMode(volume.ComplexMode.REAL) + cutplane.getColormap().setVRange(0, 100) + cutplane.setNormal((0, 0, 1)) + + # Picking on data without anything displayed + cutplane.setVisible(False) + picking = list(self.widget.pickItems(*self._widgetCenter())) + self.assertEqual(len(picking), 0) + + # Picking on data with the cut plane + cutplane.setVisible(True) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), cutplane) + data = picking[0].getData() + self.assertEqual(len(data), 1) + self.assertEqual(picking[0].getPositions().shape, (1, 3)) + self.assertTrue(numpy.array_equal( + data, + refData[picking[0].getIndices()])) + + # Picking on data with an isosurface + isosurface = volume.addIsosurface( + level=500, color=(1., 0., 0., .5)) + picking = list(self.widget.pickItems(*self._widgetCenter())) + self.assertEqual(len(picking), 2) + self.assertIs(picking[0].getItem(), cutplane) + self.assertIs(picking[1].getItem(), isosurface) + self.assertEqual(picking[1].getPositions().shape, (1, 3)) + data = picking[1].getData() + self.assertEqual(len(data), 1) + self.assertTrue(numpy.array_equal( + data, + refData[picking[1].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) + + self.widget.clearItems() + + def testPickMesh(self): + """Test picking of Mesh items""" + + triangles = items.Mesh() + triangles.setData( + position=((0, 0, 0), (1, 0, 0), (1, 1, 0), + (0, 0, 0), (1, 1, 0), (0, 1, 0)), + color=(1, 0, 0, 1), + mode='triangles') + triangleStrip = items.Mesh() + triangleStrip.setData( + position=(((1, 0, 0), (0, 0, 0), (1, 1, 0), (0, 1, 0))), + color=(0, 1, 0, 1), + mode='triangle_strip') + triangleFan = items.Mesh() + triangleFan.setData( + position=((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)), + color=(0, 0, 1, 1), + mode='fan') + + for item in (triangles, triangleStrip, triangleFan): + with self.subTest(mode=item.getDrawMode()): + # Add item + self.widget.clearItems() + self.widget.addItem(item) + self.widget.resetZoom('front') + self.qapp.processEvents() + + # Picking on data (at widget center) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), item) + nbPos = len(picking[0].getPositions()) + data = picking[0].getData() + self.assertEqual(nbPos, len(data)) + self.assertTrue(numpy.array_equal( + data, + item.getPositionData()[picking[0].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) + + def testPickMeshWithIndices(self): + """Test picking of Mesh items defined by indices""" + + triangles = items.Mesh() + triangles.setData( + position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)), + color=(1, 0, 0, 1), + indices=numpy.array( # dummy triangles and square + (0, 0, 1, 0, 1, 2, 1, 2, 3), dtype=numpy.uint8), + mode='triangles') + triangleStrip = items.Mesh() + triangleStrip.setData( + position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)), + color=(0, 1, 0, 1), + indices=numpy.array( # dummy triangles and square + (1, 0, 0, 1, 2, 3), dtype=numpy.uint8), + mode='triangle_strip') + triangleFan = items.Mesh() + triangleFan.setData( + position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)), + color=(0, 0, 1, 1), + indices=numpy.array( # dummy triangle, square, dummy + (1, 1, 0, 2, 3, 3), dtype=numpy.uint8), + mode='fan') + + for item in (triangles, triangleStrip, triangleFan): + with self.subTest(mode=item.getDrawMode()): + # Add item + self.widget.clearItems() + self.widget.addItem(item) + self.widget.resetZoom('front') + self.qapp.processEvents() + + # Picking on data (at widget center) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), item) + nbPos = len(picking[0].getPositions()) + data = picking[0].getData() + self.assertEqual(nbPos, len(data)) + self.assertTrue(numpy.array_equal( + data, + item.getPositionData()[picking[0].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) + + def testPickCylindricalMesh(self): + """Test picking of Box, Cylinder and Hexagon items""" + + positions = numpy.array(((0., 0., 0.), (1., 1., 0.), (2., 2., 0.))) + box = items.Box() + box.setData(position=positions) + cylinder = items.Cylinder() + cylinder.setData(position=positions) + hexagon = items.Hexagon() + hexagon.setData(position=positions) + + for item in (box, cylinder, hexagon): + with self.subTest(item=item.__class__.__name__): + # Add item + self.widget.clearItems() + self.widget.addItem(item) + self.widget.resetZoom('front') + self.qapp.processEvents() + + # Picking on data (at widget center) + picking = list(self.widget.pickItems(*self._widgetCenter())) + + self.assertEqual(len(picking), 1) + self.assertIs(picking[0].getItem(), item) + nbPos = len(picking[0].getPositions()) + data = picking[0].getData() + print(item.__class__.__name__, [positions[1]], data) + self.assertTrue(numpy.all(numpy.equal(positions[1], data))) + self.assertEqual(nbPos, len(data)) + self.assertTrue(numpy.array_equal( + data, + item.getPosition()[picking[0].getIndices()])) + + # Picking outside data + picking = list(self.widget.pickItems(1, 1)) + self.assertEqual(len(picking), 0) diff --git a/src/silx/gui/plot3d/test/testSceneWindow.py b/src/silx/gui/plot3d/test/testSceneWindow.py new file mode 100644 index 0000000..6b61335 --- /dev/null +++ b/src/silx/gui/plot3d/test/testSceneWindow.py @@ -0,0 +1,233 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2019-2021 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. +# ###########################################################################*/ +"""Test SceneWindow""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "22/03/2019" + + +import unittest + +import numpy + +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui import qt + +from silx.gui.plot3d.SceneWindow import SceneWindow +from silx.gui.plot3d.items import HeightMapData, HeightMapRGBA + +class TestSceneWindow(TestCaseQt, ParametricTestCase): + """Tests SceneWidget picking feature""" + + def setUp(self): + super(TestSceneWindow, self).setUp() + self.window = SceneWindow() + self.window.show() + self.qWaitForWindowExposed(self.window) + + def tearDown(self): + self.qapp.processEvents() + self.window.setAttribute(qt.Qt.WA_DeleteOnClose) + self.window.close() + del self.window + super(TestSceneWindow, self).tearDown() + + def testAdd(self): + """Test add basic scene primitive""" + sceneWidget = self.window.getSceneWidget() + items = [] + + # RGB image + image = sceneWidget.addImage(numpy.random.random( + 10*10*3).astype(numpy.float32).reshape(10, 10, 3)) + image.setLabel('RGB image') + items.append(image) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # Data image + image = sceneWidget.addImage( + numpy.arange(100, dtype=numpy.float32).reshape(10, 10)) + image.setTranslation(10.) + items.append(image) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # 2D scatter + scatter = sceneWidget.add2DScatter( + *numpy.random.random(3000).astype(numpy.float32).reshape(3, -1), + index=0) + scatter.setTranslation(0, 10) + scatter.setScale(10, 10, 10) + items.insert(0, scatter) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # 3D scatter + scatter = sceneWidget.add3DScatter( + *numpy.random.random(4000).astype(numpy.float32).reshape(4, -1)) + scatter.setTranslation(10, 10) + scatter.setScale(10, 10, 10) + items.append(scatter) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # 3D array of float + volume = sceneWidget.addVolume( + numpy.arange(10**3, dtype=numpy.float32).reshape(10, 10, 10)) + volume.setTranslation(0, 0, 10) + volume.setRotation(45, (0, 0, 1)) + volume.addIsosurface(500, 'red') + volume.getCutPlanes()[0].getColormap().setName('viridis') + items.append(volume) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # 3D array of complex + volume = sceneWidget.addVolume( + numpy.arange(10**3).reshape(10, 10, 10).astype(numpy.complex64)) + volume.setTranslation(10, 0, 10) + volume.setRotation(45, (0, 0, 1)) + volume.setComplexMode(volume.ComplexMode.REAL) + volume.addIsosurface(500, (1., 0., 0., .5)) + items.append(volume) + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + sceneWidget.resetZoom('front') + self.qapp.processEvents() + + def testHeightMap(self): + """Test height map items""" + sceneWidget = self.window.getSceneWidget() + + height = numpy.arange(10000).reshape(100, 100) /100. + + for shape in ((100, 100), (4, 5), (150, 20), (110, 110)): + with self.subTest(shape=shape): + items = [] + + # Colormapped data height map + data = numpy.arange(numpy.prod(shape)).astype(numpy.float32).reshape(shape) + + heightmap = HeightMapData() + heightmap.setData(height) + heightmap.setColormappedData(data) + heightmap.getColormap().setName('viridis') + items.append(heightmap) + sceneWidget.addItem(heightmap) + + # RGBA height map + colors = numpy.zeros(shape + (3,), dtype=numpy.float32) + colors[:, :, 1] = numpy.random.random(shape) + + heightmap = HeightMapRGBA() + heightmap.setData(height) + heightmap.setColorData(colors) + heightmap.setTranslation(100., 0., 0.) + items.append(heightmap) + sceneWidget.addItem(heightmap) + + self.assertEqual(sceneWidget.getItems(), tuple(items)) + sceneWidget.resetZoom('front') + self.qapp.processEvents() + sceneWidget.clearItems() + + def testChangeContent(self): + """Test add/remove/clear items""" + sceneWidget = self.window.getSceneWidget() + items = [] + + # Add 2 images + image = numpy.arange(100, dtype=numpy.float32).reshape(10, 10) + items.append(sceneWidget.addImage(image)) + items.append(sceneWidget.addImage(image)) + self.qapp.processEvents() + self.assertEqual(sceneWidget.getItems(), tuple(items)) + + # Clear + sceneWidget.clearItems() + self.qapp.processEvents() + self.assertEqual(sceneWidget.getItems(), ()) + + # Add 2 images and remove first one + image = numpy.arange(100, dtype=numpy.float32).reshape(10, 10) + sceneWidget.addImage(image) + items = (sceneWidget.addImage(image),) + self.qapp.processEvents() + + sceneWidget.removeItem(sceneWidget.getItems()[0]) + self.qapp.processEvents() + self.assertEqual(sceneWidget.getItems(), items) + + def testColors(self): + """Test setting scene colors""" + sceneWidget = self.window.getSceneWidget() + + color = qt.QColor(128, 128, 128) + sceneWidget.setBackgroundColor(color) + self.assertEqual(sceneWidget.getBackgroundColor(), color) + + color = qt.QColor(0, 0, 0) + sceneWidget.setForegroundColor(color) + self.assertEqual(sceneWidget.getForegroundColor(), color) + + color = qt.QColor(255, 0, 0) + sceneWidget.setTextColor(color) + self.assertEqual(sceneWidget.getTextColor(), color) + + color = qt.QColor(0, 255, 0) + sceneWidget.setHighlightColor(color) + self.assertEqual(sceneWidget.getHighlightColor(), color) + + self.qapp.processEvents() + + def testInteractiveMode(self): + """Test changing interactive mode""" + sceneWidget = self.window.getSceneWidget() + center = numpy.array((sceneWidget.width() //2, sceneWidget.height() // 2)) + + self.mouseMove(sceneWidget, pos=center) + self.mouseClick(sceneWidget, qt.Qt.LeftButton, pos=center) + + volume = sceneWidget.addVolume( + numpy.arange(10**3).astype(numpy.float32).reshape(10, 10, 10)) + sceneWidget.selection().setCurrentItem( volume.getCutPlanes()[0]) + sceneWidget.resetZoom('side') + + for mode in (None, 'rotate', 'pan', 'panSelectedPlane'): + with self.subTest(mode=mode): + sceneWidget.setInteractiveMode(mode) + self.qapp.processEvents() + self.assertEqual(sceneWidget.getInteractiveMode(), mode) + + self.mouseMove(sceneWidget, pos=center) + self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center) + self.mouseMove(sceneWidget, pos=center-10) + self.mouseMove(sceneWidget, pos=center-20) + self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20) + + self.keyPress(sceneWidget, qt.Qt.Key_Control) + self.mouseMove(sceneWidget, pos=center) + self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center) + self.mouseMove(sceneWidget, pos=center-10) + self.mouseMove(sceneWidget, pos=center-20) + self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20) + self.keyRelease(sceneWidget, qt.Qt.Key_Control) diff --git a/src/silx/gui/plot3d/test/testStatsWidget.py b/src/silx/gui/plot3d/test/testStatsWidget.py new file mode 100644 index 0000000..d452eb5 --- /dev/null +++ b/src/silx/gui/plot3d/test/testStatsWidget.py @@ -0,0 +1,201 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 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 +# 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. +# ###########################################################################*/ +"""Test silx.gui.plot.StatsWidget with SceneWidget and ScalarFieldView""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "25/01/2019" + + +import unittest + +import numpy + +from silx.utils.testutils import ParametricTestCase +from silx.gui.utils.testutils import TestCaseQt +from silx.gui.plot.stats.stats import Stats +from silx.gui import qt + +from silx.gui.plot.StatsWidget import BasicStatsWidget + +from silx.gui.plot3d.ScalarFieldView import ScalarFieldView +from silx.gui.plot3d.SceneWidget import SceneWidget, items + + +class TestSceneWidget(TestCaseQt, ParametricTestCase): + """Tests StatsWidget combined with SceneWidget""" + + def setUp(self): + super(TestSceneWidget, self).setUp() + self.sceneWidget = SceneWidget() + self.sceneWidget.resize(300, 300) + self.sceneWidget.show() + self.statsWidget = BasicStatsWidget() + self.statsWidget.setPlot(self.sceneWidget) + # self.qWaitForWindowExposed(self.sceneWidget) + + def tearDown(self): + Stats._getContext.cache_clear() + self.qapp.processEvents() + self.sceneWidget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.sceneWidget.close() + del self.sceneWidget + self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.statsWidget.close() + del self.statsWidget + super(TestSceneWidget, self).tearDown() + + def test(self): + """Test StatsWidget with SceneWidget""" + # Prepare scene + + # Data image + image = self.sceneWidget.addImage(numpy.arange(100).reshape(10, 10)) + image.setLabel('Image') + # RGB image + imageRGB = self.sceneWidget.addImage( + numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3)) + imageRGB.setLabel('RGB Image') + # 2D scatter + data = numpy.arange(100) + scatter2D = self.sceneWidget.add2DScatter(x=data, y=data, value=data) + scatter2D.setLabel('2D Scatter') + # 3D scatter + scatter3D = self.sceneWidget.add3DScatter(x=data, y=data, z=data, value=data) + scatter3D.setLabel('3D Scatter') + # Add a group + group = items.GroupItem() + self.sceneWidget.addItem(group) + # 3D scalar field + data = numpy.arange(64**3).reshape(64, 64, 64) + scalarField = items.ScalarField3D() + scalarField.setData(data, copy=False) + scalarField.setLabel('3D Scalar field') + group.addItem(scalarField) + + statsTable = self.statsWidget._getStatsTable() + + # Test selection only + self.statsWidget.setDisplayOnlyActiveItem(True) + self.assertEqual(statsTable.rowCount(), 0) + + self.sceneWidget.selection().setCurrentItem(group) + self.assertEqual(statsTable.rowCount(), 0) + + for item in (image, scatter2D, scatter3D, scalarField): + with self.subTest('selection only', item=item.getLabel()): + self.sceneWidget.selection().setCurrentItem(item) + self.assertEqual(statsTable.rowCount(), 1) + self._checkItem(item) + + # Test all data + self.statsWidget.setDisplayOnlyActiveItem(False) + self.assertEqual(statsTable.rowCount(), 4) + + for item in (image, scatter2D, scatter3D, scalarField): + with self.subTest('all items', item=item.getLabel()): + self._checkItem(item) + + def _checkItem(self, item): + """Check that item is in StatsTable and that stats are OK + + :param silx.gui.plot3d.items.Item3D item: + """ + if isinstance(item, (items.Scatter2D, items.Scatter3D)): + data = item.getValueData(copy=False) + else: + data = item.getData(copy=False) + + statsTable = self.statsWidget._getStatsTable() + tableItems = statsTable._itemToTableItems(item) + self.assertTrue(len(tableItems) > 0) + self.assertEqual(tableItems['legend'].text(), item.getLabel()) + self.assertEqual(float(tableItems['min'].text()), numpy.min(data)) + self.assertEqual(float(tableItems['max'].text()), numpy.max(data)) + # TODO + + +class TestScalarFieldView(TestCaseQt): + """Tests StatsWidget combined with ScalarFieldView""" + + def setUp(self): + super(TestScalarFieldView, self).setUp() + self.scalarFieldView = ScalarFieldView() + self.scalarFieldView.resize(300, 300) + self.scalarFieldView.show() + self.statsWidget = BasicStatsWidget() + self.statsWidget.setPlot(self.scalarFieldView) + # self.qWaitForWindowExposed(self.sceneWidget) + + def tearDown(self): + Stats._getContext.cache_clear() + self.qapp.processEvents() + self.scalarFieldView.setAttribute(qt.Qt.WA_DeleteOnClose) + self.scalarFieldView.close() + del self.scalarFieldView + self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.statsWidget.close() + del self.statsWidget + super(TestScalarFieldView, self).tearDown() + + def _getTextFor(self, row, name): + """Returns text in table at given row for column name + + :param int row: Row number in the table + :param str name: Column id + :rtype: Union[str,None] + """ + statsTable = self.statsWidget._getStatsTable() + + for column in range(statsTable.columnCount()): + headerItem = statsTable.horizontalHeaderItem(column) + if headerItem.data(qt.Qt.UserRole) == name: + tableItem = statsTable.item(row, column) + return tableItem.text() + + return None + + def test(self): + """Test StatsWidget with ScalarFieldView""" + data = numpy.arange(64**3, dtype=numpy.float64).reshape(64, 64, 64) + self.scalarFieldView.setData(data) + + statsTable = self.statsWidget._getStatsTable() + + # Test selection only + self.statsWidget.setDisplayOnlyActiveItem(True) + self.assertEqual(statsTable.rowCount(), 1) + + # Test all data + self.statsWidget.setDisplayOnlyActiveItem(False) + self.assertEqual(statsTable.rowCount(), 1) + + for column in range(statsTable.columnCount()): + self.assertEqual(float(self._getTextFor(0, 'min')), numpy.min(data)) + self.assertEqual(float(self._getTextFor(0, 'max')), numpy.max(data)) + sum_ = numpy.sum(data) + comz = numpy.sum(numpy.arange(data.shape[0]) * numpy.sum(data, axis=(1, 2))) / sum_ + comy = numpy.sum(numpy.arange(data.shape[1]) * numpy.sum(data, axis=(0, 2))) / sum_ + comx = numpy.sum(numpy.arange(data.shape[2]) * numpy.sum(data, axis=(0, 1))) / sum_ + self.assertEqual(self._getTextFor(0, 'COM'), str((comx, comy, comz))) |