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,595 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Tuple, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from edgefirst.validator.metrics import Metrics, Plots
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from edgefirst.validator.evaluators import ValidationParameters
|
|
11
|
+
from edgefirst.validator.metrics import (DetectionStats,
|
|
12
|
+
DetectionLabelData)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_precision(tp: int, fp: int) -> float:
|
|
16
|
+
"""
|
|
17
|
+
Calculates the precision = tp / (tp + fp).
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
tp: int
|
|
22
|
+
The number of true positives.
|
|
23
|
+
fp: int
|
|
24
|
+
The number of false positives.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
float
|
|
29
|
+
Resulting value is the result of tp / (tp + fp).
|
|
30
|
+
"""
|
|
31
|
+
if tp + fp == 0:
|
|
32
|
+
return 0.0
|
|
33
|
+
return tp / (tp + fp)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compute_recall(tp: int, fn: int) -> float:
|
|
37
|
+
"""
|
|
38
|
+
Calculates recall = tp / (tp + fn).
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
tp: int
|
|
43
|
+
The number of true positives.
|
|
44
|
+
fn: int
|
|
45
|
+
The number of false negatives.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
float
|
|
50
|
+
Resulting value is the result of tp / (tp + fn).
|
|
51
|
+
"""
|
|
52
|
+
if tp + fn == 0:
|
|
53
|
+
return 0.0
|
|
54
|
+
return tp / (tp + fn)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compute_accuracy(tp: int, fp: int, fn: int) -> float:
|
|
58
|
+
"""
|
|
59
|
+
Calculates the accuracy = tp / (tp + fp + fn).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
tp: int
|
|
64
|
+
The number of true positives.
|
|
65
|
+
fp: int
|
|
66
|
+
The number of false positives.
|
|
67
|
+
fn: int
|
|
68
|
+
The number of false negatives.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
float
|
|
73
|
+
Resulting value is the result of tp / (tp + fp + fn).
|
|
74
|
+
"""
|
|
75
|
+
if tp + fp + fn == 0:
|
|
76
|
+
return 0.0
|
|
77
|
+
return tp / (tp + fp + fn)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def compute_ap(
|
|
81
|
+
recall: list,
|
|
82
|
+
precision: list,
|
|
83
|
+
v5_metric: bool = True,
|
|
84
|
+
method: str = "interp"
|
|
85
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
86
|
+
"""
|
|
87
|
+
Compute the average precision, given the recall and precision curves.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
recall: list
|
|
92
|
+
The recall curve.
|
|
93
|
+
precision: list
|
|
94
|
+
The precision curve.
|
|
95
|
+
v5_metric: bool
|
|
96
|
+
Assume maximum recall to be 1.0, as in YOLOv5, MMDetetion etc.
|
|
97
|
+
method: str
|
|
98
|
+
Type of area integration to perform, other forms
|
|
99
|
+
include 'continuous', by default use 'interp'.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
ap: np.ndarray
|
|
104
|
+
The average precision values based on the area under the curve.
|
|
105
|
+
mpre: np.ndarray
|
|
106
|
+
Mean precision from the precision curve.
|
|
107
|
+
mrec: np.ndarray
|
|
108
|
+
Mean recall from the recall curve.
|
|
109
|
+
"""
|
|
110
|
+
# Append sentinel values to beginning and end
|
|
111
|
+
if v5_metric: # New YOLOv5 metric, same as MMDetection and Detectron2 repositories
|
|
112
|
+
mrec = np.concatenate(([0.0], recall, [1.0]))
|
|
113
|
+
else: # Old YOLOv5 metric, i.e. default YOLOv7 metric
|
|
114
|
+
mrec = np.concatenate(([0.0], recall, [recall[-1] + 0.01]))
|
|
115
|
+
mpre = np.concatenate(([1.0], precision, [0.0]))
|
|
116
|
+
|
|
117
|
+
# Compute the precision envelope
|
|
118
|
+
mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
|
|
119
|
+
|
|
120
|
+
# Integrate area under curve
|
|
121
|
+
# methods: 'continuous', 'interp'
|
|
122
|
+
if method.lower() == "interp":
|
|
123
|
+
x = np.linspace(0, 1, 101) # 101-point interp (COCO)
|
|
124
|
+
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
|
|
125
|
+
else: # 'continuous'
|
|
126
|
+
# points where x axis (recall) changes
|
|
127
|
+
i = np.nonzero(mrec[1:] != mrec[:-1])[0]
|
|
128
|
+
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve
|
|
129
|
+
return ap, mpre, mrec
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def smooth(y: np.ndarray, f: float = 0.05) -> np.ndarray:
|
|
133
|
+
"""
|
|
134
|
+
Applies box filter smoothing to array `y`
|
|
135
|
+
with fraction `f`, yielding a smoothed array.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
y: np.ndarray
|
|
140
|
+
The array to apply a smoothening effect.
|
|
141
|
+
f: float
|
|
142
|
+
Smooth fraction.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
np.ndarray
|
|
147
|
+
The array with a smoothening effect.
|
|
148
|
+
"""
|
|
149
|
+
nf = round(len(y) * f * 2) // 2 + \
|
|
150
|
+
1 # number of filter elements (must be odd)
|
|
151
|
+
p = np.ones(nf // 2) # ones padding
|
|
152
|
+
yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded
|
|
153
|
+
return np.convolve(yp, np.ones(nf) / nf, mode="valid") # y-smoothed
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DetectionMetrics:
|
|
157
|
+
"""
|
|
158
|
+
Runs the metric computations for object detection. The resulting
|
|
159
|
+
metrics will be populated in the `Metrics` object that is created once
|
|
160
|
+
initialized.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
parameters: ValidationParameters
|
|
165
|
+
This contains validation parameters set from the command line.
|
|
166
|
+
detection_stats: DetectionStats
|
|
167
|
+
This is container of the pre-metrics computations per class.
|
|
168
|
+
model_name: str
|
|
169
|
+
The base name of the model being validated.
|
|
170
|
+
dataset_name: str
|
|
171
|
+
The base name of the validation dataset.
|
|
172
|
+
save_path: str
|
|
173
|
+
The path to save the metrics on disk.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
parameters: ValidationParameters,
|
|
179
|
+
detection_stats: DetectionStats,
|
|
180
|
+
model_name: str = "Model",
|
|
181
|
+
dataset_name: str = "Dataset",
|
|
182
|
+
save_path: str = None,
|
|
183
|
+
labels: list = []
|
|
184
|
+
):
|
|
185
|
+
self.parameters = parameters
|
|
186
|
+
self.plots = Plots(labels=labels)
|
|
187
|
+
self.detection_stats = detection_stats
|
|
188
|
+
self.metrics = Metrics(model=model_name, dataset=dataset_name)
|
|
189
|
+
self.metrics.save_path = save_path
|
|
190
|
+
self.labels = labels
|
|
191
|
+
|
|
192
|
+
def run_metrics(self):
|
|
193
|
+
"""
|
|
194
|
+
Method process for gathering all metrics used for the detection
|
|
195
|
+
validation. Either runs methods to reproduce metrics in Ultralytics (default)
|
|
196
|
+
or other forms of methods in EdgeFirst or YOLOv7.
|
|
197
|
+
"""
|
|
198
|
+
if self.parameters.method in ["ultralytics", "yolov7"]:
|
|
199
|
+
self.compute_yolo_metrics()
|
|
200
|
+
else:
|
|
201
|
+
self.compute_edgefirst_metrics()
|
|
202
|
+
|
|
203
|
+
def compute_yolo_metrics(self):
|
|
204
|
+
"""
|
|
205
|
+
Calculates metrics based on YOLO (Ultralytics and YOLOv7)
|
|
206
|
+
validation methods.
|
|
207
|
+
"""
|
|
208
|
+
p, r, ap, f1 = self.ap_per_class(
|
|
209
|
+
tp=np.concatenate(self.detection_stats.stats["tp"], axis=0),
|
|
210
|
+
conf=np.concatenate(self.detection_stats.stats["conf"], axis=0),
|
|
211
|
+
pred_cls=np.concatenate(
|
|
212
|
+
self.detection_stats.stats["pred_cls"], axis=0),
|
|
213
|
+
target_cls=np.concatenate(
|
|
214
|
+
self.detection_stats.stats["target_cls"], axis=0),
|
|
215
|
+
v5_metric=self.parameters.method == "ultralytics"
|
|
216
|
+
)
|
|
217
|
+
ap50, ap75, ap = ap[:, 0], ap[:, 5], ap.mean(1) # AP@0.5, AP@0.5:0.95
|
|
218
|
+
|
|
219
|
+
self.metrics.precision["map"] = {"0.50": ap50.mean(),
|
|
220
|
+
"0.75": ap75.mean(),
|
|
221
|
+
"0.50:0.95": ap.mean()}
|
|
222
|
+
self.metrics.precision["mean"] = p.mean()
|
|
223
|
+
self.metrics.recall["mean"] = r.mean()
|
|
224
|
+
self.metrics.f1["mean"] = f1.mean()
|
|
225
|
+
|
|
226
|
+
def compute_edgefirst_metrics(self):
|
|
227
|
+
"""
|
|
228
|
+
Calculates the EdgeFirst metrics. Populates the
|
|
229
|
+
results in the `Metrics` object.
|
|
230
|
+
"""
|
|
231
|
+
nc = len(self.detection_stats.stats)
|
|
232
|
+
_, _, ap, _ = self.ap_per_class(
|
|
233
|
+
np.array(self.detection_stats.tp),
|
|
234
|
+
np.array(self.detection_stats.conf),
|
|
235
|
+
np.array(self.detection_stats.pred_cls),
|
|
236
|
+
np.array(self.detection_stats.target_cls),
|
|
237
|
+
v5_metric=True
|
|
238
|
+
)
|
|
239
|
+
ap50, ap75, ap = ap[:, 0], ap[:, 5], ap.mean(1) # AP@0.5, AP@0.5:0.95
|
|
240
|
+
# These arrays contain the metrics for each class where the rows
|
|
241
|
+
# represent the class and the columns represent the IoU thresholds:
|
|
242
|
+
# 0.00 to 1.00 in 0.05 intervals.
|
|
243
|
+
maps, mars, maccs = np.zeros((nc, 20)), np.zeros(
|
|
244
|
+
(nc, 20)), np.zeros((nc, 20))
|
|
245
|
+
|
|
246
|
+
if nc > 0:
|
|
247
|
+
for ic, label_data in enumerate(self.detection_stats.stats):
|
|
248
|
+
self.metrics.tp += label_data.get_tp_count(
|
|
249
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
250
|
+
score_threshold=self.parameters.score_threshold)
|
|
251
|
+
self.metrics.cfp += label_data.get_cfp_count(
|
|
252
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
253
|
+
score_threshold=self.parameters.score_threshold)
|
|
254
|
+
self.metrics.lfp += label_data.get_lfp_count(
|
|
255
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
256
|
+
score_threshold=self.parameters.score_threshold)
|
|
257
|
+
|
|
258
|
+
# Build TP vs FP scores.
|
|
259
|
+
self.plots.tp_scores.append(
|
|
260
|
+
label_data.get_tp_scores(self.parameters.iou_threshold))
|
|
261
|
+
self.plots.fp_scores.append(
|
|
262
|
+
label_data.get_lfp_scores(self.parameters.iou_threshold))
|
|
263
|
+
|
|
264
|
+
# Build TP vs FP IoUs.
|
|
265
|
+
self.plots.tp_ious.append(
|
|
266
|
+
label_data.get_tp_iou(self.parameters.iou_threshold))
|
|
267
|
+
self.plots.fp_ious.append(
|
|
268
|
+
label_data.get_lfp_iou(self.parameters.iou_threshold))
|
|
269
|
+
|
|
270
|
+
class_metrics, class_truth_values = self.compute_class_metrics(
|
|
271
|
+
label_data,
|
|
272
|
+
self.parameters.iou_threshold
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
data = {
|
|
276
|
+
'precision': class_metrics[0],
|
|
277
|
+
'recall': class_metrics[1],
|
|
278
|
+
'accuracy': class_metrics[2],
|
|
279
|
+
'tp': class_truth_values[0],
|
|
280
|
+
'fn': class_truth_values[3],
|
|
281
|
+
'fp': class_truth_values[1] + class_truth_values[2],
|
|
282
|
+
'gt': label_data.ground_truths
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Convert labels to string
|
|
286
|
+
cls = str(label_data.label)
|
|
287
|
+
if self.labels is not None and len(self.labels) > 0:
|
|
288
|
+
if not isinstance(cls, str) or (
|
|
289
|
+
isinstance(cls, str) and cls.isdigit()):
|
|
290
|
+
cls = self.labels[int(cls)]
|
|
291
|
+
|
|
292
|
+
self.plots.append_class_histogram_data(cls, data)
|
|
293
|
+
|
|
294
|
+
for it, iou_threshold in enumerate(np.arange(0.00, 1, 0.05)):
|
|
295
|
+
class_metrics, _ = self.compute_class_metrics(
|
|
296
|
+
label_data,
|
|
297
|
+
iou_threshold
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# The index of the class ic and the index of the IoU
|
|
301
|
+
# threshold it will contain the metric: precision, recall,
|
|
302
|
+
# and accuracy of the class.
|
|
303
|
+
mars[ic][it] = class_metrics[1]
|
|
304
|
+
maccs[ic][it] = class_metrics[2]
|
|
305
|
+
# This mAP computation is the average of the precision
|
|
306
|
+
# of each class.
|
|
307
|
+
# maps[ic][it] = class_metrics[0] #NOSONAR
|
|
308
|
+
|
|
309
|
+
mean_metrics = self.compute_mean_average_metrics(
|
|
310
|
+
maps, mars, maccs, nc)
|
|
311
|
+
self.metrics.precision["map"] = {
|
|
312
|
+
"0.50": 0.0 if np.isnan(ap50.mean()) else ap50.mean(),
|
|
313
|
+
"0.75": 0.0 if np.isnan(ap75.mean()) else ap75.mean(),
|
|
314
|
+
"0.50:0.95": 0.0 if np.isnan(ap.mean()) else ap.mean()
|
|
315
|
+
}
|
|
316
|
+
self.metrics.recall["mar"] = {
|
|
317
|
+
"0.50": mean_metrics[1][0],
|
|
318
|
+
"0.75": mean_metrics[1][1],
|
|
319
|
+
"0.50:0.95": mean_metrics[1][2]
|
|
320
|
+
}
|
|
321
|
+
self.metrics.accuracy["macc"] = {
|
|
322
|
+
"0.50": mean_metrics[2][0],
|
|
323
|
+
"0.75": mean_metrics[2][1],
|
|
324
|
+
"0.50:0.95": mean_metrics[2][2]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
overall_metrics = self.compute_overall_metrics()
|
|
328
|
+
self.metrics.precision["overall"] = overall_metrics[0]
|
|
329
|
+
self.metrics.recall["overall"] = overall_metrics[1]
|
|
330
|
+
self.metrics.accuracy["overall"] = overall_metrics[2]
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
data = {
|
|
334
|
+
'precision': 0,
|
|
335
|
+
'recall': 0,
|
|
336
|
+
'accuracy': 0,
|
|
337
|
+
'tp': 0,
|
|
338
|
+
'fn': 0,
|
|
339
|
+
'fp': 0,
|
|
340
|
+
'gt': 0
|
|
341
|
+
}
|
|
342
|
+
self.plots.append_class_histogram_data("No label", data)
|
|
343
|
+
|
|
344
|
+
def ap_per_class(
|
|
345
|
+
self,
|
|
346
|
+
tp: np.ndarray,
|
|
347
|
+
conf: np.ndarray,
|
|
348
|
+
pred_cls: np.ndarray,
|
|
349
|
+
target_cls: np.ndarray,
|
|
350
|
+
eps: float = 1e-16,
|
|
351
|
+
v5_metric: bool = True
|
|
352
|
+
):
|
|
353
|
+
"""
|
|
354
|
+
Compute the average precision, given the recall and precision curves.
|
|
355
|
+
Source:: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
|
356
|
+
Source:: https://github.com/ultralytics/yolov5/blob/master/utils/metrics.py#L29
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
tp: np.ndarray
|
|
361
|
+
True positives (nparray, nx1 or nx10).
|
|
362
|
+
conf: np.ndarray
|
|
363
|
+
Objectness value from 0-1 (nparray).
|
|
364
|
+
pred_cls: np.ndarray
|
|
365
|
+
Predicted object classes (nparray).
|
|
366
|
+
target_cls: np.ndarray
|
|
367
|
+
True object classes (nparray).
|
|
368
|
+
eps: float
|
|
369
|
+
Minimum value to avoid division by zero.
|
|
370
|
+
v5_metrc: bool
|
|
371
|
+
Assume maximum recall to be 1.0, as in YOLOv5, MMDetetion etc.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
376
|
+
The average precision as computed in py-faster-rcnn.
|
|
377
|
+
"""
|
|
378
|
+
# Sort by objectness
|
|
379
|
+
i = np.argsort(-conf)
|
|
380
|
+
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
|
|
381
|
+
|
|
382
|
+
# Find unique classes
|
|
383
|
+
unique_classes, nt = np.unique(target_cls, return_counts=True)
|
|
384
|
+
nc = unique_classes.shape[0] # number of classes, number of detections
|
|
385
|
+
|
|
386
|
+
# Create Precision-Recall curve and compute AP for each class
|
|
387
|
+
px, py = np.linspace(0, 1, 1000), [] # for plotting
|
|
388
|
+
ap, p, r = np.zeros((nc, 10)), np.zeros(
|
|
389
|
+
(nc, 1000)), np.zeros((nc, 1000))
|
|
390
|
+
|
|
391
|
+
for ci, c in enumerate(unique_classes):
|
|
392
|
+
# NOTE: Do not use np.nonzero, otherwise i.sum() will fail.
|
|
393
|
+
i = pred_cls == c
|
|
394
|
+
n_l = nt[ci] # number of labels
|
|
395
|
+
n_p = i.sum() # number of predictions
|
|
396
|
+
|
|
397
|
+
if n_p == 0 or n_l == 0:
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
# Accumulate FPs and TPs
|
|
401
|
+
fpc = (1 - tp[i]).cumsum(0)
|
|
402
|
+
tpc = tp[i].cumsum(0)
|
|
403
|
+
|
|
404
|
+
# Recall
|
|
405
|
+
recall = tpc / (n_l + eps) # recall curve
|
|
406
|
+
# negative x, xp because xp decreases
|
|
407
|
+
r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0)
|
|
408
|
+
|
|
409
|
+
# Precision
|
|
410
|
+
precision = tpc / (tpc + fpc) # precision curve
|
|
411
|
+
p[ci] = np.interp(-px, -conf[i], precision[:, 0],
|
|
412
|
+
left=1) # p at pr_score
|
|
413
|
+
|
|
414
|
+
# AP from recall-precision curve
|
|
415
|
+
for j in range(tp.shape[1]):
|
|
416
|
+
ap[ci, j], mpre, mrec = compute_ap(recall[:, j],
|
|
417
|
+
precision[:, j],
|
|
418
|
+
v5_metric=v5_metric)
|
|
419
|
+
if j == 0:
|
|
420
|
+
# precision at mAP@0.5
|
|
421
|
+
py.append(np.interp(px, mrec, mpre))
|
|
422
|
+
|
|
423
|
+
# Compute F1 (harmonic mean of precision and recall)
|
|
424
|
+
f1 = 2 * p * r / (p + r + eps)
|
|
425
|
+
|
|
426
|
+
# Used for plotting precision recall curve.
|
|
427
|
+
self.plots.px = px
|
|
428
|
+
self.plots.py = py
|
|
429
|
+
self.plots.precision = p
|
|
430
|
+
self.plots.recall = r
|
|
431
|
+
self.plots.average_precision = ap
|
|
432
|
+
self.plots.curve_labels = unique_classes
|
|
433
|
+
self.plots.f1 = f1
|
|
434
|
+
|
|
435
|
+
if v5_metric:
|
|
436
|
+
i = smooth(f1.mean(0), 0.1).argmax() # max F1 index YOLOv5
|
|
437
|
+
else:
|
|
438
|
+
i = f1.mean(0).argmax() # max F1 index # YOLOv7
|
|
439
|
+
return p[:, i], r[:, i], ap, f1[:, i]
|
|
440
|
+
|
|
441
|
+
def compute_class_metrics(
|
|
442
|
+
self,
|
|
443
|
+
label_data: DetectionLabelData,
|
|
444
|
+
iou_threshold: float,
|
|
445
|
+
) -> Tuple[np.ndarray, list]:
|
|
446
|
+
"""
|
|
447
|
+
This is an EdgeFirst validation method.
|
|
448
|
+
|
|
449
|
+
Returns the precision, recall, and accuracy metrics of a
|
|
450
|
+
specific class at the specified validation IoU threshold.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
label_data: DetectionLabelData
|
|
455
|
+
This is a container of the truth values of a specific class.
|
|
456
|
+
iou_threshold: float
|
|
457
|
+
The validation IoU threshold to consider as true positives.
|
|
458
|
+
|
|
459
|
+
Returns
|
|
460
|
+
-------
|
|
461
|
+
class_metrics: np.ndarray (1, 3)
|
|
462
|
+
This contains the values for precision, recall, and accuracy
|
|
463
|
+
of the class representing the label data container.
|
|
464
|
+
class_truth_values: list
|
|
465
|
+
This contains the values for true positives, classification
|
|
466
|
+
false positives, localization false positives, and
|
|
467
|
+
false negatives for the class representing the label data
|
|
468
|
+
container.
|
|
469
|
+
"""
|
|
470
|
+
# These are the truth values just for the specified class in the
|
|
471
|
+
# data container: true positives, false positives, and false negatives.
|
|
472
|
+
tp = label_data.get_tp_count(iou_threshold=iou_threshold,
|
|
473
|
+
score_threshold=self.parameters.score_threshold)
|
|
474
|
+
cfp = label_data.get_cfp_count(iou_threshold=iou_threshold,
|
|
475
|
+
score_threshold=self.parameters.score_threshold)
|
|
476
|
+
lfp = label_data.get_lfp_count(iou_threshold=iou_threshold,
|
|
477
|
+
score_threshold=self.parameters.score_threshold)
|
|
478
|
+
fn = label_data.get_fn_count(iou_threshold=iou_threshold,
|
|
479
|
+
score_threshold=self.parameters.score_threshold)
|
|
480
|
+
|
|
481
|
+
class_metrics = np.zeros(3)
|
|
482
|
+
class_truth_values = [tp, cfp, lfp, fn]
|
|
483
|
+
|
|
484
|
+
if tp == 0:
|
|
485
|
+
if cfp + lfp == 0:
|
|
486
|
+
class_metrics[0] = np.nan
|
|
487
|
+
if fn == 0:
|
|
488
|
+
class_metrics[1] = np.nan
|
|
489
|
+
if cfp + lfp + fn == 0:
|
|
490
|
+
class_metrics[2] = np.nan
|
|
491
|
+
else:
|
|
492
|
+
class_metrics[0] = compute_precision(tp, cfp + lfp)
|
|
493
|
+
class_metrics[1] = compute_recall(tp, fn)
|
|
494
|
+
class_metrics[2] = compute_accuracy(tp, cfp + lfp, fn)
|
|
495
|
+
|
|
496
|
+
return class_metrics, class_truth_values
|
|
497
|
+
|
|
498
|
+
def compute_mean_average_metrics(
|
|
499
|
+
self,
|
|
500
|
+
maps: np.ndarray,
|
|
501
|
+
mars: np.ndarray,
|
|
502
|
+
maccs: np.ndarray,
|
|
503
|
+
nc: int
|
|
504
|
+
) -> Tuple[list, list, list]:
|
|
505
|
+
"""
|
|
506
|
+
This is an EdgeFirst validation method.
|
|
507
|
+
|
|
508
|
+
Given an array of precision, recall, and accuracy at 0.50 to 0.95 IoU
|
|
509
|
+
thresholds, this will return values only at IoU thresholds,
|
|
510
|
+
0.50, 0.75, and 0.50-0.95 averages.
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
maps: np.ndarray (1,20)
|
|
515
|
+
precision values from different IoU thresholds.
|
|
516
|
+
mars: np.ndarray (1,20)
|
|
517
|
+
recall values from different IoU thresholds.
|
|
518
|
+
maccs: np.ndarray (1,20)
|
|
519
|
+
accuracy values from different IoU thresholds.
|
|
520
|
+
nc: int
|
|
521
|
+
The number of classes.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
metric_map: list
|
|
526
|
+
This contains the precision values at IoU thresholds
|
|
527
|
+
0.50, 0.75, 0.50-0.95
|
|
528
|
+
metric_mar: list
|
|
529
|
+
This contains the recall values at IoU thresholds
|
|
530
|
+
0.50, 0.75, 0.50-0.95
|
|
531
|
+
metric_maccuracy: list
|
|
532
|
+
This contains the accuracy values at IoU thresholds
|
|
533
|
+
0.50, 0.75, 0.50-0.95
|
|
534
|
+
"""
|
|
535
|
+
# These arrays are essentially the mAP, mAR, and mACC across the
|
|
536
|
+
# IoU thresholds 0.00 to 1.00 in 0.05 intervals with shape (1, 20).
|
|
537
|
+
maps = np.nansum(maps, axis=0) / nc
|
|
538
|
+
mars = np.nansum(mars, axis=0) / nc
|
|
539
|
+
maccs = np.nansum(maccs, axis=0) / nc
|
|
540
|
+
|
|
541
|
+
# These are the mAP, mAR, and mACC 0.5-0.95 IoU thresholds.
|
|
542
|
+
map_5095 = np.sum(maps[10:]) / 10
|
|
543
|
+
mar_5095 = np.sum(mars[10:]) / 10
|
|
544
|
+
macc_5095 = np.sum(maccs[10:]) / 10
|
|
545
|
+
|
|
546
|
+
# This list contains mAP, mAR, mACC 0.50, 0.75, and 0.5-0.95.
|
|
547
|
+
metric_map = [maps[10], maps[15], map_5095]
|
|
548
|
+
metric_mar = [mars[10], mars[15], mar_5095]
|
|
549
|
+
metric_maccuracy = [maccs[10], maccs[15], macc_5095]
|
|
550
|
+
|
|
551
|
+
return metric_map, metric_mar, metric_maccuracy
|
|
552
|
+
|
|
553
|
+
def compute_overall_metrics(self) -> Tuple[float, float, float]:
|
|
554
|
+
"""
|
|
555
|
+
This is an EdgeFirst Validation method.
|
|
556
|
+
|
|
557
|
+
Computes the overall metrics.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
precision: float
|
|
562
|
+
overall precision = sum tp /
|
|
563
|
+
(sum tp + sum fp (localization + classification)).
|
|
564
|
+
recall: float
|
|
565
|
+
overall recall = sum tp /
|
|
566
|
+
(sum tp + sum fn + sum fp (localization)).
|
|
567
|
+
accuracy: float
|
|
568
|
+
overall accuracy = sum tp /
|
|
569
|
+
(sum tp + sum fn + sum fp (localization + classification)).
|
|
570
|
+
"""
|
|
571
|
+
precision, recall, accuracy = 0., 0., 0.
|
|
572
|
+
|
|
573
|
+
if self.metrics.tp > 0:
|
|
574
|
+
precision = compute_precision(
|
|
575
|
+
self.metrics.tp,
|
|
576
|
+
self.metrics.cfp + self.metrics.lfp
|
|
577
|
+
)
|
|
578
|
+
recall = compute_recall(
|
|
579
|
+
self.metrics.tp,
|
|
580
|
+
self.metrics.fn + self.metrics.cfp
|
|
581
|
+
)
|
|
582
|
+
accuracy = compute_accuracy(
|
|
583
|
+
self.metrics.tp,
|
|
584
|
+
self.metrics.cfp + self.metrics.lfp,
|
|
585
|
+
self.metrics.fn
|
|
586
|
+
)
|
|
587
|
+
return precision, recall, accuracy
|
|
588
|
+
|
|
589
|
+
def reset(self):
|
|
590
|
+
"""
|
|
591
|
+
Reset the metric containers.
|
|
592
|
+
"""
|
|
593
|
+
self.plots.reset()
|
|
594
|
+
self.detection_stats.reset()
|
|
595
|
+
self.metrics.reset()
|