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,558 @@
|
|
|
1
|
+
"""Detection loading from various sources for post-hoc visualisation."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Iterator, List, Optional
|
|
7
|
+
import csv
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ..detectors.base import Detection
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DetectionSource(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Abstract base class for loading detection.
|
|
18
|
+
|
|
19
|
+
Provides a common interface for different detection sources (CSV files,
|
|
20
|
+
iterators, databases, etc) to be used with the visualiser.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_detections_for_frame(self, frame_number: int) -> List[Detection]:
|
|
25
|
+
"""
|
|
26
|
+
Get all detections for a specific frame number.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
frame_number: The frame index to retrieve detections for.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of Detection objects for that frame (empty list if none).
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_source_file(self) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Get the source video file path these detections belong to.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
String path or URI of the source video.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_frame_numbers_with_detections(self) -> List[int]:
|
|
50
|
+
"""
|
|
51
|
+
Get list of frame numbers that have detections.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Sorted list of frame numbers containing at least one detection.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_total_detection_count(self) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Get the total number of detections across all frames.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Total detection count.
|
|
65
|
+
"""
|
|
66
|
+
return sum(
|
|
67
|
+
len(self.get_detections_for_frame(f))
|
|
68
|
+
for f in self.get_frame_numbers_with_detections()
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_frame_count_with_detections(self) -> int:
|
|
73
|
+
"""
|
|
74
|
+
Get the number of frames that have at least one detection.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Number of frames with detections.
|
|
78
|
+
"""
|
|
79
|
+
return len(self.get_frame_numbers_with_detections())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class FrameDetections:
|
|
84
|
+
"""
|
|
85
|
+
Container for detections grouped by frame number.
|
|
86
|
+
|
|
87
|
+
A simple data structure that can be used directly or as a base for more
|
|
88
|
+
complex detection sources.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
source_file: Path or URI of the source file
|
|
92
|
+
detections_by_frame: Dictionary mapping frame numbers to lists of
|
|
93
|
+
Detection objects.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
detections = FrameDetections(
|
|
97
|
+
source_file="s3://my-bucket/video.ts",
|
|
98
|
+
detections_by_frame={
|
|
99
|
+
10: [Detection(...), Detection(...)],
|
|
100
|
+
15: [Detection(...)]
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
frame_10_dets = detections.get_detections_for_frame(10)
|
|
104
|
+
"""
|
|
105
|
+
source_file: str
|
|
106
|
+
detections_by_frame: Dict[int, List[Detection]] = field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_detections_for_frame(self, frame_number: int) -> List[Detection]:
|
|
110
|
+
"""Get detections for a specific frame."""
|
|
111
|
+
return self.detections_by_frame.get(frame_number, [])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_frame_numbers_with_detections(self) -> List[int]:
|
|
115
|
+
"""Get sorted list of frame numbers with detections."""
|
|
116
|
+
return sorted(self.detections_by_frame.keys())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def add_detection(self, detection: Detection) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Add a detection to the appropriate frame.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
detection: Detection to add.
|
|
125
|
+
"""
|
|
126
|
+
frame_num = detection.frame_number
|
|
127
|
+
if frame_num not in self.detections_by_frame:
|
|
128
|
+
self.detections_by_frame[frame_num] = []
|
|
129
|
+
self.detections_by_frame[frame_num].append(detection)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_total_detection_count(self) -> int:
|
|
133
|
+
"""Get total number of detections."""
|
|
134
|
+
return sum(len(dets) for dets in self.detections_by_frame.values())
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CSVDetectionLoader(DetectionSource):
|
|
138
|
+
"""
|
|
139
|
+
Load detections from a CSV file.
|
|
140
|
+
|
|
141
|
+
Expects CSV format matching Detection.to_csv_row() output:
|
|
142
|
+
source_file, timestamp, frame_number, xc, yc, width, height, confidence
|
|
143
|
+
|
|
144
|
+
Handles:
|
|
145
|
+
- CSV files with headers
|
|
146
|
+
- Multiple source files in one CSV (filterable)
|
|
147
|
+
- Missing or empty confidence values
|
|
148
|
+
- Whitespace in values
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
csv_path: Path to the CSV file.
|
|
152
|
+
source_file_filter: If set, only load detections for this source.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
# Load all detections from CSV
|
|
156
|
+
loader = CSVDetectionLoader("results/detections.csv")
|
|
157
|
+
|
|
158
|
+
# Load only detections for a specific video
|
|
159
|
+
loader = CSVDetectionLoader(
|
|
160
|
+
"results/all_detections.csv",
|
|
161
|
+
source_file="footage/video_001.ts"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Use with visualiser
|
|
165
|
+
for frame_num in loader.get_frame_numbers_with_detections():
|
|
166
|
+
detections = loader.get_detections_for_frame(frame_num)
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
# Expected CSV columns
|
|
170
|
+
REQUIRED_COLUMNS = {
|
|
171
|
+
"source_file",
|
|
172
|
+
"timestamp",
|
|
173
|
+
"frame_number",
|
|
174
|
+
"xc",
|
|
175
|
+
"yc",
|
|
176
|
+
"width",
|
|
177
|
+
"height",
|
|
178
|
+
}
|
|
179
|
+
OPTIONAL_COLUMNS = {
|
|
180
|
+
"confidence",
|
|
181
|
+
"label",
|
|
182
|
+
"track_id",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
csv_path: str,
|
|
189
|
+
source_file: Optional[str] = None
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
Load detections from CSV.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
csv_path: Path to CSV file with detections.
|
|
196
|
+
source_file: If provided, only load detections matching this source.
|
|
197
|
+
Useful when a single CSV contains detections from multiple videos.
|
|
198
|
+
If None and CSV contains multiple sources, uses the first one found.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
FileNotFoundError: If CSV file does not exist.
|
|
202
|
+
ValueError: If CSV is missing required columns.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
self._csv_path = Path(csv_path)
|
|
206
|
+
self._source_file_filter = source_file
|
|
207
|
+
self._detections_by_frame: Dict[int, List[Detection]] = {}
|
|
208
|
+
self._source_file: str = ""
|
|
209
|
+
self._sources_in_file: List[str] = []
|
|
210
|
+
|
|
211
|
+
self._load()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _load(self) -> None:
|
|
215
|
+
"""
|
|
216
|
+
Load and index detections from a CSV.
|
|
217
|
+
"""
|
|
218
|
+
if not self._csv_path.exists():
|
|
219
|
+
raise FileNotFoundError(f"CSV file not found: {self._csv_path}")
|
|
220
|
+
|
|
221
|
+
logger.debug(f"Loading detections from CSV: {self._csv_path}")
|
|
222
|
+
|
|
223
|
+
with open(self._csv_path, "r", newline="", encoding="utf-8-sig") as f:
|
|
224
|
+
reader = csv.DictReader(f)
|
|
225
|
+
|
|
226
|
+
# Validate columns
|
|
227
|
+
if reader.fieldnames is None:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"CSV file is empty or has no header: {self._csv_path}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
columns = set(reader.fieldnames)
|
|
233
|
+
missing = self.REQUIRED_COLUMNS - columns
|
|
234
|
+
if missing:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"CSV file is missing required columns: {missing}. "
|
|
237
|
+
f"Found: {columns}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
has_confidence = "confidence" in columns
|
|
241
|
+
sources_seen: set = set()
|
|
242
|
+
|
|
243
|
+
for row_num, row in enumerate(reader, start = 2): # Start at 2 (1-indexed + header)
|
|
244
|
+
try:
|
|
245
|
+
detection = self._parse_row(row, has_confidence)
|
|
246
|
+
except (ValueError, KeyError) as e:
|
|
247
|
+
logger.warning(
|
|
248
|
+
f"Skipping invalid row {row_num} in {self._csv_path}: {e}"
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Track sources seen
|
|
253
|
+
sources_seen.add(detection.source_file)
|
|
254
|
+
|
|
255
|
+
# Apply source filter
|
|
256
|
+
if self._source_file_filter is not None:
|
|
257
|
+
if not self._matches_source(detection.source_file):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Set source file from first matching detection
|
|
261
|
+
if not self._source_file:
|
|
262
|
+
self._source_file = detection.source_file
|
|
263
|
+
|
|
264
|
+
# Index by frame number
|
|
265
|
+
frame_num = detection.frame_number
|
|
266
|
+
if frame_num not in self._detections_by_frame:
|
|
267
|
+
self._detections_by_frame[frame_num] = []
|
|
268
|
+
self._detections_by_frame[frame_num].append(detection)
|
|
269
|
+
|
|
270
|
+
self._sources_in_file = sorted(sources_seen)
|
|
271
|
+
|
|
272
|
+
# Logger summary
|
|
273
|
+
total_detections = sum(len(d) for d in self._detections_by_frame.values())
|
|
274
|
+
frame_count = len(self._detections_by_frame)
|
|
275
|
+
|
|
276
|
+
logger.info(
|
|
277
|
+
f"Loaded {total_detections} detections across {frame_count} frames "
|
|
278
|
+
f"from: {self._csv_path}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if len(self._sources_in_file) > 1:
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"CSV contains {len(self._sources_in_file)} source files: "
|
|
284
|
+
f"{self._sources_in_file}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _parse_row(
|
|
289
|
+
self,
|
|
290
|
+
row: Dict[str, str],
|
|
291
|
+
has_confidence: bool
|
|
292
|
+
) -> Detection:
|
|
293
|
+
"""
|
|
294
|
+
Parse a CSV row into a Detection object.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
row: Dictionary from csv.DictReader.
|
|
298
|
+
has_confidence: Whether the CSV has a confidence column.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Detection object.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
ValueError: If required fields are missing or invalid.
|
|
305
|
+
"""
|
|
306
|
+
# Parse confidence (may be empty string or missing)
|
|
307
|
+
confidence = None
|
|
308
|
+
if has_confidence:
|
|
309
|
+
conf_str = row.get("confidence", "").strip()
|
|
310
|
+
if conf_str:
|
|
311
|
+
confidence = float(conf_str)
|
|
312
|
+
|
|
313
|
+
# Parse label (may be empty string or missing)
|
|
314
|
+
label = None
|
|
315
|
+
label_str = row.get("label", "").strip()
|
|
316
|
+
if label_str:
|
|
317
|
+
label = label_str
|
|
318
|
+
|
|
319
|
+
# Parse the track ID (may be empty string or missing)
|
|
320
|
+
track_id = None
|
|
321
|
+
track_id_str = row.get("track_id", "").strip()
|
|
322
|
+
if track_id_str:
|
|
323
|
+
track_id = track_id_str
|
|
324
|
+
|
|
325
|
+
return Detection(
|
|
326
|
+
source_file=row["source_file"].strip(),
|
|
327
|
+
timestamp=float(row["timestamp"].strip()),
|
|
328
|
+
frame_number=int(row["frame_number"].strip()),
|
|
329
|
+
xc=float(row["xc"].strip()),
|
|
330
|
+
yc=float(row["yc"].strip()),
|
|
331
|
+
width=float(row["width"].strip()),
|
|
332
|
+
height=float(row["height"].strip()),
|
|
333
|
+
confidence=confidence,
|
|
334
|
+
label=label,
|
|
335
|
+
track_id=track_id
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _matches_source(self, source_file: str) -> bool:
|
|
340
|
+
"""
|
|
341
|
+
Check if a source file matches the filter.
|
|
342
|
+
|
|
343
|
+
Handles potential differences in path formatting (e.g., relative vs
|
|
344
|
+
absolute, different separators).
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
source_file: Source file path from detection.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
True if it matches the filter (or no filter set).
|
|
351
|
+
"""
|
|
352
|
+
if self._source_file_filter is None:
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
# Exact match
|
|
356
|
+
if source_file == self._source_file_filter:
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
# Try normalised path comparison for local files
|
|
360
|
+
try:
|
|
361
|
+
source_path = Path(source_file)
|
|
362
|
+
filter_path = Path(self._source_file_filter)
|
|
363
|
+
|
|
364
|
+
# Compare resolved paths if both are local files
|
|
365
|
+
if source_path.exists() and filter_path.exists():
|
|
366
|
+
return source_path.resolve() == filter_path.resolve()
|
|
367
|
+
|
|
368
|
+
# Compare just the filename as fallback
|
|
369
|
+
return source_path.name == filter_path.name
|
|
370
|
+
except (OSError, ValueError):
|
|
371
|
+
# Path comparison failed, stick with exact match (already False)
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_detections_for_frame(self, frame_number: int) -> List[Detection]:
|
|
376
|
+
"""Get all detections for a specific frame number."""
|
|
377
|
+
return self._detections_by_frame.get(frame_number, [])
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_source_file(self) -> str:
|
|
381
|
+
"""Get the source video file path."""
|
|
382
|
+
return self._source_file
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def get_frame_numbers_with_detections(self) -> List[int]:
|
|
386
|
+
"""Get sorted list of frame numbers with detections."""
|
|
387
|
+
return sorted(self._detections_by_frame.keys())
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def csv_path(self) -> Path:
|
|
392
|
+
"""Get the path to the CSV file."""
|
|
393
|
+
return self._csv_path
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def sources_in_file(self) -> List[str]:
|
|
398
|
+
"""Get list of all source files found in the CSV."""
|
|
399
|
+
return self._sources_in_file
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class IteratorDetectionSource(DetectionSource):
|
|
403
|
+
"""
|
|
404
|
+
Wrap an iterator of detections for use with visualiser.
|
|
405
|
+
|
|
406
|
+
Buffers all detections into a frame-indexed structure for random access.
|
|
407
|
+
Useful for streaming visualisation directly from detector output.
|
|
408
|
+
|
|
409
|
+
Note:
|
|
410
|
+
This class buffers ALL detections in memory. For very long videos
|
|
411
|
+
with many detections, consider using CSVDetectionLoader instead
|
|
412
|
+
(write to CSV first, then load).
|
|
413
|
+
|
|
414
|
+
Attributes:
|
|
415
|
+
source_file: Path or URI of the source video.
|
|
416
|
+
|
|
417
|
+
Example:
|
|
418
|
+
# From detector output
|
|
419
|
+
detections = list(detector.process_frame(frame, context))
|
|
420
|
+
source = IteratorDetectionSource(iter(detections), "video.ts")
|
|
421
|
+
|
|
422
|
+
# From a generator
|
|
423
|
+
def detection_generator():
|
|
424
|
+
for frame_num in range(100):
|
|
425
|
+
yield Detection(...)
|
|
426
|
+
|
|
427
|
+
source = IteratorDetectionSource(detection_generator(), "video.ts")
|
|
428
|
+
"""
|
|
429
|
+
def __init__(
|
|
430
|
+
self,
|
|
431
|
+
detections: Iterator[Detection],
|
|
432
|
+
source_file: str,
|
|
433
|
+
):
|
|
434
|
+
"""
|
|
435
|
+
Create detection source from iterator.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
detections: Iterator yielding Detection objects.
|
|
439
|
+
source_file: Source video file path/URI (for metadata).
|
|
440
|
+
|
|
441
|
+
Note:
|
|
442
|
+
The iterator is fully consumed during initialisation.
|
|
443
|
+
"""
|
|
444
|
+
self._source_file = source_file
|
|
445
|
+
self._detections_by_frame: Dict[int, List[Detection]] = {}
|
|
446
|
+
self._buffer(detections)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _buffer(self, detections: Iterator[Detection]) -> None:
|
|
450
|
+
"""Buffer all detections into frame index."""
|
|
451
|
+
count = 0
|
|
452
|
+
for det in detections:
|
|
453
|
+
frame_num = det.frame_number
|
|
454
|
+
if frame_num not in self._detections_by_frame:
|
|
455
|
+
self._detections_by_frame[frame_num] = []
|
|
456
|
+
self._detections_by_frame[frame_num].append(det)
|
|
457
|
+
count += 1
|
|
458
|
+
|
|
459
|
+
logger.debug(
|
|
460
|
+
f"Buffered {count} detections across "
|
|
461
|
+
f"{len(self._detections_by_frame)} frames"
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def get_detections_for_frame(self, frame_number: int) -> List[Detection]:
|
|
466
|
+
"""Get all detections for a specific frame number."""
|
|
467
|
+
return self._detections_by_frame.get(frame_number, [])
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def get_source_file(self) -> str:
|
|
471
|
+
"""Get the source video file path."""
|
|
472
|
+
return self._source_file
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def get_frame_numbers_with_detections(self) -> List[int]:
|
|
476
|
+
"""Get sorted list of frame numbers with detections."""
|
|
477
|
+
return sorted(self._detections_by_frame.keys())
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ListDetectionSource(DetectionSource):
|
|
481
|
+
"""
|
|
482
|
+
Create detection source from a list of detections.
|
|
483
|
+
|
|
484
|
+
Simpler alternative to IteratorDetectionSource when detections
|
|
485
|
+
are already in a list.
|
|
486
|
+
|
|
487
|
+
Example:
|
|
488
|
+
detections = [det1, det2, det3]
|
|
489
|
+
source = ListDetectionSource(detections, "video.ts")
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
def __init__(
|
|
493
|
+
self,
|
|
494
|
+
detections: List[Detection],
|
|
495
|
+
source_file: str,
|
|
496
|
+
):
|
|
497
|
+
"""
|
|
498
|
+
Create detection source from list.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
detections: List of Detection objects.
|
|
502
|
+
source_file: Source video file path/URI (for metadata).
|
|
503
|
+
"""
|
|
504
|
+
self._source_file = source_file
|
|
505
|
+
self._detections_by_frame: Dict[int, List[Detection]] = {}
|
|
506
|
+
|
|
507
|
+
for det in detections:
|
|
508
|
+
frame_num = det.frame_number
|
|
509
|
+
if frame_num not in self._detections_by_frame:
|
|
510
|
+
self._detections_by_frame[frame_num] = []
|
|
511
|
+
self._detections_by_frame[frame_num].append(det)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def get_detections_for_frame(self, frame_number: int) -> List[Detection]:
|
|
515
|
+
"""Get all detections for a specific frame number."""
|
|
516
|
+
return self._detections_by_frame.get(frame_number, [])
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def get_source_file(self) -> str:
|
|
520
|
+
"""Get the source video file path."""
|
|
521
|
+
return self._source_file
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def get_frame_numbers_with_detections(self) -> List[int]:
|
|
525
|
+
"""Get sorted list of frame numbers with detections."""
|
|
526
|
+
return sorted(self._detections_by_frame.keys())
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def load_detections_from_csv(
|
|
530
|
+
csv_path: str,
|
|
531
|
+
source_file: Optional[str] = None,
|
|
532
|
+
) -> FrameDetections:
|
|
533
|
+
"""
|
|
534
|
+
Convenience function to load detections from CSV into a FrameDetections object.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
csv_path: Path to CSV file.
|
|
538
|
+
source_file: Optional source file filter.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
FrameDetections container with loaded detections.
|
|
542
|
+
|
|
543
|
+
Example:
|
|
544
|
+
detections = load_detections_from_csv("results.csv")
|
|
545
|
+
for frame_num in detections.get_frame_numbers_with_detections():
|
|
546
|
+
frame_dets = detections.get_detections_for_frame(frame_num)
|
|
547
|
+
"""
|
|
548
|
+
loader = CSVDetectionLoader(csv_path, source_file)
|
|
549
|
+
|
|
550
|
+
frame_detections = FrameDetections(
|
|
551
|
+
source_file=loader.get_source_file(),
|
|
552
|
+
detections_by_frame={
|
|
553
|
+
frame_num: loader.get_detections_for_frame(frame_num)
|
|
554
|
+
for frame_num in loader.get_frame_numbers_with_detections()
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return frame_detections
|