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,16 @@
1
+ """Video source handling."""
2
+
3
+ from .base import FrameContext, VideoMetadata, VideoSource
4
+ from .local import LocalVideoSource
5
+ from .s3 import S3VideoSource
6
+ from .discovery import discover_local_videos, discover_s3_videos
7
+
8
+ __all__ = [
9
+ "FrameContext",
10
+ "VideoMetadata",
11
+ "VideoSource",
12
+ "LocalVideoSource",
13
+ "S3VideoSource",
14
+ "discover_local_videos",
15
+ "discover_s3_videos",
16
+ ]
@@ -0,0 +1,87 @@
1
+ """Base classes for video source handling."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import Iterator, Tuple
6
+ import numpy as np
7
+
8
+
9
+ @dataclass
10
+ class VideoMetadata:
11
+ """
12
+ Metadata for a video source.
13
+
14
+ Attributes:
15
+ source_file: Path of the original source file.
16
+ fps: Frames per second of the video.
17
+ frame_count: Total number of frames in the video.
18
+ width: Width of the video frames (pixels).
19
+ height: Height of the video frames (pixels).
20
+ duration: Duration of the video (seconds).
21
+ """
22
+ source_file: str
23
+ fps: float
24
+ frame_count: int
25
+ width: int
26
+ height: int
27
+ duration: float
28
+
29
+
30
+ @dataclass
31
+ class FrameContext:
32
+ """Context information passed with each frame to detectors.
33
+
34
+ Attributes:
35
+ source_file: Path or identifier of the source video.
36
+ frame_number: Frame index within the video.
37
+ timestamp: Timestamp within the video (seconds from start).
38
+ fps: Frames per second of the source video.
39
+ """
40
+ source_file: str
41
+ frame_number: int
42
+ timestamp: float
43
+ fps: float
44
+
45
+
46
+ class VideoSource(ABC):
47
+ """Abstract base class for video sources.
48
+
49
+ Provides a common interface for loading video frames from different
50
+ backends (local files, S3, etc.).
51
+ """
52
+
53
+ @abstractmethod
54
+ def iter_frames(self) -> Iterator[Tuple[np.ndarray, FrameContext]]:
55
+ """Iterate over frames in the video.
56
+
57
+ Yields:
58
+ Tuple of (frame, context) where frame is a BGR numpy array
59
+ and context contains metadata about the frame.
60
+ """
61
+ pass
62
+
63
+ @abstractmethod
64
+ def get_metadata(self) -> VideoMetadata:
65
+ """Get metadata about the video source.
66
+
67
+ Returns:
68
+ VideoMetadata object with file information.
69
+ """
70
+ pass
71
+
72
+ def __enter__(self):
73
+ """Context manager entry."""
74
+ return self
75
+
76
+ def __exit__(self, exc_type, exc_val, exc_tb):
77
+ """Context manager exit - performs cleanup."""
78
+ self.close()
79
+ return False
80
+
81
+ def close(self) -> None:
82
+ """Release any resources held by the source.
83
+
84
+ Override this method if your source needs cleanup
85
+ (e.g., closing file handles, network connections).
86
+ """
87
+ pass
@@ -0,0 +1,154 @@
1
+ """Video source discovery functions."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from .base import VideoSource
8
+ from .local import LocalVideoSource
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def discover_local_videos(
14
+ path: str,
15
+ pattern: str = "*.ts"
16
+ ) -> List[VideoSource]:
17
+ """
18
+ Discover video files on the local file system.
19
+
20
+ Args:
21
+ path: Path to a video file or directory containing videos.
22
+ pattern: Glob pattern to match video files in directories.
23
+
24
+ Returns:
25
+ List of LocalVideoSource instances (unopened - will opened when iterated
26
+ in the pipeline).
27
+
28
+ Raises:
29
+ FileNotFoundError: If the specified path does not exist.
30
+ ValueError: If no video files are found in the specified directory.
31
+
32
+ Example:
33
+ sources = discover_local_videos("./footage/", "*.ts")
34
+ for source in sources:
35
+ with source:
36
+ for frame, context in source.iter_frames():
37
+ # process frame
38
+ """
39
+
40
+ input_path = Path(path)
41
+
42
+ if input_path.is_file():
43
+ logger.debug(f"Discovered single video: {input_path}")
44
+ return [LocalVideoSource(str(input_path))]
45
+
46
+ elif input_path.is_dir():
47
+ video_paths = list(input_path.glob(pattern))
48
+
49
+ if not video_paths:
50
+ raise ValueError(
51
+ f"No files matching '{pattern}' found in {input_path}"
52
+ )
53
+
54
+ logger.debug(f"Discovered {len(video_paths)} videos in: {input_path}")
55
+ return [LocalVideoSource(str(p)) for p in video_paths]
56
+
57
+ else:
58
+ raise FileNotFoundError(f"Path not found: {input_path}")
59
+
60
+ def discover_s3_videos(
61
+ bucket: str,
62
+ prefix: str = "",
63
+ pattern: str = "*.ts",
64
+ region_name: Optional[str] = None,
65
+ profile_name: Optional[str] = None,
66
+ endpoint_url: Optional[str] = None,
67
+ ) -> List[VideoSource]:
68
+ """
69
+ Discover video files in an S3 bucket.
70
+
71
+ Args:
72
+ bucket: S3 bucket name.
73
+ prefix: Key prefix to filter objects (e.g., "footage/2025/").
74
+ pattern: Glob pattern for matching video filenames (e.g. "*.ts").
75
+ region_name: AWS region name, if None uses the default from config.
76
+ profile_name: AWS profile name for SSO authentication. If None,
77
+ uses default credentials chain.
78
+ endpoint_url: Custom S3-compatible endpoint URL (e.g. Wasabi).
79
+
80
+ Returns:
81
+ List of S3VideoSource instances (not yet connected).
82
+
83
+ Raises:
84
+ ImportError: If boto3 is not installed.
85
+ ValueError: If no matching video files are found.
86
+
87
+ Example:
88
+ # Using SSO profile
89
+ sources = discover_s3_videos(
90
+ bucket="my-buoy-footage",
91
+ prefix="2025/10/",
92
+ pattern="*.ts",
93
+ profile_name="sso-profile-name"
94
+ )
95
+ for source in sources:
96
+ with source:
97
+ for frame, context in source.iter_frames():
98
+ # process frame
99
+ pass
100
+ """
101
+ try:
102
+ import boto3
103
+ except ImportError:
104
+ raise ImportError(
105
+ "boto3 is required for S3 support. "
106
+ "Install with `pip install boto3`."
107
+ )
108
+
109
+ from fnmatch import fnmatch
110
+ from .s3 import S3VideoSource
111
+
112
+ # Create S3 client via session (supports SSO profiles and custom endpoints)
113
+ session = boto3.Session(
114
+ profile_name=profile_name,
115
+ region_name=region_name,
116
+ )
117
+ s3_client = session.client("s3", endpoint_url=endpoint_url)
118
+ paginator = s3_client.get_paginator("list_objects_v2")
119
+
120
+ matching_keys = []
121
+
122
+ # Paginate through all objects in the bucket with the given prefix
123
+ page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix)
124
+
125
+ for page in page_iterator:
126
+ if "Contents" not in page:
127
+ continue
128
+
129
+ for obj in page["Contents"]:
130
+ key = obj["Key"]
131
+ filename = key.split("/")[-1]
132
+
133
+ if fnmatch(filename, pattern):
134
+ matching_keys.append(key)
135
+
136
+ if not matching_keys:
137
+ raise ValueError(
138
+ f"No files matching '{pattern}' found in s3://{bucket}/{prefix}"
139
+ )
140
+
141
+ # Sort for deterministic order
142
+ matching_keys.sort()
143
+
144
+ logger.debug(f"Discovered {len(matching_keys)} videos in s3://{bucket}/{prefix}")
145
+
146
+ return [
147
+ S3VideoSource(
148
+ f"s3://{bucket}/{key}",
149
+ region_name=region_name,
150
+ profile_name=profile_name,
151
+ endpoint_url=endpoint_url,
152
+ )
153
+ for key in matching_keys
154
+ ]
@@ -0,0 +1,155 @@
1
+ """Loading a locally stored video file as a video source."""
2
+
3
+ from pathlib import Path
4
+ from typing import Iterator, Tuple
5
+ import cv2
6
+ import numpy as np
7
+ import subprocess
8
+
9
+ from .base import FrameContext, VideoMetadata, VideoSource
10
+
11
+
12
+ class LocalVideoSource(VideoSource):
13
+ """
14
+ Loads video frames from a local file using OpenCV.
15
+
16
+ Attributes:
17
+ file_path: Path to the local video file.
18
+
19
+ Methods:
20
+ get_metadata: Get metadata about the video file.
21
+ iter_frames: Iterate over frames in the video file.
22
+ process_gopro: Process GoPro video to strip audio stream.
23
+ close: Release the video capture resource.
24
+
25
+ Example:
26
+ with LocalVideoSource("footage/clip_001.ts") as source:
27
+ metadata = source.get_metadata()
28
+ print(f"Processing {metadata.duration:.1f}s video")
29
+
30
+ for frame, context in source.iter_frames():
31
+ # process frame
32
+ pass
33
+ """
34
+
35
+ def __init__(self, filepath: str):
36
+ """
37
+ Initialise the video source.
38
+
39
+ Args:
40
+ filepath: Path to the local video file.
41
+
42
+ Raises:
43
+ ValueError: If the video file cannot be opened
44
+ """
45
+
46
+ self.filepath = filepath
47
+ self._cap = cv2.VideoCapture(filepath)
48
+
49
+ if not self._cap.isOpened():
50
+ raise ValueError(f"Cannot open video file: {filepath}")
51
+
52
+ def get_metadata(self) -> VideoMetadata:
53
+ """
54
+ Get metadata about the video file.
55
+
56
+ Returns:
57
+ VideoMetadata object with file propoerties.
58
+ """
59
+
60
+ fps = self._cap.get(cv2.CAP_PROP_FPS)
61
+ frame_count = int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))
62
+ width = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
63
+ height = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
64
+ duration = frame_count / fps if fps > 0 else 0.0
65
+
66
+ return VideoMetadata(
67
+ source_file=self.filepath,
68
+ fps=fps,
69
+ frame_count=frame_count,
70
+ width=width,
71
+ height=height,
72
+ duration=duration
73
+ )
74
+
75
+ def iter_frames(self) -> Iterator[Tuple[np.ndarray, FrameContext]]:
76
+ """
77
+ Iterate over frames in the video file.
78
+
79
+ Yields:
80
+ Tuple of (frame, context) where frame is a BGR numpy array
81
+ and context contains metadata about the frame.
82
+ """
83
+
84
+ fps = self._cap.get(cv2.CAP_PROP_FPS)
85
+ frame_number = 0
86
+
87
+ while True:
88
+ ret, frame = self._cap.read()
89
+
90
+ if not ret:
91
+ break
92
+
93
+ timestamp = frame_number / fps if fps > 0 else 0.0
94
+ context = FrameContext(
95
+ source_file=self.filepath,
96
+ frame_number=frame_number,
97
+ timestamp=timestamp,
98
+ fps=fps
99
+ )
100
+
101
+ yield frame, context
102
+ frame_number += 1
103
+
104
+ def process_gopro(self, processed_path: Path, overwrite: bool = False) -> None:
105
+ """
106
+ Method for processing local GoPro video to strip audio stream. This
107
+ method uses ffmpeg to strip the audio stream from the video and save
108
+ the processed video to the specified path.
109
+
110
+ The LocalVideoSource object will point to the updated processed file
111
+ after this method is called.
112
+
113
+ An ffmpeg installation is required for this method to work.
114
+
115
+ Args:
116
+ processed_path: Path at which the processed output should be
117
+ saved.
118
+ overwrite: Whether to overwrite an existing processed file.
119
+
120
+ Raises:
121
+ FileExistsError: If the processed file already exists and
122
+ overwrite is False.
123
+ """
124
+
125
+ # Ensure output path exists
126
+ processed_path.parent.mkdir(parents=True, exist_ok=True)
127
+
128
+ # Check if the file already exists
129
+ if processed_path.exists() and not overwrite:
130
+ raise FileExistsError(f"Processed file already exists: {processed_path}")
131
+
132
+ cmd = [
133
+ "ffmpeg",
134
+ "-i", str(self.filepath),
135
+ "-y", # Overwrite output files without asking
136
+ "-map", "0:v", # Map only the video stream
137
+ "-c", "copy",
138
+ str(processed_path)
139
+ ]
140
+
141
+ subprocess.run(cmd, check=True)
142
+
143
+ # Update path
144
+ self.filepath = str(processed_path)
145
+
146
+ # Update capture object
147
+ self._cap.release()
148
+ self._cap = cv2.VideoCapture(self.filepath)
149
+
150
+
151
+ def close(self) -> None:
152
+ """Release the video capture resource."""
153
+ if self._cap is not None and self._cap.isOpened():
154
+ self._cap.release()
155
+ self._cap = None
@@ -0,0 +1,231 @@
1
+ """Loading video files from AWS S3 as a video source."""
2
+
3
+ import logging
4
+ import re
5
+ from typing import Iterator, Optional, Tuple
6
+
7
+ import cv2
8
+ import numpy as np
9
+
10
+ try:
11
+ import boto3
12
+ from botocore.exceptions import ClientError
13
+ BOTO3_AVAILABLE = True
14
+ except ImportError:
15
+ BOTO3_AVAILABLE = False
16
+
17
+ from .base import FrameContext, VideoMetadata, VideoSource
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def parse_s3_uri(uri: str) -> Tuple[str, str]:
23
+ """
24
+ Parse an S3 URI into bucket and key components.
25
+
26
+ Args:
27
+ uri: S3 URI (e.g., "s3://bucket-name/path/to/.file.ts").
28
+
29
+ Returns:
30
+ Tuple of (bucket_name, key).
31
+
32
+ Raises:
33
+ ValueError: If the URI is not a valid S3 URI.
34
+ """
35
+
36
+ match = re.match(r"^s3://([^/]+)/(.+)$", uri)
37
+ if not match:
38
+ raise ValueError(
39
+ f"Invalid S3 URI: {uri}. Expected format: s3://bucket/key"
40
+ )
41
+ return match.group(1), match.group(2)
42
+
43
+
44
+ class S3VideoSource(VideoSource):
45
+ """
46
+ Loads video frames from an AWS S3 bucket using pre-signed URLs.
47
+
48
+ Uses OpenCV's HTTP streaming capability to read video directly from S3
49
+ without downloading to a temporary file.
50
+
51
+ Attributes:
52
+ uri: S3 URI to the video file (e.g., "s3://bucket-name/path/to/.file.ts").
53
+
54
+ Example:
55
+ with S3VideoSource("s3://my-bucket/footage/clip_001.ts") as source:
56
+ metadata = source.get_metadata()
57
+ print(f"Processing {metadata.duration:.1f}s video")
58
+
59
+ for frame, context in source.iter_frames():
60
+ # process frame
61
+ pass
62
+
63
+ Note:
64
+ Requires boto3 to be installed and AWS credentials configured.
65
+ Credentials are read from environment variables, ~/.aws/credentials,
66
+ IAM role (if running on AWS infrastructure), or SSO profile.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ uri: str,
72
+ presigned_url_expiry: int = 14400,
73
+ region_name: Optional[str] = None,
74
+ profile_name: Optional[str] = None,
75
+ endpoint_url: Optional[str] = None,
76
+ ):
77
+ """
78
+ Initialise the S3 video source.
79
+
80
+ Args:
81
+ uri: S3 URI to the video file (e.g., "s3://bucket/path/video.ts").
82
+ presigned_url_expiry: Expiry time in seconds for the presigned URL.
83
+ Default is 14400 (4 hours).
84
+ region_name: AWS region name. If None, uses default region from
85
+ config.
86
+ profile_name: AWS profile name for SSO authentication. If None,
87
+ uses default credentials chain.
88
+ endpoint_url: Custom S3-compatible endpoint URL (e.g. Wasabi).
89
+
90
+ Raises:
91
+ ImportError: If boto3 is not installed.
92
+ ValueError: If the URI is invalid.
93
+ """
94
+
95
+ if not BOTO3_AVAILABLE:
96
+ raise ImportError(
97
+ "boto3 is required for S3 support. "
98
+ "Install with `pip install boto3`."
99
+ )
100
+
101
+ self.uri = uri
102
+ self.presigned_url_expiry = presigned_url_expiry
103
+ self.bucket, self.key = parse_s3_uri(uri)
104
+
105
+ # Create S3 client via session (supports SSO profiles and custom endpoints)
106
+ session = boto3.Session(
107
+ profile_name=profile_name,
108
+ region_name=region_name,
109
+ )
110
+ self._s3_client = session.client("s3", endpoint_url=endpoint_url)
111
+
112
+ # State - lazy initialisation
113
+ self._presigned_url: Optional[str] = None
114
+ self._cap: Optional[cv2.VideoCapture] = None
115
+ self._metadata: Optional[VideoMetadata] = None
116
+
117
+
118
+ def _ensure_connected(self) -> None:
119
+ """
120
+ Ensure the video capture is connected and ready.
121
+
122
+ Generates pre-signed URL and open VideoCapture on first call.
123
+
124
+ Raises:
125
+ RuntimeError: If unable to open video stream.
126
+ """
127
+
128
+ if self._cap is not None:
129
+ return # Already connected
130
+
131
+ # Generate pre-signed URL
132
+ try:
133
+ self._presigned_url = self._s3_client.generate_presigned_url(
134
+ "get_object",
135
+ Params={"Bucket": self.bucket, "Key": self.key},
136
+ ExpiresIn=self.presigned_url_expiry,
137
+ )
138
+ logger.debug(f"Generated pre-signed URL for {self.uri}")
139
+ except ClientError as e:
140
+ raise RuntimeError(
141
+ f"Failed to generate pre-signed URL for {self.uri}: {e}"
142
+ )
143
+
144
+ # Open video capture
145
+ self._cap = cv2.VideoCapture(self._presigned_url)
146
+
147
+ if not self._cap.isOpened():
148
+ raise RuntimeError(
149
+ f"Cannot open video stream from S3: {self.uri}. "
150
+ "OpenCV may not support streaming this format via HTTP."
151
+ )
152
+
153
+ # Validate we can read metadata
154
+ frame_count = int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))
155
+ if frame_count <= 0:
156
+ logger.warning(
157
+ f"Frame count not available for {self.uri}. "
158
+ "Progress tracking may be inaccurate."
159
+ )
160
+
161
+
162
+ def get_metadata(self) -> VideoMetadata:
163
+ """
164
+ Get metadata about the video file.
165
+
166
+ Returns:
167
+ VideoMetadata object with file properties.
168
+ """
169
+ self._ensure_connected()
170
+
171
+ if self._metadata is not None:
172
+ return self._metadata
173
+
174
+ fps = self._cap.get(cv2.CAP_PROP_FPS)
175
+ frame_count = int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))
176
+ width = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
177
+ height = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
178
+ duration = frame_count / fps if fps > 0 else 0.0
179
+
180
+ self._metadata = VideoMetadata(
181
+ source_file=self.uri,
182
+ fps=fps,
183
+ frame_count=frame_count,
184
+ width=width,
185
+ height=height,
186
+ duration=duration,
187
+ )
188
+
189
+ return self._metadata
190
+
191
+
192
+ def iter_frames(self) -> Iterator[Tuple[np.ndarray, FrameContext]]:
193
+ """
194
+ Iterate over frames in the video file.
195
+
196
+ Yields:
197
+ Tuple of (frame, context) where frame is a BGR numpy array
198
+ and context contains metadata about the frame.
199
+ """
200
+ self._ensure_connected()
201
+
202
+ fps = self._cap.get(cv2.CAP_PROP_FPS)
203
+ frame_number = 0
204
+
205
+ while True:
206
+ ret, frame = self._cap.read()
207
+
208
+ if not ret:
209
+ break # End of video
210
+
211
+ timestamp = frame_number / fps if fps > 0 else 0.0
212
+
213
+ context = FrameContext(
214
+ source_file=self.uri,
215
+ frame_number=frame_number,
216
+ timestamp=timestamp,
217
+ fps=fps,
218
+ )
219
+
220
+ yield frame, context
221
+ frame_number += 1
222
+
223
+
224
+
225
+ def close(self) -> None:
226
+ """Release the video capture resource."""
227
+ if self._cap is not None and self._cap.isOpened():
228
+ self._cap.release()
229
+ self._cap = None
230
+ self._presigned_url = None
231
+ self._metadata = None
@@ -0,0 +1,62 @@
1
+ """Visualiser module for detection overlay on video."""
2
+
3
+ from .config import (
4
+ OutputMode,
5
+ LabelPosition,
6
+ BoundingBoxStyle,
7
+ LabelStyle,
8
+ OverlayStyle,
9
+ VideoOutputConfig,
10
+ VisualiserConfig
11
+ )
12
+ from .annotator import FrameAnnotator
13
+ from .writer import (
14
+ VideoWriterHandle,
15
+ OutputNameFunction,
16
+ extract_output_stem,
17
+ sanitise_filename
18
+ )
19
+ from .loader import (
20
+ DetectionSource,
21
+ FrameDetections,
22
+ CSVDetectionLoader,
23
+ IteratorDetectionSource,
24
+ ListDetectionSource,
25
+ load_detections_from_csv,
26
+ )
27
+ from .visualiser import (
28
+ AnnotatedFrame,
29
+ VisualisationResult,
30
+ LiveVisualiser,
31
+ PostHocVisualiser
32
+ )
33
+
34
+ __all__ = [
35
+ # Config
36
+ "OutputMode",
37
+ "LabelPosition",
38
+ "BoundingBoxStyle",
39
+ "LabelStyle",
40
+ "OverlayStyle",
41
+ "VideoOutputConfig",
42
+ "VisualiserConfig",
43
+ # Writer utilities
44
+ "VideoWriterHandle",
45
+ "OutputNameFunction",
46
+ "extract_output_stem",
47
+ "sanitise_filename",
48
+ # Annotator
49
+ "FrameAnnotator",
50
+ # Loaders
51
+ "DetectionSource",
52
+ "FrameDetections",
53
+ "CSVDetectionLoader",
54
+ "IteratorDetectionSource",
55
+ "ListDetectionSource",
56
+ "load_detections_from_csv",
57
+ # Visualisers
58
+ "AnnotatedFrame",
59
+ "VisualisationResult",
60
+ "LiveVisualiser",
61
+ "PostHocVisualiser",
62
+ ]