summaryrefslogtreecommitdiff
path: root/src/silx/image/test/test_shapes.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/image/test/test_shapes.py')
-rw-r--r--src/silx/image/test/test_shapes.py354
1 files changed, 354 insertions, 0 deletions
diff --git a/src/silx/image/test/test_shapes.py b/src/silx/image/test/test_shapes.py
new file mode 100644
index 0000000..63abc00
--- /dev/null
+++ b/src/silx/image/test/test_shapes.py
@@ -0,0 +1,354 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016 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.
+#
+# ############################################################################*/
+"""Tests for polygon functions
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "15/02/2019"
+
+
+import logging
+import unittest
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.image import shapes
+
+_logger = logging.getLogger(__name__)
+
+
+class TestPolygonFill(ParametricTestCase):
+ """basic poylgon test"""
+
+ def test_squares(self):
+ """Test polygon fill for a square polygons"""
+ mask_shape = 4, 4
+ tests = {
+ # test name: [(row min, row max), (col min, col max)]
+ 'square in': [(1, 3), (1, 3)],
+ 'square out': [(1, 3), (1, 10)],
+ 'square around': [(-1, 5), (-1, 5)],
+ }
+
+ for test_name, (rows, cols) in tests.items():
+ with self.subTest(msg=test_name, rows=rows, cols=cols,
+ mask_shape=mask_shape):
+ ref_mask = numpy.zeros(mask_shape, dtype=numpy.uint8)
+ ref_mask[max(0, rows[0]):rows[1],
+ max(0, cols[0]):cols[1]] = True
+
+ vertices = [(rows[0], cols[0]), (rows[1], cols[0]),
+ (rows[1], cols[1]), (rows[0], cols[1])]
+ mask = shapes.polygon_fill_mask(vertices, ref_mask.shape)
+ is_equal = numpy.all(numpy.equal(ref_mask, mask))
+ if not is_equal:
+ _logger.debug('%s failed with mask != ref_mask:',
+ test_name)
+ _logger.debug('result:\n%s', str(mask))
+ _logger.debug('ref:\n%s', str(ref_mask))
+ self.assertTrue(is_equal)
+
+ def test_eight(self):
+ """Tests with eight shape with different rotation and direction"""
+ ref_mask = numpy.array((
+ (1, 1, 1, 1, 1, 0),
+ (0, 1, 1, 1, 0, 0),
+ (0, 0, 1, 0, 0, 0),
+ (0, 0, 1, 0, 0, 0),
+ (0, 1, 1, 1, 0, 0),
+ (0, 0, 0, 0, 0, 0)), dtype=numpy.uint8)
+ ref_mask_rot = numpy.asarray(numpy.logical_not(ref_mask),
+ dtype=numpy.uint8)
+ ref_mask_rot[:, -1] = 0
+ ref_mask_rot[-1, :] = 0
+
+ tests = {
+ 'dir 1': ([(0, 0), (5, 5), (5, 0), (0, 5)], ref_mask),
+ 'dir 1, rot 90': ([(5, 0), (0, 5), (5, 5), (0, 0)], ref_mask_rot),
+ 'dir 1, rot 180': ([(5, 5), (0, 0), (0, 5), (5, 0)], ref_mask),
+ 'dir 1, rot -90': ([(0, 5), (5, 0), (0, 0), (5, 5)], ref_mask_rot),
+ 'dir 2': ([(0, 0), (0, 5), (5, 0), (5, 5)], ref_mask),
+ 'dir 2, rot 90': ([(5, 0), (0, 0), (5, 5), (0, 5)], ref_mask_rot),
+ 'dir 2, rot 180': ([(5, 5), (5, 0), (0, 5), (0, 0)], ref_mask),
+ 'dir 2, rot -90': ([(0, 5), (5, 5), (0, 0), (5, 0)], ref_mask_rot),
+ }
+
+ for test_name, (vertices, ref_mask) in tests.items():
+ with self.subTest(msg=test_name):
+ mask = shapes.polygon_fill_mask(vertices, ref_mask.shape)
+ is_equal = numpy.all(numpy.equal(ref_mask, mask))
+ if not is_equal:
+ _logger.debug('%s failed with mask != ref_mask:',
+ test_name)
+ _logger.debug('result:\n%s', str(mask))
+ _logger.debug('ref:\n%s', str(ref_mask))
+ self.assertTrue(is_equal)
+
+ def test_shapes(self):
+ """Tests with shapes and reference mask"""
+ tests = {
+ # name: (
+ # polygon corners as a list of (row, col),
+ # ref_mask)
+ 'concave polygon': (
+ [(1, 1), (4, 3), (1, 5), (2, 3)],
+ numpy.array((
+ (0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 0, 1, 1, 1, 0, 0, 0),
+ (0, 0, 0, 1, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0, 0, 0)), dtype=numpy.uint8)),
+ 'concave polygon partly outside mask': (
+ [(-1, -1), (4, 3), (1, 5), (2, 3)],
+ numpy.array((
+ (1, 0, 0, 0, 0, 0),
+ (0, 1, 0, 0, 0, 0),
+ (0, 0, 1, 1, 1, 0),
+ (0, 0, 0, 1, 0, 0),
+ (0, 0, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0),
+ (0, 0, 0, 0, 0, 0)), dtype=numpy.uint8)),
+ 'polygon surrounding mask': (
+ [(-1, -1), (-1, 7), (7, 7), (7, -1), (0, -1),
+ (8, -2), (8, 8), (-2, 8)],
+ numpy.zeros((6, 6), dtype=numpy.uint8))
+ }
+
+ for test_name, (vertices, ref_mask) in tests.items():
+ with self.subTest(msg=test_name):
+ mask = shapes.polygon_fill_mask(vertices, ref_mask.shape)
+ is_equal = numpy.all(numpy.equal(ref_mask, mask))
+ if not is_equal:
+ _logger.debug('%s failed with mask != ref_mask:',
+ test_name)
+ _logger.debug('result:\n%s', str(mask))
+ _logger.debug('ref:\n%s', str(ref_mask))
+ self.assertTrue(is_equal)
+
+
+class TestDrawLine(ParametricTestCase):
+ """basic draw line test"""
+
+ def test_aligned_lines(self):
+ """Test drawing horizontal, vertical and diagonal lines"""
+
+ lines = { # test_name: (drow, dcol)
+ 'Horizontal line, col0 < col1': (0, 10),
+ 'Horizontal line, col0 > col1': (0, -10),
+ 'Vertical line, row0 < row1': (10, 0),
+ 'Vertical line, row0 > row1': (-10, 0),
+ 'Diagonal col0 < col1 and row0 < row1': (10, 10),
+ 'Diagonal col0 < col1 and row0 > row1': (-10, 10),
+ 'Diagonal col0 > col1 and row0 < row1': (10, -10),
+ 'Diagonal col0 > col1 and row0 > row1': (-10, -10),
+ }
+ row0, col0 = 1, 2 # Start point
+
+ for test_name, (drow, dcol) in lines.items():
+ row1 = row0 + drow
+ col1 = col0 + dcol
+ with self.subTest(msg=test_name, drow=drow, dcol=dcol):
+ # Build reference coordinates from drow and dcol
+ if drow == 0:
+ rows = row0 * numpy.ones(abs(dcol) + 1)
+ else:
+ step = 1 if drow > 0 else -1
+ rows = row0 + numpy.arange(0, drow + step, step)
+
+ if dcol == 0:
+ cols = col0 * numpy.ones(abs(drow) + 1)
+ else:
+ step = 1 if dcol > 0 else -1
+ cols = col0 + numpy.arange(0, dcol + step, step)
+ ref_coords = rows, cols
+
+ result = shapes.draw_line(row0, col0, row1, col1)
+ self.assertTrue(self.isEqual(test_name, result, ref_coords))
+
+ def test_noline(self):
+ """Test pt0 == pt1"""
+ for width in range(4):
+ with self.subTest(width=width):
+ result = shapes.draw_line(1, 2, 1, 2, width)
+ self.assertTrue(numpy.all(numpy.equal(result, [(1,), (2,)])))
+
+ def test_lines(self):
+ """Test lines not aligned with axes for 8 slopes and directions"""
+ row0, col0 = 1, 1
+
+ dy, dx = 3, 5
+ ref_coords = numpy.array(
+ [(0, 0), (1, 1), (1, 2), (2, 3), (2, 4), (3, 5)])
+
+ # Build lines for the 8 octants from this coordinantes
+ lines = { # name: (drow, dcol, ref_coords)
+ '1st octant': (dy, dx, ref_coords),
+ '2nd octant': (dx, dy, ref_coords[:, (1, 0)]), # invert x and y
+ '3rd octant': (dx, -dy, ref_coords[:, (1, 0)] * (1, -1)),
+ '4th octant': (dy, -dx, ref_coords * (1, -1)),
+ '5th octant': (-dy, -dx, ref_coords * (-1, -1)),
+ '6th octant': (-dx, -dy, ref_coords[:, (1, 0)] * (-1, -1)),
+ '7th octant': (-dx, dy, ref_coords[:, (1, 0)] * (-1, 1)),
+ '8th octant': (-dy, dx, ref_coords * (-1, 1))
+ }
+
+ # Test with different starting points with positive and negative coords
+ for row0, col0 in ((0, 0), (2, 3), (-4, 1), (-5, -6), (8, -7)):
+ for name, (drow, dcol, ref_coords) in lines.items():
+ row1 = row0 + drow
+ col1 = col0 + dcol
+ # Transpose from ((row0, col0), ...) to (rows, cols)
+ ref_coords = numpy.transpose(ref_coords + (row0, col0))
+
+ with self.subTest(msg=name,
+ pt0=(row0, col0), pt1=(row1, col1)):
+ result = shapes.draw_line(row0, col0, row1, col1)
+ self.assertTrue(self.isEqual(name, result, ref_coords))
+
+ def test_width(self):
+ """Test of line width"""
+
+ lines = { # test_name: row0, col0, row1, col1, width, ref
+ 'horizontal w=2':
+ (0, 0, 0, 1, 2, ((0, 1, 0, 1),
+ (0, 0, 1, 1))),
+ 'horizontal w=3':
+ (0, 0, 0, 1, 3, ((-1, 0, 1, -1, 0, 1),
+ (0, 0, 0, 1, 1, 1))),
+ 'vertical w=2':
+ (0, 0, 1, 0, 2, ((0, 0, 1, 1),
+ (0, 1, 0, 1))),
+ 'vertical w=3':
+ (0, 0, 1, 0, 3, ((0, 0, 0, 1, 1, 1),
+ (-1, 0, 1, -1, 0, 1))),
+ 'diagonal w=3':
+ (0, 0, 1, 1, 3, ((-1, 0, 1, 0, 1, 2),
+ (0, 0, 0, 1, 1, 1))),
+ '1st octant w=3':
+ (0, 0, 1, 2, 3,
+ numpy.array(((-1, 0), (0, 0), (1, 0),
+ (0, 1), (1, 1), (2, 1),
+ (0, 2), (1, 2), (2, 2))).T),
+ '2nd octant w=3':
+ (0, 0, 2, 1, 3,
+ numpy.array(((0, -1), (0, 0), (0, 1),
+ (1, 0), (1, 1), (1, 2),
+ (2, 0), (2, 1), (2, 2))).T),
+ }
+
+ for test_name, (row0, col0, row1, col1, width, ref) in lines.items():
+ with self.subTest(msg=test_name,
+ pt0=(row0, col0), pt1=(row1, col1), width=width):
+ result = shapes.draw_line(row0, col0, row1, col1, width)
+ self.assertTrue(self.isEqual(test_name, result, ref))
+
+ def isEqual(self, test_name, result, ref):
+ """Test equality of two numpy arrays and log them if different"""
+ is_equal = numpy.all(numpy.equal(result, ref))
+ if not is_equal:
+ _logger.debug('%s failed with result != ref:',
+ test_name)
+ _logger.debug('result:\n%s', str(result))
+ _logger.debug('ref:\n%s', str(ref))
+ return is_equal
+
+
+class TestCircleFill(ParametricTestCase):
+ """Tests for circle filling"""
+
+ def testCircle(self):
+ """Test circle_fill with different input parameters"""
+
+ square3x3 = numpy.array(((-1, -1, -1, 0, 0, 0, 1, 1, 1),
+ (-1, 0, 1, -1, 0, 1, -1, 0, 1)))
+
+ tests = [
+ # crow, ccol, radius, ref_coords = (ref_rows, ref_cols)
+ (0, 0, 1, ((0,), (0,))),
+ (10, 15, 1, ((10,), (15,))),
+ (0, 0, 1.5, square3x3),
+ (5, 10, 2, (5 + square3x3[0], 10 + square3x3[1])),
+ (10, 20, 3.5, (
+ 10 + numpy.array((-3, -3, -3,
+ -2, -2, -2, -2, -2,
+ -1, -1, -1, -1, -1, -1, -1,
+ 0, 0, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 1, 1,
+ 2, 2, 2, 2, 2,
+ 3, 3, 3)),
+ 20 + numpy.array((-1, 0, 1,
+ -2, -1, 0, 1, 2,
+ -3, -2, -1, 0, 1, 2, 3,
+ -3, -2, -1, 0, 1, 2, 3,
+ -3, -2, -1, 0, 1, 2, 3,
+ -2, -1, 0, 1, 2,
+ -1, 0, 1)))),
+ ]
+
+ for crow, ccol, radius, ref_coords in tests:
+ with self.subTest(crow=crow, ccol=ccol, radius=radius):
+ coords = shapes.circle_fill(crow, ccol, radius)
+ is_equal = numpy.all(numpy.equal(coords, ref_coords))
+ if not is_equal:
+ _logger.debug('result:\n%s', str(coords))
+ _logger.debug('ref:\n%s', str(ref_coords))
+ self.assertTrue(is_equal)
+
+
+class TestEllipseFill(unittest.TestCase):
+ """Tests for ellipse filling"""
+
+ def testPoint(self):
+ args = [1, 1, 1, 1]
+ result = shapes.ellipse_fill(*args)
+ expected = numpy.array(([1], [1]))
+ numpy.testing.assert_array_equal(result, expected)
+
+ def testTranslatedPoint(self):
+ args = [10, 10, 1, 1]
+ result = shapes.ellipse_fill(*args)
+ expected = numpy.array(([10], [10]))
+ numpy.testing.assert_array_equal(result, expected)
+
+ def testEllipse(self):
+ args = [0, 0, 20, 10]
+ rows, cols = shapes.ellipse_fill(*args)
+ self.assertEqual(len(rows), 617)
+ self.assertEqual(rows.mean(), 0)
+ self.assertAlmostEqual(rows.std(), 10.025575, places=3)
+ self.assertEqual(len(cols), 617)
+ self.assertEqual(cols.mean(), 0)
+ self.assertAlmostEqual(cols.std(), 4.897325, places=3)
+
+ def testTranslatedEllipse(self):
+ args = [0, 0, 20, 10]
+ expected_rows, expected_cols = shapes.ellipse_fill(*args)
+ args = [10, 50, 20, 10]
+ rows, cols = shapes.ellipse_fill(*args)
+ numpy.testing.assert_allclose(rows, expected_rows + 10)
+ numpy.testing.assert_allclose(cols, expected_cols + 50)