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,402 @@
1
+ """High level visualiser orchestrators."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Callable, Iterator, List, Optional
6
+ import numpy as np
7
+
8
+ from ..source.base import VideoSource, VideoMetadata, FrameContext
9
+ from ..detectors.base import Detection
10
+ from .config import VisualiserConfig, OutputMode
11
+ from .annotator import FrameAnnotator
12
+ from .writer import VideoWriterHandle, OutputNameFunction
13
+ from .loader import DetectionSource, CSVDetectionLoader
14
+
15
+
16
+ @dataclass
17
+ class AnnotatedFrame:
18
+ """Container for an annotated frame (for streaming output)."""
19
+ frame: np.ndarray
20
+ frame_number: int
21
+ timestamp: float
22
+ detections: List[Detection]
23
+ source_file: str
24
+
25
+
26
+ @dataclass
27
+ class VisualisationResult:
28
+ """Result statistics from visualisation."""
29
+ source_file: str
30
+ output_path: Optional[str]
31
+ total_frames: int
32
+ frames_with_detections: int
33
+ total_detections: int
34
+
35
+ def summary(self) -> str:
36
+ """Create a human readable output of validation results."""
37
+ return (
38
+ f"Visualised: {Path(self.source_file).name}\n"
39
+ f" Output: {self.output_path or '(stream only)'}\n"
40
+ f" Frames: {self.total_frames} "
41
+ f"({self.frames_with_detections} with detections)\n"
42
+ f" Detections: {self.total_detections}"
43
+ )
44
+
45
+
46
+ class LiveVisualiser:
47
+ """
48
+ Visualiser for integration with the detection pipeline.
49
+
50
+ Designed to be called frame-by-frame as detections are generated,
51
+ rather than loading detections from CSV post-hoc.
52
+
53
+ Lifecycle:
54
+ 1. Create LiveVisualiser with config
55
+ 2. Call start_video() when beginning a new video
56
+ 3. Call process_frame() for each frame + its detections
57
+ 4. Call end_video() when finished with that video
58
+
59
+ Example (pipeline integration):
60
+ visualiser = LiveVisualiser(config)
61
+
62
+ for source in sources:
63
+ visualiser.start_video(source.get_metadata())
64
+
65
+ for frame, context in source.iter_frames():
66
+ detections = list(detector.process_frame(frame, context))
67
+ annotated = visualiser.process_frame(frame, detections, context)
68
+
69
+ result = visualiser.end_video()
70
+ print(result.summary())
71
+
72
+ Example (custom naming):
73
+ def device_date_namer(meta: VideoMetadata) -> str:
74
+ # Parse S3 URI to extract device and date
75
+ parts = meta.source_file.split('/')
76
+ device = parts[-4] # e.g., "cam-3-0"
77
+ date = parts[-2] # e.g., "2025-01-15"
78
+ stem = Path(meta.source_file).stem
79
+ return f"{device}_{date}_{stem}"
80
+
81
+ visualiser = LiveVisualiser(config, name_function=device_date_namer)
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ config: Optional[VisualiserConfig] = None,
87
+ name_function: Optional[OutputNameFunction] = None
88
+ ):
89
+ """
90
+ Initialise the live visualiser.
91
+
92
+ Args:
93
+ config: Visualisation configuration. If None, defaults are used.
94
+ name_function: Optional custom function to generate output filenames.
95
+ Takes VideoMetadata, returns the filename stem (without suffix
96
+ or extension). If None, uses default naming based on source path.
97
+ """
98
+ self._config = config or VisualiserConfig()
99
+ self._name_function = name_function
100
+ self._annotator = FrameAnnotator(
101
+ self._config.bbox_style,
102
+ self._config.label_style,
103
+ self._config.overlay_style,
104
+ )
105
+
106
+ # Per-video state
107
+ self._writer: Optional[VideoWriterHandle] = None
108
+ self._metadata: Optional[VideoMetadata] = None
109
+ self._frames_processed: int = 0
110
+ self._frames_with_detections: int = 0
111
+ self._total_detections: int = 0
112
+
113
+ def start_video(self, metadata: VideoMetadata) -> None:
114
+ """
115
+ Begin visualisation for a new video.
116
+
117
+ Args:
118
+ metadata: Metadata for the video being processed.
119
+
120
+ Raises:
121
+ RuntimeError: If called while another video is in progress.
122
+ """
123
+
124
+ if self._writer is not None:
125
+ raise RuntimeError(
126
+ "Previous video not ended. Call end_video() first."
127
+ )
128
+
129
+ self._metadata = metadata
130
+ self._frames_processed = 0
131
+ self._frames_with_detections = 0
132
+ self._total_detections = 0
133
+
134
+ # Open video writer if outputting to file
135
+ if self._config.output_mode in (OutputMode.FILE, OutputMode.BOTH):
136
+ self._writer = VideoWriterHandle(
137
+ self._config.video_output,
138
+ metadata,
139
+ name_function=self._name_function
140
+ )
141
+
142
+ def process_frame(
143
+ self, frame: np.ndarray,
144
+ detections: List[Detection],
145
+ context: FrameContext
146
+ ) -> Optional[AnnotatedFrame]:
147
+ """
148
+ Process a single frame with its detections.
149
+
150
+ Args:
151
+ frame: BGR image as numpy array
152
+ detections: List of detections for this frame
153
+ context: Frame context with metadata
154
+
155
+ Returns:
156
+ AnnotatedFrame if output_mode includes STREAM, else None.
157
+
158
+ Raises:
159
+ RuntimeError: If called when no video is in progress.
160
+ """
161
+
162
+ if self._metadata is None:
163
+ raise RuntimeError(
164
+ "No video in progress. Call start_video() first."
165
+ )
166
+
167
+ # Apply confidence filter
168
+ if self._config.min_confidence is not None:
169
+ detections = [
170
+ d for d in detections
171
+ if d.confidence is None
172
+ or d.confidence >= self._config.min_confidence
173
+ ]
174
+
175
+ # Skip frames without detections if configured
176
+ if self._config.only_frames_with_detections and not detections:
177
+ return None
178
+
179
+ # Update statistics
180
+ self._frames_processed += 1
181
+ self._total_detections += len(detections)
182
+ if detections:
183
+ self._frames_with_detections += 1
184
+
185
+ # Annotate frame
186
+ annotated = self._annotator.annotate_frame(
187
+ frame, detections, context, copy=True
188
+ )
189
+
190
+ # Write to video file if configured
191
+ if self._writer is not None:
192
+ self._writer.write(annotated)
193
+
194
+ # Return for streaming if configured
195
+ if self._config.output_mode in (OutputMode.STREAM, OutputMode.BOTH):
196
+ return AnnotatedFrame(
197
+ frame=annotated,
198
+ frame_number=context.frame_number,
199
+ timestamp=context.timestamp,
200
+ detections=detections,
201
+ source_file=self._metadata.source_file
202
+ )
203
+
204
+ return None
205
+
206
+ def end_video(self) -> VisualisationResult:
207
+ """
208
+ Finish visualisation for the current video.
209
+
210
+ Returns:
211
+ VisualisationResult with summary statistics.
212
+
213
+ Raises:
214
+ RuntimeError: If called when no video is in progress.
215
+ """
216
+
217
+ if self._metadata is None:
218
+ raise RuntimeError("No video in progress")
219
+
220
+ # Close video writer
221
+ output_path = None
222
+ if self._writer is not None:
223
+ output_path = str(self._writer.output_path)
224
+ self._writer.close()
225
+ self._writer = None
226
+
227
+ result = VisualisationResult(
228
+ source_file=self._metadata.source_file,
229
+ output_path=output_path,
230
+ total_frames=self._frames_processed,
231
+ frames_with_detections=self._frames_with_detections,
232
+ total_detections=self._total_detections,
233
+ )
234
+
235
+ # Reset state
236
+ self._metadata = None
237
+
238
+ return result
239
+
240
+
241
+ def __enter__(self) -> "LiveVisualiser":
242
+ return self
243
+
244
+
245
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
246
+ # Clean up if video was not properly ended
247
+ if self._writer is not None:
248
+ self._writer.close()
249
+ self._writer = None
250
+ return False
251
+
252
+
253
+ class PostHocVisualiser:
254
+ """
255
+ Visualiser for post-hoc processing from CSV detection files.
256
+
257
+ Reads detections from CSV and overlays them on the source video.
258
+
259
+ Example:
260
+ visualiser = PostHocVisualiser(config)
261
+
262
+ with LocalVideoSource("footage/video.ts") as source:
263
+ result = visualiser.visualise(source, "results/video_detections.csv")
264
+ print(result.summary())
265
+
266
+ Example (custom naming):
267
+ def custom_namer(meta: VideoMetadata) -> str:
268
+ return f"reviewed_{Path(meta.source_file).stem}"
269
+
270
+ visualiser = PostHocVisualiser(config, name_function=custom_namer)
271
+ """
272
+
273
+ def __init__(
274
+ self,
275
+ config: Optional[VisualiserConfig] = None,
276
+ name_function: Optional[OutputNameFunction] = None
277
+ ):
278
+ self._config = config or VisualiserConfig()
279
+ self._name_function = name_function
280
+ self._annotator = FrameAnnotator(
281
+ self._config.bbox_style,
282
+ self._config.label_style,
283
+ self._config.overlay_style,
284
+ )
285
+
286
+
287
+ def visualise(
288
+ self,
289
+ video_source: VideoSource,
290
+ csv_path: str,
291
+ ) -> Iterator[AnnotatedFrame]:
292
+ """
293
+ Visualise a video using detections from a CSV file.
294
+
295
+ Args:
296
+ video_source: VideoSource to read frames from.
297
+ csv_path: Path to CSV file with detections.
298
+
299
+ Yields:
300
+ AnnotatedFrame objects if output_mode includes STREAM.
301
+ """
302
+
303
+ detection_source = CSVDetectionLoader(
304
+ csv_path,
305
+ source_file=video_source.get_metadata().source_file
306
+ )
307
+ yield from self._visualise(video_source, detection_source)
308
+
309
+
310
+ def visualise_from_source(
311
+ self,
312
+ video_source: VideoSource,
313
+ detection_source: DetectionSource,
314
+ ) -> Iterator[AnnotatedFrame]:
315
+ """
316
+ Visualise a video using a detection source.
317
+
318
+ Args:
319
+ video_source: VideoSource to read frames from.
320
+ detection_source: DetectionSource to provide detections.
321
+
322
+ Yields:
323
+ AnnotatedFrame objects if output_mode includes STREAM.
324
+ """
325
+ yield from self._visualise(video_source, detection_source)
326
+
327
+
328
+ def _visualise(
329
+ self,
330
+ video_source: VideoSource,
331
+ detection_source: DetectionSource,
332
+ ) -> Iterator[AnnotatedFrame]:
333
+ """Core visualisation loop for post-hoc mode."""
334
+
335
+ metadata = video_source.get_metadata()
336
+ writer = None
337
+
338
+ # Statistics
339
+ frames_processed = 0
340
+ frames_with_detections = 0
341
+ total_detections = 0
342
+
343
+ if self._config.output_mode in (OutputMode.FILE, OutputMode.BOTH):
344
+ writer = VideoWriterHandle(
345
+ self._config.video_output,
346
+ metadata,
347
+ name_function=self._name_function
348
+ )
349
+
350
+ try:
351
+ for frame, context in video_source.iter_frames():
352
+ # Frame skipping
353
+ if context.frame_number % self._config.frame_skip != 0:
354
+ continue
355
+
356
+ # Get detections for this frame
357
+ detections = detection_source.get_detections_for_frame(
358
+ context.frame_number
359
+ )
360
+
361
+ # Apply confidence filter
362
+ if self._config.min_confidence is not None:
363
+ detections = [
364
+ d for d in detections
365
+ if d.confidence is None
366
+ or d.confidence >= self._config.min_confidence
367
+ ]
368
+
369
+ # Skip frames without detections if configured
370
+ if self._config.only_frames_with_detections and not detections:
371
+ continue
372
+
373
+ # Update statistics
374
+ frames_processed += 1
375
+ if detections:
376
+ frames_with_detections += 1
377
+ total_detections += len(detections)
378
+
379
+ # Annotate frame
380
+ annotated = self._annotator.annotate_frame(
381
+ frame, detections, context
382
+ )
383
+
384
+ # Write to file
385
+ if writer is not None:
386
+ writer.write(annotated)
387
+
388
+ # Yield for streaming
389
+ if self._config.output_mode in (OutputMode.STREAM, OutputMode.BOTH):
390
+ yield AnnotatedFrame(
391
+ frame=annotated,
392
+ frame_number=context.frame_number,
393
+ timestamp=context.timestamp,
394
+ detections=detections,
395
+ source_file=metadata.source_file
396
+ )
397
+ finally:
398
+ if writer is not None:
399
+ writer.close()
400
+
401
+
402
+
@@ -0,0 +1,245 @@
1
+ """Video file writing management."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path, PurePosixPath
5
+ from typing import Callable, Optional
6
+ import re
7
+ import cv2
8
+ import numpy as np
9
+
10
+ from ..source.base import VideoMetadata
11
+ from .config import VideoOutputConfig
12
+
13
+
14
+ # Type alias for custom naming functions
15
+ # Takes VideoMatadata, returns the output filename stem
16
+ # (without suffix/extension)
17
+ OutputNameFunction = Callable[[VideoMetadata], str]
18
+
19
+ def extract_output_stem(
20
+ source_file: str,
21
+ include_parents: int = 2
22
+ ) -> str:
23
+ """
24
+ Extract a suitable output filename stem from a source file path or URI.
25
+
26
+ Handles both local paths and S3 URIs, including enough path components to
27
+ avoid collisions when multiple videos have the same filename.
28
+
29
+ Args:
30
+ source_file: Source file path or S3 URI.
31
+ include_parents: Number of parent directories to include in the output
32
+ stem to help avoid name collisions. Default is 2.
33
+
34
+ Returns:
35
+ A sanitised string suitable for use as a filename stem.
36
+
37
+ Examples:
38
+ # Local path
39
+ extract_output_stem("/home/user/footage/2025-01-15/video_001.ts")
40
+ # -> "2025-01-15_video_001"
41
+
42
+ # S3 URI
43
+ extract_output_stem("s3://bucket/device1/cam1/2025-01-15/video_001.ts")
44
+ # -> "cam1_2025-01-15_video_001"
45
+
46
+ # S3 URI with more context
47
+ extract_output_stem("s3://bucket/device1/cam1/2025-01-15/video_001.ts", include_parents=3)
48
+ # -> "device1_cam1_2025-01-15_video_001"
49
+ """
50
+
51
+ # Handle S3 URIs - extract the key portion
52
+ s3_match = re.match(r"^s3://[^/]+/(.+)$", source_file)
53
+ if s3_match:
54
+ # Use PurePosixPath for S3 keys (always forward slashes)
55
+ path = PurePosixPath(s3_match.group(1))
56
+ else:
57
+ # Local path
58
+ path = Path(source_file)
59
+
60
+ # Get the filename stem
61
+ stem = path.stem
62
+
63
+ # Build output stem with parent directories
64
+ parts = []
65
+ for i in range(min(include_parents, len(list(path.parents)) - 1)):
66
+ parent_name = path.parents[i].name
67
+ if parent_name: # Skip empty names (root)
68
+ parts.insert(0, parent_name)
69
+ parts.append(stem)
70
+
71
+ # Join with underscores and sanitise
72
+ output_stem = "_".join(parts)
73
+
74
+ # Remove any characters that might cause filesystem issues
75
+ output_stem = re.sub(r'[<>:"/\\|?*]', '_', output_stem)
76
+
77
+ return output_stem
78
+
79
+
80
+ def sanitise_filename(name: str) -> str:
81
+ """
82
+ Sanitise a string to be safe for use as a filename.
83
+
84
+ Replaces or removes characters that are invalid in filenames across
85
+ common operating systems.
86
+
87
+ Args:
88
+ name: Input string to sanitize.
89
+
90
+ Returns:
91
+ Sanitized filename string.
92
+ """
93
+ # Replace problematic characters with underscores
94
+ sanitised = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', name)
95
+
96
+ # Collapse multiple underscores
97
+ sanitised = re.sub(r'_+', '_', sanitised)
98
+
99
+ # Strip leading/trailing underscores and whitespace
100
+ sanitised = sanitised.strip('_ ')
101
+
102
+ # Ensure we have something left
103
+ if not sanitised:
104
+ sanitised = "output"
105
+
106
+ return sanitised
107
+
108
+
109
+ @dataclass
110
+ class VideoWriterHandle:
111
+ """
112
+ Manages the lifecycle of a video file writer.
113
+
114
+ Handles opening, writing frames, and closing video files.
115
+ Uses context manager pattern for safe resource cleanup.
116
+
117
+ Output naming priority:
118
+ 1. Custom naming function (if provided)
119
+ 2. Default naming using extact_output_stem()
120
+
121
+ Example with custom naming:
122
+ def my_namer(meta: VideoMetadata) -> str:
123
+ # Extract device and date from S3 path
124
+ parts = meta.source_file.split('/')
125
+ return f"{parts[-3]}_{parts[-2]}_{Path(meta.source_file).stem}"
126
+
127
+ with VideoWriterHandle(config, metadata, name_function=my_namer) as writer:
128
+ ...
129
+ """
130
+
131
+ config: VideoOutputConfig
132
+ metadata: VideoMetadata
133
+ name_function: Optional[OutputNameFunction] = None
134
+ _writer: Optional[cv2.VideoWriter] = field(default=None, init=False)
135
+ _output_path: Optional[Path] = field(default=None, init=False)
136
+ _frame_count: int = field(default=0, init=False)
137
+
138
+ def __post_init__(self):
139
+ """Initialise and open the video writer."""
140
+ self._output_path = self._build_output_path()
141
+ self._ensure_output_dir()
142
+ self._check_overwrite()
143
+ self._open_writer()
144
+
145
+ def _build_output_path(self) -> Path:
146
+ """
147
+ Build the output file path from config and metadata.
148
+
149
+ Uses custom name function if provided, otherwise falls back to
150
+ extract_output_stem() with configured parent directory depth.
151
+ """
152
+ output_dir = Path(self.config.output_dir)
153
+
154
+ # Determine the output stem
155
+ if self.name_function is not None:
156
+ # Use custom naming function
157
+ stem = self.name_function(self.metadata)
158
+ stem = sanitise_filename(stem)
159
+ else:
160
+ # Default naming with parent directory context
161
+ stem = extract_output_stem(
162
+ self.metadata.source_file,
163
+ include_parents=self.config.include_parent_dirs
164
+ )
165
+
166
+ output_name = stem + self.config.filename_suffix + self.config.format
167
+ return output_dir / output_name
168
+
169
+
170
+ def _ensure_output_dir(self) -> None:
171
+ """Create output directory if it doesn't exist."""
172
+ self._output_path.parent.mkdir(parents=True, exist_ok=True)
173
+
174
+
175
+ def _check_overwrite(self) -> None:
176
+ """Check if output file exists and handle based on config."""
177
+ if self._output_path.exists() and not self.config.overwrite:
178
+ raise FileExistsError(
179
+ f"Output file already exists: {self._output_path}. "
180
+ "Set overwrite=True to replace."
181
+ )
182
+
183
+
184
+ def _open_writer(self) -> None:
185
+ """Open the cv2 VideoWriter with the specified configuration."""
186
+
187
+ fourcc = cv2.VideoWriter_fourcc(*self.config.codec)
188
+ fps = self.config.fps or self.metadata.fps
189
+
190
+ self._writer = cv2.VideoWriter(
191
+ str(self._output_path),
192
+ fourcc,
193
+ fps,
194
+ (self.metadata.width, self.metadata.height)
195
+ )
196
+
197
+ if not self._writer.isOpened():
198
+ raise RuntimeError(
199
+ f"Failed to open video writer for {self._output_path}"
200
+ )
201
+
202
+
203
+ def write(self, frame: np.ndarray) -> None:
204
+ """
205
+ Write a frame to the video file.
206
+
207
+ Args:
208
+ frame: BGR image as a numpy array.
209
+
210
+ Raises:
211
+ RuntimeError: If the writer is not opened.
212
+ """
213
+
214
+ if self._writer is None:
215
+ raise RuntimeError("Video writer is not opened.")
216
+
217
+ self._writer.write(frame)
218
+ self._frame_count += 1
219
+
220
+
221
+ def close(self) -> None:
222
+ """Release the video writer resource."""
223
+ if self._writer is not None:
224
+ self._writer.release()
225
+ self._writer = None
226
+
227
+
228
+ @property
229
+ def frame_count(self) -> int:
230
+ """Get the number of frames written."""
231
+ return self._frame_count
232
+
233
+
234
+ @property
235
+ def output_path(self) -> Optional[Path]:
236
+ """Get the output file path."""
237
+ return self._output_path
238
+
239
+
240
+ def __enter__(self) -> "VideoWriterHandle":
241
+ return self
242
+
243
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
244
+ self.close()
245
+ return False
File without changes
seavision/gui/app.py ADDED
@@ -0,0 +1,30 @@
1
+ """SeaVision GUI application entry point."""
2
+
3
+ import sys
4
+
5
+ def main():
6
+ """Launch the SeaVision GUI."""
7
+
8
+ # Import PySide6 with error handling if not available
9
+ try:
10
+ from PySide6.QtWidgets import QApplication
11
+ except ImportError:
12
+ print(
13
+ "PySide6 is required for the GUI. "
14
+ "Install with: pip install seavision[gui]"
15
+ )
16
+ sys.exit(1)
17
+
18
+ app = QApplication(sys.argv)
19
+ app.setApplicationName("SeaVision")
20
+
21
+ from seavision.gui.main_window import MainWindow
22
+
23
+ window = MainWindow()
24
+ window.show()
25
+
26
+ sys.exit(app.exec())
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()