Skip to content

Commit 45510d7

Browse files
authored
♻️ rework polygons, use them more like a real class (#355)
1 parent 13834be commit 45510d7

File tree

6 files changed

+91
-31
lines changed

6 files changed

+91
-31
lines changed

mindee/geometry/polygon.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
1-
from typing import Sequence
1+
from typing import List, Optional, Sequence
22

33
from mindee.geometry.minmax import get_min_max_x, get_min_max_y
44
from mindee.geometry.point import Point
55
from mindee.geometry.polygon_utils import get_centroid, is_point_in_x, is_point_in_y
66

77

8-
class Polygon(list):
8+
class Polygon(List[Point]):
99
"""
1010
Contains any number of vertex coordinates (Points).
1111
1212
Inherits from base class ``list`` so is compatible with type ``Points``.
1313
"""
1414

15+
def __init__(self, vertices: Optional[list] = None):
16+
# we should NOT allow the creation of invalid polygons, but it would be a breaking change
17+
if not vertices:
18+
vertices = []
19+
else:
20+
vertices = [Point(point[0], point[1]) for point in vertices]
21+
super().__init__(vertices)
22+
23+
def _raise_if_invalid(self) -> None:
24+
if len(self) < 3:
25+
raise ValueError("A polygon must have at least 3 vertices")
26+
1527
@property
1628
def centroid(self) -> Point:
1729
"""The central point (centroid) of the polygon."""
30+
self._raise_if_invalid()
1831
return get_centroid(self)
1932

33+
def is_point_in_x(self, point: Point) -> bool:
34+
"""
35+
Determine if the Point is in the Polygon's X-axis.
36+
37+
:param point: Point to compare
38+
"""
39+
self._raise_if_invalid()
40+
min_x, max_x = get_min_max_x(self)
41+
return is_point_in_x(point, min_x, max_x)
42+
43+
def is_point_in_y(self, point: Point) -> bool:
44+
"""
45+
Determine if the Point is in the Polygon's Y-axis.
46+
47+
:param point: Point to compare
48+
"""
49+
self._raise_if_invalid()
50+
min_y, max_y = get_min_max_y(self)
51+
return is_point_in_y(point, min_y, max_y)
52+
2053

2154
def is_point_in_polygon_x(point: Point, polygon: Polygon) -> bool:
2255
"""
56+
Deprecated, use ``is_point_in_x`` from ``Polygon`` class instead.
57+
2358
Determine if the Point is in the Polygon's X-axis.
2459
2560
:param point: Point to compare
@@ -31,6 +66,8 @@ def is_point_in_polygon_x(point: Point, polygon: Polygon) -> bool:
3166

3267
def is_point_in_polygon_y(point: Point, polygon: Polygon) -> bool:
3368
"""
69+
Deprecated, use ``is_point_in_y`` from ``Polygon`` class instead.
70+
3471
Determine if the Point is in the Polygon's Y-axis.
3572
3673
:param point: Point to compare
@@ -40,13 +77,15 @@ def is_point_in_polygon_y(point: Point, polygon: Polygon) -> bool:
4077
return is_point_in_y(point, min_y, max_y)
4178

4279

43-
def polygon_from_prediction(prediction: Sequence[list]) -> Polygon:
80+
def polygon_from_prediction(prediction: Sequence[List[float]]) -> Polygon:
4481
"""
82+
Deprecated, init ``Polygon`` class directly instead.
83+
4584
Transform a prediction into a Polygon.
4685
4786
:param prediction: API prediction.
4887
"""
49-
return Polygon(Point(point[0], point[1]) for point in prediction)
88+
return Polygon([Point(point[0], point[1]) for point in prediction])
5089

5190

5291
def merge_polygons(vertices: Sequence[Polygon]) -> Polygon:

mindee/parsing/standard/base.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Any, List, Optional, Type
22

3-
from mindee.geometry.point import Point
43
from mindee.geometry.polygon import Polygon
54
from mindee.geometry.quadrilateral import Quadrilateral, get_bounding_box
65
from mindee.parsing.common.string_dict import StringDict
@@ -16,13 +15,10 @@ class FieldPositionMixin:
1615

1716
def _set_position(self, raw_prediction: StringDict):
1817
self.bounding_box = None
19-
self.polygon = Polygon()
2018
try:
21-
self.polygon = Polygon(
22-
Point(point[0], point[1]) for point in raw_prediction["polygon"]
23-
)
24-
except (KeyError, TypeError):
25-
pass
19+
self.polygon = Polygon(raw_prediction.get("polygon", []))
20+
except TypeError:
21+
self.polygon = Polygon([])
2622
if self.polygon:
2723
self.bounding_box = get_bounding_box(self.polygon)
2824
else:

mindee/parsing/standard/position.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Optional
22

33
from mindee.error.geometry_error import GeometryError
4-
from mindee.geometry.polygon import Polygon, polygon_from_prediction
4+
from mindee.geometry.polygon import Polygon
55
from mindee.geometry.quadrilateral import Quadrilateral, quadrilateral_from_prediction
66
from mindee.parsing.common.string_dict import StringDict
77
from mindee.parsing.standard.base import BaseField
@@ -57,7 +57,7 @@ def get_polygon(key: str) -> Optional[Polygon]:
5757
if not polygon:
5858
return None
5959
try:
60-
return polygon_from_prediction(polygon)
60+
return Polygon(polygon)
6161
except GeometryError:
6262
return None
6363

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
1-
from typing import Optional
2-
3-
from mindee.geometry import Polygon
1+
from mindee.geometry.polygon import Polygon
42
from mindee.parsing.common.string_dict import StringDict
53

64

75
class FieldLocation:
86
"""Location of a field."""
97

8+
polygon: Polygon
9+
page: int
10+
1011
def __init__(self, server_response: StringDict) -> None:
1112
"""
1213
Initialize FieldLocation from server response.
1314
1415
:param server_response: Raw server response.
1516
"""
16-
self.polygon: Optional[Polygon] = None
17-
self.page: Optional[int] = None
18-
19-
if "polygon" in server_response and server_response["polygon"] is not None:
20-
self.polygon = Polygon(server_response["polygon"])
21-
22-
if "page" in server_response and isinstance(server_response["page"], int):
23-
self.page = server_response["page"]
17+
self.polygon = Polygon(server_response["polygon"])
18+
self.page = int(server_response["page"])
2419

2520
def __str__(self) -> str:
2621
"""
2722
String representation.
2823
2924
:return: String representation of the field location.
3025
"""
31-
return str(self.polygon) if self.polygon else ""
26+
return f"{self.polygon} on page {self.page}"

tests/test_geometry.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,37 +53,67 @@ def test_bounding_box_single_polygon(rectangle_a, rectangle_b, quadrangle_a):
5353

5454

5555
def test_is_point_in_polygon_y(rectangle_a, rectangle_b, quadrangle_a):
56+
rectangle_a_polygon = geometry.Polygon(rectangle_a)
57+
rectangle_b_polygon = geometry.Polygon(rectangle_b)
58+
quadrangle_a_polygon = geometry.Polygon(quadrangle_a)
59+
5660
# Should be in polygon A & B, since polygons overlap
5761
point_a = geometry.Point(0.125, 0.535)
5862
# Should only be in polygon C
5963
point_b = geometry.Point(0.300, 0.420)
6064

6165
assert geometry.is_point_in_polygon_y(point_a, rectangle_a)
66+
assert rectangle_a_polygon.is_point_in_y(point_a)
6267
assert geometry.is_point_in_polygon_y(point_a, rectangle_b)
68+
assert rectangle_b_polygon.is_point_in_y(point_a)
6369
assert geometry.is_point_in_polygon_y(point_a, quadrangle_a) is False
70+
assert quadrangle_a_polygon.is_point_in_y(point_a) is False
6471

6572
assert geometry.is_point_in_polygon_y(point_b, rectangle_a) is False
73+
assert rectangle_a_polygon.is_point_in_y(point_b) is False
6674
assert geometry.is_point_in_polygon_y(point_b, rectangle_b) is False
75+
assert rectangle_b_polygon.is_point_in_y(point_b) is False
6776
assert geometry.is_point_in_polygon_y(point_b, quadrangle_a)
77+
assert quadrangle_a_polygon.is_point_in_y(point_b)
6878

6979

7080
def test_is_point_in_polygon_x(rectangle_a, rectangle_b, quadrangle_a):
81+
rectangle_a_polygon = geometry.Polygon(rectangle_a)
82+
rectangle_b_polygon = geometry.Polygon(rectangle_b)
83+
quadrangle_a_polygon = geometry.Polygon(quadrangle_a)
84+
7185
# Should be in polygon A & B, since polygons overlap
7286
point_a = geometry.Point(0.125, 0.535)
7387
# Should only be in polygon C
7488
point_b = geometry.Point(0.300, 0.420)
7589

7690
assert geometry.is_point_in_polygon_x(point_a, rectangle_a)
91+
assert rectangle_a_polygon.is_point_in_x(point_a)
7792
assert geometry.is_point_in_polygon_x(point_a, rectangle_b)
93+
assert rectangle_b_polygon.is_point_in_x(point_a)
7894
assert geometry.is_point_in_polygon_x(point_a, quadrangle_a) is False
95+
assert quadrangle_a_polygon.is_point_in_x(point_a) is False
7996

8097
assert geometry.is_point_in_polygon_x(point_b, rectangle_a) is False
98+
assert rectangle_a_polygon.is_point_in_x(point_b) is False
8199
assert geometry.is_point_in_polygon_x(point_b, rectangle_b) is False
100+
assert rectangle_b_polygon.is_point_in_x(point_b) is False
82101
assert geometry.is_point_in_polygon_x(point_b, quadrangle_a)
83102

84103

85104
def test_get_centroid(rectangle_a):
86105
assert geometry.get_centroid(rectangle_a) == (0.149, 0.538)
106+
assert geometry.Polygon(rectangle_a).centroid == geometry.Point(0.149, 0.538)
107+
108+
109+
def test_empty_polygon():
110+
empty = geometry.Polygon()
111+
with pytest.raises(ValueError):
112+
empty.is_point_in_y([0.5, 0.5])
113+
with pytest.raises(ValueError):
114+
empty.is_point_in_x([0.5, 0.5])
115+
with pytest.raises(ValueError):
116+
empty.centroid
87117

88118

89119
def test_bounding_box_several_polygons(rectangle_b, quadrangle_a):

tests/v2/test_inference_response.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,16 @@ def test_field_locations_and_confidence() -> None:
248248
date_field: SimpleField = inference_result.inference.result.fields["date"]
249249

250250
assert date_field.locations, "date field should expose locations"
251-
loc0 = date_field.locations[0]
252-
assert loc0 is not None
253-
assert loc0.page == 0
251+
location = date_field.locations[0]
252+
assert location is not None
253+
assert location.page == 0
254254

255-
polygon = loc0.polygon
255+
polygon = location.polygon
256256
assert polygon is not None
257257
assert len(polygon[0]) == 2
258258

259-
assert polygon[0][0] == 0.948979073166918
260-
assert polygon[0][1] == 0.23097924535067715
259+
assert polygon[0].x == 0.948979073166918
260+
assert polygon[0].y == 0.23097924535067715
261261

262262
assert polygon[1][0] == 0.85422
263263
assert polygon[1][1] == 0.230072

0 commit comments

Comments
 (0)