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,727 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Union, Tuple, List
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
|
|
8
|
+
from edgefirst.validator.datasets.utils.transformations import (xyxy2xcycwh,
|
|
9
|
+
xcycwh2xyxy,
|
|
10
|
+
xyxy2xywh,
|
|
11
|
+
resize,
|
|
12
|
+
preprocess_hal,
|
|
13
|
+
preprocess_native)
|
|
14
|
+
from edgefirst.validator.runners.processing.decode import (decode_mpk_boxes,
|
|
15
|
+
decode_mpk_masks,
|
|
16
|
+
decode_yolo_boxes,
|
|
17
|
+
decode_yolo_masks,
|
|
18
|
+
decode_yolox_boxes,
|
|
19
|
+
crop_masks,
|
|
20
|
+
dequantize)
|
|
21
|
+
from edgefirst.validator.runners.processing.nms import nms, multiclass_nms
|
|
22
|
+
from edgefirst.validator.datasets.utils.fetch import get_shape
|
|
23
|
+
from edgefirst.validator.publishers.utils.logger import logger
|
|
24
|
+
from edgefirst.validator.runners.processing.outputs import Outputs
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from edgefirst.validator.evaluators import ModelParameters, TimerContext
|
|
28
|
+
|
|
29
|
+
DetOutput = Tuple[npt.NDArray[np.float32],
|
|
30
|
+
npt.NDArray[np.float32], npt.NDArray[np.uintp]]
|
|
31
|
+
SegDetOutput = Tuple[npt.NDArray[np.float32],
|
|
32
|
+
npt.NDArray[np.float32],
|
|
33
|
+
npt.NDArray[np.uintp],
|
|
34
|
+
Union[None, npt.NDArray[np.uint8],
|
|
35
|
+
List[npt.NDArray[np.uint8]]]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Runner:
|
|
39
|
+
"""
|
|
40
|
+
Abstract class that provides a template for the other runner classes.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
model: Any
|
|
45
|
+
This is typically the path to the model file or a loaded model.
|
|
46
|
+
parameters: Parameters
|
|
47
|
+
These are the model parameters set from the command line.
|
|
48
|
+
timer: TimerContext
|
|
49
|
+
A timer object for handling validation timings for the model.
|
|
50
|
+
|
|
51
|
+
Raises
|
|
52
|
+
------
|
|
53
|
+
FileNotFoundError
|
|
54
|
+
Raised if the path to the model does not exist.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, model: Any, parameters: ModelParameters,
|
|
58
|
+
timer: TimerContext):
|
|
59
|
+
self.model = model
|
|
60
|
+
self.parameters = parameters
|
|
61
|
+
self.timer = timer
|
|
62
|
+
|
|
63
|
+
self.num_boxes = 0 # The number of boxes in the model output shape.
|
|
64
|
+
self.graph_name = "main_graph"
|
|
65
|
+
self.outputs = None
|
|
66
|
+
self.decoder = None
|
|
67
|
+
|
|
68
|
+
def init_decoder(
|
|
69
|
+
self, metadata: dict, outputs: Union[List[dict], List[np.ndarray]]):
|
|
70
|
+
"""
|
|
71
|
+
Parse the model metadata and initialize the HAL decoder.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
metadata: dict
|
|
76
|
+
The contents of the model metadata for decoding the outputs.
|
|
77
|
+
outputs: Union[List[dict], List[np.ndarray]]
|
|
78
|
+
This is either a List[dict] from a TFLite output details
|
|
79
|
+
or a List[np.ndarray] containing the shapes from the model outputs.
|
|
80
|
+
"""
|
|
81
|
+
self.type = self.get_input_type()
|
|
82
|
+
shape = self.get_input_shape()
|
|
83
|
+
# Avoid shape [None, height, width, 3]
|
|
84
|
+
self.shape = np.array([d if d is not None else 1 for d in shape])
|
|
85
|
+
|
|
86
|
+
# Transpose the image to meet requirements of the channel order.
|
|
87
|
+
if shape[-1] in [2, 3, 4]:
|
|
88
|
+
height, width = shape[1:3]
|
|
89
|
+
channels = shape[-1]
|
|
90
|
+
else:
|
|
91
|
+
height, width = shape[2:4]
|
|
92
|
+
channels = shape[1]
|
|
93
|
+
self.parameters.common.transpose = True
|
|
94
|
+
|
|
95
|
+
self.parameters.common.dtype = self.type
|
|
96
|
+
self.parameters.common.shape = self.shape
|
|
97
|
+
|
|
98
|
+
# Parse the model output details in the metadata.
|
|
99
|
+
self.outputs = Outputs(
|
|
100
|
+
metadata=metadata,
|
|
101
|
+
parameters=self.parameters,
|
|
102
|
+
outputs=outputs
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if self.parameters.nms == "hal":
|
|
106
|
+
try:
|
|
107
|
+
import edgefirst_python # type: ignore
|
|
108
|
+
self.decoder = edgefirst_python.Decoder(
|
|
109
|
+
self.outputs.metadata,
|
|
110
|
+
score_threshold=self.parameters.score_threshold,
|
|
111
|
+
iou_threshold=self.parameters.iou_threshold
|
|
112
|
+
)
|
|
113
|
+
except ImportError:
|
|
114
|
+
raise ImportError(
|
|
115
|
+
"EdgeFirst HAL is needed to perform decoding using hal."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if self.parameters.common.backend == "hal":
|
|
119
|
+
try:
|
|
120
|
+
import edgefirst_python # type: ignore
|
|
121
|
+
if self.parameters.common.transpose:
|
|
122
|
+
if channels == 2:
|
|
123
|
+
raise NotImplementedError(
|
|
124
|
+
"NV16 format is currently not supported in HAL for " +
|
|
125
|
+
f"model input shape: {self.shape}")
|
|
126
|
+
elif channels == 4:
|
|
127
|
+
logger(
|
|
128
|
+
"PLANAR_RGBA is currently not supported in HAL for " +
|
|
129
|
+
f"model input shape: {self.shape}", code="WARNING"
|
|
130
|
+
)
|
|
131
|
+
fourcc = edgefirst_python.FourCC.PLANAR_RGB
|
|
132
|
+
else:
|
|
133
|
+
if channels == 2:
|
|
134
|
+
fourcc = edgefirst_python.FourCC.YUYV
|
|
135
|
+
else:
|
|
136
|
+
fourcc = edgefirst_python.FourCC.RGBA
|
|
137
|
+
self.parameters.common.input_dst = edgefirst_python.TensorImage(
|
|
138
|
+
width, height, fourcc
|
|
139
|
+
)
|
|
140
|
+
except ImportError:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
"EdgeFirst HAL is needed to perform preprocessing using hal."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def warmup(self):
|
|
146
|
+
"""
|
|
147
|
+
Run model warmup.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
logger("Running model warmup...", code="INFO")
|
|
151
|
+
|
|
152
|
+
times = []
|
|
153
|
+
height, width = get_shape(self.shape)
|
|
154
|
+
|
|
155
|
+
for _ in range(self.parameters.warmup):
|
|
156
|
+
start = time.perf_counter()
|
|
157
|
+
# Warmup input preprocessing.
|
|
158
|
+
if self.parameters.common.backend == "hal":
|
|
159
|
+
import edgefirst_python # type: ignore
|
|
160
|
+
image, _, _, _ = preprocess_hal(
|
|
161
|
+
image=edgefirst_python.TensorImage(width, height),
|
|
162
|
+
shape=self.shape,
|
|
163
|
+
input_type=self.type,
|
|
164
|
+
dst=self.parameters.common.input_dst,
|
|
165
|
+
input_tensor=self.parameters.common.input_tensor,
|
|
166
|
+
transpose=self.parameters.common.transpose,
|
|
167
|
+
preprocessing=self.parameters.common.preprocessing,
|
|
168
|
+
normalization=self.parameters.common.norm,
|
|
169
|
+
quantization=self.parameters.common.input_quantization,
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
image, _, _, _ = preprocess_native(
|
|
173
|
+
image=np.zeros((height, width, 3), dtype=np.uint8),
|
|
174
|
+
shape=self.shape,
|
|
175
|
+
input_type=self.type,
|
|
176
|
+
input_tensor=self.parameters.common.input_tensor,
|
|
177
|
+
transpose=self.parameters.common.transpose,
|
|
178
|
+
preprocessing=self.parameters.common.preprocessing,
|
|
179
|
+
normalization=self.parameters.common.norm,
|
|
180
|
+
quantization=self.parameters.common.input_quantization,
|
|
181
|
+
backend=self.parameters.common.backend,
|
|
182
|
+
)
|
|
183
|
+
self.run_single_instance(image)
|
|
184
|
+
|
|
185
|
+
elapsed = time.perf_counter() - start
|
|
186
|
+
times.append(elapsed * 1e3) # Convert to ms
|
|
187
|
+
|
|
188
|
+
message = "model warmup took %f ms (%f ms avg)" % (np.sum(times),
|
|
189
|
+
np.average(times))
|
|
190
|
+
logger(message, code="INFO")
|
|
191
|
+
self.timer.reset()
|
|
192
|
+
|
|
193
|
+
def run_single_instance(self, image: Union[str, np.ndarray]) -> Any:
|
|
194
|
+
"""Abstract Method"""
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def postprocessing(self, outputs: Union[list, np.ndarray]) -> Any:
|
|
198
|
+
"""
|
|
199
|
+
Postprocess outputs into boxes, scores, labels or masks.
|
|
200
|
+
This method will perform NMS operations where the outputs
|
|
201
|
+
will be transformed into the following format.
|
|
202
|
+
|
|
203
|
+
Models trained using ModelPack separates the outputs and
|
|
204
|
+
directly return the NMS bounding boxes, scores, and
|
|
205
|
+
labels as described below.
|
|
206
|
+
|
|
207
|
+
Models converted in YOLOv5 will be a list of length 1 which
|
|
208
|
+
has a shape of (1, number of boxes, 6) and formatted as
|
|
209
|
+
[[[xmin, ymin, xmax, ymax, confidence, label], [...], ...]].
|
|
210
|
+
|
|
211
|
+
Models converted in YOLOv7 will directly extract the
|
|
212
|
+
bounding boxes, scores, and labels from the output.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
outputs: Union[list, np.ndarray]
|
|
217
|
+
ModelPack outputs will be a list with varying lengths
|
|
218
|
+
which could either contain bounding boxes, labels, scores,
|
|
219
|
+
or masks (encoded and decoded).
|
|
220
|
+
|
|
221
|
+
Models converted in YOLOv5 has the following shape
|
|
222
|
+
(batch size, number of boxes, number of classes).
|
|
223
|
+
|
|
224
|
+
Models converted in YOLOv7 will already have NMS embedded. The
|
|
225
|
+
output has a shape of (number of boxes, 7) and formatted as
|
|
226
|
+
[[batch_id, xmin, ymin, xmax, ymax, cls, score], ...].
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
Any
|
|
231
|
+
This could either return detection outputs after NMS.
|
|
232
|
+
np.ndarray
|
|
233
|
+
The prediction bounding boxes.. [[box1], [box2], ...].
|
|
234
|
+
np.ndarray
|
|
235
|
+
The prediction labels.. [cl1, cl2, ...].
|
|
236
|
+
np.ndarray
|
|
237
|
+
The prediction confidence scores.. [score, score, ...]
|
|
238
|
+
normalized between 0 and 1.
|
|
239
|
+
This could also return segmentation masks.
|
|
240
|
+
np.ndarray
|
|
241
|
+
"""
|
|
242
|
+
# MobileNet SSD
|
|
243
|
+
if self.outputs.classes["index"] is not None:
|
|
244
|
+
with self.timer.time("output"):
|
|
245
|
+
output = outputs[0] if len(outputs) == 1 else outputs
|
|
246
|
+
output = output.numpy() if not isinstance(
|
|
247
|
+
output, (np.ndarray, list)) else output
|
|
248
|
+
|
|
249
|
+
boxes, classes, scores, _ = output
|
|
250
|
+
boxes = np.squeeze(boxes)
|
|
251
|
+
classes = np.squeeze(classes).astype(np.int32)
|
|
252
|
+
scores = np.squeeze(scores)
|
|
253
|
+
# ModelPack or Kinara
|
|
254
|
+
elif len(self.outputs.mpk_types) or (None not in
|
|
255
|
+
[self.outputs.boxes["index"],
|
|
256
|
+
self.outputs.scores["index"]]):
|
|
257
|
+
with self.timer.time("output"):
|
|
258
|
+
# Kinara
|
|
259
|
+
if self.outputs.boxes["decoder"] == "yolov8":
|
|
260
|
+
if self.parameters.nms == "hal":
|
|
261
|
+
boxes, scores, classes, masks = self.decoder.decode(
|
|
262
|
+
outputs)
|
|
263
|
+
|
|
264
|
+
h, w = get_shape(self.shape)
|
|
265
|
+
normalized_conf = np.mean((boxes >= 0) & (boxes <= 1))
|
|
266
|
+
if normalized_conf < 0.80:
|
|
267
|
+
boxes[:, [0, 2]] /= w
|
|
268
|
+
boxes[:, [1, 3]] /= h
|
|
269
|
+
else:
|
|
270
|
+
outputs = np.concatenate(outputs, axis=1)
|
|
271
|
+
# Process YOLOv8 detection with shape [1, 84, 8400].
|
|
272
|
+
boxes, classes, scores, masks = self.process_yolo(
|
|
273
|
+
[outputs])
|
|
274
|
+
|
|
275
|
+
# ModelPack
|
|
276
|
+
else:
|
|
277
|
+
if self.parameters.nms == "hal":
|
|
278
|
+
boxes, classes, scores, masks = self.process_mpk_hal(
|
|
279
|
+
outputs)
|
|
280
|
+
else:
|
|
281
|
+
boxes, classes, scores, masks = self.process_mpk(
|
|
282
|
+
outputs)
|
|
283
|
+
else:
|
|
284
|
+
# YOLOx
|
|
285
|
+
if (self.graph_name not in ["main_graph", "torch_jit", "tf2onnx"]
|
|
286
|
+
and outputs[0].shape[-1] == 85):
|
|
287
|
+
|
|
288
|
+
# HAL decoder/NMS is not yet supported and fallback to NumPy.
|
|
289
|
+
if self.parameters.nms == "hal":
|
|
290
|
+
self.parameters.nms = "numpy"
|
|
291
|
+
|
|
292
|
+
with self.timer.time("output"):
|
|
293
|
+
boxes, classes, scores = self.process_yolox(
|
|
294
|
+
outputs=outputs)
|
|
295
|
+
|
|
296
|
+
# YOLOv5, YOLOv8, YOLOv11 models.
|
|
297
|
+
else:
|
|
298
|
+
with self.timer.time("output"):
|
|
299
|
+
# Decoded outputs.
|
|
300
|
+
if len(outputs) == 1 and outputs[0].shape == (1, 300, 6):
|
|
301
|
+
height, width = get_shape(self.shape)
|
|
302
|
+
output = outputs[0].squeeze()
|
|
303
|
+
scores = output[:, 4]
|
|
304
|
+
classes = output[:, 5]
|
|
305
|
+
boxes = output[:, 0:4]
|
|
306
|
+
|
|
307
|
+
# Filter out all zero rows.
|
|
308
|
+
filt = ~np.all(boxes[:, 0:4] == 0, axis=1)
|
|
309
|
+
boxes = boxes[filt]
|
|
310
|
+
scores = scores[filt]
|
|
311
|
+
classes = classes[filt]
|
|
312
|
+
# Normalize bounding boxes if not already.
|
|
313
|
+
if boxes.shape[0] > 0:
|
|
314
|
+
normalized_conf = np.mean(
|
|
315
|
+
(boxes >= 0) & (boxes <= 1))
|
|
316
|
+
if normalized_conf < 0.80:
|
|
317
|
+
boxes[..., [0, 2]] /= width
|
|
318
|
+
boxes[..., [1, 3]] /= height
|
|
319
|
+
# No masks from this output shape.
|
|
320
|
+
masks = None
|
|
321
|
+
elif self.parameters.nms == "hal":
|
|
322
|
+
boxes, classes, scores, masks = self.process_yolo_hal(
|
|
323
|
+
outputs)
|
|
324
|
+
else:
|
|
325
|
+
boxes, classes, scores, masks = self.process_yolo(
|
|
326
|
+
outputs)
|
|
327
|
+
|
|
328
|
+
if self.parameters.common.with_boxes:
|
|
329
|
+
if self.parameters.box_format == "xcycwh":
|
|
330
|
+
boxes = xyxy2xcycwh(boxes)
|
|
331
|
+
elif self.parameters.box_format == "xywh":
|
|
332
|
+
boxes = xyxy2xywh(boxes)
|
|
333
|
+
|
|
334
|
+
if self.parameters.label_offset != 0:
|
|
335
|
+
classes += self.parameters.label_offset
|
|
336
|
+
|
|
337
|
+
if self.parameters.common.with_masks:
|
|
338
|
+
return boxes, classes, scores, masks
|
|
339
|
+
else:
|
|
340
|
+
return boxes, classes, scores
|
|
341
|
+
else:
|
|
342
|
+
return masks
|
|
343
|
+
|
|
344
|
+
def process_mpk_hal(self, outputs: List[np.ndarray]):
|
|
345
|
+
"""
|
|
346
|
+
ModelPack output decoding and postprocessing using the HAL decoder.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
outputs: List[np.ndarray]
|
|
351
|
+
ModelPack outputs will be a list with varying lengths
|
|
352
|
+
which could either contain bounding boxes, labels, scores,
|
|
353
|
+
or masks (encoded and decoded).
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
boxes : np.ndarray
|
|
358
|
+
This contains decoded and valid bounding boxes post NMS.
|
|
359
|
+
classes : np.ndarray
|
|
360
|
+
This contains decoded and valid classes post NMS.
|
|
361
|
+
scores : np.ndarray
|
|
362
|
+
This contains decoded and valid scores post NMS.
|
|
363
|
+
masks: np.ndarray
|
|
364
|
+
This is the semantic segmentation mask output from
|
|
365
|
+
ModelPack. If the model is not a segmentation model,
|
|
366
|
+
None is returned.
|
|
367
|
+
"""
|
|
368
|
+
masks = None
|
|
369
|
+
for context in self.outputs.mpk_types:
|
|
370
|
+
if context["type"] == "segmentation":
|
|
371
|
+
masks = outputs[context["index"]]
|
|
372
|
+
|
|
373
|
+
boxes, scores, classes, masks = self.decoder.decode(
|
|
374
|
+
outputs, max_boxes=self.parameters.max_detections)
|
|
375
|
+
|
|
376
|
+
height, width = get_shape(self.shape)
|
|
377
|
+
# Decoded masks.
|
|
378
|
+
if self.outputs.masks["index"] is not None:
|
|
379
|
+
masks = np.squeeze(
|
|
380
|
+
outputs[self.outputs.masks["index"]]).astype(np.uint8)
|
|
381
|
+
# Resize the mask to the model input shape.
|
|
382
|
+
masks = resize(masks, size=(width, height))
|
|
383
|
+
# Deploy argmax to the mask and convert to semantic segmentation.
|
|
384
|
+
elif len(masks):
|
|
385
|
+
masks = self.decoder.segmentation_to_mask(masks[0])
|
|
386
|
+
# Resize the mask to the model input shape.
|
|
387
|
+
masks = resize(masks, size=(width, height))
|
|
388
|
+
|
|
389
|
+
return boxes, classes, scores, masks
|
|
390
|
+
|
|
391
|
+
def process_mpk(self, outputs: List[np.ndarray]) -> SegDetOutput:
|
|
392
|
+
"""
|
|
393
|
+
ModelPack output decoding and postprocessing.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
outputs: List[np.ndarray]
|
|
398
|
+
ModelPack outputs will be a list with varying lengths
|
|
399
|
+
which could either contain bounding boxes, labels, scores,
|
|
400
|
+
or masks (encoded and decoded).
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
boxes : np.ndarray
|
|
405
|
+
This contains decoded and valid bounding boxes post NMS.
|
|
406
|
+
classes : np.ndarray
|
|
407
|
+
This contains decoded and valid classes post NMS.
|
|
408
|
+
scores : np.ndarray
|
|
409
|
+
This contains decoded and valid scores post NMS.
|
|
410
|
+
masks: np.ndarray
|
|
411
|
+
This is the semantic segmentation mask output from
|
|
412
|
+
ModelPack. If the model is not a segmentation model,
|
|
413
|
+
None is returned.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
output = outputs[0] if len(outputs) == 1 else outputs
|
|
417
|
+
output = output.numpy() if not isinstance(
|
|
418
|
+
output, (np.ndarray, list)) else output
|
|
419
|
+
|
|
420
|
+
# Fetch only (height, width) from the shape.
|
|
421
|
+
height, width = get_shape(self.shape)
|
|
422
|
+
|
|
423
|
+
boxes, scores = [], []
|
|
424
|
+
classes, masks = None, None
|
|
425
|
+
for context in self.outputs.mpk_types:
|
|
426
|
+
x = output[context["index"]]
|
|
427
|
+
if context["quantization"] is not None and x.dtype != np.float32:
|
|
428
|
+
x = dequantize(x, *context["quantization"])
|
|
429
|
+
|
|
430
|
+
if context["type"] == "detection":
|
|
431
|
+
box, score = decode_mpk_boxes(p=x, anchors=context["anchors"])
|
|
432
|
+
boxes.append(box)
|
|
433
|
+
scores.append(score)
|
|
434
|
+
|
|
435
|
+
elif context["type"] == "segmentation":
|
|
436
|
+
masks = np.squeeze(decode_mpk_masks(masks=x))
|
|
437
|
+
masks = resize(masks, size=(width, height),
|
|
438
|
+
backend=self.parameters.common.backend)
|
|
439
|
+
|
|
440
|
+
# The boxes and scores are already decoded.
|
|
441
|
+
if None not in [self.outputs.boxes["index"],
|
|
442
|
+
self.outputs.scores["index"]]:
|
|
443
|
+
|
|
444
|
+
boxes = output[self.outputs.boxes["index"]]
|
|
445
|
+
if (self.outputs.boxes["quantization"] is not None and
|
|
446
|
+
boxes.dtype != np.float32):
|
|
447
|
+
boxes = dequantize(boxes, *self.outputs.boxes["quantization"])
|
|
448
|
+
|
|
449
|
+
scores = output[self.outputs.scores["index"]]
|
|
450
|
+
if (self.outputs.scores["quantization"] is not None and
|
|
451
|
+
scores.dtype != np.float32):
|
|
452
|
+
scores = dequantize(
|
|
453
|
+
scores, *self.outputs.scores["quantization"])
|
|
454
|
+
|
|
455
|
+
if 0 not in [len(boxes), len(scores)]:
|
|
456
|
+
scores = np.concatenate(scores, axis=1).astype(np.float32)
|
|
457
|
+
boxes = np.concatenate(boxes, axis=1).astype(np.float32)
|
|
458
|
+
if scores.shape[0] == 1:
|
|
459
|
+
scores = np.squeeze(scores, axis=0)
|
|
460
|
+
if boxes.shape[0] == 1:
|
|
461
|
+
boxes = np.squeeze(boxes, axis=0)
|
|
462
|
+
|
|
463
|
+
boxes, classes, scores, _ = nms(
|
|
464
|
+
boxes=boxes,
|
|
465
|
+
scores=scores,
|
|
466
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
467
|
+
score_threshold=self.parameters.score_threshold,
|
|
468
|
+
max_detections=self.parameters.max_detections,
|
|
469
|
+
class_agnostic=self.parameters.agnostic_nms,
|
|
470
|
+
nms_type=self.parameters.nms
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Normalize bounding boxes if not already.
|
|
474
|
+
if boxes.shape[0] > 0:
|
|
475
|
+
normalized_conf = np.mean((boxes >= 0) & (boxes <= 1))
|
|
476
|
+
if normalized_conf < 0.80:
|
|
477
|
+
boxes[..., [0, 2]] /= width
|
|
478
|
+
boxes[..., [1, 3]] /= height
|
|
479
|
+
|
|
480
|
+
# Decoded masks.
|
|
481
|
+
if masks is None and self.outputs.masks["index"] is not None:
|
|
482
|
+
masks = np.squeeze(
|
|
483
|
+
output[self.outputs.masks["index"]]).astype(np.uint8)
|
|
484
|
+
masks = resize(masks, size=(width, height),
|
|
485
|
+
backend=self.parameters.common.backend)
|
|
486
|
+
|
|
487
|
+
return boxes, classes, scores, masks
|
|
488
|
+
|
|
489
|
+
def process_yolo_hal(self, outputs: List[np.ndarray]) -> SegDetOutput:
|
|
490
|
+
"""
|
|
491
|
+
Utralytics YOLO output decoding and postprocessing using the
|
|
492
|
+
HAL decoder.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
outputs: List[np.ndarray]
|
|
497
|
+
Models converted in YOLOv5 has the following shape
|
|
498
|
+
(batch size, number of boxes, number of classes) for detection.
|
|
499
|
+
|
|
500
|
+
Models converted in YOLOv7 will already have NMS embedded. The
|
|
501
|
+
output has a shape of (number of boxes, 7) and formatted as
|
|
502
|
+
[[batch_id, xmin, ymin, xmax, ymax, cls, score], ...].
|
|
503
|
+
|
|
504
|
+
For segmentation models, this will contain two arrays containing
|
|
505
|
+
the detection and segmentation outputs.
|
|
506
|
+
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
boxes : np.ndarray
|
|
510
|
+
This contains decoded and valid bounding boxes post NMS.
|
|
511
|
+
classes : np.ndarray
|
|
512
|
+
This contains decoded and valid classes post NMS.
|
|
513
|
+
scores : np.ndarray
|
|
514
|
+
This contains decoded and valid scores post NMS.
|
|
515
|
+
masks: np.ndarray
|
|
516
|
+
This is the instance segmentation mask outputs from
|
|
517
|
+
Ultralytics. If the model is not a segmentation model,
|
|
518
|
+
None is returned.
|
|
519
|
+
"""
|
|
520
|
+
h, w = get_shape(self.shape)
|
|
521
|
+
|
|
522
|
+
# NOTE: HAL Decoder requires normalized bounding boxes.
|
|
523
|
+
x = outputs[self.outputs.scores["index"]]
|
|
524
|
+
if x.shape[0] > 0 and x.dtype in [np.float32, np.float16]:
|
|
525
|
+
normalized_conf = np.mean((x[:, :4, :] >= 0) & (x[:, :4, :] <= 1))
|
|
526
|
+
if normalized_conf < 0.80:
|
|
527
|
+
x[:, [0, 2], :] /= w
|
|
528
|
+
x[:, [1, 3], :] /= h
|
|
529
|
+
outputs[self.outputs.scores["index"]] = x
|
|
530
|
+
|
|
531
|
+
# Transpose proto masks into the proper shape.
|
|
532
|
+
# NOTE: HAL decoder requires shape [1, 160, 160, 32]
|
|
533
|
+
if self.outputs.masks["index"] is not None:
|
|
534
|
+
masks = outputs[self.outputs.masks["index"]]
|
|
535
|
+
if masks.shape[1] == 32:
|
|
536
|
+
outputs[self.outputs.masks["index"]] = np.transpose(
|
|
537
|
+
masks, (0, 2, 3, 1))
|
|
538
|
+
|
|
539
|
+
boxes, scores, classes, masks = self.decoder.decode(
|
|
540
|
+
outputs, max_boxes=self.parameters.max_detections
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Filter invalid 0-dimension boxes.
|
|
544
|
+
valid = np.where((boxes[..., 0] < boxes[..., 2]) &
|
|
545
|
+
(boxes[..., 1] < boxes[..., 3]))[0]
|
|
546
|
+
boxes = boxes[valid]
|
|
547
|
+
scores = scores[valid]
|
|
548
|
+
classes = classes[valid]
|
|
549
|
+
if len(masks) > 0:
|
|
550
|
+
masks = [masks[i] for i in valid]
|
|
551
|
+
|
|
552
|
+
# Paint masks onto a fixed shape NumPy array canvas.
|
|
553
|
+
full_masks = []
|
|
554
|
+
for b, m in zip(boxes, masks):
|
|
555
|
+
# Resize the mask into the input shape of the model.
|
|
556
|
+
mask_width = round((b[2] - b[0]) * w)
|
|
557
|
+
mask_height = round((b[3] - b[1]) * h)
|
|
558
|
+
m = resize(m, size=(mask_width, mask_height))
|
|
559
|
+
mask = np.zeros((w, h, 1), dtype=np.uint8)
|
|
560
|
+
left = round(b[0] * w)
|
|
561
|
+
top = round(b[1] * h)
|
|
562
|
+
mask[top:(top + mask_height), left:(left + mask_width), 0] = m
|
|
563
|
+
|
|
564
|
+
# Run Argmax on the masks.
|
|
565
|
+
mask = self.decoder.segmentation_to_mask(mask)
|
|
566
|
+
full_masks.append(mask)
|
|
567
|
+
|
|
568
|
+
if len(full_masks):
|
|
569
|
+
masks = np.stack(full_masks, axis=0)
|
|
570
|
+
return boxes, classes, scores, masks
|
|
571
|
+
|
|
572
|
+
def process_yolo(self, outputs: List[np.ndarray]) -> SegDetOutput:
|
|
573
|
+
"""
|
|
574
|
+
Utralytics YOLO output decoding and postprocessing.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
outputs: List[np.ndarray]
|
|
579
|
+
Models converted in YOLOv5 has the following shape
|
|
580
|
+
(batch size, number of boxes, number of classes) for detection.
|
|
581
|
+
|
|
582
|
+
Models converted in YOLOv7 will already have NMS embedded. The
|
|
583
|
+
output has a shape of (number of boxes, 7) and formatted as
|
|
584
|
+
[[batch_id, xmin, ymin, xmax, ymax, cls, score], ...].
|
|
585
|
+
|
|
586
|
+
For segmentation models, this will contain two arrays containing
|
|
587
|
+
the detection and segmentation outputs.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
boxes : np.ndarray
|
|
592
|
+
This contains decoded and valid bounding boxes post NMS.
|
|
593
|
+
classes : np.ndarray
|
|
594
|
+
This contains decoded and valid classes post NMS.
|
|
595
|
+
scores : np.ndarray
|
|
596
|
+
This contains decoded and valid scores post NMS.
|
|
597
|
+
masks: np.ndarray
|
|
598
|
+
This is the instance segmentation mask outputs from
|
|
599
|
+
Ultralytics. If the model is not a segmentation model,
|
|
600
|
+
None is returned.
|
|
601
|
+
"""
|
|
602
|
+
output = outputs[0] if len(outputs) == 1 else outputs
|
|
603
|
+
output = output.numpy() if not isinstance(
|
|
604
|
+
output, (np.ndarray, list)) else output
|
|
605
|
+
|
|
606
|
+
# Fetch only (height, width) from the shape.
|
|
607
|
+
h, w = get_shape(self.shape)
|
|
608
|
+
|
|
609
|
+
x = output[self.outputs.scores["index"]]
|
|
610
|
+
if x.dtype == np.float16:
|
|
611
|
+
x = x.astype(np.float32)
|
|
612
|
+
if (self.outputs.scores["quantization"] is not None and
|
|
613
|
+
x.dtype != np.float32):
|
|
614
|
+
x = dequantize(x, *self.outputs.scores["quantization"])
|
|
615
|
+
|
|
616
|
+
boxes, scores, masks = decode_yolo_boxes(
|
|
617
|
+
p=x,
|
|
618
|
+
with_masks=self.parameters.common.with_masks,
|
|
619
|
+
nc=len(self.parameters.labels)
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
boxes, classes, scores, masks = nms(
|
|
623
|
+
boxes=boxes,
|
|
624
|
+
scores=scores,
|
|
625
|
+
masks=masks,
|
|
626
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
627
|
+
score_threshold=self.parameters.score_threshold,
|
|
628
|
+
max_detections=self.parameters.max_detections,
|
|
629
|
+
class_agnostic=self.parameters.agnostic_nms,
|
|
630
|
+
nms_type=self.parameters.nms
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Normalize bounding boxes if not already.
|
|
634
|
+
if boxes.shape[0] > 0:
|
|
635
|
+
normalized_conf = np.mean((boxes >= 0) & (boxes <= 1))
|
|
636
|
+
if not normalized_conf >= 0.80:
|
|
637
|
+
boxes[..., [0, 2]] /= w
|
|
638
|
+
boxes[..., [1, 3]] /= h
|
|
639
|
+
|
|
640
|
+
# Decode Masks.
|
|
641
|
+
if masks is not None:
|
|
642
|
+
protos = output[self.outputs.masks["index"]]
|
|
643
|
+
if protos.dtype == np.float16:
|
|
644
|
+
protos = protos.astype(np.float32)
|
|
645
|
+
if (self.outputs.masks["quantization"] is not None and
|
|
646
|
+
protos.dtype != np.float32):
|
|
647
|
+
protos = dequantize(
|
|
648
|
+
protos, *self.outputs.masks["quantization"])
|
|
649
|
+
masks = decode_yolo_masks(masks, protos=protos)
|
|
650
|
+
|
|
651
|
+
# Mask postprocessing: resize + crop.
|
|
652
|
+
if masks.shape[0] > 0:
|
|
653
|
+
masks = (masks > 0).astype(np.uint8)
|
|
654
|
+
masks = [resize(mask, size=(w, h),
|
|
655
|
+
backend=self.parameters.common.backend)
|
|
656
|
+
for mask in masks]
|
|
657
|
+
masks = np.stack(masks, axis=0)
|
|
658
|
+
masks = crop_masks(masks, boxes)
|
|
659
|
+
|
|
660
|
+
return boxes, classes, scores, masks
|
|
661
|
+
|
|
662
|
+
def process_yolox(self, outputs: List[np.ndarray]) -> DetOutput:
|
|
663
|
+
"""
|
|
664
|
+
YOLOx output decoding and postprocessing.
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
outputs: List[np.ndarray]
|
|
669
|
+
YOLOx raw output to postprocess into
|
|
670
|
+
bounding boxes, classes, scores after NMS. This
|
|
671
|
+
typically has the shape (1, 8400, 85).
|
|
672
|
+
|
|
673
|
+
Returns
|
|
674
|
+
-------
|
|
675
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
676
|
+
np.ndarray
|
|
677
|
+
The prediction bounding boxes.. [[box1], [box2], ...].
|
|
678
|
+
np.ndarray
|
|
679
|
+
The prediction labels.. [cl1, cl2, ...].
|
|
680
|
+
np.ndarray
|
|
681
|
+
The prediction confidence scores.. [score, score, ...]
|
|
682
|
+
normalized between 0 and 1.
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
output = outputs[0] if len(outputs) == 1 else outputs
|
|
686
|
+
output = output.numpy() if not isinstance(
|
|
687
|
+
output, (np.ndarray, list)) else output
|
|
688
|
+
|
|
689
|
+
height, width = get_shape(self.shape)
|
|
690
|
+
if (self.outputs.scores["quantization"] is not None and
|
|
691
|
+
output.dtype != np.float32):
|
|
692
|
+
output = dequantize(output, *self.outputs.scores["quantization"])
|
|
693
|
+
|
|
694
|
+
boxes, scores = decode_yolox_boxes(
|
|
695
|
+
p=output,
|
|
696
|
+
shape=(height, width)
|
|
697
|
+
)
|
|
698
|
+
boxes = xcycwh2xyxy(boxes=boxes)
|
|
699
|
+
|
|
700
|
+
# Typical: nms_thr=0.45, score_thr=0.1
|
|
701
|
+
dets = multiclass_nms(
|
|
702
|
+
boxes=boxes,
|
|
703
|
+
scores=scores,
|
|
704
|
+
iou_threshold=self.parameters.iou_threshold,
|
|
705
|
+
score_threshold=self.parameters.score_threshold,
|
|
706
|
+
max_detections=self.parameters.max_detections,
|
|
707
|
+
class_agnostic=self.parameters.agnostic_nms,
|
|
708
|
+
nms_type=self.parameters.nms
|
|
709
|
+
)
|
|
710
|
+
if dets is None:
|
|
711
|
+
return np.array([]), np.array([]), np.array([])
|
|
712
|
+
|
|
713
|
+
boxes = dets[:, :4]
|
|
714
|
+
scores = dets[:, 4]
|
|
715
|
+
classes = dets[:, 5]
|
|
716
|
+
|
|
717
|
+
# Normalize the bounding boxes.
|
|
718
|
+
boxes /= np.array([width, height, width, height])
|
|
719
|
+
return boxes, classes, scores
|
|
720
|
+
|
|
721
|
+
def get_input_type(self) -> str:
|
|
722
|
+
"""Abstract Method"""
|
|
723
|
+
pass
|
|
724
|
+
|
|
725
|
+
def get_input_shape(self) -> np.ndarray:
|
|
726
|
+
"""Abstract Method"""
|
|
727
|
+
pass
|