Coverage for python / lsst / images / _polygon.py: 39%

66 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:34 +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. 

11 

12from __future__ import annotations 

13 

14__all__ = ("Polygon",) 

15 

16from typing import TYPE_CHECKING, Any, overload 

17 

18import numpy as np 

19import numpy.typing as npt 

20 

21from ._geom import Box 

22 

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] 

32 

33 

34class Polygon: 

35 """A simple 2-d polygon in Euclidean coordinates, with no holes. 

36 

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 """ 

44 

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 

51 

52 @staticmethod 

53 def from_box(box: Box) -> Polygon: 

54 """Construct from an integer-coordinate box. 

55 

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 ) 

66 

67 @property 

68 def n_vertices(self) -> int: 

69 """The number of vertices in the polygon.""" 

70 return self._vertices.shape[0] 

71 

72 @property 

73 def x_vertices(self) -> np.ndarray: 

74 """The x coordinates of the vertices of the polygon. 

75 

76 This is a read-only array; polygons are immutable. 

77 """ 

78 return self._vertices[:, 0] 

79 

80 @property 

81 def y_vertices(self) -> np.ndarray: 

82 """The y coordinates of the vertices of the polygon. 

83 

84 This is a read-only array; polygons are immutable. 

85 """ 

86 return self._vertices[:, 1] 

87 

88 @property 

89 def area(self) -> float: 

90 """The area of the polygon (`float`).""" 

91 return self._get_impl().area 

92 

93 def __eq__(self, other: object) -> bool: 

94 if isinstance(other, Polygon): 

95 import shapely 

96 

97 return bool(shapely.equals(self._get_impl(), other._get_impl())) 

98 return NotImplemented 

99 

100 @overload 

101 def contains(self, other: Polygon) -> bool: ... 101 ↛ exitline 101 didn't return from function 'contains' because

102 

103 @overload 

104 def contains(self, *, x: float, y: float) -> bool: ... 104 ↛ exitline 104 didn't return from function 'contains' because

105 

106 @overload 

107 def contains(self, *, x: np.ndarray, y: np.ndarray) -> np.ndarray: ... 107 ↛ exitline 107 didn't return from function 'contains' because

108 

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. 

117 

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 

131 

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] 

142 

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) 

153 

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 

158 

159 return LegacyPolygon([Point2D(x, y) for x, y in zip(self.x_vertices, self.y_vertices)]) 

160 

161 def _get_impl(self) -> _ImplPolygon: 

162 if self._impl is None: 

163 import shapely 

164 

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