Skip to content

Commit fd3edb4

Browse files
authored
Speed up geometry functions (#217)
* Speed up geometry functions * rm comments
1 parent 9dfe7fd commit fd3edb4

File tree

3 files changed

+132
-97
lines changed

3 files changed

+132
-97
lines changed

nucleus/metrics/geometry.py

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,27 @@
55
TOLERANCE = 1e-8
66

77

8-
class GeometryPoint:
9-
def __init__(self, xy: Union[Tuple[float, float], np.ndarray]):
10-
self.xy = np.array(xy)
11-
self.x = xy[0]
12-
self.y = xy[1]
13-
14-
def __repr__(self) -> str:
15-
return f"GeometryPoint(xy=({self.xy[0]}, {self.xy[1]})"
16-
17-
def __add__(self, p: "GeometryPoint") -> "GeometryPoint":
18-
return GeometryPoint(self.xy + p.xy)
19-
20-
def __sub__(self, p: "GeometryPoint") -> "GeometryPoint":
21-
return GeometryPoint(self.xy - p.xy)
22-
23-
def __rmul__(self, scalar: float) -> "GeometryPoint":
24-
return GeometryPoint(self.xy * scalar)
25-
26-
def __mul__(self, scalar: float) -> "GeometryPoint":
27-
return GeometryPoint(self.xy * scalar)
28-
29-
# Operator @
30-
def __matmul__(self, p: "GeometryPoint") -> float:
31-
return self.xy @ p.xy
32-
33-
def length(self) -> float:
34-
return np.linalg.norm(self.xy)
35-
36-
def cmp(self, p: "GeometryPoint") -> float:
37-
return np.abs(self.xy - p.xy).sum()
38-
39-
408
class GeometryPolygon:
41-
def __init__(self, points: List[GeometryPoint]):
42-
self.points = points
43-
44-
points_x = np.array([point.x for point in points])
45-
points_y = np.array([point.y for point in points])
46-
self.signed_area = (
47-
points_x @ np.roll(points_y, 1) - points_x @ np.roll(points_y, -1)
48-
) / 2
49-
self.area = np.abs(self.signed_area)
9+
def __init__(
10+
self,
11+
points: Union[np.ndarray, List[Tuple[float, float]]],
12+
is_rectangle: bool = False,
13+
):
14+
self.points = (
15+
points if isinstance(points, np.ndarray) else np.array(points)
16+
)
17+
self.is_rectangle = is_rectangle
18+
points_x = self.points[:, 0]
19+
points_y = self.points[:, 1]
20+
if is_rectangle:
21+
self.signed_area = np.abs(self.points[2] - self.points[0]).prod()
22+
self.area = self.signed_area
23+
else:
24+
self.signed_area = (
25+
points_x @ np.roll(points_y, -1)
26+
- points_x @ np.roll(points_y, 1)
27+
) / 2
28+
self.area = np.abs(self.signed_area)
5029

5130
def __len__(self):
5231
return len(self.points)
@@ -55,25 +34,22 @@ def __getitem__(self, idx):
5534
return self.points[idx]
5635

5736
def __repr__(self) -> str:
58-
points = ", ".join([str(point) for point in self.points])
59-
return f"GeometryPolygon({points})"
60-
61-
62-
Segment = Tuple[GeometryPoint, GeometryPoint]
37+
return f"GeometryPolygon({self.points})"
6338

6439

6540
# alpha * a1 + (1 - alpha) * a2 = beta * b1 + (1 - beta) * b2
6641
def segment_intersection(
67-
segment1: Segment, segment2: Segment
68-
) -> Tuple[float, float, GeometryPoint]:
42+
segment1: Tuple[np.ndarray, np.ndarray],
43+
segment2: Tuple[np.ndarray, np.ndarray],
44+
) -> Tuple[float, float, np.ndarray]:
6945
a1, a2 = segment1
7046
b1, b2 = segment2
71-
x2_x2 = b2.x - a2.x
72-
y2_y2 = b2.y - a2.y
73-
x1x2 = a1.x - a2.x
74-
y1y2 = a1.y - a2.y
75-
y1_y2_ = b1.y - b2.y
76-
x1_x2_ = b1.x - b2.x
47+
x2_x2 = b2[0] - a2[0]
48+
y2_y2 = b2[1] - a2[1]
49+
x1x2 = a1[0] - a2[0]
50+
y1y2 = a1[1] - a2[1]
51+
y1_y2_ = b1[1] - b2[1]
52+
x1_x2_ = b1[0] - b2[0]
7753

