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.
Files changed (73) hide show
  1. seavision/__init__.py +3 -0
  2. seavision/defaults.py +38 -0
  3. seavision/edge/__init__.py +38 -0
  4. seavision/edge/bundle.py +189 -0
  5. seavision/edge/capture.py +107 -0
  6. seavision/edge/config.py +219 -0
  7. seavision/edge/postprocess.py +185 -0
  8. seavision/edge/runtime.py +416 -0
  9. seavision/edge/writer.py +167 -0
  10. seavision/engine/__init__.py +52 -0
  11. seavision/engine/detectors/__init__.py +90 -0
  12. seavision/engine/detectors/base.py +179 -0
  13. seavision/engine/detectors/motion/__init__.py +14 -0
  14. seavision/engine/detectors/motion/background.py +92 -0
  15. seavision/engine/detectors/motion/detector.py +218 -0
  16. seavision/engine/detectors/motion/stabiliser.py +174 -0
  17. seavision/engine/detectors/motion/tracker.py +174 -0
  18. seavision/engine/detectors/sam3/__init__.py +23 -0
  19. seavision/engine/detectors/sam3/config.py +309 -0
  20. seavision/engine/detectors/sam3/detector.py +1316 -0
  21. seavision/engine/detectors/sam3/native.py +490 -0
  22. seavision/engine/detectors/yolo/__init__.py +12 -0
  23. seavision/engine/detectors/yolo/config.py +39 -0
  24. seavision/engine/detectors/yolo/detector.py +236 -0
  25. seavision/engine/export/__init__.py +24 -0
  26. seavision/engine/export/config.py +160 -0
  27. seavision/engine/export/exporter.py +286 -0
  28. seavision/engine/export/validation.py +314 -0
  29. seavision/engine/postprocessor.py +738 -0
  30. seavision/engine/source/__init__.py +16 -0
  31. seavision/engine/source/base.py +87 -0
  32. seavision/engine/source/discovery.py +154 -0
  33. seavision/engine/source/local.py +155 -0
  34. seavision/engine/source/s3.py +231 -0
  35. seavision/engine/visualiser/__init__.py +62 -0
  36. seavision/engine/visualiser/annotator.py +382 -0
  37. seavision/engine/visualiser/config.py +127 -0
  38. seavision/engine/visualiser/loader.py +558 -0
  39. seavision/engine/visualiser/visualiser.py +402 -0
  40. seavision/engine/visualiser/writer.py +245 -0
  41. seavision/gui/__init__.py +0 -0
  42. seavision/gui/app.py +30 -0
  43. seavision/gui/main_window.py +891 -0
  44. seavision/gui/shared/__init__.py +0 -0
  45. seavision/gui/shared/conversion.py +55 -0
  46. seavision/gui/shared/session_utils.py +37 -0
  47. seavision/gui/validation/__init__.py +0 -0
  48. seavision/gui/validation/button_styles.py +144 -0
  49. seavision/gui/validation/detection_detail.py +164 -0
  50. seavision/gui/validation/detection_rect_item.py +486 -0
  51. seavision/gui/validation/detection_table.py +563 -0
  52. seavision/gui/validation/interactive_frame_scene.py +340 -0
  53. seavision/gui/validation/interactive_frame_view.py +252 -0
  54. seavision/gui/validation/s3_browser.py +645 -0
  55. seavision/gui/validation/seekable_source.py +262 -0
  56. seavision/gui/validation/session.py +376 -0
  57. seavision/gui/validation/tab.py +2503 -0
  58. seavision/gui/validation/transport_bar.py +172 -0
  59. seavision/gui/validation/validation_model.py +482 -0
  60. seavision/gui/validation/video_cache.py +215 -0
  61. seavision/gui/validation/video_list.py +140 -0
  62. seavision/gui/validation/video_viewer.py +150 -0
  63. seavision/gui/validation/video_worker.py +422 -0
  64. seavision/pipeline.py +856 -0
  65. seavision/resources/default.yaml +139 -0
  66. seavision/run_edge.py +96 -0
  67. seavision/run_export.py +176 -0
  68. seavision/run_pipeline.py +388 -0
  69. seavision_python-0.1.1.dist-info/METADATA +286 -0
  70. seavision_python-0.1.1.dist-info/RECORD +73 -0
  71. seavision_python-0.1.1.dist-info/WHEEL +5 -0
  72. seavision_python-0.1.1.dist-info/entry_points.txt +5 -0
  73. 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
+