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,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export validation - compare PyTorch and exported model outputs.
|
|
3
|
+
|
|
4
|
+
Runs a synthetic frame through both models and checks that the exported model
|
|
5
|
+
produces detections consistent with the original. This catches silent export
|
|
6
|
+
failures where the conversion completes without error but the exported model
|
|
7
|
+
behaves differently.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Tuple, cast
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .config import ExportConfig, ExportTarget, ValidationResult
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Thresholds for tensor deviation
|
|
22
|
+
MAX_ELEMENT_DEVIATION_THRESHOLD = 0.01
|
|
23
|
+
MEAN_ELEMENT_DEVIATION_THRESHOLD = 0.001
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_test_frame(imgsz: int) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Create a synthetic BGR test frame for validation.
|
|
29
|
+
|
|
30
|
+
The frame has structured content (gradients, shapes), rather than random
|
|
31
|
+
noise. This matters because models can behave different on structured vs.
|
|
32
|
+
random input - a model might produce zero detections on noise but produce
|
|
33
|
+
them on structured content, making noise-based validation useless.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
imgsz: Image dimension (square).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A BGR uint8 numpy array of shape (imgsz, imgsz, 3).
|
|
40
|
+
"""
|
|
41
|
+
frame = np.zeros((imgsz, imgsz, 3), dtype=np.uint8)
|
|
42
|
+
|
|
43
|
+
# Horizontal gradient in blue channel
|
|
44
|
+
frame[:, :, 0] = np.tile(
|
|
45
|
+
np.linspace(0, 255, imgsz, dtype=np.uint8), (imgsz, 1)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Vertical gradient in green channel
|
|
49
|
+
frame[:, :, 1] = np.tile(
|
|
50
|
+
np.linspace(0, 255, imgsz, dtype=np.uint8).reshape(-1, 1),
|
|
51
|
+
(1, imgsz),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# A bright rectangle in the centre (likely to trigger detections)
|
|
55
|
+
quarter = imgsz // 4
|
|
56
|
+
frame[quarter : 3 * quarter, quarter : 3 * quarter] = (200, 200, 200)
|
|
57
|
+
|
|
58
|
+
return frame
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_pytorch_raw(
|
|
62
|
+
weights_path: str,
|
|
63
|
+
frame: np.ndarray,
|
|
64
|
+
imgsz: int,
|
|
65
|
+
device: str,
|
|
66
|
+
) -> np.ndarray:
|
|
67
|
+
"""
|
|
68
|
+
Run PyTorch inference and return the raw output tensor.
|
|
69
|
+
|
|
70
|
+
Returns the raw model output BEFORE any postprocessing or NMS.
|
|
71
|
+
Shape: [1, 4+num_classes, N] — same layout as the ONNX output.
|
|
72
|
+
"""
|
|
73
|
+
import torch
|
|
74
|
+
import ultralytics
|
|
75
|
+
|
|
76
|
+
model: ultralytics.YOLO = ultralytics.YOLO(weights_path)
|
|
77
|
+
|
|
78
|
+
# Access the underlying PyTorch model to get raw output
|
|
79
|
+
torch_model = model.model
|
|
80
|
+
|
|
81
|
+
assert isinstance(torch_model, torch.nn.Module), (
|
|
82
|
+
f"Expected torch.nn.Module, got {type(torch_model)!r}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
torch_model.eval()
|
|
86
|
+
|
|
87
|
+
if device and device != "cpu":
|
|
88
|
+
torch_model = torch_model.to(device)
|
|
89
|
+
|
|
90
|
+
# Preprocess identically to the ONNX path
|
|
91
|
+
import cv2
|
|
92
|
+
h, w = frame.shape[:2]
|
|
93
|
+
r = min(imgsz / h, imgsz / w)
|
|
94
|
+
new_w, new_h = int(round(w * r)), int(round(h * r))
|
|
95
|
+
dw = (imgsz - new_w) / 2
|
|
96
|
+
dh = (imgsz - new_h) / 2
|
|
97
|
+
|
|
98
|
+
resized = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
|
99
|
+
top = int(round(dh - 0.1))
|
|
100
|
+
bottom = int(round(dh + 0.1))
|
|
101
|
+
left = int(round(dw - 0.1))
|
|
102
|
+
right = int(round(dw + 0.1))
|
|
103
|
+
padded = cv2.copyMakeBorder(
|
|
104
|
+
resized, top, bottom, left, right,
|
|
105
|
+
cv2.BORDER_CONSTANT, value=(114, 114, 114),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
blob = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
|
|
109
|
+
blob = blob.astype(np.float32) / 255.0
|
|
110
|
+
blob = np.transpose(blob, (2, 0, 1))
|
|
111
|
+
blob = np.expand_dims(blob, axis=0)
|
|
112
|
+
|
|
113
|
+
tensor = torch.from_numpy(blob)
|
|
114
|
+
if device and device != "cpu":
|
|
115
|
+
tensor = tensor.to(device)
|
|
116
|
+
|
|
117
|
+
with torch.no_grad():
|
|
118
|
+
output: torch.Tensor = torch_model(tensor)
|
|
119
|
+
|
|
120
|
+
# output may be a tuple/list — take the first element
|
|
121
|
+
if isinstance(output, (list, tuple)):
|
|
122
|
+
output = output[0]
|
|
123
|
+
|
|
124
|
+
return output.cpu().numpy()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_onnx_raw(
|
|
128
|
+
onnx_path: str,
|
|
129
|
+
frame: np.ndarray,
|
|
130
|
+
imgsz: int,
|
|
131
|
+
) -> np.ndarray:
|
|
132
|
+
"""
|
|
133
|
+
Run ONNX inference and return the raw output tensor.
|
|
134
|
+
|
|
135
|
+
Returns the raw model output BEFORE any postprocessing or NMS.
|
|
136
|
+
Shape: [1, 4+num_classes, N].
|
|
137
|
+
"""
|
|
138
|
+
import cv2
|
|
139
|
+
import onnxruntime as ort
|
|
140
|
+
|
|
141
|
+
# Preprocess — identical to _run_pytorch_raw above
|
|
142
|
+
h, w = frame.shape[:2]
|
|
143
|
+
r = min(imgsz / h, imgsz / w)
|
|
144
|
+
new_w, new_h = int(round(w * r)), int(round(h * r))
|
|
145
|
+
dw = (imgsz - new_w) / 2
|
|
146
|
+
dh = (imgsz - new_h) / 2
|
|
147
|
+
|
|
148
|
+
resized = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
|
149
|
+
top = int(round(dh - 0.1))
|
|
150
|
+
bottom = int(round(dh + 0.1))
|
|
151
|
+
left = int(round(dw - 0.1))
|
|
152
|
+
right = int(round(dw + 0.1))
|
|
153
|
+
padded = cv2.copyMakeBorder(
|
|
154
|
+
resized, top, bottom, left, right,
|
|
155
|
+
cv2.BORDER_CONSTANT, value=(114, 114, 114),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
blob = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
|
|
159
|
+
blob = blob.astype(np.float32) / 255.0
|
|
160
|
+
blob = np.transpose(blob, (2, 0, 1))
|
|
161
|
+
blob = np.expand_dims(blob, axis=0)
|
|
162
|
+
|
|
163
|
+
session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
|
|
164
|
+
input_name = session.get_inputs()[0].name
|
|
165
|
+
output = session.run(None, {input_name: blob})
|
|
166
|
+
|
|
167
|
+
result = output[0]
|
|
168
|
+
|
|
169
|
+
if not isinstance(result, np.ndarray):
|
|
170
|
+
raise TypeError(f"Expected numpy.ndarray from ONNX model, got {type(result)!r}")
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _compare_raw_outputs(
|
|
176
|
+
pytorch_output: np.ndarray,
|
|
177
|
+
onnx_output: np.ndarray,
|
|
178
|
+
) -> tuple[float, float, bool]:
|
|
179
|
+
"""
|
|
180
|
+
Compare raw model output tensors element-wise.
|
|
181
|
+
|
|
182
|
+
Returns (max_absolute_deviation, mean_absolute_deviation, shape_match).
|
|
183
|
+
"""
|
|
184
|
+
shape_match = pytorch_output.shape == onnx_output.shape
|
|
185
|
+
|
|
186
|
+
if not shape_match:
|
|
187
|
+
return float("inf"), float("inf"), False
|
|
188
|
+
|
|
189
|
+
diff = np.abs(pytorch_output - onnx_output)
|
|
190
|
+
max_dev = float(np.max(diff))
|
|
191
|
+
mean_dev = float(np.mean(diff))
|
|
192
|
+
|
|
193
|
+
return max_dev, mean_dev, True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def validate_export(
|
|
197
|
+
config: ExportConfig,
|
|
198
|
+
exported_path: str,
|
|
199
|
+
) -> ValidationResult:
|
|
200
|
+
"""
|
|
201
|
+
Validate an exported model against the original PyTorch model.
|
|
202
|
+
|
|
203
|
+
Runs a synthetic test frame through both models and compares the detection
|
|
204
|
+
outputs. This is the main entry point for export validation.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
config: The export configuration (needed for weights_path, imgsz,
|
|
208
|
+
device, and target).
|
|
209
|
+
exported_path: Path to the exported model file.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ValidationResult with pass/fail status and deviation metrics.
|
|
213
|
+
"""
|
|
214
|
+
logger.info(
|
|
215
|
+
"Validating export: %s vs %s", config.weights_path, exported_path
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
frame = _create_test_frame(config.imgsz)
|
|
219
|
+
|
|
220
|
+
# Run both models on the same preprocessed input and compare raw outputs
|
|
221
|
+
try:
|
|
222
|
+
pytorch_output = _run_pytorch_raw(
|
|
223
|
+
config.weights_path, frame, config.imgsz, config.device
|
|
224
|
+
)
|
|
225
|
+
logger.info(
|
|
226
|
+
"Pytorch raw output shape: %s", pytorch_output.shape
|
|
227
|
+
)
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
return ValidationResult(
|
|
230
|
+
passed=False,
|
|
231
|
+
details=f"PyTorch inference failed: {exc}",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if config.target == ExportTarget.ONNX:
|
|
236
|
+
onnx_output = _run_onnx_raw(exported_path, frame, config.imgsz)
|
|
237
|
+
logger.info(
|
|
238
|
+
"ONNX raw output shape: %s", onnx_output.shape
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
# Non-ONNX: file existence check only
|
|
242
|
+
exported_file = Path(exported_path)
|
|
243
|
+
if exported_file.exists():
|
|
244
|
+
size = (
|
|
245
|
+
sum(f.stat().st_size for f in exported_file.rglob("*"))
|
|
246
|
+
if exported_file.is_dir()
|
|
247
|
+
else exported_file.stat().st_size
|
|
248
|
+
)
|
|
249
|
+
if size > 0:
|
|
250
|
+
return ValidationResult(
|
|
251
|
+
passed=True,
|
|
252
|
+
details=(
|
|
253
|
+
f"File existence check passed for "
|
|
254
|
+
f"{config.target.name} export ({size / 1e6:.1f} MB). "
|
|
255
|
+
f"Detailed inference validation is only supported "
|
|
256
|
+
f"for ONNX exports."
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
return ValidationResult(
|
|
260
|
+
passed=False,
|
|
261
|
+
details=f"Exported file not found or empty: {exported_path}",
|
|
262
|
+
)
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
return ValidationResult(
|
|
265
|
+
passed=False,
|
|
266
|
+
details=f"Exported model validation failed: {exc}",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Compare raw output tensors
|
|
270
|
+
max_dev, mean_dev, shape_match = _compare_raw_outputs(
|
|
271
|
+
pytorch_output, onnx_output
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if not shape_match:
|
|
275
|
+
return ValidationResult(
|
|
276
|
+
passed=False,
|
|
277
|
+
details=(
|
|
278
|
+
f"Output shape mismatch: PyTorch {pytorch_output.shape} "
|
|
279
|
+
f"vs ONNX {onnx_output.shape}"
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Thresholds for raw tensor comparison
|
|
284
|
+
passed = (
|
|
285
|
+
max_dev < MAX_ELEMENT_DEVIATION_THRESHOLD
|
|
286
|
+
and mean_dev < MEAN_ELEMENT_DEVIATION_THRESHOLD
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
details_parts = [
|
|
290
|
+
f"Output_shape: {pytorch_output.shape}",
|
|
291
|
+
f"Max element deviation: {max_dev:.6f}",
|
|
292
|
+
f"Mean element deviation: {mean_dev:.6f}",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if not passed:
|
|
296
|
+
details_parts.append(
|
|
297
|
+
f"FAIL: Deviations exceed tolerace (max > "
|
|
298
|
+
f"{MAX_ELEMENT_DEVIATION_THRESHOLD} or mean > "
|
|
299
|
+
f"{MEAN_ELEMENT_DEVIATION_THRESHOLD})"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
result = ValidationResult(
|
|
303
|
+
passed=passed,
|
|
304
|
+
max_box_deviation=max_dev,
|
|
305
|
+
max_confidence_deviation=mean_dev,
|
|
306
|
+
details="; ".join(details_parts),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if passed:
|
|
310
|
+
logger.info("Export validation PASSED: %s", result.details)
|
|
311
|
+
else:
|
|
312
|
+
logger.warning("Export validation FAILED: %s", result.details)
|
|
313
|
+
|
|
314
|
+
return result
|