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,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