edgefirst-validator 4.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepview/modelpack/utils/argmax.py +16 -0
- edgefirst/validator/__init__.py +1 -0
- edgefirst/validator/__main__.py +375 -0
- edgefirst/validator/datasets/__init__.py +118 -0
- edgefirst/validator/datasets/cache.py +296 -0
- edgefirst/validator/datasets/core.py +250 -0
- edgefirst/validator/datasets/darknet.py +446 -0
- edgefirst/validator/datasets/database.py +1067 -0
- edgefirst/validator/datasets/instance/__init__.py +4 -0
- edgefirst/validator/datasets/instance/core.py +222 -0
- edgefirst/validator/datasets/instance/detection.py +145 -0
- edgefirst/validator/datasets/instance/multitask.py +80 -0
- edgefirst/validator/datasets/instance/segmentation.py +120 -0
- edgefirst/validator/datasets/utils/fetch.py +682 -0
- edgefirst/validator/datasets/utils/readers.py +425 -0
- edgefirst/validator/datasets/utils/transformations.py +1695 -0
- edgefirst/validator/evaluators/__init__.py +17 -0
- edgefirst/validator/evaluators/callbacks/__init__.py +3 -0
- edgefirst/validator/evaluators/callbacks/core.py +192 -0
- edgefirst/validator/evaluators/callbacks/plots.py +900 -0
- edgefirst/validator/evaluators/callbacks/studio.py +234 -0
- edgefirst/validator/evaluators/core.py +257 -0
- edgefirst/validator/evaluators/detection.py +749 -0
- edgefirst/validator/evaluators/multitask.py +270 -0
- edgefirst/validator/evaluators/parameters/__init__.py +53 -0
- edgefirst/validator/evaluators/parameters/core.py +554 -0
- edgefirst/validator/evaluators/parameters/dataset.py +239 -0
- edgefirst/validator/evaluators/parameters/model.py +338 -0
- edgefirst/validator/evaluators/parameters/validation.py +528 -0
- edgefirst/validator/evaluators/segmentation.py +729 -0
- edgefirst/validator/evaluators/utils/__init__.py +3 -0
- edgefirst/validator/evaluators/utils/classify.py +292 -0
- edgefirst/validator/evaluators/utils/match.py +262 -0
- edgefirst/validator/evaluators/utils/timer.py +132 -0
- edgefirst/validator/metrics/__init__.py +9 -0
- edgefirst/validator/metrics/data/__init__.py +7 -0
- edgefirst/validator/metrics/data/label.py +668 -0
- edgefirst/validator/metrics/data/metrics.py +759 -0
- edgefirst/validator/metrics/data/plots.py +476 -0
- edgefirst/validator/metrics/data/stats.py +507 -0
- edgefirst/validator/metrics/detection.py +595 -0
- edgefirst/validator/metrics/segmentation.py +173 -0
- edgefirst/validator/metrics/utils/math.py +717 -0
- edgefirst/validator/publishers/__init__.py +3 -0
- edgefirst/validator/publishers/console.py +147 -0
- edgefirst/validator/publishers/studio.py +128 -0
- edgefirst/validator/publishers/tensorboard.py +119 -0
- edgefirst/validator/publishers/utils/logger.py +111 -0
- edgefirst/validator/publishers/utils/table.py +403 -0
- edgefirst/validator/runners/__init__.py +8 -0
- edgefirst/validator/runners/core.py +727 -0
- edgefirst/validator/runners/deepviewrt.py +177 -0
- edgefirst/validator/runners/hailo.py +263 -0
- edgefirst/validator/runners/keras.py +150 -0
- edgefirst/validator/runners/kinara.py +265 -0
- edgefirst/validator/runners/offline.py +228 -0
- edgefirst/validator/runners/onnx.py +241 -0
- edgefirst/validator/runners/processing/decode.py +320 -0
- edgefirst/validator/runners/processing/dvapi.py +4192 -0
- edgefirst/validator/runners/processing/nms.py +637 -0
- edgefirst/validator/runners/processing/outputs.py +507 -0
- edgefirst/validator/runners/tensorrt.py +321 -0
- edgefirst/validator/runners/tflite.py +221 -0
- edgefirst/validator/validate.py +843 -0
- edgefirst/validator/visualize/__init__.py +3 -0
- edgefirst/validator/visualize/detection.py +623 -0
- edgefirst/validator/visualize/segmentation.py +281 -0
- edgefirst/validator/visualize/utils/plots.py +635 -0
- edgefirst_validator-4.2.1.dist-info/METADATA +111 -0
- edgefirst_validator-4.2.1.dist-info/RECORD +73 -0
- edgefirst_validator-4.2.1.dist-info/WHEEL +5 -0
- edgefirst_validator-4.2.1.dist-info/entry_points.txt +2 -0
- edgefirst_validator-4.2.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def mean_absolute_error(
|
|
7
|
+
y_true: Union[list, np.ndarray],
|
|
8
|
+
y_pred: Union[list, np.ndarray]
|
|
9
|
+
) -> float:
|
|
10
|
+
"""
|
|
11
|
+
Calculates the mean absolute error defined in this source::
|
|
12
|
+
https://datagy.io/mae-python/
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
y_true: Union[list, np.ndarray]
|
|
17
|
+
The true values.
|
|
18
|
+
y_pred: Union[list, np.ndarray]
|
|
19
|
+
The predicted values.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
float
|
|
24
|
+
The mean absolute error of the values comparing y_pred to y_true.
|
|
25
|
+
"""
|
|
26
|
+
return np.abs(np.subtract(y_true, y_pred)).mean()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mean_squared_error(
|
|
30
|
+
y_true: Union[list, np.ndarray],
|
|
31
|
+
y_pred: Union[list, np.ndarray]
|
|
32
|
+
) -> float:
|
|
33
|
+
"""
|
|
34
|
+
Calculates the mean squared error defined in this source:
|
|
35
|
+
https://www.geeksforgeeks.org/python-mean-squared-error/
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
y_true: Union[list, np.ndarray]
|
|
40
|
+
The true values.
|
|
41
|
+
y_pred: Union[list, np.ndarray]
|
|
42
|
+
The predicted values.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
float
|
|
47
|
+
The mean squared error of the values
|
|
48
|
+
comparing y_pred to y_true.
|
|
49
|
+
"""
|
|
50
|
+
return np.square(np.subtract(y_true, y_pred)).mean()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def batch_iou(box1: np.ndarray, box2: np.ndarray, eps: float = 1e-7):
|
|
54
|
+
"""
|
|
55
|
+
The intersection-over-union (Jaccard index) of boxes.
|
|
56
|
+
The YOLOv5 implementation taken from::
|
|
57
|
+
https://github.com/ultralytics/yolov5/blob/master/utils/metrics.py#L266
|
|
58
|
+
|
|
59
|
+
Both sets of boxes are expected to be in (xmin, ymin, xmax, ymax) format.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
box1 : np.ndarray
|
|
64
|
+
The first set of bounding boxes with the shape (n, 4)
|
|
65
|
+
in the format [xmin, ymin, xmax, ymax].
|
|
66
|
+
box2 : np.ndarray
|
|
67
|
+
The second bounding boxes with the shape (n, 4)
|
|
68
|
+
in the format [xmin, ymin, xmax, ymax].
|
|
69
|
+
eps : float
|
|
70
|
+
This is used to prevent division by 0.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
np.ndarray
|
|
75
|
+
An IoU 2D matrix which calculates
|
|
76
|
+
the IoU between box1 (row) and
|
|
77
|
+
box2 (column).
|
|
78
|
+
"""
|
|
79
|
+
if 0 in [len(box1), len(box2)]:
|
|
80
|
+
return np.zeros((len(box1), len(box2)), dtype=np.float32)
|
|
81
|
+
|
|
82
|
+
a1, a2 = np.expand_dims(
|
|
83
|
+
box1[:, [0, 1]], 1), np.expand_dims(box1[:, [2, 3]], 1)
|
|
84
|
+
b1, b2 = np.expand_dims(
|
|
85
|
+
box2[:, [0, 1]], 0), np.expand_dims(box2[:, [2, 3]], 0)
|
|
86
|
+
|
|
87
|
+
inter = np.minimum(a2, b2) - np.maximum(a1, b1)
|
|
88
|
+
inter = np.prod(np.clip(inter, a_min=0., a_max=np.max(inter)), axis=2)
|
|
89
|
+
return inter / ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter + eps)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mask_iou(mask1: np.ndarray, mask2: np.ndarray,
|
|
93
|
+
eps: float = 1e-7) -> np.ndarray:
|
|
94
|
+
"""
|
|
95
|
+
The intersection-over-union (Jaccard index) of masks.
|
|
96
|
+
The Ultralytics implementation taken from::
|
|
97
|
+
https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/metrics.py#L147
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
mask1 : np.ndarray
|
|
102
|
+
Array of shape (N, n), N ground truth masks, flattened.
|
|
103
|
+
This array must have floating point values for correct IoUs.
|
|
104
|
+
mask2 : np.ndarray
|
|
105
|
+
Array of shape (M, n), M predicted masks, flattened.
|
|
106
|
+
This array must have floating point values for correct IoUs.
|
|
107
|
+
eps : float, optional
|
|
108
|
+
Small epsilon to prevent division by zero.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
np.ndarray
|
|
113
|
+
IoU matrix of shape (N, M) representing mask IoUs.
|
|
114
|
+
"""
|
|
115
|
+
# Compute intersection
|
|
116
|
+
intersection = np.clip(mask1 @ mask2.T, 0, None) # shape: (N, M)
|
|
117
|
+
|
|
118
|
+
# Compute union
|
|
119
|
+
area1 = mask1.sum(axis=1, keepdims=True) # shape: (N, 1)
|
|
120
|
+
area2 = mask2.sum(axis=1, keepdims=True).T # shape: (1, M)
|
|
121
|
+
union = area1 + area2 - intersection
|
|
122
|
+
|
|
123
|
+
# Compute IoU
|
|
124
|
+
return intersection / (union + eps)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def iou_2d(
|
|
128
|
+
box1: Union[list, np.ndarray],
|
|
129
|
+
box2: Union[list, np.ndarray],
|
|
130
|
+
eps: float = 1e-7
|
|
131
|
+
) -> float:
|
|
132
|
+
"""
|
|
133
|
+
Computes the IoU between a single ground truth and prediction
|
|
134
|
+
bounding boxes. IoU computation method retrieved from::
|
|
135
|
+
https://gist.github.com/meyerjo/dd3533edc97c81258898f60d8978eddc
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
box1: Union[list, np.ndarray]
|
|
140
|
+
This is a bounding box [xmin, ymin, xmax, ymax].
|
|
141
|
+
box2: Union[list, np.ndarray]
|
|
142
|
+
This is a bounding box [xmin, ymin, xmax, ymax].
|
|
143
|
+
eps: float
|
|
144
|
+
Avoids division by zero errors. Default to 1e-7.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
float
|
|
149
|
+
The IoU score between two bounding boxes.
|
|
150
|
+
|
|
151
|
+
Exceptions
|
|
152
|
+
----------
|
|
153
|
+
ValueError
|
|
154
|
+
Raised if the provided boxes for the ground truth
|
|
155
|
+
and prediction does not have a length of four.
|
|
156
|
+
"""
|
|
157
|
+
if len(box1) != 4 or len(box2) != 4:
|
|
158
|
+
raise ValueError("The provided bounding boxes does not meet "
|
|
159
|
+
"expected lengths [xmin, ymin, xmax, ymax]")
|
|
160
|
+
|
|
161
|
+
# Determine the (x, y)-coordinates of the intersection rectangle.
|
|
162
|
+
x_a = max(box1[0], box2[0])
|
|
163
|
+
y_a = max(box1[1], box2[1])
|
|
164
|
+
x_b = min(box1[2], box2[2])
|
|
165
|
+
y_b = min(box1[3], box2[3])
|
|
166
|
+
|
|
167
|
+
# Compute the area of intersection rectangle.
|
|
168
|
+
inter_area = max((x_b - x_a, 0)) * max((y_b - y_a), 0)
|
|
169
|
+
if inter_area == 0:
|
|
170
|
+
return 0.
|
|
171
|
+
# Compute the area of both the prediction and ground-truth rectangles.
|
|
172
|
+
box_a_area = abs((box1[2] - box1[0]) * (box1[3] - box1[1]))
|
|
173
|
+
box_b_area = abs((box2[2] - box2[0]) * (box2[3] - box2[1]))
|
|
174
|
+
|
|
175
|
+
# Compute the intersection over union by taking the intersection
|
|
176
|
+
# area and dividing it by the sum of prediction + ground-truth
|
|
177
|
+
# areas - the interesection area
|
|
178
|
+
iou = inter_area / float(box_a_area + box_b_area - inter_area)
|
|
179
|
+
|
|
180
|
+
assert 0. <= iou <= 1. + eps, f"The IoU {iou} is out of bounds."
|
|
181
|
+
|
|
182
|
+
# Return the intersection over union value.
|
|
183
|
+
return iou
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def iou_3d(
|
|
187
|
+
corners1: Union[list, np.ndarray],
|
|
188
|
+
corners2: Union[list, np.ndarray]
|
|
189
|
+
) -> float:
|
|
190
|
+
"""
|
|
191
|
+
Computes the 3D IoU between ground truth and detection bounding boxes.
|
|
192
|
+
Source:: https://github.com/varunagrawal/bbox/blob/master/bbox/metrics.py#L139
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
corners1: Union[list, np.ndarray]
|
|
197
|
+
This is an (8, 3) array of 8 corners for a single 3D bounding box where
|
|
198
|
+
rows represents one point and columns represents the [x,y,z] coordinates.
|
|
199
|
+
corners2: Union[list, np.ndarray]
|
|
200
|
+
This is an (8, 3) array of 8 corners for a single 3D bounding box where
|
|
201
|
+
rows represents one point and columns represents the [x,y,z] coordinates.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
float
|
|
206
|
+
This is the 3D IoU between ground truth and prediction bounding boxes.
|
|
207
|
+
"""
|
|
208
|
+
# Check if the two boxes don't overlap.
|
|
209
|
+
if not polygon_collision(corners1[0:4, [0, 2]], corners2[0:4, [0, 2]]):
|
|
210
|
+
return 0.0
|
|
211
|
+
|
|
212
|
+
# Intersection of the x,z plane.
|
|
213
|
+
intersection_points = polygon_intersection(
|
|
214
|
+
corners1[0:4, [0, 2]], corners2[0:4, [0, 2]])
|
|
215
|
+
# If intersection_points is empty, means the boxes don't intersect
|
|
216
|
+
if len(intersection_points) == 0:
|
|
217
|
+
return 0.0
|
|
218
|
+
inter_area = polygon_area(intersection_points)
|
|
219
|
+
|
|
220
|
+
ymax = np.minimum(corners1[4, 1], corners2[4, 1])
|
|
221
|
+
ymin = np.maximum(corners1[0, 1], corners2[0, 1])
|
|
222
|
+
inter_vol = inter_area * np.maximum(0, ymax - ymin)
|
|
223
|
+
|
|
224
|
+
vol1 = box3d_vol(corners1)
|
|
225
|
+
vol2 = box3d_vol(corners2)
|
|
226
|
+
union_vol = (vol1 + vol2 - inter_vol)
|
|
227
|
+
|
|
228
|
+
iou = inter_vol / union_vol
|
|
229
|
+
# set nan and +/- inf to 0
|
|
230
|
+
if np.isinf(iou) or np.isnan(iou):
|
|
231
|
+
iou = 0
|
|
232
|
+
return iou
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def minkowski_distance(
|
|
236
|
+
center1: Union[list, np.ndarray],
|
|
237
|
+
center2: Union[list, np.ndarray],
|
|
238
|
+
p: int = 2
|
|
239
|
+
) -> float:
|
|
240
|
+
"""
|
|
241
|
+
Calculates the Minkowski distance between two points.
|
|
242
|
+
If p is 1, then this would be the Hamming distance.
|
|
243
|
+
If p is 2, then this would be the Euclidean distance.
|
|
244
|
+
https://www.analyticsvidhya.com/blog/2020/02/4-types-of-distance-metrics-in-machine-learning/
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
center1: list or np.ndarray
|
|
249
|
+
The 2D [x,y] or 3D [x,y,z] coordinates
|
|
250
|
+
for the first point.
|
|
251
|
+
center2: list or np.ndarray
|
|
252
|
+
The 2D [x,y] or 3D [x,y,z] coordinates
|
|
253
|
+
for the second point.
|
|
254
|
+
p: int
|
|
255
|
+
The order in the minkowski distance computation.
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
float
|
|
260
|
+
The distance between two points.
|
|
261
|
+
"""
|
|
262
|
+
return np.power(np.sum(np.power(np.absolute(center1 - center2), p)), 1 / p)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def cosine_similarity(
|
|
266
|
+
center1: Union[list, np.ndarray],
|
|
267
|
+
center2: Union[list, np.ndarray],
|
|
268
|
+
normalize: bool = False
|
|
269
|
+
) -> float:
|
|
270
|
+
"""
|
|
271
|
+
The cosine similarity between two vectors is the dot product
|
|
272
|
+
of the vectors over the product of the magnitudes of the vectors.
|
|
273
|
+
https://en.wikipedia.org/wiki/Cosine_similarity
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
center1: list or np.ndarray
|
|
278
|
+
The 2D [x,y] or 3D [x,y,z] coordinates
|
|
279
|
+
for the first point.
|
|
280
|
+
center2: list or np.ndarray
|
|
281
|
+
The 2D [x,y] or 3D [x,y,z] coordinates
|
|
282
|
+
for the second point.
|
|
283
|
+
normalize: bool
|
|
284
|
+
If this is set to true, this normalizes the metric to be within
|
|
285
|
+
the range of 0 and 1. This is used such that the metric behaves
|
|
286
|
+
similar to an IoU for object detection. Otherwise, by default
|
|
287
|
+
it is -1 for perpendicular vectors and 1 for orthogonal.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
float
|
|
292
|
+
The distance between two points.
|
|
293
|
+
"""
|
|
294
|
+
cosine = np.dot(center1, center2) / \
|
|
295
|
+
(np.linalg.norm(center1) * np.linalg.norm(center2))
|
|
296
|
+
# normalize ranges -1 to 1 into 0 to 1.
|
|
297
|
+
if normalize:
|
|
298
|
+
cosine = (cosine + 1) / 2
|
|
299
|
+
return cosine
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def sigmoid(p: np.ndarray) -> np.ndarray:
|
|
303
|
+
"""
|
|
304
|
+
The sigmoid function that maps values between 0 and 1.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
p: np.ndarray
|
|
309
|
+
An array of values to transform.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
np.ndarray
|
|
314
|
+
An array of values with sigmoid applied.
|
|
315
|
+
"""
|
|
316
|
+
return 1 / (1 + np.exp(-p))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def softmax(x: np.ndarray) -> np.ndarray:
|
|
320
|
+
"""
|
|
321
|
+
Transform values between 0 and 1.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
x: np.ndarray
|
|
326
|
+
An array of values to transform.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
np.ndarray
|
|
331
|
+
The array with softmax transformations.
|
|
332
|
+
"""
|
|
333
|
+
# Subtract the maximum for numerical stability
|
|
334
|
+
e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
|
|
335
|
+
return e_x / np.sum(e_x, axis=-1, keepdims=True)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def localize_distance(
|
|
339
|
+
box1: Union[list, np.ndarray],
|
|
340
|
+
box2: Union[list, np.ndarray],
|
|
341
|
+
leniency_factor: int = 2
|
|
342
|
+
) -> float:
|
|
343
|
+
"""
|
|
344
|
+
Given the diagonal of the smaller bounding box, the center distance
|
|
345
|
+
between the bounding boxes will only be considered if the diagonal length
|
|
346
|
+
does not exceed the number of times as the leniency factor when compared
|
|
347
|
+
against the center distance calculated.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
box1: list or np.ndarray
|
|
352
|
+
This is a bounding box [xmin, ymin, xmax, ymax].
|
|
353
|
+
box2: list or np.ndarray
|
|
354
|
+
This is a bounding box [xmin, ymin, xmax, ymax].
|
|
355
|
+
leniency_factor: int
|
|
356
|
+
This is the maximum times the diagonal of the smaller bounding
|
|
357
|
+
box should fit inside the center distances.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
float
|
|
362
|
+
The restricted distance between the centers of bounding boxes. If
|
|
363
|
+
it does not meet the leniency criteria, it will return the maximum
|
|
364
|
+
distance of 1.
|
|
365
|
+
"""
|
|
366
|
+
diagonal = min(minkowski_distance(box1[0:2], box1[2:4]),
|
|
367
|
+
minkowski_distance(box2[0:2], box2[2:4]))
|
|
368
|
+
center_distance = minkowski_distance(box1, box2)
|
|
369
|
+
if int(center_distance / diagonal) <= leniency_factor:
|
|
370
|
+
return center_distance
|
|
371
|
+
# Validation takes 1-center_distance, so returning 1. would indicate far
|
|
372
|
+
# apart.
|
|
373
|
+
return 1.
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
Useful functions to deal with 3D geometry.
|
|
378
|
+
Source: https://github.com/varunagrawal/bbox/blob/master/bbox/geometry.py
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def x_rotation(angle: float) -> np.ndarray:
|
|
383
|
+
"""
|
|
384
|
+
Rotation around the x-axis.
|
|
385
|
+
Source: https://en.wikipedia.org/wiki/Rotation_matrix
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
angle: float
|
|
390
|
+
The angle of rotation in radians.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
np.ndarrau
|
|
395
|
+
Rotation matrix around the x-axis.
|
|
396
|
+
"""
|
|
397
|
+
c = np.cos(angle)
|
|
398
|
+
s = np.sin(angle)
|
|
399
|
+
return np.array([[1, 0, 0],
|
|
400
|
+
[0, c, -s],
|
|
401
|
+
[0, s, c]])
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def y_rotation(angle: float) -> np.ndarray:
|
|
405
|
+
"""
|
|
406
|
+
Rotation around the y-axis.
|
|
407
|
+
Source: https://en.wikipedia.org/wiki/Rotation_matrix
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
angle: float
|
|
412
|
+
The angle of rotation in radians.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
np.ndarray
|
|
417
|
+
Rotation matrix around the y-axis.
|
|
418
|
+
"""
|
|
419
|
+
c = np.cos(angle)
|
|
420
|
+
s = np.sin(angle)
|
|
421
|
+
return np.array([[c, 0, s],
|
|
422
|
+
[0, 1, 0],
|
|
423
|
+
[-s, 0, c]])
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def z_rotation(angle: float) -> np.ndarray:
|
|
427
|
+
"""
|
|
428
|
+
Rotation around the z-axis.
|
|
429
|
+
Source: https://en.wikipedia.org/wiki/Rotation_matrix
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
angle: float
|
|
434
|
+
The angle of rotation in radians.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
np.ndarray
|
|
439
|
+
Rotation matrix around the z-axis.
|
|
440
|
+
"""
|
|
441
|
+
c = np.cos(angle)
|
|
442
|
+
s = np.sin(angle)
|
|
443
|
+
return np.array([[c, -s, 0],
|
|
444
|
+
[s, c, 0],
|
|
445
|
+
[0, 0, 1]])
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def transform(
|
|
449
|
+
box_size: Union[list, np.ndarray, tuple],
|
|
450
|
+
heading_angle: float,
|
|
451
|
+
center: Union[list, np.ndarray, tuple]
|
|
452
|
+
) -> np.ndarray:
|
|
453
|
+
"""
|
|
454
|
+
Provides rotations and formation of the 3D box corners.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
box_size: Union[list, np.ndarray, tuple]
|
|
459
|
+
Can be unpacked to width, height, length respectively.
|
|
460
|
+
heading_angle: float
|
|
461
|
+
The angle in radians to rotate around the y-axis.
|
|
462
|
+
center: Union[list, np.ndarray, tuple]
|
|
463
|
+
In the order of the x-center, y-center, and z-center coordinates.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
np.ndarray
|
|
468
|
+
This is an (3, 8) array which represents the 3D bounding box corners
|
|
469
|
+
where rows are the [x,y,z] coordinates and columns
|
|
470
|
+
are the 8 corner points.
|
|
471
|
+
"""
|
|
472
|
+
R = y_rotation(heading_angle)
|
|
473
|
+
w, h, l = box_size
|
|
474
|
+
x_corners = [-l / 2, l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2, -l / 2]
|
|
475
|
+
y_corners = [-h / 2, -h / 2, -h / 2, -h / 2, h / 2, h / 2, h / 2, h / 2]
|
|
476
|
+
z_corners = [-w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2, w / 2]
|
|
477
|
+
|
|
478
|
+
corners_3d = np.dot(R, np.vstack([x_corners, y_corners, z_corners]))
|
|
479
|
+
corners_3d[0, :] = corners_3d[0, :] + center[0]
|
|
480
|
+
corners_3d[1, :] = corners_3d[1, :] + center[1]
|
|
481
|
+
corners_3d[2, :] = corners_3d[2, :] + center[2]
|
|
482
|
+
return corners_3d
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def box3d_vol(corners: np.ndarray) -> float:
|
|
486
|
+
"""
|
|
487
|
+
Computes the volume of the 3D bounding box based on the corners provided.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
corners: np.ndarray
|
|
492
|
+
This is an (8, 3) array of the bounding box corners
|
|
493
|
+
with no assumption on axis direction.
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
float
|
|
498
|
+
The volume of the bounding box.
|
|
499
|
+
"""
|
|
500
|
+
a = np.sqrt(np.sum((corners[0, :] - corners[1, :])**2))
|
|
501
|
+
b = np.sqrt(np.sum((corners[1, :] - corners[2, :])**2))
|
|
502
|
+
c = np.sqrt(np.sum((corners[0, :] - corners[4, :])**2))
|
|
503
|
+
return a * b * c
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def get_corners(sizes: list, box_angles: list, centers: list) -> list:
|
|
507
|
+
"""
|
|
508
|
+
Transforms a list of sizes, angles, and centers into 3D box corners.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
sizes: list
|
|
513
|
+
Contains lists that can be unpacked
|
|
514
|
+
to width, height, length respectively.
|
|
515
|
+
box_angles: list
|
|
516
|
+
Contains the angles in radians to rotate around the y-axis.
|
|
517
|
+
centers: list
|
|
518
|
+
Contains lists in the order of the
|
|
519
|
+
x-center, y-center, and z-center coordinates.
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
list
|
|
524
|
+
A list of (8,3) corners.
|
|
525
|
+
"""
|
|
526
|
+
# corner formats should be [[[x,y,z], [x,y,z], ... x6]]
|
|
527
|
+
corners = list()
|
|
528
|
+
if 0 not in [len(sizes), len(box_angles), len(centers)]:
|
|
529
|
+
for size, angle, center in zip(sizes, box_angles, centers):
|
|
530
|
+
corners.append(transform(size, angle, center))
|
|
531
|
+
return corners
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def get_plane(a, b, c):
|
|
535
|
+
"""
|
|
536
|
+
Get plane equation from 3 points.
|
|
537
|
+
Returns the coefficients of `ax + by + cz + d = 0`
|
|
538
|
+
"""
|
|
539
|
+
ab = b - a
|
|
540
|
+
ac = c - a
|
|
541
|
+
|
|
542
|
+
x = np.cross(ab, ac)
|
|
543
|
+
d = -np.dot(x, a)
|
|
544
|
+
pl = np.hstack((x, d))
|
|
545
|
+
return pl
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def point_plane_dist(pt, plane, signed: bool = False):
|
|
549
|
+
"""
|
|
550
|
+
Get the signed distance from a point `pt` to a plane `plane`.
|
|
551
|
+
Reference: http://mathworld.wolfram.com/Point-PlaneDistance.html
|
|
552
|
+
|
|
553
|
+
Plane is of the format [A, B, C, D], where the plane equation is Ax+By+Cz+D=0
|
|
554
|
+
Point is of the form [x, y, z]
|
|
555
|
+
`signed` flag indicates whether to return signed distance.
|
|
556
|
+
"""
|
|
557
|
+
v = plane[0:3]
|
|
558
|
+
dist = (np.dot(v, pt) + plane[3]) / np.linalg.norm(v)
|
|
559
|
+
|
|
560
|
+
if signed:
|
|
561
|
+
return dist
|
|
562
|
+
else:
|
|
563
|
+
return np.abs(dist)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def edges_of(vertices):
|
|
567
|
+
"""
|
|
568
|
+
Return the vectors for the edges of the polygon defined by `vertices`.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
vertices: list of vertices of the polygon.
|
|
572
|
+
"""
|
|
573
|
+
edges = []
|
|
574
|
+
N = len(vertices)
|
|
575
|
+
|
|
576
|
+
for i in range(N):
|
|
577
|
+
edge = vertices[(i + 1) % N] - vertices[i]
|
|
578
|
+
edges.append(edge)
|
|
579
|
+
|
|
580
|
+
return edges
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def orthogonal(v):
|
|
584
|
+
"""
|
|
585
|
+
Return a 90 degree clockwise rotation of the vector `v`.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
v: 2D array representing a vector.
|
|
589
|
+
"""
|
|
590
|
+
return np.array([-v[1], v[0]])
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def is_separating_axis(o, p1, p2):
|
|
594
|
+
"""
|
|
595
|
+
Return True and the push vector if `o` is a separating axis
|
|
596
|
+
of `p1` and `p2`. Otherwise, return False and None.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
o: 2D array representing a vector.
|
|
600
|
+
p1: 2D array of points representing a polygon.
|
|
601
|
+
p2: 2D array of points representing a polygon.
|
|
602
|
+
"""
|
|
603
|
+
min1, max1 = float('+inf'), float('-inf')
|
|
604
|
+
min2, max2 = float('+inf'), float('-inf')
|
|
605
|
+
|
|
606
|
+
for v in p1:
|
|
607
|
+
projection = np.dot(v, o)
|
|
608
|
+
|
|
609
|
+
min1 = min(min1, projection)
|
|
610
|
+
max1 = max(max1, projection)
|
|
611
|
+
|
|
612
|
+
for v in p2:
|
|
613
|
+
projection = np.dot(v, o)
|
|
614
|
+
|
|
615
|
+
min2 = min(min2, projection)
|
|
616
|
+
max2 = max(max2, projection)
|
|
617
|
+
|
|
618
|
+
if max1 >= min2 and max2 >= min1:
|
|
619
|
+
d = min(max2 - min1, max1 - min2)
|
|
620
|
+
# push a bit more than needed so the shapes do not overlap in future
|
|
621
|
+
# tests due to float precision
|
|
622
|
+
d_over_o_squared = d / np.dot(o, o) + 1e-10
|
|
623
|
+
pv = d_over_o_squared * o
|
|
624
|
+
return False, pv
|
|
625
|
+
else:
|
|
626
|
+
return True, None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def polygon_collision(p1, p2):
|
|
630
|
+
"""
|
|
631
|
+
Return True if the shapes collide. Otherwise, return False.
|
|
632
|
+
|
|
633
|
+
p1 and p2 are np.arrays, the vertices of the polygons in the
|
|
634
|
+
counterclockwise direction.
|
|
635
|
+
|
|
636
|
+
Source: https://hackmd.io/s/ryFmIZrsl
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
p1: 2D array of points representing a polygon.
|
|
640
|
+
p2: 2D array of points representing a polygon.
|
|
641
|
+
"""
|
|
642
|
+
edges = edges_of(p1)
|
|
643
|
+
edges += edges_of(p2)
|
|
644
|
+
orthogonals = [orthogonal(e) for e in edges]
|
|
645
|
+
|
|
646
|
+
push_vectors = []
|
|
647
|
+
for o in orthogonals:
|
|
648
|
+
separates, pv = is_separating_axis(o, p1, p2)
|
|
649
|
+
|
|
650
|
+
if separates:
|
|
651
|
+
# they do not collide and there is no push vector
|
|
652
|
+
return False
|
|
653
|
+
else:
|
|
654
|
+
push_vectors.append(pv)
|
|
655
|
+
|
|
656
|
+
return True
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def polygon_area(polygon):
|
|
660
|
+
"""
|
|
661
|
+
Get the area of a polygon which is represented by a 2D array of points.
|
|
662
|
+
Area is computed using the Shoelace Algorithm.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
polygon: 2D array of points.
|
|
666
|
+
"""
|
|
667
|
+
x = polygon[:, 0]
|
|
668
|
+
y = polygon[:, 1]
|
|
669
|
+
area = (np.dot(x, np.roll(y, -1)) - np.dot(np.roll(x, -1), y))
|
|
670
|
+
return np.abs(area) / 2
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def polygon_intersection(poly1, poly2):
|
|
674
|
+
"""
|
|
675
|
+
Use the Sutherland-Hodgman algorithm to
|
|
676
|
+
compute the intersection of 2 convex polygons.
|
|
677
|
+
"""
|
|
678
|
+
def line_intersection(e1, e2, s, e):
|
|
679
|
+
dc = e1 - e2
|
|
680
|
+
dp = s - e
|
|
681
|
+
n1 = np.cross(e1, e2)
|
|
682
|
+
n2 = np.cross(s, e)
|
|
683
|
+
n3 = 1.0 / (np.cross(dc, dp))
|
|
684
|
+
return np.array([(n1 * dp[0] - n2 * dc[0]) * n3,
|
|
685
|
+
(n1 * dp[1] - n2 * dc[1]) * n3])
|
|
686
|
+
|
|
687
|
+
def is_inside_edge(p, e1, e2):
|
|
688
|
+
"""Return True if e is inside edge (e1, e2)"""
|
|
689
|
+
return np.cross(e2 - e1, p - e1) >= 0
|
|
690
|
+
|
|
691
|
+
output_list = poly1
|
|
692
|
+
# e1 and e2 are the edge vertices for each edge in the clipping polygon
|
|
693
|
+
e1 = poly2[-1]
|
|
694
|
+
|
|
695
|
+
for e2 in poly2:
|
|
696
|
+
# If there is no point of intersection
|
|
697
|
+
if len(output_list) == 0:
|
|
698
|
+
break
|
|
699
|
+
|
|
700
|
+
input_list = output_list
|
|
701
|
+
output_list = []
|
|
702
|
+
s = input_list[-1]
|
|
703
|
+
|
|
704
|
+
for e in input_list:
|
|
705
|
+
if is_inside_edge(e, e1, e2):
|
|
706
|
+
# if s in not inside edge (e1, e2)
|
|
707
|
+
if not is_inside_edge(s, e1, e2):
|
|
708
|
+
# line intersects edge hence we compute intersection point
|
|
709
|
+
output_list.append(line_intersection(e1, e2, s, e))
|
|
710
|
+
output_list.append(e)
|
|
711
|
+
# is s inside edge (e1, e2)
|
|
712
|
+
elif is_inside_edge(s, e1, e2):
|
|
713
|
+
output_list.append(line_intersection(e1, e2, s, e))
|
|
714
|
+
|
|
715
|
+
s = e
|
|
716
|
+
e1 = e2
|
|
717
|
+
return np.array(output_list)
|