Coverage for tests/test_polygon.py: 12%
268 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-26 02:33 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-26 02:33 -0700
1# This file is part of afw.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import pickle
23import unittest
25import numpy as np
27import lsst.utils.tests
28import lsst.geom
29import lsst.afw.geom as afwGeom
30import lsst.afw.image # noqa: F401 required by Polygon.createImage
32display = False
33doPause = False # If False, autoscan plots with 2 sec delay. If True, impose manual closing of plots.
36def circle(radius, num, x0=0.0, y0=0.0):
37 """Generate points on a circle
39 @param radius: radius of circle
40 @param num: number of points
41 @param x0,y0: Offset in x,y
42 @return x,y coordinates as numpy array
43 """
44 theta = np.linspace(0, 2*np.pi, num=num, endpoint=False)
45 x = radius*np.cos(theta) + x0
46 y = radius*np.sin(theta) + y0
47 return np.array([x, y]).transpose()
50class PolygonTest(lsst.utils.tests.TestCase):
52 def setUp(self):
53 self.x0 = 0.0
54 self.y0 = 0.0
56 def polygon(self, num, radius=1.0, x0=None, y0=None):
57 """Generate a polygon
59 @param num: Number of points
60 @param radius: Radius of polygon
61 @param x0,y0: Offset of center
62 @return polygon
63 """
64 if x0 is None:
65 x0 = self.x0
66 if y0 is None:
67 y0 = self.y0
68 points = circle(radius, num, x0=x0, y0=y0)
69 return afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in reversed(points)])
71 def square(self, size=1.0, x0=0, y0=0):
72 """Generate a square
74 @param size: Half-length of the sides
75 @param x0,y0: Offset of center
76 """
77 return afwGeom.Polygon([lsst.geom.Point2D(size*x + x0, size*y + y0) for
78 x, y in ((-1, -1), (-1, 1), (1, 1), (1, -1))])
80 def testGetters(self):
81 """Test Polygon getters"""
82 for num in range(3, 30):
83 poly = self.polygon(num)
84 self.assertEqual(poly, poly)
85 self.assertNotEqual(poly, self.square(1.0, 2.0, 3.0))
86 self.assertEqual(poly.getNumEdges(), num)
87 # One extra for the closing point
88 self.assertEqual(len(poly.getVertices()), num + 1)
89 self.assertEqual(len(poly.getEdges()), num)
90 perimeter = 0.0
91 for p1, p2 in poly.getEdges():
92 perimeter += np.hypot(p1.getX() - p2.getX(),
93 p1.getY() - p2.getY())
94 self.assertAlmostEqual(poly.calculatePerimeter(), perimeter)
96 size = 3.0
97 poly = self.square(size=size)
98 self.assertEqual(poly.calculateArea(), (2*size)**2)
99 self.assertEqual(poly.calculatePerimeter(), 2*size*4)
100 edges = poly.getEdges()
101 self.assertEqual(len(edges), 4)
102 perimeter = 0.0
103 for p1, p2 in edges:
104 self.assertEqual(abs(p1.getX()), size)
105 self.assertEqual(abs(p1.getY()), size)
106 self.assertEqual(abs(p2.getX()), size)
107 self.assertEqual(abs(p2.getY()), size)
108 self.assertNotEqual(p1, p2)
110 def testPickle(self):
111 for num in range(3, 30):
112 poly = self.polygon(num)
113 self.assertEqual(pickle.loads(pickle.dumps(poly)), poly)
115 def testFromBox(self):
116 size = 1.0
117 poly1 = self.square(size=size)
118 box = lsst.geom.Box2D(lsst.geom.Point2D(-1.0, -1.0),
119 lsst.geom.Point2D(1.0, 1.0))
120 poly2 = afwGeom.Polygon(box)
121 self.assertEqual(poly1, poly2)
123 def testBBox(self):
124 """Test Polygon.getBBox"""
125 size = 3.0
126 poly = self.square(size=size)
127 box = poly.getBBox()
128 self.assertEqual(box.getMinX(), -size)
129 self.assertEqual(box.getMinY(), -size)
130 self.assertEqual(box.getMaxX(), size)
131 self.assertEqual(box.getMaxY(), size)
133 def testCenter(self):
134 """Test Polygon.calculateCenter"""
135 for num in range(3, 30):
136 poly = self.polygon(num)
137 center = poly.calculateCenter()
138 self.assertAlmostEqual(center.getX(), self.x0)
139 self.assertAlmostEqual(center.getY(), self.y0)
141 def testContains(self):
142 """Test Polygon.contains"""
143 radius = 1.0
144 for num in range(3, 30):
145 poly = self.polygon(num, radius=radius)
146 self.assertTrue(poly.contains(lsst.geom.Point2D(self.x0, self.y0)))
147 self.assertFalse(poly.contains(
148 lsst.geom.Point2D(self.x0 + radius, self.y0 + radius)))
150 def testOverlaps(self):
151 """Test Polygon.overlaps"""
152 radius = 1.0
153 for num in range(3, 30):
154 poly1 = self.polygon(num, radius=radius)
155 poly2 = self.polygon(num, radius=radius, x0=radius, y0=radius)
156 poly3 = self.polygon(num, radius=2*radius)
157 poly4 = self.polygon(num, radius=radius, x0=3*radius, y0=3*radius)
158 self.assertTrue(poly1.overlaps(poly2))
159 self.assertTrue(poly2.overlaps(poly1))
160 self.assertTrue(poly1.overlaps(poly3))
161 self.assertTrue(poly3.overlaps(poly1))
162 self.assertFalse(poly1.overlaps(poly4))
163 self.assertFalse(poly4.overlaps(poly1))
165 def testIntersection(self):
166 """Test Polygon.intersection"""
167 poly1 = self.square(2.0, -1.0, -1.0)
168 poly2 = self.square(2.0, +1.0, +1.0)
169 poly3 = self.square(1.0, 0.0, 0.0)
170 poly4 = self.square(1.0, +5.0, +5.0)
172 # intersectionSingle: assumes there's a single intersection (convex
173 # polygons)
174 self.assertEqual(poly1.intersectionSingle(poly2), poly3)
175 self.assertEqual(poly2.intersectionSingle(poly1), poly3)
176 self.assertRaises(afwGeom.SinglePolygonException,
177 poly1.intersectionSingle, poly4)
178 self.assertRaises(afwGeom.SinglePolygonException,
179 poly4.intersectionSingle, poly1)
181 # intersection: no assumptions
182 polyList1 = poly1.intersection(poly2)
183 polyList2 = poly2.intersection(poly1)
184 self.assertEqual(polyList1, polyList2)
185 self.assertEqual(len(polyList1), 1)
186 self.assertEqual(polyList1[0], poly3)
187 polyList3 = poly1.intersection(poly4)
188 polyList4 = poly4.intersection(poly1)
189 self.assertEqual(polyList3, polyList4)
190 self.assertEqual(len(polyList3), 0)
192 def testUnion(self):
193 """Test Polygon.union"""
194 poly1 = self.square(2.0, -1.0, -1.0)
195 poly2 = self.square(2.0, +1.0, +1.0)
196 poly3 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in
197 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +1.0), (-1.0, +3.0),
198 (+3.0, +3.0), (+3.0, -1.0), (+1.0, -1.0), (+1.0, -3.0))])
199 poly4 = self.square(1.0, +5.0, +5.0)
201 # unionSingle: assumes there's a single union (intersecting polygons)
202 self.assertEqual(poly1.unionSingle(poly2), poly3)
203 self.assertEqual(poly2.unionSingle(poly1), poly3)
204 self.assertRaises(afwGeom.SinglePolygonException, poly1.unionSingle, poly4)
205 self.assertRaises(afwGeom.SinglePolygonException, poly4.unionSingle, poly1)
207 # union: no assumptions
208 polyList1 = poly1.union(poly2)
209 polyList2 = poly2.union(poly1)
210 self.assertEqual(polyList1, polyList2)
211 self.assertEqual(len(polyList1), 1)
212 self.assertEqual(polyList1[0], poly3)
213 polyList3 = poly1.union(poly4)
214 polyList4 = poly4.union(poly1)
215 self.assertEqual(len(polyList3), 2)
216 self.assertEqual(len(polyList3), len(polyList4))
217 self.assertTrue((polyList3[0] == polyList4[0] and polyList3[1] == polyList4[1])
218 or (polyList3[0] == polyList4[1] and polyList3[1] == polyList4[0]))
219 self.assertTrue((polyList3[0] == poly1 and polyList3[1] == poly4)
220 or (polyList3[0] == poly4 and polyList3[1] == poly1))
222 def testSymDifference(self):
223 """Test Polygon.symDifference"""
224 poly1 = self.square(2.0, -1.0, -1.0)
225 poly2 = self.square(2.0, +1.0, +1.0)
227 poly3 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in
228 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +1.0),
229 (-1.0, -1.0), (+1.0, -1.0), (1.0, -3.0))])
230 poly4 = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in
231 ((-1.0, +1.0), (-1.0, +3.0), (+3.0, +3.0),
232 (+3.0, -1.0), (+1.0, -1.0), (1.0, +1.0))])
234 diff1 = poly1.symDifference(poly2)
235 diff2 = poly2.symDifference(poly1)
237 self.assertEqual(len(diff1), 2)
238 self.assertEqual(len(diff2), 2)
239 self.assertTrue((diff1[0] == diff2[0] and diff1[1] == diff2[1])
240 or (diff1[1] == diff2[0] and diff1[0] == diff2[1]))
241 self.assertTrue((diff1[0] == poly3 and diff1[1] == poly4)
242 or (diff1[1] == poly3 and diff1[0] == poly4))
244 def testConvexHull(self):
245 """Test Polygon.convexHull"""
246 poly1 = self.square(2.0, -1.0, -1.0)
247 poly2 = self.square(2.0, +1.0, +1.0)
248 poly = poly1.unionSingle(poly2)
249 expected = afwGeom.Polygon([lsst.geom.Point2D(x, y) for x, y in
250 ((-3.0, -3.0), (-3.0, +1.0), (-1.0, +3.0),
251 (+3.0, +3.0), (+3.0, -1.0), (+1.0, -3.0))])
252 self.assertEqual(poly.convexHull(), expected)
254 def testImage(self):
255 """Test Polygon.createImage"""
256 if display:
257 import lsst.afw.display as afwDisplay
259 for i, num in enumerate(range(3, 30)):
260 # We need to test at different centers, because the
261 # boost::intersection algorithm depends sensitively on
262 # input floating-point values, and you will get different
263 # aliasing depending on the central pixel value when
264 # generating the polygon from numpy values.
265 for cent in [-75, -50, -25, 0, 25, 50, 75]:
266 poly = self.polygon(num, 25, cent, cent)
267 box = lsst.geom.Box2I(lsst.geom.Point2I(cent - 60, cent - 60),
268 lsst.geom.Extent2I(115, 115))
269 image = poly.createImage(box)
270 if display:
271 disp = afwDisplay.Display(frame=i + 1)
272 disp.mtv(image, title=f"Polygon nside={num}")
273 for p1, p2 in poly.getEdges():
274 disp.line((p1, p2))
275 # Some computations of the image area have such large aliasing
276 # in boost::intersection that the precision required here is 0.025.
277 self.assertFloatsAlmostEqual(
278 image.getArray().sum(), poly.calculateArea(), rtol=0.025)
280 def testTransform(self):
281 """Test constructor for Polygon involving transforms"""
282 box = lsst.geom.Box2D(lsst.geom.Point2D(0.0, 0.0),
283 lsst.geom.Point2D(123.4, 567.8))
284 poly1 = afwGeom.Polygon(box)
285 scale = 1.5
286 shift = lsst.geom.Extent2D(3.0, 4.0)
287 affineTransform = lsst.geom.AffineTransform.makeTranslation(shift) * \
288 lsst.geom.AffineTransform.makeScaling(scale)
289 transform22 = afwGeom.makeTransform(affineTransform)
290 transformedVertices = transform22.applyForward(box.getCorners())
291 expect = afwGeom.Polygon(transformedVertices)
292 poly1 = afwGeom.Polygon(box, affineTransform)
293 poly2 = afwGeom.Polygon(box, transform22)
294 self.assertEqual(poly1, expect)
295 self.assertEqual(poly2, expect)
297 def testIteration(self):
298 """Test iteration over polygon"""
299 for num in range(3, 30):
300 poly = self.polygon(num)
301 self.assertEqual(len(poly), num)
302 points1 = [p for p in poly]
303 points2 = poly.getVertices()
304 self.assertEqual(len(points1), num + 1)
305 self.assertEqual(len(points2), num + 1)
306 self.assertEqual(points2[0], points2[-1]) # Closed representation
307 for p1, p2 in zip(points1, points2):
308 self.assertEqual(p1, p2)
309 for i, p1 in enumerate(points1):
310 self.assertEqual(poly[i], p1)
312 def testSubSample(self):
313 """Test Polygon.subSample"""
314 if display:
315 import matplotlib.pyplot as plt
316 for num in range(3, 30):
317 poly = self.polygon(num)
318 sub = poly.subSample(2)
320 if display:
321 axes = poly.plot(c='b')
322 axes.set_aspect("equal")
323 axes.set_title(f"Polygon nside={num}")
324 sub.plot(axes, c='r')
325 if not doPause:
326 try:
327 plt.pause(2)
328 plt.close()
329 except Exception:
330 print(f"{str(self)}: plt.pause() failed. Please close plots when done.")
331 plt.show()
332 else:
333 print(f"{str(self)}: Please close plots when done.")
334 plt.show()
336 self.assertEqual(len(sub), 2*num)
337 self.assertAlmostEqual(sub.calculateArea(), poly.calculateArea())
338 self.assertAlmostEqual(
339 sub.calculatePerimeter(), poly.calculatePerimeter())
340 polyCenter = poly.calculateCenter()
341 subCenter = sub.calculateCenter()
342 self.assertAlmostEqual(polyCenter[0], subCenter[0])
343 self.assertAlmostEqual(polyCenter[1], subCenter[1])
344 for i in range(num):
345 self.assertEqual(sub[2*i], poly[i])
347 sub = poly.subSample(0.1)
348 self.assertAlmostEqual(sub.calculateArea(), poly.calculateArea())
349 self.assertAlmostEqual(
350 sub.calculatePerimeter(), poly.calculatePerimeter())
351 polyCenter = poly.calculateCenter()
352 subCenter = sub.calculateCenter()
353 self.assertAlmostEqual(polyCenter[0], subCenter[0])
354 self.assertAlmostEqual(polyCenter[1], subCenter[1])
356 def testTransform2(self):
357 if display:
358 import matplotlib.pyplot as plt
359 scale = 2.0
360 shift = lsst.geom.Extent2D(3.0, 4.0)
361 affineTransform = lsst.geom.AffineTransform.makeTranslation(shift) * \
362 lsst.geom.AffineTransform.makeScaling(scale)
363 transform22 = afwGeom.makeTransform(affineTransform)
364 for num in range(3, 30):
365 small = self.polygon(num, 1.0, 0.0, 0.0)
366 large1 = small.transform(affineTransform)
367 large2 = small.transform(transform22)
368 expect = self.polygon(num, scale, shift[0], shift[1])
369 self.assertEqual(large1, expect)
370 self.assertEqual(large2, expect)
372 if display:
373 axes = small.plot(c='k')
374 axes.set_aspect("equal")
375 axes.set_title(f"AffineTransform: Polygon nside={num}")
376 large1.plot(axes, c='b')
377 if not doPause:
378 try:
379 plt.pause(2)
380 plt.close()
381 except Exception:
382 print(f"{str(self)}: plt.pause() failed. Please close plots when done.")
383 plt.show()
384 else:
385 print(f"{str(self)}: Please close plots when done.")
386 plt.show()
388 def testReadWrite(self):
389 """Test that polygons can be read and written to fits files"""
390 for num in range(3, 30):
391 poly = self.polygon(num)
392 with lsst.utils.tests.getTempFilePath(".fits") as filename:
393 poly.writeFits(filename)
394 poly2 = afwGeom.Polygon.readFits(filename)
395 self.assertEqual(poly, poly2)
398class TestMemory(lsst.utils.tests.MemoryTestCase):
399 pass
402def setup_module(module):
403 lsst.utils.tests.init()
406if __name__ == "__main__": 406 ↛ 407line 406 didn't jump to line 407, because the condition on line 406 was never true
407 lsst.utils.tests.init()
408 unittest.main()