seavision-python 0.1.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.
- seavision/__init__.py +3 -0
- seavision/defaults.py +38 -0
- seavision/edge/__init__.py +38 -0
- seavision/edge/bundle.py +189 -0
- seavision/edge/capture.py +107 -0
- seavision/edge/config.py +219 -0
- seavision/edge/postprocess.py +185 -0
- seavision/edge/runtime.py +416 -0
- seavision/edge/writer.py +167 -0
- seavision/engine/__init__.py +52 -0
- seavision/engine/detectors/__init__.py +90 -0
- seavision/engine/detectors/base.py +179 -0
- seavision/engine/detectors/motion/__init__.py +14 -0
- seavision/engine/detectors/motion/background.py +92 -0
- seavision/engine/detectors/motion/detector.py +218 -0
- seavision/engine/detectors/motion/stabiliser.py +174 -0
- seavision/engine/detectors/motion/tracker.py +174 -0
- seavision/engine/detectors/sam3/__init__.py +23 -0
- seavision/engine/detectors/sam3/config.py +309 -0
- seavision/engine/detectors/sam3/detector.py +1316 -0
- seavision/engine/detectors/sam3/native.py +490 -0
- seavision/engine/detectors/yolo/__init__.py +12 -0
- seavision/engine/detectors/yolo/config.py +39 -0
- seavision/engine/detectors/yolo/detector.py +236 -0
- seavision/engine/export/__init__.py +24 -0
- seavision/engine/export/config.py +160 -0
- seavision/engine/export/exporter.py +286 -0
- seavision/engine/export/validation.py +314 -0
- seavision/engine/postprocessor.py +738 -0
- seavision/engine/source/__init__.py +16 -0
- seavision/engine/source/base.py +87 -0
- seavision/engine/source/discovery.py +154 -0
- seavision/engine/source/local.py +155 -0
- seavision/engine/source/s3.py +231 -0
- seavision/engine/visualiser/__init__.py +62 -0
- seavision/engine/visualiser/annotator.py +382 -0
- seavision/engine/visualiser/config.py +127 -0
- seavision/engine/visualiser/loader.py +558 -0
- seavision/engine/visualiser/visualiser.py +402 -0
- seavision/engine/visualiser/writer.py +245 -0
- seavision/gui/__init__.py +0 -0
- seavision/gui/app.py +30 -0
- seavision/gui/main_window.py +891 -0
- seavision/gui/shared/__init__.py +0 -0
- seavision/gui/shared/conversion.py +55 -0
- seavision/gui/shared/session_utils.py +37 -0
- seavision/gui/validation/__init__.py +0 -0
- seavision/gui/validation/button_styles.py +144 -0
- seavision/gui/validation/detection_detail.py +164 -0
- seavision/gui/validation/detection_rect_item.py +486 -0
- seavision/gui/validation/detection_table.py +563 -0
- seavision/gui/validation/interactive_frame_scene.py +340 -0
- seavision/gui/validation/interactive_frame_view.py +252 -0
- seavision/gui/validation/s3_browser.py +645 -0
- seavision/gui/validation/seekable_source.py +262 -0
- seavision/gui/validation/session.py +376 -0
- seavision/gui/validation/tab.py +2503 -0
- seavision/gui/validation/transport_bar.py +172 -0
- seavision/gui/validation/validation_model.py +482 -0
- seavision/gui/validation/video_cache.py +215 -0
- seavision/gui/validation/video_list.py +140 -0
- seavision/gui/validation/video_viewer.py +150 -0
- seavision/gui/validation/video_worker.py +422 -0
- seavision/pipeline.py +856 -0
- seavision/resources/default.yaml +139 -0
- seavision/run_edge.py +96 -0
- seavision/run_export.py +176 -0
- seavision/run_pipeline.py +388 -0
- seavision_python-0.1.1.dist-info/METADATA +286 -0
- seavision_python-0.1.1.dist-info/RECORD +73 -0
- seavision_python-0.1.1.dist-info/WHEEL +5 -0
- seavision_python-0.1.1.dist-info/entry_points.txt +5 -0
- seavision_python-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Frame annotation - drawing detections on video frames."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from ..detectors.base import Detection
|
|
9
|
+
from ..source.base import FrameContext
|
|
10
|
+
from .config import (
|
|
11
|
+
BoundingBoxStyle,
|
|
12
|
+
LabelStyle,
|
|
13
|
+
LabelPosition,
|
|
14
|
+
OverlayStyle,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
class FrameAnnotator:
|
|
20
|
+
"""
|
|
21
|
+
Draws detection annotations on video frames.
|
|
22
|
+
|
|
23
|
+
Stateless - Can be reused across frames and videos. This is the core
|
|
24
|
+
rendering component used by both live and post-hoc visualisers.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
annotator = FrameAnnotator(box_style, label_style)
|
|
28
|
+
annotated = annotator.annotate_frame(frame, detections)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
bbox_style: Optional[BoundingBoxStyle] = None,
|
|
34
|
+
label_style: Optional[LabelStyle] = None,
|
|
35
|
+
overlay_style: Optional[OverlayStyle] = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialise the FrameAnnotator with bounding box, label and overlay
|
|
39
|
+
styles.
|
|
40
|
+
"""
|
|
41
|
+
self._bbox_style = bbox_style or BoundingBoxStyle()
|
|
42
|
+
self._label_style = label_style or LabelStyle()
|
|
43
|
+
self._overlay_style = overlay_style or OverlayStyle()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def annotate_frame(
|
|
47
|
+
self,
|
|
48
|
+
frame: np.ndarray,
|
|
49
|
+
detections: List[Detection],
|
|
50
|
+
context: Optional[FrameContext] = None,
|
|
51
|
+
copy: bool = True
|
|
52
|
+
) -> np.ndarray:
|
|
53
|
+
"""
|
|
54
|
+
Draw all detections and overlays on the given frame.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
frame: BGR image as a numpy array (H x W x 3).
|
|
58
|
+
detections: List of Detection objects to render.
|
|
59
|
+
context: Optional FrameContext for frame-level overlays.
|
|
60
|
+
copy: If True, a copy of the frame is made before annotation.
|
|
61
|
+
If False, the frame is annotated in-place.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Annotated frame as a numpy array.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if copy:
|
|
68
|
+
frame = frame.copy()
|
|
69
|
+
|
|
70
|
+
for detection in detections:
|
|
71
|
+
self._draw_detection(frame, detection)
|
|
72
|
+
|
|
73
|
+
# Draw frame-level overlay is enabled and context provided
|
|
74
|
+
if self._overlay_style.enabled:
|
|
75
|
+
if context is not None:
|
|
76
|
+
self._draw_overlay(frame, context, len(detections))
|
|
77
|
+
else:
|
|
78
|
+
logger.warning(
|
|
79
|
+
"Overlay style enabled but no FrameContext provided. Not" \
|
|
80
|
+
" drawing overlay."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return frame
|
|
84
|
+
|
|
85
|
+
def _draw_detection(
|
|
86
|
+
self,
|
|
87
|
+
frame: np.ndarray,
|
|
88
|
+
detection: Detection
|
|
89
|
+
) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Draw a single detection (box and label)
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
frame: BGR image as a numpy array (H x W x 3).
|
|
95
|
+
detection: Detection object to render.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
None - frame is modified in-place.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# Convert centre format to corner format
|
|
102
|
+
x1 = int(detection.xc - detection.width / 2)
|
|
103
|
+
y1 = int(detection.yc - detection.height / 2)
|
|
104
|
+
x2 = int(detection.xc + detection.width / 2)
|
|
105
|
+
y2 = int(detection.yc + detection.height / 2)
|
|
106
|
+
|
|
107
|
+
# Choose colour based on optional per-class mapping
|
|
108
|
+
colour = self._bbox_style.colour
|
|
109
|
+
if (
|
|
110
|
+
self._bbox_style.class_colours is not None
|
|
111
|
+
and detection.label is not None
|
|
112
|
+
and detection.label in self._bbox_style.class_colours
|
|
113
|
+
):
|
|
114
|
+
colour = self._bbox_style.class_colours[detection.label]
|
|
115
|
+
|
|
116
|
+
# Draw bounding box if enabled
|
|
117
|
+
if self._bbox_style.draw_box:
|
|
118
|
+
cv2.rectangle(
|
|
119
|
+
frame,
|
|
120
|
+
(x1, y1),
|
|
121
|
+
(x2, y2),
|
|
122
|
+
colour,
|
|
123
|
+
self._bbox_style.thickness
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Draw centre point if enabled
|
|
127
|
+
if self._bbox_style.draw_centre:
|
|
128
|
+
centre_colour = (
|
|
129
|
+
self._bbox_style.centre_colour
|
|
130
|
+
or colour
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
cv2.circle(
|
|
134
|
+
frame,
|
|
135
|
+
(int(detection.xc), int(detection.yc)),
|
|
136
|
+
self._bbox_style.centre_radius,
|
|
137
|
+
centre_colour,
|
|
138
|
+
-1
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Draw label if enabled
|
|
142
|
+
if self._label_style.enabled:
|
|
143
|
+
self._draw_label(frame, detection, (x1, y1, x2, y2))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _draw_label(
|
|
147
|
+
self,
|
|
148
|
+
frame: np.ndarray,
|
|
149
|
+
detection: Detection,
|
|
150
|
+
bbox: Tuple[int, int, int, int]
|
|
151
|
+
) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Draw label for a detection.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
frame: BGR image as a numpy array (H x W x 3).
|
|
157
|
+
detection: Detection object to render.
|
|
158
|
+
bbox: Bounding box as (x1, y1, x2, y2).
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
label_text = self._format_label(detection)
|
|
162
|
+
|
|
163
|
+
if not label_text:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
x1, y1, x2, y2 = bbox
|
|
167
|
+
frame_h, frame_w = frame.shape[:2]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Get text size for positioning
|
|
171
|
+
(text_w, text_h), baseline = cv2.getTextSize(
|
|
172
|
+
label_text,
|
|
173
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
174
|
+
self._label_style.font_scale,
|
|
175
|
+
self._label_style.font_thickness
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Calculate position based on config
|
|
179
|
+
text_x, text_y = self._calculate_label_position(
|
|
180
|
+
text_w, text_h, baseline, bbox, frame.shape
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Draw background if configured
|
|
184
|
+
if self._label_style.background_colour is not None:
|
|
185
|
+
padding = self._label_style.padding
|
|
186
|
+
|
|
187
|
+
# Calculate background rectangle bounds
|
|
188
|
+
bg_x1 = text_x - padding
|
|
189
|
+
bg_y1 = text_y - text_h - padding
|
|
190
|
+
bg_x2 = text_x + text_w + padding
|
|
191
|
+
bg_y2 = text_y + baseline + padding
|
|
192
|
+
|
|
193
|
+
# Clamp to frame bounds
|
|
194
|
+
bg_x1_clamped = max(0, bg_x1)
|
|
195
|
+
bg_y1_clamped = max(0, bg_y1)
|
|
196
|
+
bg_x2_clamped = min(frame_w, bg_x2)
|
|
197
|
+
bg_y2_clamped = min(frame_h, bg_y2)
|
|
198
|
+
|
|
199
|
+
# Only draw if rectangle has positive area after clamping
|
|
200
|
+
if bg_x2_clamped > bg_x1_clamped and bg_y2_clamped > bg_y1_clamped:
|
|
201
|
+
cv2.rectangle(
|
|
202
|
+
frame,
|
|
203
|
+
(bg_x1_clamped, bg_y1_clamped),
|
|
204
|
+
(bg_x2_clamped, bg_y2_clamped),
|
|
205
|
+
self._label_style.background_colour,
|
|
206
|
+
-1
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Clamp text position to ensure it starts within frame
|
|
210
|
+
# (OpenCV will clip the rendering, but starting position must be valid)
|
|
211
|
+
text_x = max(0, min(text_x, frame_w - 1))
|
|
212
|
+
text_y = max(text_h, min(text_y, frame_h - 1))
|
|
213
|
+
|
|
214
|
+
# Draw text
|
|
215
|
+
cv2.putText(
|
|
216
|
+
frame,
|
|
217
|
+
label_text,
|
|
218
|
+
(text_x, text_y),
|
|
219
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
220
|
+
self._label_style.font_scale,
|
|
221
|
+
self._label_style.colour,
|
|
222
|
+
self._label_style.font_thickness
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _format_label(self, detection:Detection) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Format the label text for a detection based on configuration.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
detection: Detection object to format.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if self._label_style.custom_format:
|
|
235
|
+
return self._label_style.custom_format.format(
|
|
236
|
+
frame_number=detection.frame_number,
|
|
237
|
+
confidence=detection.confidence or 0.0,
|
|
238
|
+
xc=detection.xc,
|
|
239
|
+
yc=detection.yc,
|
|
240
|
+
width=detection.width,
|
|
241
|
+
height=detection.height,
|
|
242
|
+
label=detection.label or "",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
parts = []
|
|
246
|
+
# Class label text
|
|
247
|
+
if self._label_style.show_label and detection.label is not None:
|
|
248
|
+
parts.append(str(detection.label))
|
|
249
|
+
if self._label_style.show_frame_number:
|
|
250
|
+
parts.append(f"Frame: {detection.frame_number}")
|
|
251
|
+
if self._label_style.show_confidence and detection.confidence is not None:
|
|
252
|
+
parts.append(f"Conf: {detection.confidence:.2f}")
|
|
253
|
+
|
|
254
|
+
return " ".join(parts) if parts else ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _calculate_label_position(
|
|
258
|
+
self,
|
|
259
|
+
text_w: int,
|
|
260
|
+
text_h: int,
|
|
261
|
+
baseline: int,
|
|
262
|
+
bbox: Tuple[int, int, int, int],
|
|
263
|
+
frame_shape: Tuple[int, ...]
|
|
264
|
+
) -> Tuple[int, int]:
|
|
265
|
+
"""
|
|
266
|
+
Calculate Pixel position for label based on config.
|
|
267
|
+
|
|
268
|
+
Ensures labels remain within frame bounds, adjusting position if the
|
|
269
|
+
preffered location would place text outside the frame.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
x1, y1, x2, y2 = bbox
|
|
273
|
+
padding = self._label_style.padding
|
|
274
|
+
frame_h, frame_w = frame_shape[:2]
|
|
275
|
+
|
|
276
|
+
pos = self._label_style.position
|
|
277
|
+
if pos == LabelPosition.TOP_LEFT:
|
|
278
|
+
x = x1 + padding
|
|
279
|
+
y = y1 - padding - baseline
|
|
280
|
+
elif pos == LabelPosition.TOP_RIGHT:
|
|
281
|
+
x = x2 - text_w - padding
|
|
282
|
+
y = y1 - padding - baseline
|
|
283
|
+
elif pos == LabelPosition.BOTTOM_LEFT:
|
|
284
|
+
x = x1 + padding
|
|
285
|
+
y = y2 + text_h + padding
|
|
286
|
+
elif pos == LabelPosition.BOTTOM_RIGHT:
|
|
287
|
+
x = x2 - text_w - padding
|
|
288
|
+
y = y2 + text_h + padding
|
|
289
|
+
elif pos == LabelPosition.ABOVE:
|
|
290
|
+
x = x1
|
|
291
|
+
y = y1 - padding - baseline
|
|
292
|
+
else: # BELOW
|
|
293
|
+
x = x1
|
|
294
|
+
y = y2 + text_h + padding
|
|
295
|
+
|
|
296
|
+
# Ensure text doesn't extend beyond right edge
|
|
297
|
+
if x + text_w > frame_w:
|
|
298
|
+
x = frame_w - text_w - padding
|
|
299
|
+
|
|
300
|
+
# Ensure text doesn't extend beyond left edge
|
|
301
|
+
if x < padding:
|
|
302
|
+
x = padding
|
|
303
|
+
|
|
304
|
+
# If label would be above the frame, flip to below the box
|
|
305
|
+
if y - text_h < 0:
|
|
306
|
+
y = y2 + text_h + padding
|
|
307
|
+
|
|
308
|
+
# If label would be below frame, flip to top of the bbox
|
|
309
|
+
if y + baseline > frame_h:
|
|
310
|
+
y = y1 - padding - baseline
|
|
311
|
+
|
|
312
|
+
# If still outside (i.e. bounding box fills frame), clamp to bottom
|
|
313
|
+
if y - text_h < 0:
|
|
314
|
+
y = text_h + padding
|
|
315
|
+
|
|
316
|
+
return (int(x), int(y))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _draw_overlay(
|
|
320
|
+
self,
|
|
321
|
+
frame:np.ndarray,
|
|
322
|
+
context: FrameContext,
|
|
323
|
+
detection_count: int
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Draw frame-level overlay information."""
|
|
326
|
+
|
|
327
|
+
parts = []
|
|
328
|
+
|
|
329
|
+
if self._overlay_style.show_frame_number:
|
|
330
|
+
parts.append(f"Frame: {context.frame_number}")
|
|
331
|
+
if self._overlay_style.show_timestamp:
|
|
332
|
+
parts.append(f"Time: {context.timestamp:.2f}s")
|
|
333
|
+
if self._overlay_style.show_detection_count:
|
|
334
|
+
parts.append(f"Detections: {detection_count}")
|
|
335
|
+
|
|
336
|
+
if not parts:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
text = " | ".join(parts)
|
|
340
|
+
position = self._overlay_style.position
|
|
341
|
+
frame_h, frame_w = frame.shape[:2]
|
|
342
|
+
|
|
343
|
+
# Get text size for background
|
|
344
|
+
(text_w, text_h), baseline = cv2.getTextSize(
|
|
345
|
+
text,
|
|
346
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
347
|
+
self._overlay_style.font_scale,
|
|
348
|
+
self._overlay_style.font_thickness
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Clamp position to frame bounds
|
|
352
|
+
text_x = max(0, min(position[0], frame_w - text_w - 1))
|
|
353
|
+
text_y = max(text_h, min(position[1], frame_h - 1))
|
|
354
|
+
|
|
355
|
+
# Draw background
|
|
356
|
+
if self._overlay_style.background_colour is not None:
|
|
357
|
+
padding = 5
|
|
358
|
+
|
|
359
|
+
bg_x1 = max(0, text_x - padding)
|
|
360
|
+
bg_y1 = max(0, text_y - text_h - padding)
|
|
361
|
+
bg_x2 = min(frame_w, text_x + text_w + padding)
|
|
362
|
+
bg_y2 = min(frame_h, text_y + baseline + padding)
|
|
363
|
+
|
|
364
|
+
if bg_x2 > bg_x1 and bg_y2 > bg_y1:
|
|
365
|
+
cv2.rectangle(
|
|
366
|
+
frame,
|
|
367
|
+
(bg_x1, bg_y1),
|
|
368
|
+
(bg_x2, bg_y2),
|
|
369
|
+
self._overlay_style.background_colour,
|
|
370
|
+
-1
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Draw text
|
|
374
|
+
cv2.putText(
|
|
375
|
+
frame,
|
|
376
|
+
text,
|
|
377
|
+
(text_x, text_y),
|
|
378
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
379
|
+
self._overlay_style.font_scale,
|
|
380
|
+
self._overlay_style.colour,
|
|
381
|
+
self._overlay_style.font_thickness
|
|
382
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Configuration data classes for the visualiser module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from typing import Tuple, Optional, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OutputMode(Enum):
|
|
9
|
+
"""Output mode for the visualiser."""
|
|
10
|
+
FILE = auto() # Write frames to video file
|
|
11
|
+
STREAM = auto() # Yield frames for GUI integration
|
|
12
|
+
BOTH = auto() # Write frames to file and yield for GUI integration
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LabelPosition(Enum):
|
|
16
|
+
"""Position of label relative to bounding box."""
|
|
17
|
+
TOP_LEFT = auto()
|
|
18
|
+
TOP_RIGHT = auto()
|
|
19
|
+
BOTTOM_LEFT = auto()
|
|
20
|
+
BOTTOM_RIGHT = auto()
|
|
21
|
+
ABOVE = auto()
|
|
22
|
+
BELOW = auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BoundingBoxStyle:
|
|
27
|
+
"""Style configuration for bounding box rendering."""
|
|
28
|
+
draw_box: bool = True
|
|
29
|
+
colour: Tuple[int, int, int] = (0, 255, 0) # BGR format green
|
|
30
|
+
thickness: int = 2
|
|
31
|
+
draw_centre: bool = False
|
|
32
|
+
centre_radius: int = 3
|
|
33
|
+
centre_colour: Tuple[int, int, int] = (0, 255, 0) # BGR format green
|
|
34
|
+
# Optional per-class colours, keyed by Detection.label string. If a
|
|
35
|
+
# detection's label matches a key in this dict, that colour will be used
|
|
36
|
+
# instead of the default "colour" value.
|
|
37
|
+
class_colours: Optional[Dict[str, Tuple[int, int, int]]] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LabelStyle:
|
|
42
|
+
"""Style configuration for label rendering."""
|
|
43
|
+
enabled: bool = True
|
|
44
|
+
font_scale: float = 0.5
|
|
45
|
+
font_thickness: int = 1
|
|
46
|
+
colour: Tuple[int, int, int] = (255, 255, 255) # BGR format white
|
|
47
|
+
background_colour: Optional[Tuple[int, int, int]] = (0, 0, 0) # BGR format black
|
|
48
|
+
position: LabelPosition = LabelPosition.TOP_LEFT
|
|
49
|
+
padding: int = 2
|
|
50
|
+
# Whether to include the Detection.label text (if present) in the
|
|
51
|
+
# rendered label string.
|
|
52
|
+
show_label: bool = True
|
|
53
|
+
show_confidence: bool = True
|
|
54
|
+
show_frame_number: bool = False
|
|
55
|
+
custom_format: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class OverlayStyle:
|
|
60
|
+
"""Style configuration for frame-level overlay information"""
|
|
61
|
+
enabled: bool = False
|
|
62
|
+
show_frame_number: bool = True
|
|
63
|
+
show_timestamp: bool = True
|
|
64
|
+
show_detection_count: bool = True
|
|
65
|
+
font_scale: float = 0.6
|
|
66
|
+
font_thickness: int = 1
|
|
67
|
+
colour: Tuple[int, int, int] = (255, 255, 255)
|
|
68
|
+
background_colour: Optional[Tuple[int, int, int]] = (0, 0, 0)
|
|
69
|
+
position: Tuple[int, int] = (10, 25) # Top-left corner offset
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class VideoOutputConfig:
|
|
74
|
+
"""
|
|
75
|
+
Configuration for video file output.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
output_dir: Directory where output videos will be saved.
|
|
79
|
+
filename_suffix: Suffix added to the output filename stem
|
|
80
|
+
(before the extension).
|
|
81
|
+
codec: FourCC codec string for video encoding.
|
|
82
|
+
format: Output file extension (including the dot).
|
|
83
|
+
fps: Output frame rate. If None, matches source video.
|
|
84
|
+
overwrite: If True, overwrite existing output files.
|
|
85
|
+
include_parent_dirs: Number of parent directory levels to include
|
|
86
|
+
in the output filename to avoid collisions. Only used when
|
|
87
|
+
no custom name function is provided.
|
|
88
|
+
"""
|
|
89
|
+
output_dir: str = "./visualised_output"
|
|
90
|
+
filename_suffix: str = "_visualised"
|
|
91
|
+
codec: str = "mp4v"
|
|
92
|
+
format: str = ".mp4"
|
|
93
|
+
fps: Optional[float] = None
|
|
94
|
+
overwrite: bool = False
|
|
95
|
+
include_parent_dirs: int = 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class VisualiserConfig:
|
|
100
|
+
"""
|
|
101
|
+
Main configuration for the visualiser module.
|
|
102
|
+
|
|
103
|
+
Used by both LiveVisualiser (for pipeline integration) and PostHocVisualiser
|
|
104
|
+
(CSV-based outputs).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
output_mode: OutputMode = OutputMode.FILE
|
|
108
|
+
video_output: VideoOutputConfig = field(
|
|
109
|
+
default_factory=VideoOutputConfig
|
|
110
|
+
)
|
|
111
|
+
bbox_style: BoundingBoxStyle = field(
|
|
112
|
+
default_factory=BoundingBoxStyle
|
|
113
|
+
)
|
|
114
|
+
label_style: LabelStyle = field(
|
|
115
|
+
default_factory=LabelStyle
|
|
116
|
+
)
|
|
117
|
+
overlay_style: OverlayStyle = field(
|
|
118
|
+
default_factory=OverlayStyle
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Filtering options
|
|
122
|
+
min_confidence: Optional[float] = None # Minimum confidence to visualise
|
|
123
|
+
|
|
124
|
+
# Frame options - for post hoc mode
|
|
125
|
+
only_frames_with_detections: bool = False # Only visualise frames with detections
|
|
126
|
+
frame_skip: int = 1
|
|
127
|
+
|