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,1067 @@
1
+ """
2
+ Implementations for reading LMDB databases.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import json
9
+ import glob
10
+ from typing import TYPE_CHECKING, Union, Tuple
11
+
12
+ import lmdb
13
+ import numpy as np
14
+ import polars as pl
15
+
16
+ from edgefirst.validator.datasets.utils.fetch import get_shape
17
+ from edgefirst.validator.publishers.utils.logger import logger
18
+ from edgefirst.validator.datasets.utils.transformations import (scale,
19
+ preprocess_hal,
20
+ preprocess_native,
21
+ format_segments,
22
+ resample_segments)
23
+ from edgefirst.validator.datasets import Dataset
24
+ from edgefirst.validator.datasets import (SegmentationInstance,
25
+ DetectionInstance,
26
+ MultitaskInstance)
27
+
28
+ if TYPE_CHECKING:
29
+ from edgefirst.validator.evaluators import DatasetParameters, TimerContext
30
+ from edgefirst.validator.datasets import Instance
31
+
32
+
33
+ class EdgeFirstDatabase(Dataset):
34
+ def __init__(
35
+ self,
36
+ source: str,
37
+ parameters: DatasetParameters,
38
+ timer: TimerContext,
39
+ info_dataset: dict = None,
40
+ sessions: Union[list, tuple] = None,
41
+ ):
42
+ """
43
+ Reads EdgeFirst Database/Datasets.
44
+
45
+ Parameters
46
+ ----------
47
+ source: str
48
+ This is the path to the Arrow file containing the
49
+ annotations of EdgeFirst Datasets.
50
+ parameters: DatasetParameters
51
+ This contains dataset parameters set from the command line.
52
+ timer: TimerContext
53
+ A timer object for handling validation timings for the model.
54
+ info_dataset: dict
55
+ Contains information such as:
56
+
57
+ .. code-block:: python
58
+
59
+ {
60
+ "classes": [list of unique labels],
61
+ "validation":
62
+ {
63
+ "images: 'path to the images',
64
+ "annotations": 'path to the annotations'
65
+ }
66
+ }
67
+
68
+ *Note: the classes are optional and the path to the images
69
+ and annotations can be the same.*
70
+ sessions: Union[list, tuple]
71
+ Filter to only use the specified sessions. By default, use
72
+ all the sessions.
73
+
74
+ Raises
75
+ ------
76
+ FileNotFoundError
77
+ Raised if the source provided does not exist.
78
+ ValueError
79
+ Raised if the dataset does not contain any files.
80
+ """
81
+ # Locate the arrow file.
82
+ source = glob.glob(source if source.endswith(
83
+ "*.arrow") else os.path.join(source, "*.arrow"))[0]
84
+
85
+ super(EdgeFirstDatabase, self).__init__(
86
+ source=source,
87
+ parameters=parameters,
88
+ timer=timer,
89
+ info_dataset=info_dataset
90
+ )
91
+
92
+ if not os.path.exists(self.source):
93
+ raise FileNotFoundError("The dataset *.arrow file does not exist.")
94
+
95
+ self.root_folder = os.path.dirname(self.source)
96
+ if not os.path.exists(self.root_folder):
97
+ raise FileNotFoundError(
98
+ "Dataset folder was not found at: ", self.root_folder)
99
+
100
+ # Find all images.
101
+ self.all_images = glob.glob(
102
+ os.path.join(self.root_folder, "**", "*"),
103
+ recursive=True)
104
+
105
+ self.all_images_dict = {}
106
+ for image in self.all_images:
107
+ if not os.path.isfile(image) or \
108
+ image.endswith(".txt") or \
109
+ image.endswith(".mask.png") or \
110
+ image.endswith(".depth.png") or \
111
+ image.endswith(".radar.png") or \
112
+ image.endswith(".radar.pcd") or \
113
+ image.endswith(".lidar.png") or \
114
+ image.endswith(".lidar.pcd") or \
115
+ image.endswith(".lidar.reflect") or \
116
+ image.endswith(".lidar.jpeg"):
117
+ continue
118
+
119
+ name = os.path.splitext(os.path.basename(image))[0]
120
+ if name.endswith(".camera"):
121
+ name = name[:-7]
122
+ self.all_images_dict[name] = image
123
+
124
+ # Read the dataframe.
125
+ self.dataframe = pl.scan_ipc(self.source)
126
+
127
+ if sessions is not None:
128
+ self.dataframe = self.dataframe.filter(
129
+ pl.col("name").is_in(sessions))
130
+ self.dataframe = self.dataframe.with_row_index().collect()
131
+
132
+ self.samples = self.dataframe.group_by(["name", "frame"]) \
133
+ .agg(pl.col("index")) \
134
+ .get_column("index").to_list()
135
+
136
+ if len(self.samples) == 0:
137
+ raise ValueError(
138
+ "There are no validation samples found in this dataset.")
139
+
140
+ if self.dataframe["label"].null_count() == len(
141
+ self.dataframe["label"]):
142
+ raise ValueError("There are no annotations in this dataset.")
143
+
144
+ # List the classes based on the label column of the dataframe.
145
+ if self.parameters.labels is None:
146
+ if "label_index" in self.dataframe.columns:
147
+ pairs = (self.dataframe
148
+ .filter(pl.col("label").is_not_null())
149
+ .select(["label", "label_index"])
150
+ .unique())
151
+ # Not every label is annotated in the dataset. In place with
152
+ # Null.
153
+ labels = ["Null"] * (max(pairs["label_index"]) + 1)
154
+
155
+ for label, index in zip(pairs["label"], pairs["label_index"]):
156
+ labels[index] = label
157
+ self.parameters.labels = labels
158
+ else:
159
+ # Fallback: unique labels sorted alphabetically (previous
160
+ # behavior)
161
+ self.parameters.labels = (
162
+ self.dataframe
163
+ .filter(pl.col("label").is_not_null())
164
+ .select(pl.col("label"))
165
+ .unique()
166
+ .get_column("label")
167
+ .to_list()
168
+ )
169
+ self.parameters.labels.sort()
170
+
171
+ def verify_dataset(self):
172
+ """
173
+ Verify that the dataset contains ground truth annotations.
174
+ """
175
+ mask_col = self.dataframe["mask"]
176
+ box_col = self.dataframe["box2d"]
177
+ is_all_mask_null = mask_col.null_count() == len(mask_col)
178
+ is_all_box_null = box_col.null_count() == len(box_col)
179
+
180
+ if is_all_mask_null and is_all_box_null:
181
+ raise ValueError("There are no annotations in this dataset.")
182
+ elif (self.parameters.common.with_masks and
183
+ not self.parameters.common.with_boxes):
184
+ if is_all_mask_null:
185
+ raise ValueError("There are no mask annotations in the dataset " +
186
+ "to validate the segmentation model.")
187
+ elif (self.parameters.common.with_boxes and
188
+ not self.parameters.common.with_masks):
189
+ if is_all_box_null:
190
+ raise ValueError("There are no box annotations in the dataset " +
191
+ "to validate the detection model.")
192
+ else:
193
+ if is_all_mask_null:
194
+ logger("There were no mask annotations found in this dataset.",
195
+ code="WARNING")
196
+
197
+ if is_all_box_null:
198
+ logger("There were no box annotations found in this dataset.",
199
+ code="WARNING")
200
+
201
+ def collect_samples(self) -> list:
202
+ """
203
+ Collect all samples in the dataset.
204
+
205
+ Returns
206
+ -------
207
+ list
208
+ A sample contains the indices in
209
+ the dataframe that points to all the annotations
210
+ for the sample image.
211
+ """
212
+ return self.samples
213
+
214
+ def name(self, sample: list) -> str:
215
+ """
216
+ Fetch the name of the dataset sample.
217
+
218
+ Parameters
219
+ ----------
220
+ sample: list
221
+ A single dataset sample contains the indices
222
+ in the dataframe pointing to all the annotations
223
+ in the dataset for this sample.
224
+
225
+ Returns
226
+ -------
227
+ str
228
+ The name of the sample. This is typically
229
+ the basename of the image.
230
+ """
231
+ index = sample[0]
232
+ name = self.dataframe.item(index, "name")
233
+ frame = self.dataframe.item(index, "frame")
234
+ if frame is None:
235
+ return name
236
+ return f"{name}_{frame}"
237
+
238
+ def image(self,
239
+ sample: list) -> Tuple[np.ndarray, np.ndarray, list, tuple]:
240
+ """
241
+ Reads the image file from the dataset. This method should
242
+ also handle any image preprocessing specified when caching is
243
+ required. Image preprocessing will include image resizing, letterbox,
244
+ or padding and transformations to either YUYV or RGBA.
245
+
246
+ Parameters
247
+ ----------
248
+ sample: list
249
+ A single dataset sample contains the indices
250
+ in the dataframe pointing to all the annotations
251
+ in the dataset for this sample.
252
+
253
+ Returns
254
+ -------
255
+ image: np.ndarray
256
+ The image input after being preprocessed.
257
+ visual_image: np.ndarray
258
+ The image that is used for visualization post
259
+ letterbox, padding, resize transformations.
260
+ shapes: list
261
+ This is used to scale the bounding boxes of the ground
262
+ truth and the model detections based on the letterbox/padding
263
+ transformation.
264
+
265
+ .. code-block:: python
266
+
267
+ [[input_height, input_width],
268
+ [[scale_y, scale_x], [pad_w, pad_h]]]
269
+ image_shape: tuple
270
+ The original image dimensions.
271
+
272
+ Raises
273
+ ------
274
+ FileNotFoundError
275
+ Raised if the image file does not exist in the dataset.
276
+ """
277
+ name = self.name(sample)
278
+ image = self.all_images_dict.get(name)
279
+
280
+ if image is None:
281
+ raise FileNotFoundError(f"Image '{name}' was not found")
282
+
283
+ # Read the image.
284
+ image = self.load_image(image, backend=self.parameters.common.backend)
285
+
286
+ with self.timer.time("input"):
287
+ # Preprocess the image.
288
+ if self.parameters.common.backend == "hal":
289
+ image, visual_image, shapes, image_shape = preprocess_hal(
290
+ image=image,
291
+ shape=self.parameters.common.shape,
292
+ input_type=self.parameters.common.dtype,
293
+ dst=self.parameters.common.input_dst,
294
+ transpose=self.parameters.common.transpose,
295
+ input_tensor=self.parameters.common.input_tensor,
296
+ preprocessing=self.parameters.common.preprocessing,
297
+ normalization=self.parameters.common.norm,
298
+ quantization=self.parameters.common.input_quantization,
299
+ visualize=self.parameters.visualize
300
+ )
301
+ else:
302
+ image, visual_image, shapes, image_shape = preprocess_native(
303
+ image=image,
304
+ shape=self.parameters.common.shape,
305
+ input_type=self.parameters.common.dtype,
306
+ transpose=self.parameters.common.transpose,
307
+ input_tensor=self.parameters.common.input_tensor,
308
+ preprocessing=self.parameters.common.preprocessing,
309
+ normalization=self.parameters.common.norm,
310
+ quantization=self.parameters.common.input_quantization,
311
+ backend=self.parameters.common.backend
312
+ )
313
+ return image, visual_image, shapes, image_shape
314
+
315
+ def labels(self, sample: list) -> np.ndarray:
316
+ """
317
+ Fetch the labels at the specified sample.
318
+
319
+ Parameters
320
+ ----------
321
+ sample: list
322
+ A single dataset sample contains the indices
323
+ in the dataframe pointing to all the annotations
324
+ in the dataset for this sample.
325
+
326
+ Returns
327
+ -------
328
+ np.ndarray
329
+ The labels in the sample containing np.int32 elements.
330
+ """
331
+ if "label_index" in self.dataframe.columns:
332
+ col = "label_index"
333
+ else:
334
+ col = "label"
335
+
336
+ labels = (
337
+ self.dataframe.lazy()
338
+ .filter(pl.col(col).is_not_null())
339
+ .filter(pl.col("index").is_in(sample))
340
+ .select(col)
341
+ .collect()
342
+ .get_column(col)
343
+ .to_list()
344
+ )
345
+
346
+ if col == "label":
347
+ labels = np.array([
348
+ self.parameters.labels.index(label) for label in labels],
349
+ dtype=np.int32
350
+ )
351
+ else:
352
+ labels = (np.array(labels, dtype=np.int32) +
353
+ self.parameters.label_offset)
354
+ return labels
355
+
356
+ def boxes(self, sample: list) -> np.ndarray:
357
+ """
358
+ Fetches the bounding box annotations at the specified sample.
359
+
360
+ Parameters
361
+ ----------
362
+ sample: list
363
+ A single dataset sample contains the indices
364
+ in the dataframe pointing to all the annotations
365
+ in the dataset for this sample.
366
+
367
+ Returns
368
+ -------
369
+ np.ndarray
370
+ The bounding box array. This array is formatted
371
+ as [xmin, ymin, xmax, ymax, label].
372
+ """
373
+ boxes = self.dataframe.lazy()
374
+ boxes = boxes.filter(pl.col("box2d").is_not_null())
375
+ boxes = boxes.filter(pl.col("index").is_in(sample))
376
+
377
+ if "label_index" in self.dataframe.columns:
378
+ col = "label_index"
379
+ else:
380
+ col = "label"
381
+ boxes = boxes.select([pl.col(col), "box2d"])
382
+ data = boxes.collect()
383
+ boxes = data.get_column("box2d").to_numpy()
384
+
385
+ if col == "label_index":
386
+ labels = (data.get_column(col).to_numpy().astype(np.float32) +
387
+ self.parameters.label_offset)
388
+ else:
389
+ labels = data.get_column(col)
390
+ labels = labels.to_list()
391
+ labels = np.array([
392
+ self.parameters.labels.index(label) for label in labels],
393
+ dtype=np.float32
394
+ )
395
+ return np.hstack([boxes, labels[:, None]])
396
+
397
+ def segments(
398
+ self,
399
+ sample: list,
400
+ image_shape: tuple,
401
+ resample: int = 1000
402
+ ) -> Tuple[np.ndarray, np.ndarray]:
403
+ """
404
+ Fetches the mask annotations as polygons.
405
+
406
+ Parameters
407
+ ----------
408
+ sample: list
409
+ A single dataset sample contains the indices
410
+ in the dataframe pointing to all the annotations
411
+ in the dataset for this sample.
412
+ image_shape: tuple
413
+ The original dimensions of the image as (height, width).
414
+ resample: int
415
+ The number of points to resample the segments.
416
+
417
+ Returns
418
+ -------
419
+ segments: np.ndarray
420
+ A flattened array containing [x, y, x, y, ... nan, ...]
421
+ coordinates for the mask polygons where each mask
422
+ is separated by NaN to indicate a separate object.
423
+ labels: np.ndarray
424
+ Returns the labels for each mask.
425
+ """
426
+ polygons = self.dataframe.lazy()
427
+ polygons = polygons.filter(pl.col("mask").is_not_null())
428
+ polygons = polygons.filter(pl.col("index").is_in(sample))
429
+
430
+ if "label_index" in self.dataframe.columns:
431
+ col = "label_index"
432
+ else:
433
+ col = "label"
434
+ polygons = polygons.filter(
435
+ pl.col('label').is_in(self.parameters.labels))
436
+ polygons = polygons.select([pl.col(col), "mask"])
437
+ polygons = polygons.collect()
438
+
439
+ if col == "label_index":
440
+ labels = (polygons.get_column(col).to_numpy().astype(np.int32) +
441
+ self.parameters.label_offset)
442
+ else:
443
+ labels = polygons.get_column(col).to_list()
444
+ # Conversion to integer.
445
+ labels = np.array([self.parameters.labels.index(label)
446
+ for label in labels], dtype=np.int32)
447
+ polygons = polygons.get_column("mask").to_numpy()
448
+
449
+ segments = []
450
+ for polygon in polygons:
451
+ if len(polygon) == 0:
452
+ continue
453
+ # Use numpy operations to speed up the process
454
+ valid_indices = np.ma.clump_unmasked(
455
+ np.ma.masked_invalid(polygon))
456
+ # Contours is a single object with multiple masks, the length
457
+ # of the contours is the number of masks of this object.
458
+ contours = [polygon[s] for s in valid_indices]
459
+ # A weak solution as it combines masks of the same object, but
460
+ # it reproduces the format from Ultralytics as polygons (n, p, 2)
461
+ # where n is the number of object, p is the number of points,
462
+ # and 2 (x, y) coordinate points.
463
+ contours = np.concatenate(contours).reshape(-1, 2)
464
+ segments.append(contours)
465
+
466
+ # Get the original shape of the image.
467
+ height, width = image_shape
468
+ # Segments are being resampled.
469
+ # https://github.com/ultralytics/ultralytics/blob/main/ultralytics/data/dataset.py#L274
470
+ # NOTE: do NOT resample oriented boxes.
471
+ if len(segments) > 0:
472
+ # make sure segments interpolate correctly if
473
+ # original length is greater than resample.
474
+ max_len = max(len(s) for s in segments)
475
+ resample = (max_len + 1) if resample < max_len else resample
476
+ # list[np.array(resample, 2)] * num_samples
477
+ segments = np.stack(resample_segments(
478
+ segments, n=resample), axis=0)
479
+ # Denormalize segments.
480
+ segments[..., 0] *= width
481
+ segments[..., 1] *= height
482
+ else:
483
+ segments = np.zeros((0, resample, 2), dtype=np.float32)
484
+
485
+ return segments, labels
486
+
487
+ def mask(
488
+ self,
489
+ sample: list,
490
+ shapes: list,
491
+ image_shape: tuple
492
+ ) -> Tuple[np.ndarray, np.ndarray]:
493
+ """
494
+ Fetches the mask annotations at the specified sample.
495
+
496
+ Parameters
497
+ ----------
498
+ sample: list
499
+ A single dataset sample contains the indices
500
+ in the dataframe pointing to all the annotations
501
+ in the dataset for this sample.
502
+ shapes: list
503
+ This is used to scale the bounding boxes of the ground
504
+ truth and the model detections based on the letterbox/padding
505
+ transformation.
506
+
507
+ .. code-block:: python
508
+
509
+ [[input_height, input_width],
510
+ [[scale_y, scale_x], [pad_w, pad_h]]]
511
+ image_shape: tuple
512
+ This contains the original image dimensions (height, width).
513
+
514
+ Returns
515
+ -------
516
+ masks: np.ndarray
517
+ The mask array in the shape (n, height, width) if its
518
+ an instance segmentation. Otherwise for semantic segmentation
519
+ n = 1.
520
+ sorted_idx: np.ndarray
521
+ Resorting the ground truth based on these indices.
522
+ """
523
+ segments, labels = self.segments(sample, image_shape=image_shape)
524
+
525
+ imgsz = shapes[0]
526
+ ratio_pad = shapes[1]
527
+
528
+ # Scale ground truth mask to center around objects
529
+ # in an image with padding transformation.
530
+ if self.parameters.common.preprocessing == "pad":
531
+ ratio_pad[1] = [0.0, 0.0]
532
+
533
+ masks, sorted_idx = format_segments(
534
+ segments=segments,
535
+ shape=imgsz,
536
+ ratio_pad=ratio_pad,
537
+ colors=labels,
538
+ semantic=self.parameters.common.semantic,
539
+ backend=self.parameters.common.backend
540
+ )
541
+ return masks, sorted_idx
542
+
543
+ def build_detection_instance(self, sample: list) -> DetectionInstance:
544
+ """
545
+ Builds a 2D detection instance container.
546
+
547
+ Parameters
548
+ ----------
549
+ sample: list
550
+ A single dataset sample contains the indices
551
+ in the dataframe pointing to all the annotations
552
+ in the dataset for this sample.
553
+
554
+ Returns
555
+ -------
556
+ DetectionInstance
557
+ The ground truth instance objects contains the 2D bounding boxes
558
+ and the labels representing the ground truth of the image.
559
+ """
560
+ image, visual_image, shapes, image_shape = self.image(sample)
561
+ height, width = get_shape(image.shape)
562
+ name = self.name(sample)
563
+ name = os.path.basename(self.all_images_dict.get(name))
564
+
565
+ boxes = self.boxes(sample)
566
+ # Transform the ground truth boxes based on the preprocessed image.
567
+ if len(boxes):
568
+ labels = boxes[..., 4]
569
+ boxes = boxes[..., 0:4]
570
+ # If the boxes are denormalized, normalize the boxes.
571
+ boxes = (self.normalizer(boxes, image_shape)
572
+ if self.normalizer else boxes)
573
+ # Transform the boxes to xyxy format if required.
574
+ boxes = self.transformer(boxes) if self.transformer else boxes
575
+
576
+ # Scale ground truth coordinates to center around objects
577
+ # in an image with letterbox transformation.
578
+ if self.parameters.common.preprocessing == "letterbox":
579
+ boxes = scale(
580
+ boxes=boxes,
581
+ w=shapes[1][0][1] * image_shape[1],
582
+ h=shapes[1][0][0] * image_shape[0],
583
+ padw=shapes[1][1][0],
584
+ padh=shapes[1][1][1],
585
+ )
586
+ # Scale ground truth coordinates to center around objects
587
+ # in an image with padding transformation.
588
+ elif self.parameters.common.preprocessing == "pad":
589
+ boxes = scale(
590
+ boxes=boxes,
591
+ w=shapes[1][0][1] * width,
592
+ h=shapes[1][0][0] * height,
593
+ )
594
+ # Scale ground truth coordinates to center around objects
595
+ # in an image with resize transformation.
596
+ else:
597
+ # Denormalize boxes
598
+ boxes *= np.array([width, height, width, height])
599
+ else:
600
+ labels = np.array([])
601
+
602
+ instance = DetectionInstance(name)
603
+ instance.image = image
604
+ instance.visual_image = visual_image
605
+ instance.height = height
606
+ instance.width = width
607
+ instance.boxes = boxes.astype(np.float32)
608
+ instance.labels = labels.astype(np.int32)
609
+ instance.shapes = shapes
610
+ instance.image_shape = image_shape
611
+ return instance
612
+
613
+ def build_segmentation_instance(
614
+ self, sample: list) -> SegmentationInstance:
615
+ """
616
+ Builds a segmentation instance container.
617
+
618
+ Parameters
619
+ ----------
620
+ sample: list
621
+ A single dataset sample contains the indices
622
+ in the dataframe pointing to all the annotations
623
+ in the dataset for this sample.
624
+
625
+ Returns
626
+ -------
627
+ SegmentationInstance
628
+ The ground truth instance objects contains the polygon, mask,
629
+ and the labels representing the ground truth of the image.
630
+ """
631
+
632
+ image, visual_image, shapes, image_shape = self.image(sample)
633
+ height, width = get_shape(image.shape)
634
+ name = self.name(sample)
635
+ name = os.path.basename(self.all_images_dict.get(name))
636
+
637
+ masks, _ = self.mask(sample, shapes, image_shape)
638
+
639
+ instance = SegmentationInstance(name)
640
+ instance.image = image
641
+ instance.visual_image = visual_image
642
+ instance.height = height
643
+ instance.width = width
644
+ instance.mask = masks
645
+ instance.shapes = shapes
646
+ instance.image_shape = image_shape
647
+ return instance
648
+
649
+ def build_multitask_instance(self, sample: list) -> MultitaskInstance:
650
+ """
651
+ Builds a multitask instance container.
652
+
653
+ Parameters
654
+ ----------
655
+ sample: list
656
+ A single dataset sample contains the indices
657
+ in the dataframe pointing to all the annotations
658
+ in the dataset for this sample.
659
+
660
+ Returns
661
+ -------
662
+ MultitaskInstance
663
+ The ground truth instance objects contains the bounding boxes
664
+ and the segmentation mask representing the ground truth of
665
+ the image
666
+ """
667
+
668
+ image, visual_image, shapes, image_shape = self.image(sample)
669
+ height, width = get_shape(image.shape)
670
+ name = self.name(sample)
671
+ name = os.path.basename(self.all_images_dict.get(name))
672
+
673
+ # Transform the ground truth boxes based on the preprocessed image.
674
+ boxes = self.boxes(sample)
675
+ if len(boxes):
676
+ labels = boxes[..., 4]
677
+ boxes = boxes[..., 0:4]
678
+ boxes = (self.normalizer(boxes, image_shape)
679
+ if self.normalizer else boxes)
680
+ boxes = self.transformer(boxes) if self.transformer else boxes
681
+
682
+ # Scale ground truth coordinates to center around objects
683
+ # in an image with letterbox transformation.
684
+ if self.parameters.common.preprocessing == "letterbox":
685
+ boxes = scale(
686
+ boxes=boxes,
687
+ w=shapes[1][0][1] * image_shape[1],
688
+ h=shapes[1][0][0] * image_shape[0],
689
+ padw=shapes[1][1][0],
690
+ padh=shapes[1][1][1],
691
+ )
692
+ # Scale ground truth coordinates to center around objects
693
+ # in an image with padding transformation.
694
+ elif self.parameters.common.preprocessing == "pad":
695
+ boxes = scale(
696
+ boxes=boxes,
697
+ w=shapes[1][0][1] * width,
698
+ h=shapes[1][0][0] * height,
699
+ )
700
+ # Scale ground truth coordinates to center around objects
701
+ # in an image with resize transformation.
702
+ else:
703
+ # Denormalize boxes
704
+ boxes *= np.array([width, height, width, height])
705
+ else:
706
+ labels = self.labels(sample)
707
+
708
+ masks, sorted_idx = self.mask(sample, shapes, image_shape)
709
+ if sorted_idx is not None and len(sorted_idx) > 0:
710
+ if len(labels):
711
+ labels = labels[sorted_idx]
712
+ if len(boxes):
713
+ boxes = boxes[sorted_idx]
714
+
715
+ instance = MultitaskInstance(name)
716
+ instance.image = image
717
+ instance.visual_image = visual_image
718
+ instance.height = height
719
+ instance.width = width
720
+ instance.boxes = boxes.astype(np.float32)
721
+ instance.labels = labels.astype(np.int32)
722
+ instance.mask = masks
723
+ instance.shapes = shapes
724
+ instance.image_shape = image_shape
725
+ return instance
726
+
727
+
728
+ class LMDBDatabase(Dataset):
729
+ """
730
+ Reads from LMDB database cache. This is the cache file.
731
+ It should already store preprocessed images and annotations. The
732
+ shape for the images across all samples remains consistent to the
733
+ input shape of the model.
734
+
735
+ Parameters
736
+ ----------
737
+ MAP_SIZE : int
738
+ The maximum size of the LMDB database.
739
+ source: str
740
+ This is the path to the LMDB Database file.
741
+ parameters: DatasetParameters
742
+ This contains dataset parameters set from the command line.
743
+ timer: TimerContext
744
+ A timer object for handling validation timings for the model.
745
+ info_dataset: dict
746
+ Contains information such as:
747
+
748
+ .. code-block:: python
749
+
750
+ {
751
+ "classes": [list of unique labels],
752
+ "validation":
753
+ {
754
+ "images: 'path to the images',
755
+ "annotations": 'path to the annotations'
756
+ }
757
+ }
758
+
759
+ *Note: the classes are optional and the path to the images
760
+ and annotations can be the same.*
761
+
762
+ Raises
763
+ ------
764
+ FileNotFoundError
765
+ Raised if the source provided does not exist.
766
+ """
767
+
768
+ MAP_SIZE = 32 * 1024 * 1024 * 1024
769
+
770
+ def __init__(
771
+ self,
772
+ source: str,
773
+ parameters: DatasetParameters,
774
+ timer: TimerContext,
775
+ info_dataset: dict = None,
776
+ ):
777
+ super(LMDBDatabase, self).__init__(
778
+ source=source,
779
+ parameters=parameters,
780
+ timer=timer,
781
+ info_dataset=info_dataset
782
+ )
783
+
784
+ if not os.path.isfile(self.source):
785
+ raise FileNotFoundError("The cache was not found at: ", source)
786
+
787
+ self.db = lmdb.open(
788
+ str(self.source).encode(),
789
+ map_size=LMDBDatabase.MAP_SIZE,
790
+ max_dbs=10,
791
+ subdir=False,
792
+ lock=False
793
+ )
794
+ self.classes_db = self.db.open_db(b'classes')
795
+ self.names_db = self.db.open_db(b'names')
796
+ self.images_db = self.db.open_db(b'images')
797
+ self.visual_images_db = self.db.open_db(b'visual')
798
+ self.boxes_db = self.db.open_db(b'box2d')
799
+ self.labels_db = self.db.open_db(b'labels')
800
+ self.masks_db = self.db.open_db(b'masks')
801
+
802
+ with self.db.begin() as txn:
803
+ classes = txn.get(b'classes', db=self.classes_db)
804
+ if classes is not None:
805
+ classes = json.loads(classes.decode())
806
+ self.parameters.labels = [str(c) for c in classes]
807
+
808
+ with self.db.begin() as txn:
809
+ cur = txn.cursor(self.names_db)
810
+ keys = [key.decode() for key, _ in cur]
811
+ self.samples = keys
812
+
813
+ if len(self.samples) == 0:
814
+ raise ValueError(
815
+ "There are no validation samples found in this dataset.")
816
+
817
+ def __del__(self):
818
+ """
819
+ Closes the database.
820
+ """
821
+ self.db.close()
822
+
823
+ def collect_samples(self) -> list:
824
+ """
825
+ Collect all samples in the dataset.
826
+
827
+ Returns
828
+ -------
829
+ list
830
+ A sample contains the indices in
831
+ the dataframe that points to all the annotations
832
+ for the sample image.
833
+ """
834
+ return self.samples
835
+
836
+ def name(self, index: int) -> str:
837
+ """
838
+ Fetch the name of the dataset sample.
839
+
840
+ Parameters
841
+ ----------
842
+ index: int
843
+ The dataset sample index.
844
+
845
+ Returns
846
+ -------
847
+ str
848
+ The name of the sample.
849
+ This is typically the basename of the image.
850
+ """
851
+ return self.samples[index]
852
+
853
+ def image(self, sample: str) -> Tuple[np.ndarray, np.ndarray, list, tuple]:
854
+ """
855
+ Fetches the preprocessed image stored in the cache.
856
+
857
+ Parameters
858
+ ----------
859
+ sample: str
860
+ The image name to fetch the sample.
861
+
862
+ Returns
863
+ -------
864
+ image: np.ndarray
865
+ The image input after being preprocessed.
866
+ visual_image: np.ndarray
867
+ The image that is used for visualization post
868
+ letterbox, padding, resize transformations.
869
+ shapes: list
870
+ This is used to scale the bounding boxes of the ground
871
+ truth and the model detections based on the letterbox/padding
872
+ transformation.
873
+
874
+ .. code-block:: python
875
+
876
+ [[input_height, input_width],
877
+ [[scale_y, scale_x], [pad_w, pad_h]]]
878
+ image_shape: tuple
879
+ The original image dimensions.
880
+ """
881
+ with self.db.begin(buffers=True) as txn:
882
+ image_shape_tx = txn.get(
883
+ f'{sample}/im_shape'.encode(), db=self.images_db)
884
+ image_shape = tuple(np.frombuffer(image_shape_tx, dtype=np.int32))
885
+
886
+ shapes_tx = txn.get(f'{sample}/shapes'.encode(), db=self.images_db)
887
+ shapes = json.loads(bytes(shapes_tx).decode())
888
+
889
+ shape_tx = txn.get(
890
+ f'{sample}/shape'.encode(), db=self.images_db)
891
+ shape = tuple(np.frombuffer(shape_tx, dtype=np.int32))
892
+ image_tx = txn.get(sample.encode(), db=self.images_db)
893
+ image = np.frombuffer(
894
+ image_tx, dtype=self.parameters.common.dtype).reshape(shape)
895
+
896
+ shape_tx = txn.get(
897
+ f'{sample}/shape'.encode(), db=self.visual_images_db)
898
+ if shape_tx is not None:
899
+ shape = tuple(np.frombuffer(shape_tx, dtype=np.int32))
900
+ visual_image = None
901
+ visual_tx = txn.get(sample.encode(), db=self.visual_images_db)
902
+ if visual_tx is not None:
903
+ visual_image = np.frombuffer(
904
+ visual_tx, dtype=np.uint8).reshape(shape)
905
+ return image, visual_image, shapes, image_shape
906
+
907
+ def labels(self, sample: str) -> np.ndarray:
908
+ """
909
+ Fetch the labels stored in the cache.
910
+
911
+ Parameters
912
+ ----------
913
+ sample: str
914
+ The image name to fetch the sample.
915
+
916
+ Returns
917
+ -------
918
+ np.ndarray
919
+ The labels in the sample containing np.int32 elements.
920
+ """
921
+ with self.db.begin(buffers=True) as txn:
922
+ labels_tx = txn.get(sample.encode(), db=self.labels_db)
923
+ labels = np.frombuffer(labels_tx, dtype=np.int32)
924
+ return labels
925
+
926
+ def boxes(self, sample: str) -> np.ndarray:
927
+ """
928
+ Fetches the boxes stored in the cache.
929
+
930
+ Parameters
931
+ -----------
932
+ sample: str
933
+ The image name to fetch the sample.
934
+
935
+ Returns
936
+ -------
937
+ np.ndarray
938
+ The bounding box array.
939
+ This array is formatted as [xmin, ymin, xmax, ymax]
940
+ normalized FP32 coordinates.
941
+ """
942
+ with self.db.begin(buffers=True) as txn:
943
+ boxes_tx = txn.get(sample.encode(), db=self.boxes_db)
944
+ boxes = np.frombuffer(boxes_tx, dtype=np.float32)
945
+ boxes = boxes.reshape(-1, 4)
946
+ return boxes
947
+
948
+ def mask(self, sample: str) -> np.ndarray:
949
+ """
950
+ Fetches the masks stored in the cache.
951
+
952
+ Parameters
953
+ -----------
954
+ sample: str
955
+ The image name to fetch the sample.
956
+
957
+ Returns
958
+ -------
959
+ np.ndarray
960
+ The masks array.
961
+ """
962
+ with self.db.begin(buffers=True) as txn:
963
+ mask_shape_tx = txn.get(f'{sample}/mask_shape'.encode(),
964
+ db=self.masks_db)
965
+ mask_shape = tuple(
966
+ np.frombuffer(mask_shape_tx, dtype=np.int32))
967
+
968
+ masks_tx = txn.get(sample.encode(), db=self.masks_db)
969
+ mask = np.frombuffer(masks_tx, dtype=np.uint8)
970
+ mask = mask.reshape(mask_shape)
971
+ return mask
972
+
973
+ def build_detection_instance(self, sample: str) -> DetectionInstance:
974
+ """
975
+ Builds a 2D detection instance container.
976
+
977
+ Parameters
978
+ ----------
979
+ sample: str
980
+ The image name to fetch the sample.
981
+
982
+ Returns
983
+ -------
984
+ DetectionInstance
985
+ The ground truth instance objects contains the 2D bounding boxes
986
+ and the labels representing the ground truth of the image.
987
+ """
988
+ image, visual_image, shapes, image_shape = self.image(sample)
989
+ height, width = get_shape(image.shape)
990
+
991
+ boxes = self.boxes(sample)
992
+ labels = self.labels(sample)
993
+ instance = DetectionInstance(sample)
994
+ instance.image = image
995
+ instance.visual_image = visual_image
996
+ instance.height = height
997
+ instance.width = width
998
+ instance.boxes = boxes
999
+ instance.labels = labels
1000
+ instance.shapes = shapes
1001
+ instance.image_shape = image_shape
1002
+ return instance
1003
+
1004
+ def build_segmentation_instance(self, sample: str) -> SegmentationInstance:
1005
+ """
1006
+ Builds a segmentation instance container.
1007
+
1008
+ Parameters
1009
+ ----------
1010
+ sample: str
1011
+ The image name to fetch the sample.
1012
+
1013
+ Returns
1014
+ -------
1015
+ SegmentationInstance
1016
+ The ground truth instance objects contains the polygon, mask,
1017
+ and the labels representing the ground truth of the image.
1018
+ """
1019
+ image, visual_image, shapes, image_shape = self.image(sample)
1020
+ height, width = get_shape(image.shape)
1021
+ masks = self.mask(sample)
1022
+
1023
+ instance = SegmentationInstance(sample)
1024
+ instance.image = image
1025
+ instance.visual_image = visual_image
1026
+ instance.height = height
1027
+ instance.width = width
1028
+ instance.mask = masks
1029
+ instance.shapes = shapes
1030
+ instance.image_shape = image_shape
1031
+ return instance
1032
+
1033
+ def build_multitask_instance(self, sample: str) -> MultitaskInstance:
1034
+ """
1035
+ Builds a multitask instance container.
1036
+
1037
+ Parameters
1038
+ ----------
1039
+ sample: str
1040
+ The image name to fetch the sample.
1041
+
1042
+ Returns
1043
+ -------
1044
+ MultitaskInstance
1045
+ The ground truth instance objects contains the bounding boxes
1046
+ and the segmentation mask representing the ground truth of
1047
+ the image
1048
+ """
1049
+
1050
+ image, visual_image, shapes, image_shape = self.image(sample)
1051
+ height, width = get_shape(image.shape)
1052
+
1053
+ boxes = self.boxes(sample)
1054
+ labels = self.labels(sample)
1055
+ masks = self.mask(sample)
1056
+
1057
+ instance = MultitaskInstance(sample)
1058
+ instance.image = image
1059
+ instance.visual_image = visual_image
1060
+ instance.height = height
1061
+ instance.width = width
1062
+ instance.boxes = boxes
1063
+ instance.labels = labels
1064
+ instance.mask = masks
1065
+ instance.shapes = shapes
1066
+ instance.image_shape = image_shape
1067
+ return instance