Coverage for python / lsst / images / _polygon.py: 39%
66 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:36 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:36 +0000
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("Polygon",)
16from typing import TYPE_CHECKING, Any, overload
18import numpy as np
19import numpy.typing as npt
21from ._geom import Box
23if TYPE_CHECKING:
24 try:
25 from shapely import Polygon as _ImplPolygon
26 except ImportError:
27 type _ImplPolygon = Any # type: ignore[no-redef]
28 try:
29 from lsst.afw.geom import Polygon as LegacyPolygon
30 except ImportError:
31 type LegacyPolygon = Any # type: ignore[no-redef]
34class Polygon:
35 """A simple 2-d polygon in Euclidean coordinates, with no holes.
37 Parameters
38 ----------
39 x_vertices
40 The x coordinates of the vertices of the polygon.
41 y_vertices
42 The y coordinate of the vertices of the polygon.
43 """
45 def __init__(self, *, x_vertices: npt.ArrayLike, y_vertices: npt.ArrayLike):
46 self._vertices = np.stack(
47 [np.asarray(x_vertices).flat, np.asarray(y_vertices).flat], dtype=np.float64
48 ).transpose()
49 self._vertices.flags.writeable = False
50 self._impl: _ImplPolygon | None = None
52 @staticmethod
53 def from_box(box: Box) -> Polygon:
54 """Construct from an integer-coordinate box.
56 Notes
57 -----
58 Because the integer min and max coordinates of the box are
59 interpreted as pixel centers, these are expanded by 0.5 on all sides
60 before using them to form the polygon vertices.
61 """
62 return Polygon(
63 x_vertices=[box.x.min - 0.5, box.x.min - 0.5, box.x.max + 0.5, box.x.max + 0.5],
64 y_vertices=[box.y.min - 0.5, box.y.max + 0.5, box.y.max + 0.5, box.y.min - 0.5],
65 )
67 @property
68 def n_vertices(self) -> int:
69 """The number of vertices in the polygon."""
70 return self._vertices.shape[0]
72 @property
73 def x_vertices(self) -> np.ndarray:
74 """The x coordinates of the vertices of the polygon.
76 This is a read-only array; polygons are immutable.
77 """
78 return self._vertices[:, 0]
80 @property
81 def y_vertices(self) -> np.ndarray:
82 """The y coordinates of the vertices of the polygon.
84 This is a read-only array; polygons are immutable.
85 """
86 return self._vertices[:, 1]
88 @property
89 def area(self) -> float:
90 """The area of the polygon (`float`)."""
91 return self._get_impl().area
93 def __eq__(self, other: object) -> bool:
94 if isinstance(other, Polygon):
95 import shapely
97 return bool(shapely.equals(self._get_impl(), other._get_impl()))
98 return NotImplemented
100 @overload
101 def contains(self, other: Polygon) -> bool: ... 101 ↛ exitline 101 didn't return from function 'contains' because
103 @overload
104 def contains(self, *, x: float, y: float) -> bool: ... 104 ↛ exitline 104 didn't return from function 'contains' because
106 @overload
107 def contains(self, *, x: np.ndarray, y: np.ndarray) -> np.ndarray: ... 107 ↛ exitline 107 didn't return from function 'contains' because
109 def contains(
110 self,
111 other: Polygon | None = None,
112 *,
113 x: float | np.ndarray | None = None,
114 y: float | np.ndarray | None = None,
115 ) -> bool | np.ndarray:
116 """Test whether the polygon contains the given points or polygon.
118 Parameters
119 ----------
120 other
121 Another polygon to compare to. Not compatible with the ``y`` and
122 ``x`` arguments.
123 x
124 One or more floating-point X coordinates to test for containment.
125 If an array, an array of results will be returned.
126 y
127 One or more floating-point Y coordinates to test for containment.
128 If an array, an array of results will be returned.
129 """
130 import shapely
132 impl = self._get_impl()
133 if other is not None:
134 if x is not None or y is not None:
135 raise TypeError("Too many arguments to 'SimplePolygon.contains'.")
136 return impl.contains(other._get_impl())
137 elif x is None or y is None:
138 raise TypeError("Not enough arguments to 'SimplePolygon.contains'.")
139 else:
140 # Quibbles about bool vs numpy.bool_ as the return type.
141 return shapely.contains_xy(impl, x=x, y=y) # type: ignore[return-value]
143 @staticmethod
144 def from_legacy(legacy: LegacyPolygon) -> Polygon:
145 """Convert from a legacy `lsst.afw.geom.Polygon` instance."""
146 vertices = legacy.getVertices()
147 x_vertices = np.zeros(len(vertices), dtype=np.float64)
148 y_vertices = np.zeros(len(vertices), dtype=np.float64)
149 for n, point in enumerate(vertices):
150 x_vertices[n] = point.x
151 y_vertices[n] = point.y
152 return Polygon(x_vertices=x_vertices, y_vertices=y_vertices)
154 def to_legacy(self) -> LegacyPolygon:
155 """Convert to a legacy `lsst.afw.geom.Polygon` instance."""
156 from lsst.afw.geom import Polygon as LegacyPolygon
157 from lsst.geom import Point2D
159 return LegacyPolygon([Point2D(x, y) for x, y in zip(self.x_vertices, self.y_vertices)])
161 def _get_impl(self) -> _ImplPolygon:
162 if self._impl is None:
163 import shapely
165 self._impl = shapely.Polygon(self._vertices)
166 # 'prepare' preps whatever index structures etc. might be useful
167 # for accelerating various predicates.
168 shapely.prepare(self._impl)
169 return self._impl