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,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