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.
Files changed (73) hide show
  1. deepview/modelpack/utils/argmax.py +16 -0
  2. edgefirst/validator/__init__.py +1 -0
  3. edgefirst/validator/__main__.py +375 -0
  4. edgefirst/validator/datasets/__init__.py +118 -0
  5. edgefirst/validator/datasets/cache.py +296 -0
  6. edgefirst/validator/datasets/core.py +250 -0
  7. edgefirst/validator/datasets/darknet.py +446 -0
  8. edgefirst/validator/datasets/database.py +1067 -0
  9. edgefirst/validator/datasets/instance/__init__.py +4 -0
  10. edgefirst/validator/datasets/instance/core.py +222 -0
  11. edgefirst/validator/datasets/instance/detection.py +145 -0
  12. edgefirst/validator/datasets/instance/multitask.py +80 -0
  13. edgefirst/validator/datasets/instance/segmentation.py +120 -0
  14. edgefirst/validator/datasets/utils/fetch.py +682 -0
  15. edgefirst/validator/datasets/utils/readers.py +425 -0
  16. edgefirst/validator/datasets/utils/transformations.py +1695 -0
  17. edgefirst/validator/evaluators/__init__.py +17 -0
  18. edgefirst/validator/evaluators/callbacks/__init__.py +3 -0
  19. edgefirst/validator/evaluators/callbacks/core.py +192 -0
  20. edgefirst/validator/evaluators/callbacks/plots.py +900 -0
  21. edgefirst/validator/evaluators/callbacks/studio.py +234 -0
  22. edgefirst/validator/evaluators/core.py +257 -0
  23. edgefirst/validator/evaluators/detection.py +749 -0
  24. edgefirst/validator/evaluators/multitask.py +270 -0
  25. edgefirst/validator/evaluators/parameters/__init__.py +53 -0
  26. edgefirst/validator/evaluators/parameters/core.py +554 -0
  27. edgefirst/validator/evaluators/parameters/dataset.py +239 -0
  28. edgefirst/validator/evaluators/parameters/model.py +338 -0
  29. edgefirst/validator/evaluators/parameters/validation.py +528 -0
  30. edgefirst/validator/evaluators/segmentation.py +729 -0
  31. edgefirst/validator/evaluators/utils/__init__.py +3 -0
  32. edgefirst/validator/evaluators/utils/classify.py +292 -0
  33. edgefirst/validator/evaluators/utils/match.py +262 -0
  34. edgefirst/validator/evaluators/utils/timer.py +132 -0
  35. edgefirst/validator/metrics/__init__.py +9 -0
  36. edgefirst/validator/metrics/data/__init__.py +7 -0
  37. edgefirst/validator/metrics/data/label.py +668 -0
  38. edgefirst/validator/metrics/data/metrics.py +759 -0
  39. edgefirst/validator/metrics/data/plots.py +476 -0
  40. edgefirst/validator/metrics/data/stats.py +507 -0
  41. edgefirst/validator/metrics/detection.py +595 -0
  42. edgefirst/validator/metrics/segmentation.py +173 -0
  43. edgefirst/validator/metrics/utils/math.py +717 -0
  44. edgefirst/validator/publishers/__init__.py +3 -0
  45. edgefirst/validator/publishers/console.py +147 -0
  46. edgefirst/validator/publishers/studio.py +128 -0
  47. edgefirst/validator/publishers/tensorboard.py +119 -0
  48. edgefirst/validator/publishers/utils/logger.py +111 -0
  49. edgefirst/validator/publishers/utils/table.py +403 -0
  50. edgefirst/validator/runners/__init__.py +8 -0
  51. edgefirst/validator/runners/core.py +727 -0
  52. edgefirst/validator/runners/deepviewrt.py +177 -0
  53. edgefirst/validator/runners/hailo.py +263 -0
  54. edgefirst/validator/runners/keras.py +150 -0
  55. edgefirst/validator/runners/kinara.py +265 -0
  56. edgefirst/validator/runners/offline.py +228 -0
  57. edgefirst/validator/runners/onnx.py +241 -0
  58. edgefirst/validator/runners/processing/decode.py +320 -0
  59. edgefirst/validator/runners/processing/dvapi.py +4192 -0
  60. edgefirst/validator/runners/processing/nms.py +637 -0
  61. edgefirst/validator/runners/processing/outputs.py +507 -0
  62. edgefirst/validator/runners/tensorrt.py +321 -0
  63. edgefirst/validator/runners/tflite.py +221 -0
  64. edgefirst/validator/validate.py +843 -0
  65. edgefirst/validator/visualize/__init__.py +3 -0
  66. edgefirst/validator/visualize/detection.py +623 -0
  67. edgefirst/validator/visualize/segmentation.py +281 -0
  68. edgefirst/validator/visualize/utils/plots.py +635 -0
  69. edgefirst_validator-4.2.1.dist-info/METADATA +111 -0
  70. edgefirst_validator-4.2.1.dist-info/RECORD +73 -0
  71. edgefirst_validator-4.2.1.dist-info/WHEEL +5 -0
  72. edgefirst_validator-4.2.1.dist-info/entry_points.txt +2 -0
  73. edgefirst_validator-4.2.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,3 @@
1
+ from edgefirst.validator.evaluators.utils.match import Matcher
2
+ from edgefirst.validator.evaluators.utils.timer import TimerContext
3
+ from edgefirst.validator.evaluators.utils.classify import DetectionClassifier
@@ -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