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,292 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Tuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from edgefirst.validator.evaluators import ValidationParameters
|
|
9
|
+
from edgefirst.validator.datasets import DetectionInstance
|
|
10
|
+
from edgefirst.validator.metrics import DetectionStats
|
|
11
|
+
from edgefirst.validator.evaluators import Matcher
|
|
12
|
+
from edgefirst.validator.metrics import Plots
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DetectionClassifier:
|
|
16
|
+
"""
|
|
17
|
+
Classifies model predictions into true positives, false positives, or
|
|
18
|
+
false negatives depending on the results of the matching algorithm
|
|
19
|
+
and the labels. Furthermore, the confusion matrix data is also being
|
|
20
|
+
collected in this stage.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
parameters: ValidationParameters
|
|
25
|
+
This contains the validation parameters set from the command line.
|
|
26
|
+
detection_stats: DetectionStats
|
|
27
|
+
This stores the number of true positives, false positives, and
|
|
28
|
+
false negatives per label found throughout validation.
|
|
29
|
+
matcher: Matcher
|
|
30
|
+
The matcher object that matches the predictions to the
|
|
31
|
+
ground truth and contains matching results.
|
|
32
|
+
plots: PlotSummary
|
|
33
|
+
This is a container for the data to draw the plots.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
parameters: ValidationParameters,
|
|
39
|
+
detection_stats: DetectionStats,
|
|
40
|
+
matcher: Matcher,
|
|
41
|
+
plots: Plots
|
|
42
|
+
):
|
|
43
|
+
self.parameters = parameters
|
|
44
|
+
self.detection_stats = detection_stats
|
|
45
|
+
self.matcher = matcher
|
|
46
|
+
self.plots = plots
|
|
47
|
+
|
|
48
|
+
def classify(
|
|
49
|
+
self,
|
|
50
|
+
gt_instance: DetectionInstance,
|
|
51
|
+
dt_instance: DetectionInstance
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Classifies the matched, missed, and extra detections
|
|
55
|
+
into true positives, localization and classification false positives,
|
|
56
|
+
and false negatives.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
gt_instance: DetectionInstance
|
|
61
|
+
The ground truth instance that contains the
|
|
62
|
+
ground truth boxes and labels.
|
|
63
|
+
dt_instance: DetectionInstance
|
|
64
|
+
The detection instance that contains the model
|
|
65
|
+
prediction boxes, labels, and scores for the image.
|
|
66
|
+
"""
|
|
67
|
+
self.classify_matches(gt_instance=gt_instance, dt_instance=dt_instance)
|
|
68
|
+
self.classify_unmatched_dt(dt_instance)
|
|
69
|
+
self.classify_unmatched_gt(gt_instance)
|
|
70
|
+
self.setup_yolo_map(gt_instance=gt_instance, dt_instance=dt_instance)
|
|
71
|
+
|
|
72
|
+
def classify_matches(
|
|
73
|
+
self,
|
|
74
|
+
gt_instance: DetectionInstance,
|
|
75
|
+
dt_instance: DetectionInstance
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Classifies the matching ground truth to detection boxes
|
|
79
|
+
into true positives or classification false positives.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
gt_instance: DetectionInstance
|
|
84
|
+
The ground truth instance that contains the
|
|
85
|
+
ground truth boxes and labels.
|
|
86
|
+
dt_instance: DetectionInstance
|
|
87
|
+
The detection instance that contains the model
|
|
88
|
+
prediction boxes, labels, and scores for the image.
|
|
89
|
+
"""
|
|
90
|
+
for match in self.matcher.index_matches:
|
|
91
|
+
dt_label = dt_instance.labels[match[0]]
|
|
92
|
+
gt_label = gt_instance.labels[match[1]]
|
|
93
|
+
score = dt_instance.scores[match[0]]
|
|
94
|
+
iou = self.matcher.iou_list[match[0]]
|
|
95
|
+
|
|
96
|
+
if dt_label != gt_label:
|
|
97
|
+
label_data = self.detection_stats.get_label_data(dt_label)
|
|
98
|
+
if label_data:
|
|
99
|
+
label_data.add_cfp(iou, score)
|
|
100
|
+
|
|
101
|
+
label_data = self.detection_stats.get_label_data(gt_label)
|
|
102
|
+
if label_data:
|
|
103
|
+
label_data.add_ground_truths()
|
|
104
|
+
if dt_label == gt_label:
|
|
105
|
+
label_data.add_tp(iou, score)
|
|
106
|
+
|
|
107
|
+
# Format confusion matrix
|
|
108
|
+
if isinstance(gt_label, str):
|
|
109
|
+
gt_label = self.plots.confusion_labels.index(gt_label)
|
|
110
|
+
else:
|
|
111
|
+
gt_label = self.plots.confusion_labels.index(
|
|
112
|
+
self.plots.labels[int(gt_label)])
|
|
113
|
+
|
|
114
|
+
if isinstance(dt_label, str):
|
|
115
|
+
dt_label = self.plots.confusion_labels.index(dt_label)
|
|
116
|
+
else:
|
|
117
|
+
dt_label = self.plots.confusion_labels.index(
|
|
118
|
+
self.plots.labels[int(dt_label)])
|
|
119
|
+
|
|
120
|
+
if iou >= self.parameters.iou_threshold:
|
|
121
|
+
self.plots.confusion_matrix[dt_label, gt_label] += 1
|
|
122
|
+
else:
|
|
123
|
+
self.plots.confusion_matrix[dt_label, 0] += 1 # False positive
|
|
124
|
+
self.plots.confusion_matrix[0, gt_label] += 1 # False negative
|
|
125
|
+
|
|
126
|
+
def classify_unmatched_dt(self, dt_instance: DetectionInstance):
|
|
127
|
+
"""
|
|
128
|
+
Classifies the extra predictions into localization false positives.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
dt_instance: DetectionInstance
|
|
133
|
+
The detection instance that contains the model
|
|
134
|
+
prediction boxes, labels, and scores for the image.
|
|
135
|
+
"""
|
|
136
|
+
for extra in self.matcher.index_unmatched_dt:
|
|
137
|
+
dt_label = dt_instance.labels[extra]
|
|
138
|
+
score = dt_instance.scores[extra]
|
|
139
|
+
|
|
140
|
+
label_data = self.detection_stats.get_label_data(dt_label)
|
|
141
|
+
if label_data:
|
|
142
|
+
label_data.add_lfp(score)
|
|
143
|
+
|
|
144
|
+
# Format confusion matrix
|
|
145
|
+
if isinstance(dt_label, str):
|
|
146
|
+
dt_label = self.plots.confusion_labels.index(dt_label)
|
|
147
|
+
else:
|
|
148
|
+
dt_label = self.plots.confusion_labels.index(
|
|
149
|
+
self.plots.labels[int(dt_label)])
|
|
150
|
+
self.plots.confusion_matrix[dt_label, 0] += 1 # False positive
|
|
151
|
+
|
|
152
|
+
def classify_unmatched_gt(self, gt_instance: DetectionInstance):
|
|
153
|
+
"""
|
|
154
|
+
Classifies the missed predictions into false negatives.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
gt_instance: DetectionInstance
|
|
159
|
+
The ground truth instance that contains the
|
|
160
|
+
ground truth boxes and labels.
|
|
161
|
+
"""
|
|
162
|
+
for miss in self.matcher.index_unmatched_gt:
|
|
163
|
+
gt_label = gt_instance.labels[miss]
|
|
164
|
+
|
|
165
|
+
label_data = self.detection_stats.get_label_data(gt_label)
|
|
166
|
+
if label_data:
|
|
167
|
+
label_data.add_ground_truths()
|
|
168
|
+
|
|
169
|
+
# Format confusion matrix
|
|
170
|
+
if isinstance(gt_label, str):
|
|
171
|
+
gt_label = self.plots.confusion_labels.index(gt_label)
|
|
172
|
+
else:
|
|
173
|
+
gt_label = self.plots.confusion_labels.index(
|
|
174
|
+
self.plots.labels[int(gt_label)])
|
|
175
|
+
self.plots.confusion_matrix[0, gt_label] += 1 # False negative
|
|
176
|
+
|
|
177
|
+
def setup_yolo_map(
|
|
178
|
+
self,
|
|
179
|
+
gt_instance: DetectionInstance,
|
|
180
|
+
dt_instance: DetectionInstance
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Formulates the variables needed to utilize the functionality of
|
|
184
|
+
calculating the average percision per class in YOLOv5.
|
|
185
|
+
https://github.com/ultralytics/yolov5/blob/master/utils/metrics.py#L29.
|
|
186
|
+
|
|
187
|
+
The following parameters are followed:
|
|
188
|
+
|
|
189
|
+
tp: (nx10) np.ndarray
|
|
190
|
+
This contains the True and False for each detection (rows) for
|
|
191
|
+
each IoU at 0.50-0.95 (columns).
|
|
192
|
+
conf: (nx1) np.ndarray
|
|
193
|
+
The confidence scores of each detections.
|
|
194
|
+
pred_cls: (nx1) np.ndarray
|
|
195
|
+
The prediction classes.
|
|
196
|
+
target_cls: (nx1) np.ndarray
|
|
197
|
+
The ground truth classes.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
gt_instance: DetectionInstance
|
|
202
|
+
The ground truth instance that contains the
|
|
203
|
+
ground truth boxes and labels.
|
|
204
|
+
dt_instance: DetectionInstance
|
|
205
|
+
The detection instance that contains the model
|
|
206
|
+
prediction boxes, labels, and scores for the image.
|
|
207
|
+
"""
|
|
208
|
+
for match in self.matcher.index_matches:
|
|
209
|
+
dt_label = dt_instance.labels[match[0]]
|
|
210
|
+
gt_label = gt_instance.labels[match[1]]
|
|
211
|
+
score = dt_instance.scores[match[0]]
|
|
212
|
+
iou = self.matcher.iou_list[match[0]]
|
|
213
|
+
|
|
214
|
+
if dt_label != gt_label:
|
|
215
|
+
tp = [False for _ in self.detection_stats.ious]
|
|
216
|
+
else:
|
|
217
|
+
tp = [iou >= x for x in self.detection_stats.ious]
|
|
218
|
+
|
|
219
|
+
self.detection_stats.tp.append(tp)
|
|
220
|
+
self.detection_stats.conf.append(score)
|
|
221
|
+
self.detection_stats.pred_cls.append(dt_label)
|
|
222
|
+
self.detection_stats.target_cls.append(gt_label)
|
|
223
|
+
|
|
224
|
+
for extra in self.matcher.index_unmatched_dt:
|
|
225
|
+
dt_label = dt_instance.labels[extra]
|
|
226
|
+
score = dt_instance.scores[extra]
|
|
227
|
+
|
|
228
|
+
tp = [False for _ in self.detection_stats.ious]
|
|
229
|
+
self.detection_stats.tp.append(tp)
|
|
230
|
+
self.detection_stats.conf.append(score)
|
|
231
|
+
self.detection_stats.pred_cls.append(dt_label)
|
|
232
|
+
|
|
233
|
+
for miss in self.matcher.index_unmatched_gt:
|
|
234
|
+
gt_label = gt_instance.labels[miss]
|
|
235
|
+
self.detection_stats.target_cls.append(gt_label)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def classify_mask(
|
|
239
|
+
gt_class_mask: np.ndarray,
|
|
240
|
+
dt_class_mask: np.ndarray,
|
|
241
|
+
exclude_background: np.ndarray = True
|
|
242
|
+
) -> Tuple[int, int, int]:
|
|
243
|
+
"""
|
|
244
|
+
Classifies if the pixels are either true predictions or false predictions.
|
|
245
|
+
Note the masks provided can also be multiclass, however this function
|
|
246
|
+
is used primarily to find the true predictions and false predictions
|
|
247
|
+
per class.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
gt_class_mask: (height, width) np.ndarray
|
|
252
|
+
2D binary array representing pixels forming the image ground truth.
|
|
253
|
+
1 represents the class being classified and 0 are the rest of
|
|
254
|
+
the classes.
|
|
255
|
+
dt_class_mask: (height, width) np.ndarray
|
|
256
|
+
2D binary array representing pixels forming the image prediction.
|
|
257
|
+
1 represents the class being classified and 0 are the rest of
|
|
258
|
+
the classes.
|
|
259
|
+
exclude_background: bool
|
|
260
|
+
Specify to avoid background to background
|
|
261
|
+
predictions and ground truths as true predictions.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
true_predictions: int
|
|
266
|
+
The number of true predictions pixels in the image.
|
|
267
|
+
false_predictions: int
|
|
268
|
+
The number of false predictions pixels in the image.
|
|
269
|
+
union: int
|
|
270
|
+
The union between ground truths and model predictions occurs
|
|
271
|
+
when both arrays are non-zero. The union is the sum of
|
|
272
|
+
true predictions and false predictions.
|
|
273
|
+
"""
|
|
274
|
+
gt_mask_flat = gt_class_mask.flatten()
|
|
275
|
+
dt_mask_flat = dt_class_mask.flatten()
|
|
276
|
+
|
|
277
|
+
if exclude_background:
|
|
278
|
+
# Do not consider 0 against 0 as true predictions. 0 means another class
|
|
279
|
+
# not just background. True predictions are 1 against 1 which means this
|
|
280
|
+
# current class.
|
|
281
|
+
true_predictions = np.sum(
|
|
282
|
+
(gt_mask_flat == dt_mask_flat) & (gt_mask_flat > 0) & (dt_mask_flat > 0))
|
|
283
|
+
|
|
284
|
+
# The union between ground truths and predictions where both are
|
|
285
|
+
# non-zero.
|
|
286
|
+
union = np.sum((gt_mask_flat != 0) | (dt_mask_flat != 0))
|
|
287
|
+
else:
|
|
288
|
+
true_predictions = np.sum(gt_mask_flat == dt_mask_flat)
|
|
289
|
+
union = len(gt_mask_flat)
|
|
290
|
+
|
|
291
|
+
false_predictions = np.sum(gt_mask_flat != dt_mask_flat)
|
|
292
|
+
return true_predictions, false_predictions, union
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from edgefirst.validator.metrics.utils.math import iou_2d, localize_distance
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from edgefirst.validator.evaluators import ValidationParameters
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Matcher:
|
|
13
|
+
"""
|
|
14
|
+
The Matching Algorithm used in EdgeFirst Validation. This algorithm
|
|
15
|
+
will run matching recursively to find the best matches based on IoU
|
|
16
|
+
with a preference to a matching ground truth and detection labels.
|
|
17
|
+
The Matching and Classification rules is documented in::
|
|
18
|
+
https://au-zone.atlassian.net/wiki/spaces/DV/pages/2325938299/DeepView-Validator+Matching+and+Classification+Rules
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
parameters: ValidationParameters
|
|
23
|
+
This contains the validation parameters set from the command line.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, parameters: ValidationParameters):
|
|
27
|
+
self.parameters = parameters
|
|
28
|
+
self.gt_boxes = list()
|
|
29
|
+
self.gt_labels = list()
|
|
30
|
+
self.dt_boxes = list()
|
|
31
|
+
self.dt_labels = list()
|
|
32
|
+
|
|
33
|
+
# This contains the IoUs of each detection to ground truth match.
|
|
34
|
+
self.iou_list = list()
|
|
35
|
+
# An IoU grid where rows are the ground truths
|
|
36
|
+
# and the predictions are the columns.
|
|
37
|
+
self.iou_grid = list()
|
|
38
|
+
# The matches containing ground truth and detection indices:
|
|
39
|
+
# [[gti, dti], [gti, dti], ..].
|
|
40
|
+
self.index_matches = list()
|
|
41
|
+
# The prediction indices that were not matched.
|
|
42
|
+
self.index_unmatched_dt = list()
|
|
43
|
+
# The ground truth indices that were not matched.
|
|
44
|
+
self.index_unmatched_gt = list()
|
|
45
|
+
|
|
46
|
+
def set_boxes(
|
|
47
|
+
self,
|
|
48
|
+
gt_boxes: list,
|
|
49
|
+
gt_labels: list,
|
|
50
|
+
dt_boxes: list,
|
|
51
|
+
dt_labels: list
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Sets the bounding boxes and labels for the ground truth and model
|
|
55
|
+
predictions to match the bounding boxes described in the index_matches.
|
|
56
|
+
Calling this method will also reset the previous matching results.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
gt_boxes: list
|
|
61
|
+
The ground truth bounding boxes in the format [xmin, ymin, xmax, ymax].
|
|
62
|
+
gt_labels: list
|
|
63
|
+
The ground truth labels for each bounding box. This can either
|
|
64
|
+
contain strings or integers.
|
|
65
|
+
dt_boxes: list
|
|
66
|
+
The prediction bounding boxes in the format [xmin, ymin, xmax, ymax].
|
|
67
|
+
dt_labels: list
|
|
68
|
+
The prediction labels for each bounding box. This can either
|
|
69
|
+
contain strings or integers.
|
|
70
|
+
"""
|
|
71
|
+
# Setting boxes requires reset for a new matching process.
|
|
72
|
+
self.reset()
|
|
73
|
+
self.gt_boxes = gt_boxes
|
|
74
|
+
self.gt_labels = gt_labels
|
|
75
|
+
self.dt_boxes = dt_boxes
|
|
76
|
+
self.dt_labels = dt_labels
|
|
77
|
+
|
|
78
|
+
self.iou_list = np.zeros(len(self.dt_boxes))
|
|
79
|
+
self.iou_grid = np.zeros((len(self.gt_boxes), len(self.dt_boxes)))
|
|
80
|
+
|
|
81
|
+
# The prediction indices that were not matched.
|
|
82
|
+
self.index_unmatched_dt = list(range(0, len(self.dt_boxes)))
|
|
83
|
+
# The ground truth indices that were not matched.
|
|
84
|
+
self.index_unmatched_gt = list(range(0, len(self.gt_boxes)))
|
|
85
|
+
|
|
86
|
+
def match(
|
|
87
|
+
self,
|
|
88
|
+
gt_boxes: list,
|
|
89
|
+
gt_labels: list,
|
|
90
|
+
dt_boxes: list,
|
|
91
|
+
dt_labels: list
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
The matching algorithm which matches the predictions to ground truth
|
|
95
|
+
based on matching labels first and then by highest IoU or lowest
|
|
96
|
+
centerpoint distance between boxes.
|
|
97
|
+
|
|
98
|
+
This algorithm also incorporates recursive calls to
|
|
99
|
+
perform rematching of ground truth that were unmatched due to
|
|
100
|
+
duplicative matches, but the rematching is based on the next best IoU.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
gt_boxes: list
|
|
105
|
+
The ground truth bounding boxes in the format [xmin, ymin, xmax, ymax].
|
|
106
|
+
gt_labels: list
|
|
107
|
+
The ground truth labels for each bounding box. This can either
|
|
108
|
+
contain strings or integers.
|
|
109
|
+
dt_boxes: list
|
|
110
|
+
The prediction bounding boxes in the format [xmin, ymin, xmax, ymax].
|
|
111
|
+
dt_labels: list
|
|
112
|
+
The prediction labels for each bounding box. This can either
|
|
113
|
+
contain strings or integers.
|
|
114
|
+
"""
|
|
115
|
+
self.set_boxes(
|
|
116
|
+
gt_boxes=gt_boxes,
|
|
117
|
+
gt_labels=gt_labels,
|
|
118
|
+
dt_boxes=dt_boxes,
|
|
119
|
+
dt_labels=dt_labels
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if 0 in [len(self.gt_boxes), len(self.dt_boxes)]:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
for gti, gt in enumerate(self.gt_boxes):
|
|
126
|
+
# A list of prediction indices with
|
|
127
|
+
# matching labels as the ground truth.
|
|
128
|
+
dti_reflective, iou_reflective = list(), list()
|
|
129
|
+
gt_label = self.gt_labels[gti]
|
|
130
|
+
|
|
131
|
+
for dti, dt in enumerate(self.dt_boxes):
|
|
132
|
+
self.iou_grid[gti][dti] = self.get_metric(gt, dt)
|
|
133
|
+
|
|
134
|
+
dt_label = self.dt_labels[dti]
|
|
135
|
+
if dt_label == gt_label:
|
|
136
|
+
dti_reflective.append(dti)
|
|
137
|
+
iou_reflective.append(self.iou_grid[gti][dti])
|
|
138
|
+
|
|
139
|
+
# A potential match is the detection that produced the highest IoU.
|
|
140
|
+
dti = np.argmax(self.iou_grid[gti])
|
|
141
|
+
iou = max(self.iou_grid[gti])
|
|
142
|
+
# If there is no intersection, it cannot be a match.
|
|
143
|
+
if iou < 0:
|
|
144
|
+
continue
|
|
145
|
+
# Only match if the IoU between matching ground truth and detection
|
|
146
|
+
# labels > 0.
|
|
147
|
+
if len(dti_reflective) and max(
|
|
148
|
+
iou_reflective) >= self.parameters.iou_threshold:
|
|
149
|
+
# The IoU of the detections with the same labels
|
|
150
|
+
# as the ground truth. A potential match is the
|
|
151
|
+
# detection with the same label as the ground truth.
|
|
152
|
+
dti = dti_reflective[np.argmax(iou_reflective)]
|
|
153
|
+
iou = max(iou_reflective)
|
|
154
|
+
self.compare_matches(dti, gti, iou)
|
|
155
|
+
|
|
156
|
+
# Find the unmatched predictions
|
|
157
|
+
for match in self.index_matches:
|
|
158
|
+
self.index_unmatched_dt.remove(match[0])
|
|
159
|
+
self.index_unmatched_gt.remove(match[1])
|
|
160
|
+
|
|
161
|
+
def compare_matches(self, dti: int, gti: int, iou: float):
|
|
162
|
+
"""
|
|
163
|
+
Checks if duplicate matches exists. A duplicate match is when the
|
|
164
|
+
same detection is being matched to more than one ground truth.
|
|
165
|
+
The IoUs are compared and the better IoU is the true match and the
|
|
166
|
+
ground truth of the other match is then rematch to the next best IoU,
|
|
167
|
+
but it performs a recursive call to check if the next best IoU
|
|
168
|
+
also generates a duplicate match.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
dti: int
|
|
173
|
+
The detection index being matched to the current ground truth.
|
|
174
|
+
gti: int
|
|
175
|
+
The current ground truth matched to the detection.
|
|
176
|
+
iou: float
|
|
177
|
+
The current best IoU that was computed for the current ground
|
|
178
|
+
truth against all detections.
|
|
179
|
+
"""
|
|
180
|
+
twice_matched = [(d, g) for d, g in self.index_matches if d == dti]
|
|
181
|
+
assert len(twice_matched) < 2, "More than two duplicate matches occurred."
|
|
182
|
+
|
|
183
|
+
if len(twice_matched) == 1:
|
|
184
|
+
# Compare the IoUs between duplicate matches.
|
|
185
|
+
dti, pre_gti = twice_matched[0]
|
|
186
|
+
if iou > self.iou_list[dti]:
|
|
187
|
+
self.index_matches.remove((dti, pre_gti))
|
|
188
|
+
self.iou_list[dti] = iou
|
|
189
|
+
self.index_matches.append((dti, gti))
|
|
190
|
+
|
|
191
|
+
# Rematch pre_gti
|
|
192
|
+
self.iou_grid[pre_gti][dti] = 0.
|
|
193
|
+
dti = np.argmax(self.iou_grid[pre_gti])
|
|
194
|
+
iou = max(self.iou_grid[pre_gti])
|
|
195
|
+
if iou > 0:
|
|
196
|
+
self.compare_matches(dti, pre_gti, iou)
|
|
197
|
+
else:
|
|
198
|
+
# Rematch gti
|
|
199
|
+
self.iou_grid[gti][dti] = 0.
|
|
200
|
+
dti = np.argmax(self.iou_grid[gti])
|
|
201
|
+
iou = max(self.iou_grid[gti])
|
|
202
|
+
if iou > 0:
|
|
203
|
+
self.compare_matches(dti, gti, iou)
|
|
204
|
+
else:
|
|
205
|
+
if iou > 0:
|
|
206
|
+
self.iou_list[dti] = iou
|
|
207
|
+
self.index_matches.append((dti, gti))
|
|
208
|
+
|
|
209
|
+
def get_metric(
|
|
210
|
+
self,
|
|
211
|
+
gt: Union[list, np.ndarray],
|
|
212
|
+
dt: Union[list, np.ndarray],
|
|
213
|
+
) -> float:
|
|
214
|
+
"""
|
|
215
|
+
Computes either the 3D or 2D IoU or centerpoint distances
|
|
216
|
+
and stores the values in the IoU grid.
|
|
217
|
+
|
|
218
|
+
When the iou_first flag is False, IoU is
|
|
219
|
+
considered 0 if the classes don't match.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
gt: Union[list, np.ndarray]
|
|
224
|
+
This either contains ground truth bounding boxes
|
|
225
|
+
if 2D validation or 3D box corners if 3D validation.
|
|
226
|
+
dt: Union[list, np.ndarray]
|
|
227
|
+
This either contains prediction bounding boxes
|
|
228
|
+
if 2D validation or 3D box corners if 3D validation.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
float
|
|
233
|
+
The IoU or centerpoint distance between two boxes.
|
|
234
|
+
|
|
235
|
+
Raises
|
|
236
|
+
------
|
|
237
|
+
TypeError
|
|
238
|
+
Raised if an invalid metric type is specified.
|
|
239
|
+
"""
|
|
240
|
+
if self.parameters.metric == "iou":
|
|
241
|
+
return iou_2d(dt.astype(float), gt.astype(float))
|
|
242
|
+
elif self.parameters.metric == "centerpoint":
|
|
243
|
+
return 1 - localize_distance(
|
|
244
|
+
dt.astype(float),
|
|
245
|
+
gt.astype(float),
|
|
246
|
+
leniency_factor=self.parameters.matching_leniency
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
raise TypeError(
|
|
250
|
+
"Unknown matching matching metric specified: {}".format(
|
|
251
|
+
self.parameters.metric
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
def reset(self):
|
|
255
|
+
"""
|
|
256
|
+
Resets the containers to allow for a new matching process.
|
|
257
|
+
"""
|
|
258
|
+
self.iou_list = list()
|
|
259
|
+
self.iou_grid = list()
|
|
260
|
+
self.index_matches = list()
|
|
261
|
+
self.index_unmatched_dt = list()
|
|
262
|
+
self.index_unmatched_gt = list()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimerContext:
|
|
8
|
+
"""
|
|
9
|
+
This class provides methods for timing measurements of the
|
|
10
|
+
validation process and acts as a container of the measured timings.
|
|
11
|
+
The stages of timings are as follows:
|
|
12
|
+
|
|
13
|
+
* input => Image read and preprocessing
|
|
14
|
+
* inference => Inference Timings
|
|
15
|
+
* output => Output decoding and postprocessing
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.stages = ["input", "inference", "output"]
|
|
20
|
+
self.__timings = {stage: [] for stage in self.stages}
|
|
21
|
+
self.__start_time = None
|
|
22
|
+
self.to_ms = 1e3
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def time(self, stage: str):
|
|
26
|
+
"""
|
|
27
|
+
Context manager to time a section
|
|
28
|
+
and store duration in list.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
stage: str
|
|
33
|
+
The key to store the timing measurement
|
|
34
|
+
for a specific stage such as "input", "inference",
|
|
35
|
+
or "output".
|
|
36
|
+
"""
|
|
37
|
+
self.__start_time = time.perf_counter()
|
|
38
|
+
yield
|
|
39
|
+
elapsed = time.perf_counter() - self.__start_time
|
|
40
|
+
self.__timings[stage].append(elapsed * self.to_ms)
|
|
41
|
+
self.__start_time = None
|
|
42
|
+
|
|
43
|
+
def add_time(self, stage: str, ds_ms: float):
|
|
44
|
+
"""
|
|
45
|
+
Add time to the last timining measurement
|
|
46
|
+
of the current stage in the timings.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
stage: str
|
|
51
|
+
The key to store the timing measurement
|
|
52
|
+
for a specific stage such as "input", "inference",
|
|
53
|
+
or "output".
|
|
54
|
+
ds_ms: float
|
|
55
|
+
The time duration in milli seconds to add.
|
|
56
|
+
"""
|
|
57
|
+
if len(self.__timings[stage]):
|
|
58
|
+
self.__timings[stage][-1] += ds_ms
|
|
59
|
+
|
|
60
|
+
def get_average_time(self, stage: str) -> float:
|
|
61
|
+
"""
|
|
62
|
+
Returns the average time for the specific stage.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
stage: str
|
|
67
|
+
The key to fetch the timing measurements
|
|
68
|
+
for a specific stage such as "input", "inference",
|
|
69
|
+
or "output".
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
float
|
|
74
|
+
The average timings for a given stage.
|
|
75
|
+
"""
|
|
76
|
+
times = self.__timings.get(stage, [])
|
|
77
|
+
return np.mean(times) if len(times) else 0.0
|
|
78
|
+
|
|
79
|
+
def get_max_time(self, stage: str) -> float:
|
|
80
|
+
"""
|
|
81
|
+
Returns the maximum time for the specific stage.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
stage: str
|
|
86
|
+
The key to fetch the timing measurements
|
|
87
|
+
for a specific stage such as "input", "inference",
|
|
88
|
+
or "output".
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
float
|
|
93
|
+
The maximum timing for a given stage.
|
|
94
|
+
"""
|
|
95
|
+
times = self.__timings.get(stage, [])
|
|
96
|
+
return np.max(times) if len(times) else 0.0
|
|
97
|
+
|
|
98
|
+
def get_min_time(self, stage: str) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Returns the minimum time for the specific stage.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
stage: str
|
|
105
|
+
The key to fetch the timing measurements
|
|
106
|
+
for a specific stage such as "input", "inference",
|
|
107
|
+
or "output".
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
float
|
|
112
|
+
The minimum timing for a given stage.
|
|
113
|
+
"""
|
|
114
|
+
times = self.__timings.get(stage, [])
|
|
115
|
+
return np.min(times) if len(times) else 0.0
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Grabs the timing summary such as the average, max, min
|
|
120
|
+
for each stages and stores it as a dictionary.
|
|
121
|
+
"""
|
|
122
|
+
timings = {}
|
|
123
|
+
for stage in ["input", "inference", "output"]:
|
|
124
|
+
timings[f"min_{stage}_time"] = self.get_min_time(stage)
|
|
125
|
+
timings[f"max_{stage}_time"] = self.get_max_time(stage)
|
|
126
|
+
timings[f"avg_{stage}_time"] = self.get_average_time(stage)
|
|
127
|
+
return timings
|
|
128
|
+
|
|
129
|
+
def reset(self):
|
|
130
|
+
"""Clears all stored timings."""
|
|
131
|
+
self.__timings = {stage: [] for stage in self.stages}
|
|
132
|
+
self.__start_time = None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from edgefirst.validator.metrics.data.label import (DetectionLabelData,
|
|
2
|
+
SegmentationLabelData)
|
|
3
|
+
from edgefirst.validator.metrics.data.stats import (YOLOStats,
|
|
4
|
+
DetectionStats,
|
|
5
|
+
SegmentationStats)
|
|
6
|
+
from edgefirst.validator.metrics.data.plots import Plots, MultitaskPlots
|
|
7
|
+
from edgefirst.validator.metrics.data.metrics import Metrics, MultitaskMetrics
|
|
8
|
+
from edgefirst.validator.metrics.detection import DetectionMetrics
|
|
9
|
+
from edgefirst.validator.metrics.segmentation import SegmentationMetrics
|