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
|
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("—")
|