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,749 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, List
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import matplotlib.figure
|
|
8
|
+
|
|
9
|
+
from edgefirst.validator.metrics.utils.math import batch_iou
|
|
10
|
+
from edgefirst.validator.visualize.utils.plots import (figure2numpy,
|
|
11
|
+
plot_pr_curve,
|
|
12
|
+
plot_mc_curve,
|
|
13
|
+
plot_score_histogram,
|
|
14
|
+
plot_confusion_matrix,
|
|
15
|
+
plot_classification_detection)
|
|
16
|
+
from edgefirst.validator.datasets.utils.transformations import (clamp_boxes,
|
|
17
|
+
ignore_boxes)
|
|
18
|
+
from edgefirst.validator.datasets.utils.fetch import get_shape
|
|
19
|
+
from edgefirst.validator.visualize import DetectionDrawer
|
|
20
|
+
from edgefirst.validator.datasets import DetectionInstance
|
|
21
|
+
from edgefirst.validator.visualize.utils.plots import ConfusionMatrix
|
|
22
|
+
from edgefirst.validator.runners import (OfflineRunner, DeepViewRTRunner)
|
|
23
|
+
from edgefirst.validator.evaluators import Evaluator, Matcher, DetectionClassifier
|
|
24
|
+
from edgefirst.validator.metrics import YOLOStats, DetectionStats, DetectionMetrics
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from edgefirst.validator.evaluators import CombinedParameters
|
|
28
|
+
from edgefirst.validator.datasets import Dataset
|
|
29
|
+
from edgefirst.validator.runners import Runner
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class YOLOValidator(Evaluator):
|
|
33
|
+
"""
|
|
34
|
+
Reproduce the validation methods implemented in Ultralytics and other
|
|
35
|
+
variations such as YOLOv7 for detection. Reproduces the metrics in
|
|
36
|
+
Ultralytics to allow comparable metrics between EdgeFirst models
|
|
37
|
+
and Ultralytics models.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
parameters: CombinedParameters
|
|
42
|
+
This is a container for the model, dataset, and validation parameters
|
|
43
|
+
set from the command line.
|
|
44
|
+
runner: Runner
|
|
45
|
+
A type of model runner object responsible for running the model
|
|
46
|
+
for inference provided with an input image to produce bounding boxes.
|
|
47
|
+
dataset: Dataset
|
|
48
|
+
A type of dataset object responsible for reading different types
|
|
49
|
+
of datasets such as Darknet, TFRecords, or EdgeFirst Datasets.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
parameters: CombinedParameters,
|
|
55
|
+
runner: Runner = None,
|
|
56
|
+
dataset: Dataset = None,
|
|
57
|
+
):
|
|
58
|
+
super(YOLOValidator, self).__init__(
|
|
59
|
+
parameters=parameters, runner=runner, dataset=dataset)
|
|
60
|
+
|
|
61
|
+
self.detection_stats = YOLOStats()
|
|
62
|
+
self.confusion_matrix = ConfusionMatrix(
|
|
63
|
+
nc=len(self.parameters.dataset.labels),
|
|
64
|
+
iou_thres=self.parameters.validation.iou_threshold,
|
|
65
|
+
offset=int(not any(s.lower() == "background"
|
|
66
|
+
for s in self.parameters.dataset.labels))
|
|
67
|
+
)
|
|
68
|
+
self.metrics = DetectionMetrics(
|
|
69
|
+
parameters=self.parameters.validation,
|
|
70
|
+
detection_stats=self.detection_stats,
|
|
71
|
+
model_name=self.model_name,
|
|
72
|
+
dataset_name=self.dataset_name,
|
|
73
|
+
save_path=self.save_path,
|
|
74
|
+
labels=self.parameters.dataset.labels
|
|
75
|
+
)
|
|
76
|
+
self.metrics.plots.initialize_confusion_matrix()
|
|
77
|
+
self.drawer = DetectionDrawer()
|
|
78
|
+
self.matcher = None
|
|
79
|
+
|
|
80
|
+
def instance_collector(self):
|
|
81
|
+
"""
|
|
82
|
+
Collects the instances from the ground truth and runs
|
|
83
|
+
model inference on a single image to collect the instance for
|
|
84
|
+
the model predictions.
|
|
85
|
+
|
|
86
|
+
Yields
|
|
87
|
+
------
|
|
88
|
+
dict
|
|
89
|
+
This yields one image instance from the ground truth
|
|
90
|
+
and model predictions with keys "gt_instance" and "dt_instance".
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
gt_instance: DetectionInstance
|
|
94
|
+
for gt_instance in self.dataset:
|
|
95
|
+
if isinstance(self.runner, (OfflineRunner, DeepViewRTRunner)):
|
|
96
|
+
detections = self.runner.run_single_instance(
|
|
97
|
+
image=gt_instance.image_path
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
detections = self.runner.run_single_instance(
|
|
101
|
+
image=gt_instance.image
|
|
102
|
+
)
|
|
103
|
+
self.filter_gt(gt_instance)
|
|
104
|
+
|
|
105
|
+
if detections is None:
|
|
106
|
+
yield {
|
|
107
|
+
"gt_instance": gt_instance,
|
|
108
|
+
"dt_instance": None
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dt_instance = DetectionInstance(gt_instance.image_path)
|
|
112
|
+
boxes, labels, scores = detections
|
|
113
|
+
dt_instance.height = gt_instance.height
|
|
114
|
+
dt_instance.width = gt_instance.width
|
|
115
|
+
dt_instance.boxes = boxes
|
|
116
|
+
dt_instance.labels = labels
|
|
117
|
+
dt_instance.scores = scores
|
|
118
|
+
self.filter_dt(dt_instance)
|
|
119
|
+
|
|
120
|
+
yield {
|
|
121
|
+
"gt_instance": gt_instance,
|
|
122
|
+
"dt_instance": dt_instance,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def filter_dt(self, dt_instance: DetectionInstance):
|
|
126
|
+
"""
|
|
127
|
+
Apply validation filters to the prediction bounding boxes.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
dt_instance: DetectionInstance
|
|
132
|
+
The model detections container of the bounding boxes, labels,
|
|
133
|
+
and scores for a single image/sample.
|
|
134
|
+
"""
|
|
135
|
+
if self.parameters.validation.ignore_boxes:
|
|
136
|
+
boxes, labels, scores = ignore_boxes(
|
|
137
|
+
ignore=self.parameters.validation.ignore_boxes,
|
|
138
|
+
boxes=dt_instance.boxes,
|
|
139
|
+
labels=dt_instance.labels,
|
|
140
|
+
scores=dt_instance.scores,
|
|
141
|
+
shape=(dt_instance.height, dt_instance.width)
|
|
142
|
+
)
|
|
143
|
+
dt_instance.boxes = boxes
|
|
144
|
+
dt_instance.labels = labels
|
|
145
|
+
dt_instance.scores = scores
|
|
146
|
+
if self.parameters.validation.clamp_boxes:
|
|
147
|
+
dt_instance.boxes = clamp_boxes(
|
|
148
|
+
boxes=dt_instance.boxes,
|
|
149
|
+
clamp=self.parameters.validation.clamp_boxes,
|
|
150
|
+
shape=(dt_instance.height, dt_instance.width)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Prediction bounding boxes are already centered around objects
|
|
154
|
+
# in images with letterbox, padding, or resize transformations.
|
|
155
|
+
# This operation will only denormalize the bounding box coordinates.
|
|
156
|
+
if len(dt_instance.boxes) and dt_instance.shapes is not None:
|
|
157
|
+
# The model input shape.
|
|
158
|
+
height, width = get_shape(self.parameters.model.common.shape)
|
|
159
|
+
dt_instance.boxes *= np.array([width, height, width, height])
|
|
160
|
+
|
|
161
|
+
# If the model and dataset labels are not equal, it is required
|
|
162
|
+
# to map the indices properly to match the ground truth and the
|
|
163
|
+
# detections.
|
|
164
|
+
if self.parameters.model.labels != self.parameters.dataset.labels:
|
|
165
|
+
try:
|
|
166
|
+
dt_instance.labels = np.array([
|
|
167
|
+
self.parameters.dataset.labels.index(
|
|
168
|
+
self.parameters.model.labels[int(cls)])
|
|
169
|
+
if self.parameters.model.labels[int(cls)]
|
|
170
|
+
in self.parameters.dataset.labels else cls for cls in
|
|
171
|
+
dt_instance.labels])
|
|
172
|
+
except IndexError:
|
|
173
|
+
raise IndexError("Model index out of range. " +
|
|
174
|
+
"Try specifying the path to the model's " +
|
|
175
|
+
"labels via `--model-labels <path to labels.txt>`.")
|
|
176
|
+
|
|
177
|
+
def filter_gt(self, gt_instance: DetectionInstance):
|
|
178
|
+
"""
|
|
179
|
+
Apply validation filters for the ground truth bounding boxes.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
gt_instance: DetectionInstance
|
|
184
|
+
The ground truth container for the bounding boxes, labels
|
|
185
|
+
for a single image instance.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
if self.parameters.validation.ignore_boxes:
|
|
189
|
+
boxes, labels, scores = ignore_boxes(
|
|
190
|
+
ignore=self.parameters.validation.ignore_boxes,
|
|
191
|
+
boxes=gt_instance.boxes,
|
|
192
|
+
labels=gt_instance.labels,
|
|
193
|
+
scores=gt_instance.scores,
|
|
194
|
+
shape=(gt_instance.height, gt_instance.width)
|
|
195
|
+
)
|
|
196
|
+
gt_instance.boxes = boxes
|
|
197
|
+
gt_instance.labels = labels
|
|
198
|
+
gt_instance.scores = scores
|
|
199
|
+
if self.parameters.validation.clamp_boxes:
|
|
200
|
+
gt_instance.boxes = clamp_boxes(
|
|
201
|
+
boxes=gt_instance.boxes,
|
|
202
|
+
clamp=self.parameters.validation.clamp_boxes,
|
|
203
|
+
shape=(gt_instance.height, gt_instance.width)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def match_predictions(
|
|
207
|
+
self,
|
|
208
|
+
pred_classes: np.ndarray,
|
|
209
|
+
true_classes: np.ndarray,
|
|
210
|
+
iou: np.ndarray
|
|
211
|
+
) -> np.ndarray:
|
|
212
|
+
"""
|
|
213
|
+
Match predictions to ground truth using IoU.
|
|
214
|
+
Function implementation was taken from Ultralytics::
|
|
215
|
+
https://github.com/ultralytics/ultralytics/blob/main/ultralytics/engine/validator.py#L251
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
pred_classes: np.ndarray
|
|
220
|
+
Predicted class indices of shape (N,).
|
|
221
|
+
true_classes: np.ndarray
|
|
222
|
+
Target class indices of shape (M,).
|
|
223
|
+
iou: np.ndarray
|
|
224
|
+
An NxM tensor containing the pairwise IoU
|
|
225
|
+
values for predictions and ground truth.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
np.ndarray
|
|
230
|
+
Correct tensor of shape (N, 10) for 10 IoU thresholds.
|
|
231
|
+
"""
|
|
232
|
+
correct = np.zeros(
|
|
233
|
+
(pred_classes.shape[0], self.detection_stats.ious.shape[0]),
|
|
234
|
+
dtype=bool
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
correct_class = (true_classes[:, None] == pred_classes).astype(
|
|
238
|
+
np.float32) # shape (N, M)
|
|
239
|
+
iou = iou * correct_class
|
|
240
|
+
|
|
241
|
+
for i, threshold in enumerate(self.detection_stats.ious):
|
|
242
|
+
# IoU > threshold and classes match
|
|
243
|
+
matches = np.nonzero(iou >= threshold)
|
|
244
|
+
matches = np.array(matches).T
|
|
245
|
+
if matches.shape[0]:
|
|
246
|
+
if matches.shape[0] > 1:
|
|
247
|
+
matches = matches[iou[matches[:, 0],
|
|
248
|
+
matches[:, 1]].argsort()[::-1]]
|
|
249
|
+
matches = matches[np.unique(
|
|
250
|
+
matches[:, 1], return_index=True)[1]]
|
|
251
|
+
matches = matches[np.unique(
|
|
252
|
+
matches[:, 0], return_index=True)[1]]
|
|
253
|
+
correct[matches[:, 1].astype(int), i] = True
|
|
254
|
+
return correct
|
|
255
|
+
|
|
256
|
+
def process_batch_v5(
|
|
257
|
+
self,
|
|
258
|
+
dt_instance: DetectionInstance,
|
|
259
|
+
gt_instance: DetectionInstance
|
|
260
|
+
) -> np.ndarray:
|
|
261
|
+
"""
|
|
262
|
+
Return the correct prediction matrix. Function implementation was taken
|
|
263
|
+
from YOLOv5: https://github.com/ultralytics/yolov5/blob/master/val.py#L94.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
-----------
|
|
267
|
+
dt_instance: DetectionInstance
|
|
268
|
+
A prediction instance container of the boxes, labels, and scores.
|
|
269
|
+
gt_instance: DetectionInstance
|
|
270
|
+
A ground truth instance container of the boxes and the labels.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
np.ndarray
|
|
275
|
+
(array[n, 10]) where n denotes the classes for 10 IoU levels. This
|
|
276
|
+
is a true positive array.
|
|
277
|
+
"""
|
|
278
|
+
iou = batch_iou(gt_instance.boxes, dt_instance.boxes)
|
|
279
|
+
return self.match_predictions(
|
|
280
|
+
pred_classes=dt_instance.labels,
|
|
281
|
+
true_classes=gt_instance.labels if len(
|
|
282
|
+
gt_instance.boxes) else np.array([]),
|
|
283
|
+
iou=iou
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def process_batch_v7(
|
|
287
|
+
self,
|
|
288
|
+
dt_instance: DetectionInstance,
|
|
289
|
+
gt_instance: DetectionInstance
|
|
290
|
+
) -> np.ndarray:
|
|
291
|
+
"""
|
|
292
|
+
Return the correct prediction matrix. Function implementation was taken
|
|
293
|
+
from YOLOv7: https://github.com/WongKinYiu/yolov7/blob/main/test.py#L179
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
-----------
|
|
297
|
+
dt_instance: DetectionInstance
|
|
298
|
+
A prediction instance container of the boxes, labels, and scores.
|
|
299
|
+
gt_instance: DetectionInstance
|
|
300
|
+
A ground truth instance container of the boxes and the labels.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
np.ndarray
|
|
305
|
+
(array[N, 10]) where n denotes the classes for 10 IoU levels. This
|
|
306
|
+
is a true positive array..
|
|
307
|
+
"""
|
|
308
|
+
correct = np.zeros(
|
|
309
|
+
(dt_instance.boxes.shape[0],
|
|
310
|
+
self.detection_stats.ious.shape[0])).astype(bool)
|
|
311
|
+
gt_labels = gt_instance.labels if len(
|
|
312
|
+
gt_instance.boxes) else np.array([])
|
|
313
|
+
detected = [] # target indices
|
|
314
|
+
# Generate a similar format to YOLOv5 for visualization purposes.
|
|
315
|
+
matches = []
|
|
316
|
+
for cls in np.unique(gt_labels):
|
|
317
|
+
ti = np.flatnonzero(cls == gt_labels) # target indices
|
|
318
|
+
# prediction indices
|
|
319
|
+
pi = np.flatnonzero(cls == dt_instance.labels)
|
|
320
|
+
|
|
321
|
+
# Search for detections
|
|
322
|
+
if pi.shape[0]:
|
|
323
|
+
ious = batch_iou(dt_instance.boxes[pi], gt_instance.boxes[ti])
|
|
324
|
+
i = ious.argmax(1)
|
|
325
|
+
ious = ious.max(axis=1)
|
|
326
|
+
detected_set = set()
|
|
327
|
+
for j in np.flatnonzero(ious > self.detection_stats.ious[0]):
|
|
328
|
+
d = ti[i[j]] # detected target
|
|
329
|
+
# The ious[j] is always a tensor of 1 value.
|
|
330
|
+
matches.append([ti[i[j]], pi[j], ious[j]])
|
|
331
|
+
if d.item() not in detected_set:
|
|
332
|
+
detected_set.add(d.item())
|
|
333
|
+
detected.append(d)
|
|
334
|
+
# iou_thres is 1xn
|
|
335
|
+
correct[pi[j]] = ious[j] > self.detection_stats.ious
|
|
336
|
+
# all targets already located in image
|
|
337
|
+
if len(detected) == len(gt_instance.boxes):
|
|
338
|
+
break
|
|
339
|
+
return correct
|
|
340
|
+
|
|
341
|
+
def evaluate(self, instance: dict):
|
|
342
|
+
"""
|
|
343
|
+
Run model evaluation using Ultralytics or YOLOv7 validation methods.
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
instance: dict
|
|
348
|
+
This contains the ground truth and model prediction instances
|
|
349
|
+
with keys "gt_instance", "dt_instance".
|
|
350
|
+
"""
|
|
351
|
+
gt_instance: DetectionInstance = instance.get("gt_instance")
|
|
352
|
+
dt_instance: DetectionInstance = instance.get("dt_instance")
|
|
353
|
+
|
|
354
|
+
niou = len(self.detection_stats.ious)
|
|
355
|
+
nl = len(gt_instance.labels) # The number of ground truths.
|
|
356
|
+
nd = len(dt_instance.labels) # The number of predictions.
|
|
357
|
+
tcls = gt_instance.labels.tolist() if nl else [] # target class.
|
|
358
|
+
|
|
359
|
+
self.metrics.metrics.add_ground_truths(nl)
|
|
360
|
+
self.metrics.metrics.add_predictions(nd)
|
|
361
|
+
|
|
362
|
+
if nl:
|
|
363
|
+
if nd == 0:
|
|
364
|
+
if self.parameters.validation.plots:
|
|
365
|
+
self.confusion_matrix.process_batch(dt_instance=None,
|
|
366
|
+
gt_instance=gt_instance)
|
|
367
|
+
self.detection_stats.stats["tp"].append(
|
|
368
|
+
np.zeros((0, niou), dtype=bool))
|
|
369
|
+
self.detection_stats.stats["conf"].append(np.array([]))
|
|
370
|
+
self.detection_stats.stats["pred_cls"].append(np.array([]))
|
|
371
|
+
self.detection_stats.stats["target_cls"].append(tcls)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Ultralytics method.
|
|
375
|
+
if self.parameters.validation.method == "ultralytics":
|
|
376
|
+
correct = self.process_batch_v5(dt_instance=dt_instance,
|
|
377
|
+
gt_instance=gt_instance)
|
|
378
|
+
# YOLOv7 method.
|
|
379
|
+
elif self.parameters.validation.method == "yolov7":
|
|
380
|
+
correct = self.process_batch_v7(dt_instance=dt_instance,
|
|
381
|
+
gt_instance=gt_instance)
|
|
382
|
+
|
|
383
|
+
if self.parameters.validation.plots:
|
|
384
|
+
self.confusion_matrix.process_batch(dt_instance, gt_instance)
|
|
385
|
+
else:
|
|
386
|
+
correct = np.zeros((dt_instance.boxes.shape[0], niou)).astype(bool)
|
|
387
|
+
|
|
388
|
+
if nd:
|
|
389
|
+
self.detection_stats.stats["tp"].append(correct)
|
|
390
|
+
self.detection_stats.stats["conf"].append(dt_instance.scores)
|
|
391
|
+
self.detection_stats.stats["pred_cls"].append(dt_instance.labels)
|
|
392
|
+
self.detection_stats.stats["target_cls"].append(tcls)
|
|
393
|
+
else:
|
|
394
|
+
self.detection_stats.stats["tp"].append(
|
|
395
|
+
np.zeros((0, niou), dtype=bool))
|
|
396
|
+
self.detection_stats.stats["conf"].append(np.array([]))
|
|
397
|
+
self.detection_stats.stats["pred_cls"].append(np.array([]))
|
|
398
|
+
self.detection_stats.stats["target_cls"].append(tcls)
|
|
399
|
+
|
|
400
|
+
def visualize(
|
|
401
|
+
self,
|
|
402
|
+
gt_instance: DetectionInstance,
|
|
403
|
+
dt_instance: DetectionInstance,
|
|
404
|
+
epoch: int = 0
|
|
405
|
+
):
|
|
406
|
+
"""
|
|
407
|
+
Draw bounding box results on the image and save the results in disk
|
|
408
|
+
or publish into Tensorboard.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
gt_instance: DetectionInstance
|
|
413
|
+
This is the ground truth instance which contains bounding
|
|
414
|
+
boxes and labels to draw.
|
|
415
|
+
dt_instance: DetectionInstance
|
|
416
|
+
This is the model detection instance which contains the
|
|
417
|
+
bounding boxes, labels, and confidence scores to draw.
|
|
418
|
+
epoch: int
|
|
419
|
+
This is the training epoch number. This
|
|
420
|
+
parameter is internal for ModelPack usage.
|
|
421
|
+
Standalone validation does not use this parameter.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
image = self.drawer.draw_2d_bounding_boxes(
|
|
425
|
+
gt_instance=gt_instance,
|
|
426
|
+
dt_instance=dt_instance,
|
|
427
|
+
matcher=self.matcher,
|
|
428
|
+
validation_iou=self.parameters.validation.iou_threshold,
|
|
429
|
+
validation_score=self.parameters.validation.score_threshold,
|
|
430
|
+
method=self.parameters.validation.method,
|
|
431
|
+
labels=self.parameters.dataset.labels
|
|
432
|
+
)
|
|
433
|
+
if self.parameters.validation.visualize:
|
|
434
|
+
image.save(os.path.join(self.parameters.validation.visualize,
|
|
435
|
+
os.path.basename(gt_instance.image_path)))
|
|
436
|
+
elif self.tensorboard_writer:
|
|
437
|
+
self.tensorboard_writer(
|
|
438
|
+
np.asarray(image), gt_instance.image_path, step=epoch)
|
|
439
|
+
|
|
440
|
+
def get_plots(self) -> List[matplotlib.figure.Figure]:
|
|
441
|
+
"""
|
|
442
|
+
Reproduces the validation charts from Ultralytics.
|
|
443
|
+
These plots are Matplotlib figures.
|
|
444
|
+
|
|
445
|
+
Returns
|
|
446
|
+
-------
|
|
447
|
+
List[matplotlib.figure.Figure]
|
|
448
|
+
This contains matplotlib figures of the plots.
|
|
449
|
+
"""
|
|
450
|
+
fig_confusion_matrix = self.confusion_matrix.plot(
|
|
451
|
+
names=self.metrics.plots.confusion_labels
|
|
452
|
+
)
|
|
453
|
+
fig_prec_rec_curve = plot_pr_curve(
|
|
454
|
+
precision=self.metrics.plots.py,
|
|
455
|
+
recall=self.metrics.plots.px,
|
|
456
|
+
ap=self.metrics.plots.average_precision,
|
|
457
|
+
names=self.parameters.dataset.labels,
|
|
458
|
+
model=self.metrics.metrics.model,
|
|
459
|
+
iou_threshold=self.parameters.validation.iou_threshold
|
|
460
|
+
)
|
|
461
|
+
fig_f1_curve = plot_mc_curve(
|
|
462
|
+
px=self.metrics.plots.px,
|
|
463
|
+
py=self.metrics.plots.f1,
|
|
464
|
+
names=self.parameters.dataset.labels,
|
|
465
|
+
model=self.metrics.metrics.model,
|
|
466
|
+
ylabel='F1'
|
|
467
|
+
)
|
|
468
|
+
fig_prec_curve = plot_mc_curve(
|
|
469
|
+
px=self.metrics.plots.px,
|
|
470
|
+
py=self.metrics.plots.precision,
|
|
471
|
+
names=self.parameters.dataset.labels,
|
|
472
|
+
model=self.metrics.metrics.model,
|
|
473
|
+
ylabel='Precision'
|
|
474
|
+
)
|
|
475
|
+
fig_rec_curve = plot_mc_curve(
|
|
476
|
+
px=self.metrics.plots.px,
|
|
477
|
+
py=self.metrics.plots.recall,
|
|
478
|
+
names=self.parameters.dataset.labels,
|
|
479
|
+
model=self.metrics.metrics.model,
|
|
480
|
+
ylabel='Recall'
|
|
481
|
+
)
|
|
482
|
+
return [fig_confusion_matrix,
|
|
483
|
+
fig_prec_rec_curve,
|
|
484
|
+
fig_f1_curve,
|
|
485
|
+
fig_prec_curve,
|
|
486
|
+
fig_rec_curve]
|
|
487
|
+
|
|
488
|
+
def save_plots(self, plots: List[matplotlib.figure.Figure]):
|
|
489
|
+
"""
|
|
490
|
+
Saves the validation plots as image files in disk.
|
|
491
|
+
|
|
492
|
+
Parameters
|
|
493
|
+
----------
|
|
494
|
+
plots: List[matplotlib.figure.Figure]
|
|
495
|
+
This is the list of matplotlib figures to save.
|
|
496
|
+
"""
|
|
497
|
+
plots[0].savefig(
|
|
498
|
+
f"{self.parameters.validation.visualize}/confusion_matrix.png",
|
|
499
|
+
bbox_inches="tight")
|
|
500
|
+
|
|
501
|
+
plots[1].savefig(
|
|
502
|
+
f"{self.parameters.validation.visualize}/prec_rec_curve.png",
|
|
503
|
+
bbox_inches="tight")
|
|
504
|
+
plots[2].savefig(
|
|
505
|
+
f"{self.parameters.validation.visualize}/F1_curve.png",
|
|
506
|
+
bbox_inches="tight"
|
|
507
|
+
)
|
|
508
|
+
plots[3].savefig(
|
|
509
|
+
f"{self.parameters.validation.visualize}/P_curve.png",
|
|
510
|
+
bbox_inches="tight"
|
|
511
|
+
)
|
|
512
|
+
plots[4].savefig(
|
|
513
|
+
f"{self.parameters.validation.visualize}/R_curve.png",
|
|
514
|
+
bbox_inches="tight"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def publish_plots(
|
|
518
|
+
self, plots: List[matplotlib.figure.Figure], epoch: int = 0):
|
|
519
|
+
"""
|
|
520
|
+
Publishes the validation plots into Tensorboard.
|
|
521
|
+
|
|
522
|
+
Parameters
|
|
523
|
+
----------
|
|
524
|
+
plots: List[matplotlib.figure.Figure]
|
|
525
|
+
This is the list of matplotlib figures to save.
|
|
526
|
+
epoch: int
|
|
527
|
+
The training epoch number used for ModelPack training usage.
|
|
528
|
+
"""
|
|
529
|
+
nimage_confusion_matrix = figure2numpy(plots[0])
|
|
530
|
+
nimage_precision_recall = figure2numpy(plots[1])
|
|
531
|
+
nimage_f1 = figure2numpy(plots[2])
|
|
532
|
+
nimage_prec = figure2numpy(plots[3])
|
|
533
|
+
nimage_rec = figure2numpy(plots[4])
|
|
534
|
+
|
|
535
|
+
self.tensorboard_writer(
|
|
536
|
+
nimage_confusion_matrix,
|
|
537
|
+
f"{self.metrics.metrics.model}_confusion_matrix.png",
|
|
538
|
+
step=epoch
|
|
539
|
+
)
|
|
540
|
+
self.tensorboard_writer(
|
|
541
|
+
nimage_precision_recall,
|
|
542
|
+
f"{self.metrics.metrics.model}_precision_recall.png",
|
|
543
|
+
step=epoch
|
|
544
|
+
)
|
|
545
|
+
self.tensorboard_writer(
|
|
546
|
+
nimage_f1,
|
|
547
|
+
f"{self.metrics.metrics.model}_F1_curve.png",
|
|
548
|
+
step=epoch
|
|
549
|
+
)
|
|
550
|
+
self.tensorboard_writer(
|
|
551
|
+
nimage_prec,
|
|
552
|
+
f"{self.metrics.metrics.model}_P_curve.png",
|
|
553
|
+
step=epoch
|
|
554
|
+
)
|
|
555
|
+
self.tensorboard_writer(
|
|
556
|
+
nimage_rec,
|
|
557
|
+
f"{self.metrics.metrics.model}_R_curve.png",
|
|
558
|
+
step=epoch
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class EdgeFirstValidator(YOLOValidator):
|
|
563
|
+
"""
|
|
564
|
+
Define the validation methods for EdgeFirst. Reproduces EdgeFirst matching
|
|
565
|
+
and metrics for object detection.
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
parameters: CombinedParameters
|
|
570
|
+
This is a container for the model, dataset, and validation parameters
|
|
571
|
+
set from the command line.
|
|
572
|
+
runner: Runner
|
|
573
|
+
A type of model runner object responsible for running the model
|
|
574
|
+
for inference provided with an input image to produce bounding boxes.
|
|
575
|
+
dataset: Dataset
|
|
576
|
+
A type of dataset object responsible for reading different types
|
|
577
|
+
of datasets such as Darknet, TFRecords, or EdgeFirst Datasets.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
def __init__(
|
|
581
|
+
self,
|
|
582
|
+
parameters: CombinedParameters,
|
|
583
|
+
runner: Runner = None,
|
|
584
|
+
dataset: Dataset = None,
|
|
585
|
+
):
|
|
586
|
+
super(EdgeFirstValidator, self).__init__(
|
|
587
|
+
parameters=parameters, runner=runner, dataset=dataset)
|
|
588
|
+
|
|
589
|
+
self.detection_stats = DetectionStats()
|
|
590
|
+
self.matcher = Matcher(parameters=parameters.validation)
|
|
591
|
+
self.metrics = DetectionMetrics(
|
|
592
|
+
parameters=self.parameters.validation,
|
|
593
|
+
detection_stats=self.detection_stats,
|
|
594
|
+
model_name=self.model_name,
|
|
595
|
+
dataset_name=self.dataset_name,
|
|
596
|
+
save_path=self.save_path,
|
|
597
|
+
labels=self.parameters.dataset.labels
|
|
598
|
+
)
|
|
599
|
+
self.classifier = DetectionClassifier(
|
|
600
|
+
parameters=self.parameters.validation,
|
|
601
|
+
detection_stats=self.detection_stats,
|
|
602
|
+
matcher=self.matcher,
|
|
603
|
+
plots=self.metrics.plots
|
|
604
|
+
)
|
|
605
|
+
self.metrics.plots.initialize_confusion_matrix()
|
|
606
|
+
|
|
607
|
+
def evaluate(self, instance: dict):
|
|
608
|
+
"""
|
|
609
|
+
Run model evaluation using EdgeFirst validation methods.
|
|
610
|
+
|
|
611
|
+
Parameters
|
|
612
|
+
----------
|
|
613
|
+
instance: dict
|
|
614
|
+
This contains the ground truth and model predictions instances
|
|
615
|
+
with keys "gt_instance" and "dt_instance".
|
|
616
|
+
"""
|
|
617
|
+
gt_instance: DetectionInstance = instance.get("gt_instance")
|
|
618
|
+
dt_instance: DetectionInstance = instance.get("dt_instance")
|
|
619
|
+
|
|
620
|
+
self.detection_stats.capture_class(dt_instance.labels)
|
|
621
|
+
self.detection_stats.capture_class(gt_instance.labels)
|
|
622
|
+
|
|
623
|
+
self.metrics.metrics.add_ground_truths(len(gt_instance.labels))
|
|
624
|
+
self.metrics.metrics.add_predictions(len(dt_instance.labels))
|
|
625
|
+
|
|
626
|
+
self.matcher.match(
|
|
627
|
+
gt_boxes=gt_instance.boxes,
|
|
628
|
+
gt_labels=gt_instance.labels,
|
|
629
|
+
dt_boxes=dt_instance.boxes,
|
|
630
|
+
dt_labels=dt_instance.labels,
|
|
631
|
+
)
|
|
632
|
+
self.classifier.classify(
|
|
633
|
+
gt_instance=gt_instance,
|
|
634
|
+
dt_instance=dt_instance
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
def get_plots(self) -> List[matplotlib.figure.Figure]:
|
|
638
|
+
"""
|
|
639
|
+
Generate EdgeFirst validation plots.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
List[matplotlib.figure.Figure]
|
|
644
|
+
This contains matplotlib figures of the plots.
|
|
645
|
+
"""
|
|
646
|
+
fig_confusion_matrix = plot_confusion_matrix(
|
|
647
|
+
confusion_data=self.metrics.plots.confusion_matrix,
|
|
648
|
+
labels=self.metrics.plots.confusion_labels,
|
|
649
|
+
model=self.metrics.metrics.model
|
|
650
|
+
)
|
|
651
|
+
fig_prec_rec_curve = plot_pr_curve(
|
|
652
|
+
precision=self.metrics.plots.py,
|
|
653
|
+
recall=self.metrics.plots.px,
|
|
654
|
+
ap=self.metrics.plots.average_precision,
|
|
655
|
+
names=self.parameters.dataset.labels,
|
|
656
|
+
model=self.metrics.metrics.model,
|
|
657
|
+
iou_threshold=self.parameters.validation.iou_threshold
|
|
658
|
+
)
|
|
659
|
+
fig_class_metrics = plot_classification_detection(
|
|
660
|
+
class_histogram_data=self.metrics.plots.class_histogram_data,
|
|
661
|
+
model=self.metrics.metrics.model,
|
|
662
|
+
)
|
|
663
|
+
fig_score_metrics = plot_score_histogram(
|
|
664
|
+
tp_scores=np.concatenate(self.metrics.plots.tp_scores, axis=0),
|
|
665
|
+
fp_scores=np.concatenate(self.metrics.plots.fp_scores, axis=0),
|
|
666
|
+
model=self.metrics.metrics.model
|
|
667
|
+
)
|
|
668
|
+
fig_iou_metrics = plot_score_histogram(
|
|
669
|
+
tp_scores=np.concatenate(self.metrics.plots.tp_ious, axis=0),
|
|
670
|
+
fp_scores=np.concatenate(self.metrics.plots.fp_ious, axis=0),
|
|
671
|
+
model=self.metrics.metrics.model,
|
|
672
|
+
title="Histogram of TP vs FP IoUs",
|
|
673
|
+
xlabel="IoU"
|
|
674
|
+
)
|
|
675
|
+
return [fig_confusion_matrix, fig_prec_rec_curve,
|
|
676
|
+
fig_class_metrics, fig_score_metrics, fig_iou_metrics]
|
|
677
|
+
|
|
678
|
+
def save_plots(self, plots: List[matplotlib.figure.Figure]):
|
|
679
|
+
"""
|
|
680
|
+
Saves the validation plots as image files in disk.
|
|
681
|
+
|
|
682
|
+
Parameters
|
|
683
|
+
----------
|
|
684
|
+
plots: List[matplotlib.figure.Figure]
|
|
685
|
+
This is the list of matplotlib figures to save.
|
|
686
|
+
"""
|
|
687
|
+
plots[0].savefig(
|
|
688
|
+
f"{self.parameters.validation.visualize}/confusion_matrix.png",
|
|
689
|
+
bbox_inches="tight")
|
|
690
|
+
|
|
691
|
+
plots[1].savefig(
|
|
692
|
+
f"{self.parameters.validation.visualize}/prec_rec_curve.png",
|
|
693
|
+
bbox_inches="tight")
|
|
694
|
+
|
|
695
|
+
plots[2].savefig(
|
|
696
|
+
f"{self.parameters.validation.visualize}/class_scores.png",
|
|
697
|
+
bbox_inches="tight")
|
|
698
|
+
|
|
699
|
+
plots[3].savefig(
|
|
700
|
+
f"{self.parameters.validation.visualize}/histogram_scores.png",
|
|
701
|
+
bbox_inches="tight")
|
|
702
|
+
|
|
703
|
+
plots[4].savefig(
|
|
704
|
+
f"{self.parameters.validation.visualize}/histogram_ious.png",
|
|
705
|
+
bbox_inches="tight")
|
|
706
|
+
|
|
707
|
+
def publish_plots(
|
|
708
|
+
self, plots: List[matplotlib.figure.Figure], epoch: int = 0):
|
|
709
|
+
"""
|
|
710
|
+
Publishes the validation plots into Tensorboard.
|
|
711
|
+
|
|
712
|
+
Parameters
|
|
713
|
+
----------
|
|
714
|
+
plots: List[matplotlib.figure.Figure]
|
|
715
|
+
This is the list of matplotlib figures to save.
|
|
716
|
+
epoch: int
|
|
717
|
+
The training epoch number used for ModelPack training usage.
|
|
718
|
+
"""
|
|
719
|
+
nimage_confusion_matrix = figure2numpy(plots[0])
|
|
720
|
+
nimage_precision_recall = figure2numpy(plots[1])
|
|
721
|
+
nimage_class = figure2numpy(plots[2])
|
|
722
|
+
nimage_score = figure2numpy(plots[3])
|
|
723
|
+
nimage_iou = figure2numpy(plots[4])
|
|
724
|
+
|
|
725
|
+
self.tensorboard_writer(
|
|
726
|
+
nimage_confusion_matrix,
|
|
727
|
+
f"{self.metrics.metrics.model}_confusion_matrix.png",
|
|
728
|
+
step=epoch
|
|
729
|
+
)
|
|
730
|
+
self.tensorboard_writer(
|
|
731
|
+
nimage_precision_recall,
|
|
732
|
+
f"{self.metrics.metrics.model}_precision_recall.png",
|
|
733
|
+
step=epoch
|
|
734
|
+
)
|
|
735
|
+
self.tensorboard_writer(
|
|
736
|
+
nimage_class,
|
|
737
|
+
f"{self.metrics.metrics.model}_scores.png",
|
|
738
|
+
step=epoch
|
|
739
|
+
)
|
|
740
|
+
self.tensorboard_writer(
|
|
741
|
+
nimage_score,
|
|
742
|
+
f"{self.metrics.metrics.model}_histogram_scores.png",
|
|
743
|
+
step=epoch
|
|
744
|
+
)
|
|
745
|
+
self.tensorboard_writer(
|
|
746
|
+
nimage_iou,
|
|
747
|
+
f"{self.metrics.metrics.model}_histogram_ious.png",
|
|
748
|
+
step=epoch
|
|
749
|
+
)
|