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