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
File without changes
@@ -0,0 +1,55 @@
1
+ """Conversion utilities between OpenCV/numpy and Qt image types."""
2
+
3
+ import numpy as np
4
+ from PySide6.QtGui import QImage
5
+
6
+ def numpy_bgr_to_qimage(frame: np.ndarray) -> QImage:
7
+ """
8
+ Convert a BGR or BGRA numpy array to a QImage.
9
+
10
+ Args:
11
+ frame: numpy array of shape (H, W, 3) for BGR or (H, W, 4) for BGRA.
12
+ Must be dtype uint8 and contiguous.
13
+
14
+ Returns:
15
+ A QImage that owns its own pixel data (safe to use after the numpy array
16
+ is freed).
17
+
18
+ Raises:
19
+ ValueError: If the input array is not 3D or does not have 3 channels
20
+ """
21
+
22
+ # Check the shape of the input array - images are 3D arrays
23
+ if frame.ndim != 3:
24
+ raise ValueError(
25
+ f"Expected 3D array (H, W, C), got shape {frame.shape}"
26
+ )
27
+
28
+ height, width, channels = frame.shape
29
+
30
+ # Check there are three colour channels (BGR)
31
+ if channels != 3:
32
+ raise ValueError(
33
+ f"Expected 3 channels (BGR), got {channels}"
34
+ )
35
+
36
+ # Make the array contiguous in memory (C order) to ensure QImage can use it
37
+ # directly
38
+ frame = np.ascontiguousarray(frame)
39
+ bytes_per_line = channels * width
40
+
41
+ # Create the QImage
42
+ image = QImage(
43
+ frame.data,
44
+ width,
45
+ height,
46
+ bytes_per_line,
47
+ QImage.Format.Format_BGR888
48
+ )
49
+
50
+ # Use image.copy() to ensure the QImage owns its own data, so it remains
51
+ # valid even if the original numpy array is freed
52
+
53
+ # TODO: This approach is performance intensive see note in Phase 1 guide.
54
+ # A more fficient approach may need to be implemented here.
55
+ return image.copy()
@@ -0,0 +1,37 @@
1
+ """Utility functions for session management in the GUI."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def resolve_video_paths(
7
+ sources_in_file: list[str],
8
+ video_dir: str,
9
+ ) -> tuple[dict[str, str], list[str]]:
10
+ """
11
+ Resolve CSV source names to local file paths.
12
+
13
+ This is extracted from ValidationTab.open_session for testability.
14
+
15
+ Args:
16
+ sources_in_file: Source file values from the CSV.
17
+ video_dir: Local directory to search for matching videos.
18
+
19
+ Returns:
20
+ Tuple of (resolved_mapping, s3_sources) where resolved_mapping
21
+ maps source names to local paths and s3_sources lists any S3
22
+ URIs that couldn't be resolved.
23
+ """
24
+ video_dir_path = Path(video_dir)
25
+ resolved: dict[str, str] = {}
26
+ s3_sources: list[str] = []
27
+
28
+ for source in sources_in_file:
29
+ if source.startswith("s3://"):
30
+ s3_sources.append(source)
31
+ continue
32
+
33
+ candidate = video_dir_path / Path(source).name
34
+ if candidate.exists():
35
+ resolved[source] = str(candidate)
36
+
37
+ return resolved, s3_sources
File without changes
@@ -0,0 +1,144 @@
1
+ """
2
+ Style sheet strings for validation GUI buttons.
3
+ """
4
+
5
+ # --- Main review action buttons ---
6
+ _BUTTON_STYLE_CONFIRM = """
7
+ QPushButton {
8
+ background-color: #2d7a4d;
9
+ color: white;
10
+ padding: 8px 16px;
11
+ font-weight: bold;
12
+ border: 1px solid #1e5a35;
13
+ border-radius: 4px;
14
+ }
15
+ QPushButton:hover {
16
+ background-color: #359a5d;
17
+ }
18
+ QPushButton:pressed {
19
+ background-color: #1e5a35;
20
+ }
21
+ QPushButton:disabled {
22
+ background-color: #3a3a3a;
23
+ color: #777;
24
+ border-color: #333;
25
+ }
26
+ QPushButton:focus {
27
+ border: 2px solid #5dba7d;
28
+ }
29
+ """
30
+
31
+ _BUTTON_STYLE_REJECT = """
32
+ QPushButton {
33
+ background-color: #a03030;
34
+ color: white;
35
+ padding: 8px 16px;
36
+ font-weight: bold;
37
+ border: 1px solid #7a2020;
38
+ border-radius: 4px;
39
+ }
40
+ QPushButton:hover {
41
+ background-color: #c04040;
42
+ }
43
+ QPushButton:pressed {
44
+ background-color: #7a2020;
45
+ }
46
+ QPushButton:disabled {
47
+ background-color: #3a3a3a;
48
+ color: #777;
49
+ border-color: #333;
50
+ }
51
+ QPushButton:focus {
52
+ border: 2px solid #e06060;
53
+ }
54
+ """
55
+
56
+ _BUTTON_STYLE_SKIP = """
57
+ QPushButton {
58
+ background-color: #4a4a4a;
59
+ color: #ddd;
60
+ padding: 8px 16px;
61
+ font-weight: bold;
62
+ border: 1px solid #3a3a3a;
63
+ border-radius: 4px;
64
+ }
65
+ QPushButton:hover {
66
+ background-color: #5a5a5a;
67
+ }
68
+ QPushButton:pressed {
69
+ background-color: #3a3a3a;
70
+ }
71
+ QPushButton:disabled {
72
+ background-color: #2a2a2a;
73
+ color: #555;
74
+ border-color: #222;
75
+ }
76
+ QPushButton:focus {
77
+ border: 2px solid #7a7a7a;
78
+ }
79
+ """
80
+
81
+ _BUTTON_STYLE_ADD = """
82
+ QPushButton {
83
+ background-color: #2d6da8;
84
+ color: white;
85
+ padding: 8px 16px;
86
+ border: 1px solid #1e4a7a;
87
+ border-radius: 4px;
88
+ }
89
+ QPushButton:hover {
90
+ background-color: #3d8dc8;
91
+ }
92
+ QPushButton:pressed {
93
+ background-color: #1e4a7a;
94
+ }
95
+ QPushButton:disabled {
96
+ background-color: #3a3a3a;
97
+ color: #777;
98
+ border-color: #333;
99
+ }
100
+ QPushButton:checked {
101
+ background-color: #1a4a7a;
102
+ border: 2px solid #88bbee;
103
+ }
104
+ QPushButton:focus {
105
+ border: 2px solid #6daddd;
106
+ }
107
+ """
108
+
109
+
110
+ class ReviewButtonStyles:
111
+ """Predefined styles for the main review action buttons."""
112
+
113
+ def __init__(self):
114
+ self.confirm = _BUTTON_STYLE_CONFIRM
115
+ self.reject = _BUTTON_STYLE_REJECT
116
+ self.skip = _BUTTON_STYLE_SKIP
117
+ self.add = _BUTTON_STYLE_ADD
118
+
119
+
120
+ # --- Transport bar buttons ---
121
+ _BUTTON_STYLE_TRANSPORT = """
122
+ QPushButton {
123
+ padding: 4px 8px;
124
+ border: 1px solid #555;
125
+ border-radius: 3px;
126
+ background-color: #3a3a3a;
127
+ color: #ddd;
128
+ font-weight: bold;
129
+ }
130
+ QPushButton:hover { background-color: #4a4a4a; }
131
+ QPushButton:pressed { background-color: #2a2a2a; }
132
+ QPushButton:disabled {
133
+ background-color: #2a2a2a;
134
+ color: #555;
135
+ border-color: #333;
136
+ }
137
+ """
138
+
139
+
140
+ class TransportButtonStyles:
141
+ """Predefined style for transport bar buttons."""
142
+
143
+ def __init__(self):
144
+ self.transport = _BUTTON_STYLE_TRANSPORT
@@ -0,0 +1,164 @@
1
+ """
2
+ Detection detail panel - shows properties of the selected detection.
3
+ Will eventually be extended to allow editing of bounding box position.
4
+ """
5
+
6
+ from PySide6.QtWidgets import QFormLayout, QLabel, QWidget
7
+
8
+ from seavision.engine.detectors.base import Detection
9
+ from seavision.gui.validation.validation_model import (
10
+ ValidatedDetection,
11
+ ValidationStatus
12
+ )
13
+
14
+ class DetectionDetailPanel(QWidget):
15
+ """
16
+ Read-only panel displaying all properties of a selected detection.
17
+
18
+ Uses QFormLayout for a clean label-value grid. Updated whenever the table
19
+ selection changes. Shows placeholder dashes when no detection is selected.
20
+ """
21
+
22
+ def __init__(self, parent=None):
23
+ super().__init__(parent)
24
+ self._build_layout()
25
+ self.clear()
26
+
27
+ def _build_layout(self) -> None:
28
+ """Create the label-value pairs."""
29
+ layout = QFormLayout(self)
30
+
31
+ self._status_value = QLabel()
32
+ self._status_value.setStyleSheet(
33
+ "font-size: 14px; font-weight: bold; padding: 4px 0;"
34
+ )
35
+ layout.addRow("Status:", self._status_value)
36
+
37
+ self._frame_value = QLabel()
38
+ layout.addRow("Frame:", self._frame_value)
39
+
40
+ self._time_value = QLabel()
41
+ layout.addRow("Timestamp:", self._time_value)
42
+
43
+ self._confidence_value = QLabel()
44
+ layout.addRow("Confidence:", self._confidence_value)
45
+
46
+ self._label_value = QLabel()
47
+ layout.addRow("Label:", self._label_value)
48
+
49
+ self._track_value = QLabel()
50
+ layout.addRow("Track ID:", self._track_value)
51
+
52
+ self._position_value = QLabel()
53
+ layout.addRow("Centre coordinates (xc, yc):", self._position_value)
54
+
55
+ self._bbox_coords_value = QLabel()
56
+ layout.addRow("Bounding box (x1, y1, x2, y2):", self._bbox_coords_value)
57
+
58
+ self._size_value = QLabel()
59
+ layout.addRow("Size:", self._size_value)
60
+
61
+ self._area_value = QLabel()
62
+ layout.addRow("Area:", self._area_value)
63
+
64
+ def set_detection(
65
+ self, vd: ValidatedDetection, fps: float | None = None
66
+ ) -> None:
67
+ """
68
+ Update all fields to show the given detection's properties.
69
+
70
+ Args:
71
+ vd: A ValidatedDetection object.
72
+ fps: Video frame rate for timestamp calculation.
73
+ """
74
+ detection = vd.detection
75
+
76
+ self._frame_value.setText(str(detection.frame_number))
77
+
78
+ if fps is not None and fps > 0:
79
+ timestamp = detection.frame_number / fps
80
+ mins = int(timestamp // 60)
81
+ secs = timestamp % 60
82
+ self._time_value.setText(f"{mins}:{secs:04.1f}")
83
+ else:
84
+ self._time_value.setText("—")
85
+
86
+ if detection.confidence is not None:
87
+ self._confidence_value.setText(f"{detection.confidence:.3f}")
88
+ else:
89
+ self._confidence_value.setText("—")
90
+
91
+ self._label_value.setText(detection.label if detection.label else "—")
92
+
93
+ if detection.track_id is not None:
94
+ self._track_value.setText(str(detection.track_id))
95
+ else:
96
+ self._track_value.setText("—")
97
+
98
+ # Use corrected geometry if available
99
+ if vd.corrected_geometry is not None:
100
+ geom = vd.corrected_geometry
101
+ xc = geom["xc"]
102
+ yc = geom["yc"]
103
+ w = geom["width"]
104
+ h = geom["height"]
105
+ else:
106
+ xc = detection.xc
107
+ yc = detection.yc
108
+ w = detection.width
109
+ h = detection.height
110
+
111
+ # Position: centre coordinates
112
+ self._position_value.setText(
113
+ f"({xc:.1f}, {yc:.1f})"
114
+ )
115
+
116
+ # Bounding box coordinates
117
+ x1 = xc - w / 2
118
+ y1 = yc - h / 2
119
+ x2 = xc + w / 2
120
+ y2 = yc + h / 2
121
+ self._bbox_coords_value.setText(
122
+ f"({x1:.1f}, {y1:.1f}, {x2:.1f}, {y2:.1f})"
123
+ )
124
+
125
+ # Size: width x height
126
+ self._size_value.setText(
127
+ f"{w:.1f} x {h:.1f}"
128
+ )
129
+
130
+ # Area: width * height
131
+ area = w * h
132
+ self._area_value.setText(f"{area:.0f} px²")
133
+
134
+ # Status — prominent display with colour coding
135
+ status_config = {
136
+ ValidationStatus.PENDING: ("Pending", "#cc8800"),
137
+ ValidationStatus.CONFIRMED: ("Confirmed", "#2d8a4e"),
138
+ ValidationStatus.REJECTED: ("Rejected", "#c0392b"),
139
+ ValidationStatus.SKIPPED: ("Skipped", "#888888"),
140
+ ValidationStatus.CORRECTED: ("Corrected", "#2d6da8"),
141
+ }
142
+ label, colour = status_config.get(vd.status, ("Unknown", "#000000"))
143
+ display_text = label
144
+ if vd.is_manual:
145
+ display_text += " (manual)"
146
+
147
+ self._status_value.setText(display_text)
148
+ self._status_value.setStyleSheet(
149
+ f"font-size: 14px; font-weight: bold; padding: 4px 0; "
150
+ f"color: {colour};"
151
+ )
152
+
153
+ def clear(self) -> None:
154
+ """Reset all fields to a placeholder state."""
155
+ self._frame_value.setText("—")
156
+ self._time_value.setText("—")
157
+ self._confidence_value.setText("—")
158
+ self._label_value.setText("—")
159
+ self._status_value.setText("—")
160
+ self._track_value.setText("—")
161
+ self._position_value.setText("—")
162
+ self._bbox_coords_value.setText("—")
163
+ self._size_value.setText("—")
164
+ self._area_value.setText("—")