7854
if np.abs(y1_y2_ * x1x2 - x1_x2_ * y1y2) < TOLERANCE:
7955
beta = 1.0
@@ -139,13 +115,13 @@ def convex_polygon_intersection_area(
139115

140116
def unique(ar):
141117
res = []
142-
for i, _ in enumerate(ar):
143-
if _.cmp(ar[i - 1]) > TOLERANCE:
144-
res.append(_)
118+
for i, a in enumerate(ar):
119+
if np.abs(a - ar[i - 1]).sum() > TOLERANCE:
120+
res.append(a)
145121

146122
return res
147123

148-
ps = sorted(ps, key=lambda x: (x.x + TOLERANCE * x.y))
124+
ps = sorted(ps, key=lambda x: (x[0] + TOLERANCE * x[1]))
149125
ps = unique(ps)
150126

151127
if len(ps) == 0:
@@ -157,25 +133,63 @@ def unique(ar):
157133
res.append(tmp)
158134
ps = sorted(
159135
ps[1:],
160-
key=lambda x: -(
161-
(x - tmp) @ GeometryPoint((0, 1)) / (x - tmp).length()
162-
),
136+
key=lambda x: -((x - tmp) @ np.array((0, 1)) / len(x - tmp)),
163137
)
164138
res.extend(ps)
165139

166140
return GeometryPolygon(res).signed_area * sign
167141

168142

143+
def area(box):
144+
if box[2] <= box[0] or box[3] <= box[1]:
145+
return 0
146+
return (box[2] - box[0]) * (box[3] - box[1])
147+
148+
149+
def iou(box_a, box_b):
150+
box_c = intersection(box_a, box_b)
151+
return area(box_c) / (area(box_a) + area(box_b) - area(box_c))
152+
153+
154+
def intersection(box_a, box_b):
155+
"""boxes are left, top, right, bottom where left < right and top < bottom"""
156+
box_c = [
157+
max(box_a[0], box_b[0]),
158+
max(box_a[1], box_b[1]),
159+
min(box_a[2], box_b[2]),
160+
min(box_a[3], box_b[3]),
161+
]
162+
return box_c
163+
164+
165+
def rectangle_intersection_area(
166+
polygon_a: GeometryPolygon, polygon_b: GeometryPolygon
167+
) -> float:
168+
minx_a, miny_a = np.min(polygon_a.points, axis=0)
169+
maxx_a, maxy_a = np.max(polygon_a.points, axis=0)
170+
minx_b, miny_b = np.min(polygon_b.points, axis=0)
171+
maxx_b, maxy_b = np.max(polygon_b.points, axis=0)
172+
173+
minx_c = max(minx_a, minx_b)
174+
miny_c = max(miny_a, miny_b)
175+
maxx_c = min(maxx_a, maxx_b)
176+
maxy_c = min(maxy_a, maxy_b)
177+
return max(maxx_c - minx_c, 0) * max(maxy_c - miny_c, 0)
178+
179+
169180
def polygon_intersection_area(
170181
polygon_a: GeometryPolygon, polygon_b: GeometryPolygon
171182
) -> float:
183+
if polygon_a.is_rectangle and polygon_b.is_rectangle:
184+
return rectangle_intersection_area(polygon_a, polygon_b)
185+
172186
na = len(polygon_a)
173187
nb = len(polygon_b)
174188
res = 0.0
175189
for i in range(1, na - 1):
176-
sa = [polygon_a[0], polygon_a[i], polygon_a[i + 1]]
190+
sa = polygon_a[[0, i, i + 1]]
177191
for j in range(1, nb - 1):
178-
sb = [polygon_b[0], polygon_b[j], polygon_b[j + 1]]
192+
sb = polygon_b[[0, j, j + 1]]
179193
tmp = convex_polygon_intersection_area(
180194
GeometryPolygon(sa), GeometryPolygon(sb)
181195
)

nucleus/metrics/polygon_utils.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from .base import MetricResult
1212
from .errors import PolygonAnnotationTypeError
13-
from .geometry import GeometryPoint, GeometryPolygon, polygon_intersection_area
13+
from .geometry import GeometryPolygon, polygon_intersection_area
1414

1515
BoxOrPolygonPrediction = TypeVar(
1616
"BoxOrPolygonPrediction", BoxPrediction, PolygonPrediction
@@ -35,19 +35,12 @@ def polygon_annotation_to_geometry(
3535
xmax = annotation.x + annotation.width / 2
3636
ymin = annotation.y - annotation.height / 2
3737
ymax = annotation.y + annotation.height / 2
38-
points = [
39-
GeometryPoint((xmin, ymin)),
40-
GeometryPoint((xmax, ymin)),
41-
GeometryPoint((xmax, ymax)),
42-
GeometryPoint((xmin, ymax)),
43-
]
44-
return GeometryPolygon(points)
38+
points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
39+
return GeometryPolygon(points=points, is_rectangle=True)
4540
elif isinstance(annotation, PolygonAnnotation):
4641
return GeometryPolygon(
47-
[
48-
GeometryPoint((point.x, point.y))
49-
for point in annotation.vertices
50-
]
42+
points=[(point.x, point.y) for point in annotation.vertices],
43+
is_rectangle=False,
5144
)
5245
else:
5346
raise PolygonAnnotationTypeError()

tests/metrics/test_geometry.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,86 @@
22
import pytest
33

44
from nucleus.metrics.geometry import (
5-
GeometryPoint,
65
GeometryPolygon,
76
convex_polygon_intersection_area,
87
polygon_intersection_area,
98
segment_intersection,
109
)
1110

1211
RECTANGLE1 = GeometryPolygon(
13-
[
14-
GeometryPoint((0, 0)),
15-
GeometryPoint((100, 0)),
16-
GeometryPoint((100, 100)),
17-
GeometryPoint((0, 100)),
18-
]
12+
points=[
13+
(0, 0),
14+
(100, 0),
15+
(100, 100),
16+
(0, 100),
17+
],
18+
is_rectangle=True,
1919
)
2020

2121
RECTANGLE2 = GeometryPolygon(
22-
[
23-
GeometryPoint((50, 50)),
24-
GeometryPoint((50, 150)),
25-
GeometryPoint((150, 150)),
26-
GeometryPoint((150, 50)),
27-
]
22+
points=[
23+
(50, 50),
24+
(50, 150),
25+
(150, 150),
26+
(150, 50),
27+
],
28+
is_rectangle=True,
2829
)
2930

30-
SEGMENT1 = (GeometryPoint((0, 0)), GeometryPoint((0, 50)))
31+
POLYGON1 = GeometryPolygon(
32+
points=[
33+
(0, 0),
34+
(100, 0),
35+
(100, 100),
36+
(0, 100),
37+
],
38+
is_rectangle=False,
39+
)
40+
41+
POLYGON2 = GeometryPolygon(
42+
points=[
43+
(50, 50),
44+
(50, 150),
45+
(150, 150),
46+
(150, 50),
47+
],
48+
is_rectangle=False,
49+
)
3150

32-
SEGMENT2 = (GeometryPoint((-25, 25)), GeometryPoint((25, 25)))
51+
SEGMENT1 = (np.array((0, 0)), np.array((0, 50)))
52+
53+
SEGMENT2 = (np.array((-25, 25)), np.array((25, 25)))
3354

3455

3556
def test_segment_intersection():
3657
alpha, beta, intersection = segment_intersection(SEGMENT1, SEGMENT2)
3758
assert alpha == pytest.approx(0.5)
3859
assert beta == pytest.approx(0.5)
39-
assert intersection.x == pytest.approx(0)
40-
assert intersection.y == pytest.approx(25)
60+
assert intersection[0] == pytest.approx(0)
61+
assert intersection[1] == pytest.approx(25)
62+
63+
64+
def test_rectangle_intersection_area():
65+
intersection = polygon_intersection_area(RECTANGLE1, RECTANGLE2)
66+
assert intersection == pytest.approx(2500)
4167

4268

4369
def test_convex_polygon_intersection_area():
44-
intersection = np.abs(
45-
convex_polygon_intersection_area(RECTANGLE1, RECTANGLE2)
46-
)
70+
intersection = np.abs(convex_polygon_intersection_area(POLYGON1, POLYGON2))
4771
assert intersection == pytest.approx(2500)
4872

4973

5074
def test_polygon_intersection_area():
51-
intersection = polygon_intersection_area(RECTANGLE1, RECTANGLE2)
75+
intersection = polygon_intersection_area(POLYGON1, POLYGON2)
5276
assert intersection == pytest.approx(2500)
5377

5478

5579
def test_polygon_area():
56-
assert RECTANGLE1.signed_area == -10000
80+
assert RECTANGLE1.signed_area == 10000
5781
assert RECTANGLE1.area == 10000
5882
assert RECTANGLE2.signed_area == 10000
5983
assert RECTANGLE2.area == 10000
84+
assert POLYGON1.signed_area == 10000
85+
assert POLYGON1.area == 10000
86+
assert POLYGON2.signed_area == -10000
87+
assert POLYGON2.area == 10000

0 commit comments

Comments
 (0)