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,623 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
+
|
|
8
|
+
from edgefirst.validator.visualize import Colors
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from edgefirst.validator.datasets import DetectionInstance
|
|
12
|
+
from edgefirst.validator.evaluators import Matcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DetectionDrawer:
|
|
16
|
+
"""
|
|
17
|
+
This class draws detection bounding boxes on the image showing
|
|
18
|
+
the validation results from the ground truth and model prediction
|
|
19
|
+
matches.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
|
|
24
|
+
self.messages = {
|
|
25
|
+
"Match": "%s %.2f%% %.2f", # Format (label, score, IoU)
|
|
26
|
+
"Match Loc": "LOC: %s %.2f%% %.2f", # Format (label, score, IoU)
|
|
27
|
+
"Loc": "LOC: %s %.2f%%", # Format (label, score)
|
|
28
|
+
"Clf": "CLF: %s %.2f%% %.2f", # Format (label, score, IoU)
|
|
29
|
+
"Basic": "%s %.2f%%", # Format (label, score)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
self.font = ImageFont.load_default()
|
|
33
|
+
self.image_draw: ImageDraw.ImageDraw = None
|
|
34
|
+
self.colors = Colors()
|
|
35
|
+
|
|
36
|
+
def draw_2d_gt_boxes(
|
|
37
|
+
self,
|
|
38
|
+
image: Union[Image.Image, np.ndarray],
|
|
39
|
+
gt_instance: DetectionInstance,
|
|
40
|
+
method: str = "edgefirst",
|
|
41
|
+
labels: list = None,
|
|
42
|
+
) -> Image.Image:
|
|
43
|
+
"""
|
|
44
|
+
Draw the 2D ground truth bounding boxes on the image.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
image: Union[Image.Image, np.ndarray]
|
|
49
|
+
The image to overlay with boxes and texts.
|
|
50
|
+
gt_instance: DetectionInstance
|
|
51
|
+
This is the ground truth instance containing the
|
|
52
|
+
bounding boxes and their labels as normalized (xyxy) format.
|
|
53
|
+
method: str
|
|
54
|
+
The type of visualization method. By default, visualization
|
|
55
|
+
of "edgefirst" validation results are used. Otherwise,
|
|
56
|
+
"ultralytics" visualizations are used.
|
|
57
|
+
labels: list
|
|
58
|
+
A list of unique string labels to designate
|
|
59
|
+
a specific color for the label.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
Image.Image
|
|
64
|
+
The image with 2D ground truth boxes.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(image, np.ndarray):
|
|
67
|
+
image = Image.fromarray(image)
|
|
68
|
+
self.image_draw = ImageDraw.Draw(image)
|
|
69
|
+
|
|
70
|
+
# Draw ground truths
|
|
71
|
+
for label, bounding_box in zip(gt_instance.labels, gt_instance.boxes):
|
|
72
|
+
if method == "edgefirst":
|
|
73
|
+
box_position = self.format_box_position(
|
|
74
|
+
box_position=bounding_box
|
|
75
|
+
)
|
|
76
|
+
self.draw_2d_bounding_box(box_position)
|
|
77
|
+
color = "RoyalBlue"
|
|
78
|
+
else:
|
|
79
|
+
if labels is not None:
|
|
80
|
+
color = self.colors(labels.index(label))
|
|
81
|
+
else:
|
|
82
|
+
color = self.colors(0)
|
|
83
|
+
box_position = self.format_box_position(
|
|
84
|
+
box_position=bounding_box
|
|
85
|
+
)
|
|
86
|
+
self.draw_2d_bounding_box(
|
|
87
|
+
box_position=box_position,
|
|
88
|
+
color=color
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
text = str(label)
|
|
92
|
+
background_position, text_position =\
|
|
93
|
+
self.position_2d_text_background(
|
|
94
|
+
text,
|
|
95
|
+
(box_position[0][0], box_position[1][1]),
|
|
96
|
+
box_position,
|
|
97
|
+
portion=0.10
|
|
98
|
+
)
|
|
99
|
+
self.draw_text(
|
|
100
|
+
text,
|
|
101
|
+
text_position,
|
|
102
|
+
background_position=background_position,
|
|
103
|
+
background_color=color
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return image
|
|
107
|
+
|
|
108
|
+
def draw_2d_dt_boxes(
|
|
109
|
+
self,
|
|
110
|
+
image: Union[Image.Image, np.ndarray],
|
|
111
|
+
dt_instance: DetectionInstance,
|
|
112
|
+
gt_instance: DetectionInstance = None,
|
|
113
|
+
matcher: Matcher = None,
|
|
114
|
+
validation_iou: float = 0.50,
|
|
115
|
+
validation_score: float = 0.25,
|
|
116
|
+
method: str = "edgefirst",
|
|
117
|
+
labels: list = None
|
|
118
|
+
) -> Image.Image:
|
|
119
|
+
"""
|
|
120
|
+
Draw the 2D detection bounding boxes on the image.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
image: Union[Image.Image, np.ndarray]
|
|
125
|
+
The image to overlay with boxes and texts.
|
|
126
|
+
dt_instance: Instance
|
|
127
|
+
This is the prediction instance containing the bounding boxes
|
|
128
|
+
and their scores and labels.
|
|
129
|
+
gt_instance: Instance
|
|
130
|
+
This is the ground truth instance containing the
|
|
131
|
+
bounding boxes and their labels as normalized (xyxy) format.
|
|
132
|
+
matcher: Matcher
|
|
133
|
+
This contains the bounding box matches from EdgeFirst validation
|
|
134
|
+
for assigning colors to true positives, false positives, and
|
|
135
|
+
false negatives.
|
|
136
|
+
validation_iou: float
|
|
137
|
+
This is the validation IoU threshold which determines the point
|
|
138
|
+
between classifying a prediction bounding box as either a
|
|
139
|
+
true positive or a localization false positive.
|
|
140
|
+
validation_score: float
|
|
141
|
+
Filter to visualize the predictions with confident scores
|
|
142
|
+
only, score greater than this threshold set.
|
|
143
|
+
method: str
|
|
144
|
+
The type of visualization method. By default, visualization
|
|
145
|
+
of "edgefirst" validation results are used. Otherwise,
|
|
146
|
+
"ultralytics" visualizations are used.
|
|
147
|
+
labels: list
|
|
148
|
+
A list of unique string labels to designate
|
|
149
|
+
a specific color for the label.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
Image.Image
|
|
154
|
+
The image with 2D prediction boxes.
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(image, np.ndarray):
|
|
157
|
+
image = Image.fromarray(image)
|
|
158
|
+
self.image_draw = ImageDraw.Draw(image)
|
|
159
|
+
|
|
160
|
+
# Visualize EdgeFirst results.
|
|
161
|
+
if method == "edgefirst":
|
|
162
|
+
# Draw extra predictions
|
|
163
|
+
for extra in matcher.index_unmatched_dt:
|
|
164
|
+
dt_label = dt_instance.labels[extra]
|
|
165
|
+
score = dt_instance.scores[extra]
|
|
166
|
+
if score < validation_score:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
score *= 100
|
|
170
|
+
text = self.messages["Loc"] % (dt_label, score)
|
|
171
|
+
|
|
172
|
+
bounding_box = dt_instance.boxes[extra]
|
|
173
|
+
box_position = self.format_box_position(bounding_box)
|
|
174
|
+
self.draw_2d_bounding_box(box_position, "OrangeRed")
|
|
175
|
+
|
|
176
|
+
background_position, text_position = self.position_2d_text_background(
|
|
177
|
+
text,
|
|
178
|
+
(box_position[0][0], box_position[0][1]),
|
|
179
|
+
box_position
|
|
180
|
+
)
|
|
181
|
+
self.draw_text(
|
|
182
|
+
text,
|
|
183
|
+
text_position,
|
|
184
|
+
background_position=background_position,
|
|
185
|
+
background_color="OrangeRed"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Draw matches
|
|
189
|
+
for match in matcher.index_matches:
|
|
190
|
+
dt_label = dt_instance.labels[match[0]]
|
|
191
|
+
gt_label = gt_instance.labels[match[1]]
|
|
192
|
+
iou = matcher.iou_list[match[0]]
|
|
193
|
+
score = dt_instance.scores[match[0]]
|
|
194
|
+
if score < validation_score:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
score *= 100
|
|
198
|
+
text, color = self.classify_text(
|
|
199
|
+
gt_label, dt_label, score, iou, validation_iou)
|
|
200
|
+
|
|
201
|
+
bounding_box = dt_instance.boxes[match[0]]
|
|
202
|
+
box_position = self.format_box_position(bounding_box)
|
|
203
|
+
self.draw_2d_bounding_box(box_position, color)
|
|
204
|
+
|
|
205
|
+
background_position, text_position = self.position_2d_text_background(
|
|
206
|
+
text,
|
|
207
|
+
(box_position[0][0], box_position[0][1]),
|
|
208
|
+
box_position
|
|
209
|
+
)
|
|
210
|
+
self.draw_text(
|
|
211
|
+
text,
|
|
212
|
+
text_position,
|
|
213
|
+
background_position=background_position,
|
|
214
|
+
background_color=color
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Visualize Ultralytics results.
|
|
218
|
+
else:
|
|
219
|
+
if len(dt_instance.boxes):
|
|
220
|
+
filt = dt_instance.scores >= validation_score
|
|
221
|
+
dt_boxes = dt_instance.boxes[filt]
|
|
222
|
+
dt_labels = dt_instance.labels[filt]
|
|
223
|
+
dt_scores = dt_instance.scores[filt]
|
|
224
|
+
else:
|
|
225
|
+
dt_boxes = []
|
|
226
|
+
dt_labels = []
|
|
227
|
+
dt_scores = []
|
|
228
|
+
|
|
229
|
+
# Draw ground truths
|
|
230
|
+
for box, label, score in zip(dt_boxes, dt_labels, dt_scores):
|
|
231
|
+
if labels is not None:
|
|
232
|
+
color = self.colors(labels.index(label))
|
|
233
|
+
else:
|
|
234
|
+
color = self.colors(0)
|
|
235
|
+
|
|
236
|
+
box_position = self.format_box_position(box_position=box)
|
|
237
|
+
self.draw_2d_bounding_box(
|
|
238
|
+
box_position=box_position,
|
|
239
|
+
color=color
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
text = f"{label} {score:.2f}"
|
|
243
|
+
background_position, text_position =\
|
|
244
|
+
self.position_2d_text_background(
|
|
245
|
+
text,
|
|
246
|
+
(box_position[0][0], box_position[1][1]),
|
|
247
|
+
box_position,
|
|
248
|
+
portion=0.10
|
|
249
|
+
)
|
|
250
|
+
self.draw_text(
|
|
251
|
+
text,
|
|
252
|
+
text_position,
|
|
253
|
+
background_position=background_position,
|
|
254
|
+
background_color=color
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return image
|
|
258
|
+
|
|
259
|
+
def draw_2d_bounding_boxes(
|
|
260
|
+
self,
|
|
261
|
+
gt_instance: DetectionInstance,
|
|
262
|
+
dt_instance: DetectionInstance,
|
|
263
|
+
matcher: Matcher = None,
|
|
264
|
+
validation_iou: float = 0.50,
|
|
265
|
+
validation_score: float = 0.25,
|
|
266
|
+
method: str = "edgefirst",
|
|
267
|
+
labels: list = None
|
|
268
|
+
) -> Image.Image:
|
|
269
|
+
"""
|
|
270
|
+
This is the process for drawing all the 2D bounding boxes in an image.
|
|
271
|
+
This includes the ground truth and the prediction bounding boxes with
|
|
272
|
+
respective colors based on their classifications as true positives,
|
|
273
|
+
false positives, or false negatives.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
gt_instance: DetectionInstance
|
|
278
|
+
This is the ground truth instance containing the
|
|
279
|
+
bounding boxes and their labels as normalized (xyxy) format.
|
|
280
|
+
dt_instance: DetectionInstance
|
|
281
|
+
This is the prediction instance containing the bounding boxes
|
|
282
|
+
and their scores and labels.
|
|
283
|
+
matcher: Matcher
|
|
284
|
+
This contains the bounding box matches from EdgeFirst validation
|
|
285
|
+
for assigning colors to true positives, false positives, and
|
|
286
|
+
false negatives.
|
|
287
|
+
validation_iou: float
|
|
288
|
+
This is the validation IoU threshold which determines the point
|
|
289
|
+
between classifying a prediction bounding box as either a
|
|
290
|
+
true positive or a localization false positive.
|
|
291
|
+
validation_score: float
|
|
292
|
+
Filter to visualize the predictions with confident scores
|
|
293
|
+
only, score greater than this threshold set.
|
|
294
|
+
method: str
|
|
295
|
+
The type of visualization method. By default, visualization
|
|
296
|
+
of "edgefirst" validation results are used. Otherwise,
|
|
297
|
+
"ultralytics" visualizations are used.
|
|
298
|
+
labels: list
|
|
299
|
+
A list of unique string labels to designate
|
|
300
|
+
a specific color for the label.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
Image.Image
|
|
305
|
+
The image with 2D prediction and ground truth boxes.
|
|
306
|
+
"""
|
|
307
|
+
if method == "edgefirst":
|
|
308
|
+
image = Image.fromarray(gt_instance.visual_image)
|
|
309
|
+
image = self.draw_2d_gt_boxes(
|
|
310
|
+
image=image,
|
|
311
|
+
gt_instance=gt_instance
|
|
312
|
+
)
|
|
313
|
+
image = self.draw_2d_dt_boxes(
|
|
314
|
+
image=image,
|
|
315
|
+
dt_instance=dt_instance,
|
|
316
|
+
gt_instance=gt_instance,
|
|
317
|
+
matcher=matcher,
|
|
318
|
+
validation_iou=validation_iou,
|
|
319
|
+
validation_score=validation_score,
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
image = gt_instance.visual_image
|
|
323
|
+
gt_image = self.draw_2d_gt_boxes(
|
|
324
|
+
image=image.copy(),
|
|
325
|
+
gt_instance=gt_instance,
|
|
326
|
+
method="ultralytics",
|
|
327
|
+
labels=labels
|
|
328
|
+
)
|
|
329
|
+
dt_image = self.draw_2d_dt_boxes(
|
|
330
|
+
image=image.copy(),
|
|
331
|
+
dt_instance=dt_instance,
|
|
332
|
+
validation_score=validation_score,
|
|
333
|
+
method="ultralytics",
|
|
334
|
+
labels=labels
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
image = Image.new(
|
|
338
|
+
'RGB',
|
|
339
|
+
(gt_image.width + dt_image.width, dt_image.height))
|
|
340
|
+
image.paste(gt_image, (0, 0))
|
|
341
|
+
image.paste(dt_image, (dt_image.width, 0))
|
|
342
|
+
|
|
343
|
+
draw_text = ImageDraw.Draw(image)
|
|
344
|
+
draw_text.text(
|
|
345
|
+
(0, 0),
|
|
346
|
+
"GROUND TRUTH",
|
|
347
|
+
font=self.font,
|
|
348
|
+
align='left',
|
|
349
|
+
fill=(0, 0, 0)
|
|
350
|
+
)
|
|
351
|
+
draw_text.text(
|
|
352
|
+
(dt_image.width, 0),
|
|
353
|
+
"MODEL PREDICTION",
|
|
354
|
+
font=self.font,
|
|
355
|
+
align='left',
|
|
356
|
+
fill=(0, 0, 0)
|
|
357
|
+
)
|
|
358
|
+
return image
|
|
359
|
+
|
|
360
|
+
def draw_rect(
|
|
361
|
+
self,
|
|
362
|
+
selected_corners: np.ndarray,
|
|
363
|
+
color: str,
|
|
364
|
+
width: int = 2
|
|
365
|
+
):
|
|
366
|
+
"""
|
|
367
|
+
This is primarily used for drawing 3D bounding boxes which
|
|
368
|
+
consists of two rectangles and four lines.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
selected_corners: np.ndarray
|
|
373
|
+
This contains the corners of the 3D bounding box with shape
|
|
374
|
+
(3,8) representing the (x,y,z) eight corners of a 3D box.
|
|
375
|
+
color: str
|
|
376
|
+
The color to use for the line.
|
|
377
|
+
width: int
|
|
378
|
+
This is the width of the line forming the rectangle.
|
|
379
|
+
"""
|
|
380
|
+
prev = selected_corners[-1]
|
|
381
|
+
for corner in selected_corners:
|
|
382
|
+
self.image_draw.line(
|
|
383
|
+
((int(prev[0]), int(prev[1])),
|
|
384
|
+
(int(corner[0]), int(corner[1]))),
|
|
385
|
+
fill=color,
|
|
386
|
+
width=width
|
|
387
|
+
)
|
|
388
|
+
prev = corner
|
|
389
|
+
|
|
390
|
+
def draw_2d_bounding_box(
|
|
391
|
+
self,
|
|
392
|
+
box_position: tuple,
|
|
393
|
+
color: str = "RoyalBlue",
|
|
394
|
+
width: int = 3
|
|
395
|
+
):
|
|
396
|
+
"""
|
|
397
|
+
Draws a 2D bounding box on the image.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
box_position: tuple
|
|
402
|
+
((x1, y1), (x2, y2)) position of the box.
|
|
403
|
+
color: str
|
|
404
|
+
The color of the bounding box. Typically,
|
|
405
|
+
ground truth/false negatives are set to "RoyalBlue",
|
|
406
|
+
false positives are set to "OrangeRed",
|
|
407
|
+
true positives are set to "LimeGreen".
|
|
408
|
+
width: int
|
|
409
|
+
The width of the line to draw the bounding boxes.
|
|
410
|
+
"""
|
|
411
|
+
self.image_draw.rectangle(
|
|
412
|
+
box_position,
|
|
413
|
+
outline=color,
|
|
414
|
+
width=width
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def draw_text(
|
|
418
|
+
self,
|
|
419
|
+
text: str,
|
|
420
|
+
text_position: tuple,
|
|
421
|
+
color: str = "black",
|
|
422
|
+
align: str = "left",
|
|
423
|
+
background_position: tuple = None,
|
|
424
|
+
background_color: str = "RoyalBlue"
|
|
425
|
+
):
|
|
426
|
+
"""
|
|
427
|
+
Write text on the image and will also optionally
|
|
428
|
+
draw a 2D box overlay as the background of the text
|
|
429
|
+
to make it more visible.
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
text: str
|
|
434
|
+
The text to write on the image.
|
|
435
|
+
text_position: tuple
|
|
436
|
+
This is the (x, y) position on the image to write the text.
|
|
437
|
+
color: str
|
|
438
|
+
This is the color of the text.
|
|
439
|
+
align: str
|
|
440
|
+
This is the text alignment.
|
|
441
|
+
background_position: tuple
|
|
442
|
+
This is the ((x1, y1), (x2, y2)) position to draw the
|
|
443
|
+
background box of the text.
|
|
444
|
+
background_color: str
|
|
445
|
+
This is the color of the background. It is recommended to align the
|
|
446
|
+
colors with the bounding boxes to make it clear which text
|
|
447
|
+
corresponds to which.
|
|
448
|
+
"""
|
|
449
|
+
if background_position:
|
|
450
|
+
self.image_draw.rectangle(
|
|
451
|
+
background_position,
|
|
452
|
+
fill=background_color
|
|
453
|
+
)
|
|
454
|
+
self.image_draw.text(
|
|
455
|
+
text_position,
|
|
456
|
+
text,
|
|
457
|
+
font=self.font,
|
|
458
|
+
align=align,
|
|
459
|
+
fill=color
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def get_text_dimensions(self, text: str) -> Tuple[int, int]:
|
|
463
|
+
"""
|
|
464
|
+
Retrieve the text dimensions which varies
|
|
465
|
+
based on the Pillow version used.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
text: str
|
|
470
|
+
This is the text being drawn.
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
width: int
|
|
475
|
+
The width of the text in pixels.
|
|
476
|
+
height: int
|
|
477
|
+
The height of the text in pixels.
|
|
478
|
+
"""
|
|
479
|
+
if hasattr(self.font, 'getsize'): # works on older Pillow versions < 10.
|
|
480
|
+
text_width, text_height = self.font.getsize(text)
|
|
481
|
+
else:
|
|
482
|
+
# newer Pillow versions >= 10.
|
|
483
|
+
(text_width, text_height), _ = self.font.font.getsize(text)
|
|
484
|
+
return (text_width, text_height)
|
|
485
|
+
|
|
486
|
+
def position_2d_text_background(
|
|
487
|
+
self,
|
|
488
|
+
text: str,
|
|
489
|
+
text_position: tuple,
|
|
490
|
+
box_position: tuple,
|
|
491
|
+
portion: float = 0.25
|
|
492
|
+
) -> Tuple[tuple, tuple]:
|
|
493
|
+
"""
|
|
494
|
+
This positions the background of the text to make
|
|
495
|
+
it aligned with the 2D bounding box.
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
text: str
|
|
500
|
+
The text that will be drawn on the image.
|
|
501
|
+
text_position: tuple
|
|
502
|
+
This contains the (x, y) position of the text.
|
|
503
|
+
box_position: tuple
|
|
504
|
+
This contain the ((x1, y1), (x2, y2)) position
|
|
505
|
+
of the 2D bounding box.
|
|
506
|
+
portion: float
|
|
507
|
+
This is the percentage of the bounding box width to
|
|
508
|
+
resize the font.
|
|
509
|
+
|
|
510
|
+
Returns
|
|
511
|
+
-------
|
|
512
|
+
box_position: tuple
|
|
513
|
+
This is the ((x1, y1), (x2, y2)) position
|
|
514
|
+
of the text background.
|
|
515
|
+
text_position: tuple
|
|
516
|
+
This is the (x,y) position of the text
|
|
517
|
+
aligned to the background.
|
|
518
|
+
"""
|
|
519
|
+
text_width, text_height = self.get_text_dimensions(text)
|
|
520
|
+
|
|
521
|
+
# font_size = 10
|
|
522
|
+
# while (text_width < int(portion*(box_position[1][1] - box_position[0][1]))):
|
|
523
|
+
# self.font = ImageFont.load_default(size=font_size)
|
|
524
|
+
# text_width, text_height = self.get_text_dimensions(text)
|
|
525
|
+
# font_size += 1
|
|
526
|
+
|
|
527
|
+
box_text_x1 = box_position[0][0]
|
|
528
|
+
box_text_x2 = box_text_x1 + text_width
|
|
529
|
+
|
|
530
|
+
# This suggests a ground truth text is being drawn where the label is
|
|
531
|
+
# located in the bottom left of the bounding box.
|
|
532
|
+
if text_position[1] > box_position[0][1]:
|
|
533
|
+
# Keep the text within the bounding box.
|
|
534
|
+
box_text_y1 = box_position[1][1] - text_height
|
|
535
|
+
# The larger the text height, the large the offset to center.
|
|
536
|
+
text_position = (
|
|
537
|
+
text_position[0], text_position[1] - int(1.2 * text_height))
|
|
538
|
+
# A prediction text is being drawn where the labels is located
|
|
539
|
+
# in the top left of the bounding box.
|
|
540
|
+
else:
|
|
541
|
+
box_text_y1 = box_position[0][1]
|
|
542
|
+
text_position = (
|
|
543
|
+
text_position[0], text_position[1] - int(0.2 * text_height))
|
|
544
|
+
|
|
545
|
+
box_text_y2 = box_text_y1 + text_height
|
|
546
|
+
return ((box_text_x1, box_text_y1),
|
|
547
|
+
(box_text_x2, box_text_y2)), text_position
|
|
548
|
+
|
|
549
|
+
def classify_text(
|
|
550
|
+
self,
|
|
551
|
+
gt_label: str,
|
|
552
|
+
dt_label: str,
|
|
553
|
+
score: float,
|
|
554
|
+
iou: float,
|
|
555
|
+
validation_iou: float
|
|
556
|
+
) -> Tuple[str, str]:
|
|
557
|
+
"""
|
|
558
|
+
Determine the appropriate text to display and the color
|
|
559
|
+
to use based on the parameters provided.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
gt_label: str
|
|
564
|
+
This is the ground truth label.
|
|
565
|
+
dt_label: str
|
|
566
|
+
This is the prediction label.
|
|
567
|
+
score: float
|
|
568
|
+
This is the prediction score.
|
|
569
|
+
iou: float
|
|
570
|
+
This is the IoU between the ground truth and the prediction.
|
|
571
|
+
validation_iou: float
|
|
572
|
+
This IoU is the threshold of classifying predictions as either
|
|
573
|
+
true positives or localization false positives.
|
|
574
|
+
|
|
575
|
+
Returns
|
|
576
|
+
-------
|
|
577
|
+
text: str
|
|
578
|
+
This is the chosen formatted text to display.
|
|
579
|
+
color: str
|
|
580
|
+
This is the chosen color to use for the bounding box.
|
|
581
|
+
"""
|
|
582
|
+
# True Positives.
|
|
583
|
+
if dt_label == gt_label:
|
|
584
|
+
text = self.messages["Match"] % (dt_label, score, iou)
|
|
585
|
+
color = "LimeGreen"
|
|
586
|
+
# Classification False Positives.
|
|
587
|
+
else:
|
|
588
|
+
text = self.messages["Clf"] % (dt_label, score, iou)
|
|
589
|
+
color = "OrangeRed"
|
|
590
|
+
|
|
591
|
+
# Localization False Positives.
|
|
592
|
+
if iou <= validation_iou:
|
|
593
|
+
text = self.messages["Match Loc"] % (dt_label, score, iou)
|
|
594
|
+
color = "OrangeRed"
|
|
595
|
+
|
|
596
|
+
# Any unmatched or sole ground truths are false negatives.
|
|
597
|
+
return text, color
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def format_box_position(
|
|
601
|
+
box_position, width: int = 1, height: int = 1
|
|
602
|
+
) -> tuple:
|
|
603
|
+
"""
|
|
604
|
+
This denormalizes the bounding box coordinates
|
|
605
|
+
and formats it into a tuple.
|
|
606
|
+
|
|
607
|
+
Parameters
|
|
608
|
+
----------
|
|
609
|
+
box_position: list or np.ndarray
|
|
610
|
+
This is a normalized bounding box [xmin, ymin, xmax, ymax].
|
|
611
|
+
width: int
|
|
612
|
+
This is the width of the image to denormalize the box.
|
|
613
|
+
height: int
|
|
614
|
+
This is the height of the image to denormalize the box.
|
|
615
|
+
|
|
616
|
+
Returns
|
|
617
|
+
-------
|
|
618
|
+
tuple
|
|
619
|
+
Non normalized (pixels) ((xmin, ymin), (xmax, ymax)).
|
|
620
|
+
"""
|
|
621
|
+
p1 = (box_position[0] * width, box_position[1] * height)
|
|
622
|
+
p2 = (box_position[2] * width, box_position[3] * height)
|
|
623
|
+
return (p1, p2)
|