seavision 1.0.0__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 +38 -0
- seavision/calibration/README.md +41 -0
- seavision/calibration/__init__.py +7 -0
- seavision/calibration/__main__.py +45 -0
- seavision/calibration/parsers.py +58 -0
- seavision/calibration/server.py +265 -0
- seavision/calibration/static/assets/SEA.AI_logo_dark_blue_RGB.png +0 -0
- seavision/calibration/static/css/base.css +161 -0
- seavision/calibration/static/css/components.css +1108 -0
- seavision/calibration/static/index.html +224 -0
- seavision/calibration/static/js/api.js +54 -0
- seavision/calibration/static/js/browser.js +128 -0
- seavision/calibration/static/js/controls.js +32 -0
- seavision/calibration/static/js/main.js +282 -0
- seavision/calibration/static/js/viewer.js +235 -0
- seavision/calibration/utils.py +243 -0
- seavision/cli/__init__.py +78 -0
- seavision/cli/calibration.py +86 -0
- seavision/cli/compose_onnx.py +75 -0
- seavision/cli/inference.py +351 -0
- seavision/cli/models.py +65 -0
- seavision/cli/utils.py +96 -0
- seavision/deployment/__init__.py +1 -0
- seavision/deployment/compose_onnx.py +173 -0
- seavision/deployment/onnx2trt.py +141 -0
- seavision/deployment/speedtest.py +111 -0
- seavision/experimental/README.md +17 -0
- seavision/experimental/__init__.py +5 -0
- seavision/experimental/false_zoom.py +89 -0
- seavision/experimental/video_embeddings.py +159 -0
- seavision/fiveone/.gitignore +1 -0
- seavision/fiveone/__init__.py +10 -0
- seavision/fiveone/composite.py +212 -0
- seavision/fiveone/inference.py +393 -0
- seavision/fiveone/representativeness.py +86 -0
- seavision/geometry/__init__.py +10 -0
- seavision/geometry/composite.py +187 -0
- seavision/geometry/distance.py +440 -0
- seavision/geometry/pinhole.py +274 -0
- seavision/geometry/transformations.py +154 -0
- seavision/geometry/utils.py +49 -0
- seavision/inference/README.md +157 -0
- seavision/inference/__init__.py +40 -0
- seavision/inference/ahoy.py +154 -0
- seavision/inference/dan.py +300 -0
- seavision/inference/holy.py +256 -0
- seavision/inference/horizon_extraction/__init__.py +11 -0
- seavision/inference/horizon_extraction/extract.py +102 -0
- seavision/inference/horizon_extraction/fit.py +229 -0
- seavision/inference/horizon_extraction/ops.py +146 -0
- seavision/inference/infer_abc.py +120 -0
- seavision/inference/infer_onnx2torch.py +239 -0
- seavision/inference/infer_tf.py +154 -0
- seavision/inference/infer_trt.py +315 -0
- seavision/inference/louisnet.py +124 -0
- seavision/inference/mimo.py +71 -0
- seavision/inference/model_abc.py +492 -0
- seavision/inference/models.py +192 -0
- seavision/inference/postprocessing.py +950 -0
- seavision/inference/preprocessing.py +351 -0
- seavision/inference/rahi.py +119 -0
- seavision/inference/results.py +993 -0
- seavision/inference/sahi.py +142 -0
- seavision/inference/seayolo.py +101 -0
- seavision/inference/slicing.py +186 -0
- seavision/inference/utils.py +112 -0
- seavision/inference/yolo.py +148 -0
- seavision/io/__init__.py +18 -0
- seavision/io/image.py +94 -0
- seavision/io/utils.py +24 -0
- seavision/io/video.py +829 -0
- seavision/sensorfusion/__init__.py +17 -0
- seavision/sensorfusion/common.py +31 -0
- seavision/sensorfusion/fusion_abc.py +179 -0
- seavision/sensorfusion/nms.py +62 -0
- seavision/sensorfusion/utils.py +17 -0
- seavision/sensorfusion/wbf.py +50 -0
- seavision/transforms/__init__.py +20 -0
- seavision/transforms/base.py +70 -0
- seavision/transforms/clip.py +61 -0
- seavision/transforms/color.py +17 -0
- seavision/transforms/compose.py +104 -0
- seavision/transforms/enhance.py +84 -0
- seavision/transforms/normalize.py +66 -0
- seavision/utils/__init__.py +1 -0
- seavision/utils/downloads.py +148 -0
- seavision/utils/general.py +138 -0
- seavision/utils/jetson.py +202 -0
- seavision/utils/onnx_utils.py +140 -0
- seavision-1.0.0.dist-info/METADATA +112 -0
- seavision-1.0.0.dist-info/RECORD +93 -0
- seavision-1.0.0.dist-info/WHEEL +4 -0
- seavision-1.0.0.dist-info/entry_points.txt +2 -0
seavision/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""SeaVision.
|
|
2
|
+
|
|
3
|
+
Computer vision tools for R&D at SEA.AI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import calibration, geometry, inference, io
|
|
7
|
+
from . import sensorfusion as sf
|
|
8
|
+
from . import transforms as T
|
|
9
|
+
from .calibration import * # noqa: F403
|
|
10
|
+
from .geometry import * # noqa: F403
|
|
11
|
+
from .inference import * # noqa: F403
|
|
12
|
+
from .io import * # noqa: F403
|
|
13
|
+
from .utils.general import pil_side_by_side, pil_up_down
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
except ImportError:
|
|
18
|
+
from importlib_metadata import PackageNotFoundError, version # python 3.6 and 3.7
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = version("seavision")
|
|
22
|
+
except PackageNotFoundError:
|
|
23
|
+
__version__ = "0.0.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"geometry",
|
|
27
|
+
"inference",
|
|
28
|
+
"io",
|
|
29
|
+
"calibration",
|
|
30
|
+
"sf",
|
|
31
|
+
"T",
|
|
32
|
+
"pil_side_by_side",
|
|
33
|
+
"pil_up_down",
|
|
34
|
+
]
|
|
35
|
+
__all__.extend(inference.__all__)
|
|
36
|
+
__all__.extend(io.__all__)
|
|
37
|
+
__all__.extend(geometry.__all__)
|
|
38
|
+
__all__.extend(calibration.__all__)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Online Calibration
|
|
2
|
+
|
|
3
|
+
A local web tool for manually calibrating Sentry camera homography by adjusting pitch, yaw, and roll angles and visually inspecting the overlay against the RGB reference frame.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv run -m seavision.calibration
|
|
9
|
+
uv run -m seavision.calibration --recording /path/to/Sentry_recordings_2024_03_12_10_34_06
|
|
10
|
+
uv run -m seavision.calibration --port 8323 --dry-run
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open http://localhost:8323 in a browser.
|
|
14
|
+
|
|
15
|
+
## Arguments
|
|
16
|
+
|
|
17
|
+
| Argument | Default | Description |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `--recording` | — | Path to a Sentry recording directory. Optional — can also be set via the UI. |
|
|
20
|
+
| `--port` | `8323` | Port to listen on. |
|
|
21
|
+
| `--host` | `0.0.0.0` | Host / IP to bind (`0.0.0.0` = all interfaces). |
|
|
22
|
+
| `--dry-run` | `false` | Print calibration changes to terminal instead of writing to `sentry_sensors.json`. |
|
|
23
|
+
|
|
24
|
+
## UI
|
|
25
|
+
|
|
26
|
+
- **Recording** — paste a path or use the text input to load a recording directory
|
|
27
|
+
- **Angles** — adjust pitch, yaw, roll for the selected camera; the overlay updates live
|
|
28
|
+
- **Blend** — control the mix weight between the source and reference frame
|
|
29
|
+
- **JSON tab** — toggle between the image overlay and a live view of `sentry_sensors.json`; changed values are highlighted in red
|
|
30
|
+
- **Save** — writes updated values to `sentry_sensors.json`; the original is preserved as `original_sentry_sensors.json`
|
|
31
|
+
|
|
32
|
+
## Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
seavision/calibration/
|
|
36
|
+
__main__.py Entry point
|
|
37
|
+
server.py FastAPI server and API endpoints
|
|
38
|
+
utils.py Recording loading, frame extraction, homography processing
|
|
39
|
+
static/
|
|
40
|
+
index.html Single-page UI
|
|
41
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Entry point for the Sentry homography calibration web server.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
uv run -m seavision.calibration
|
|
5
|
+
uv run -m seavision.calibration --recording /path/to/Sentry_2024_03_12_10_34_06
|
|
6
|
+
uv run -m seavision.calibration --port 8323 --host 127.0.0.1 --dry-run
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from seavision.calibration.server import run_web_server
|
|
12
|
+
|
|
13
|
+
parser = argparse.ArgumentParser(description="Sentry homography calibration server")
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--recording",
|
|
16
|
+
default=None,
|
|
17
|
+
help="Path to a Sentry recording directory (optional — can be set in the UI)",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--port", type=int, default=8323, help="Port to listen on (default: 8323)"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--host",
|
|
24
|
+
default="0.0.0.0",
|
|
25
|
+
help="Host / IP to bind (default: 0.0.0.0 — all interfaces)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--reference-camera",
|
|
29
|
+
default=None,
|
|
30
|
+
help="Master view to project sources into (default: t_w)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--dry-run",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Print changes instead of writing to sentry_sensors.json",
|
|
36
|
+
)
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
run_web_server(
|
|
40
|
+
args.recording,
|
|
41
|
+
args.port,
|
|
42
|
+
host=args.host,
|
|
43
|
+
dry_run=args.dry_run,
|
|
44
|
+
reference_camera=args.reference_camera,
|
|
45
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Parsers for sensor calibration data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Union
|
|
7
|
+
|
|
8
|
+
from seavision.geometry.pinhole import Camera
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class CalibrationData:
|
|
13
|
+
"""Calibration bundle loaded from JSON (Oscar / Sentry or legacy intrinsics)."""
|
|
14
|
+
|
|
15
|
+
cameras: dict[str, Camera]
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _build_camera(camera_data: Dict[str, Any]) -> Camera:
|
|
19
|
+
"""Build a Camera from one JSON camera block."""
|
|
20
|
+
if "focal_length_x" in camera_data:
|
|
21
|
+
width, height = camera_data["resolution"]["value"]
|
|
22
|
+
fx = camera_data["focal_length_x"]["value"]
|
|
23
|
+
fy = camera_data["focal_length_y"]["value"]
|
|
24
|
+
cx = camera_data["center_x"]["value"]
|
|
25
|
+
cy = camera_data["center_y"]["value"]
|
|
26
|
+
else:
|
|
27
|
+
height, width = camera_data["resolution"]["value"]
|
|
28
|
+
f_mm = camera_data["focal_length"]["value"]
|
|
29
|
+
pixel_mm = camera_data["pixel_size"]["value"]
|
|
30
|
+
if pixel_mm == 0:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"pixel_size must be non-zero to derive focal length in pixels"
|
|
33
|
+
)
|
|
34
|
+
fx = fy = f_mm / pixel_mm
|
|
35
|
+
cx = width / 2.0
|
|
36
|
+
cy = height / 2.0
|
|
37
|
+
return Camera(
|
|
38
|
+
width=int(width),
|
|
39
|
+
height=int(height),
|
|
40
|
+
fx=float(fx),
|
|
41
|
+
fy=float(fy),
|
|
42
|
+
cx=float(cx),
|
|
43
|
+
cy=float(cy),
|
|
44
|
+
roll=float(camera_data["roll"]["value"]),
|
|
45
|
+
pitch=float(camera_data["pitch"]["value"]),
|
|
46
|
+
yaw=float(camera_data["yaw"]["value"]),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_json(cls, file_path: Union[str, Path]) -> "CalibrationData":
|
|
51
|
+
"""Parse calibration data from a JSON file."""
|
|
52
|
+
with Path(file_path).open("r", encoding="utf-8") as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
cameras = {
|
|
55
|
+
camera_id: cls._build_camera(camera_data)
|
|
56
|
+
for camera_id, camera_data in data.get("cameras", {}).items()
|
|
57
|
+
}
|
|
58
|
+
return cls(cameras=cameras)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""FastAPI server for the Sentry homography calibration tool."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import uvicorn
|
|
9
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
10
|
+
from fastapi.responses import HTMLResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
|
|
13
|
+
from seavision.calibration import CalibrationData
|
|
14
|
+
from seavision.calibration.utils import (
|
|
15
|
+
CAMERA_PATTERNS,
|
|
16
|
+
DEFAULT_REFERENCE_CAMERA,
|
|
17
|
+
image_to_base64,
|
|
18
|
+
load_first_frames,
|
|
19
|
+
load_sentry_config,
|
|
20
|
+
process_calibration,
|
|
21
|
+
read_sensors_json,
|
|
22
|
+
source_cameras_for_reference,
|
|
23
|
+
update_sensors_json,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
27
|
+
|
|
28
|
+
app = FastAPI(title="Seavision")
|
|
29
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class _ServerState:
|
|
34
|
+
"""Mutable server state shared across request handlers."""
|
|
35
|
+
|
|
36
|
+
sentry: Optional[CalibrationData] = None
|
|
37
|
+
frames: Dict[str, np.ndarray] = field(default_factory=dict)
|
|
38
|
+
recording_path: Optional[str] = None
|
|
39
|
+
reference_camera: str = DEFAULT_REFERENCE_CAMERA
|
|
40
|
+
current_camera: Optional[str] = None
|
|
41
|
+
camera_ui_values: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
|
42
|
+
dry_run: bool = False
|
|
43
|
+
recordings_root: Optional[Path] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_state = _ServerState()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_recording_path(path: str) -> Path:
|
|
50
|
+
"""Resolve and validate a recording path against the configured root."""
|
|
51
|
+
resolved = Path(path).expanduser().resolve()
|
|
52
|
+
if _state.recordings_root is not None:
|
|
53
|
+
try:
|
|
54
|
+
resolved.relative_to(_state.recordings_root)
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
raise ValueError("Access outside recordings root is not allowed") from e
|
|
57
|
+
return resolved
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _apply_recording(path: str) -> None:
|
|
61
|
+
"""Load recording data into server state (atomic: commits only on full success)."""
|
|
62
|
+
resolved = resolve_recording_path(path)
|
|
63
|
+
sentry = load_sentry_config(str(resolved), reference_camera=_state.reference_camera)
|
|
64
|
+
frames = load_first_frames(str(resolved))
|
|
65
|
+
_state.sentry = sentry
|
|
66
|
+
_state.frames = frames
|
|
67
|
+
_state.recording_path = str(resolved)
|
|
68
|
+
_state.current_camera = None
|
|
69
|
+
_state.camera_ui_values = {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.get("/", response_class=HTMLResponse)
|
|
73
|
+
def index() -> str:
|
|
74
|
+
"""Serve the calibration UI."""
|
|
75
|
+
return (STATIC_DIR / "index.html").read_text()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.get("/api/camera_sources")
|
|
79
|
+
def camera_sources() -> Dict[str, Any]:
|
|
80
|
+
"""List source cameras for the current reference (fixed at server start)."""
|
|
81
|
+
return {"cameras": source_cameras_for_reference(_state.reference_camera)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.get("/api/select_camera_source")
|
|
85
|
+
def select_camera_source(camera: str = Query(...)) -> Dict[str, Any]:
|
|
86
|
+
"""Select the active camera and return its current UI values."""
|
|
87
|
+
allowed = source_cameras_for_reference(_state.reference_camera)
|
|
88
|
+
if camera not in allowed:
|
|
89
|
+
raise HTTPException(400, f"Invalid camera: {camera}; allowed: {allowed}")
|
|
90
|
+
_state.current_camera = camera
|
|
91
|
+
if camera not in _state.camera_ui_values:
|
|
92
|
+
_state.camera_ui_values[camera] = (
|
|
93
|
+
_state.sentry.cameras[camera].extrinsics()
|
|
94
|
+
if _state.sentry is not None and camera in _state.sentry.cameras
|
|
95
|
+
else {"pitch": 0.0, "yaw": 0.0, "roll": 0.0}
|
|
96
|
+
)
|
|
97
|
+
return {"camera": camera, "values": _state.camera_ui_values[camera]}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.get("/api/process")
|
|
101
|
+
def process(
|
|
102
|
+
pitch: float = Query(0.0),
|
|
103
|
+
yaw: float = Query(0.0),
|
|
104
|
+
roll: float = Query(0.0),
|
|
105
|
+
weight: float = Query(0.5),
|
|
106
|
+
) -> Dict[str, Any]:
|
|
107
|
+
"""Run calibration and return base64-encoded result images."""
|
|
108
|
+
if _state.sentry is None or not _state.frames:
|
|
109
|
+
raise HTTPException(400, "No recording loaded")
|
|
110
|
+
if _state.current_camera is None:
|
|
111
|
+
raise HTTPException(400, "No camera source selected")
|
|
112
|
+
_state.camera_ui_values[_state.current_camera] = {
|
|
113
|
+
"pitch": pitch,
|
|
114
|
+
"yaw": yaw,
|
|
115
|
+
"roll": roll,
|
|
116
|
+
}
|
|
117
|
+
result = process_calibration(
|
|
118
|
+
_state.sentry,
|
|
119
|
+
_state.frames,
|
|
120
|
+
_state.current_camera,
|
|
121
|
+
pitch,
|
|
122
|
+
yaw,
|
|
123
|
+
roll,
|
|
124
|
+
weight,
|
|
125
|
+
reference_camera=_state.reference_camera,
|
|
126
|
+
)
|
|
127
|
+
return {"mixed": image_to_base64(result["mixed"])}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.get("/api/reset_camera")
|
|
131
|
+
def reset_camera(camera: str = Query(...)) -> Dict[str, Any]:
|
|
132
|
+
"""Reset a camera's UI values to the original values from the recording."""
|
|
133
|
+
allowed = source_cameras_for_reference(_state.reference_camera)
|
|
134
|
+
if camera not in allowed:
|
|
135
|
+
raise HTTPException(400, f"Invalid camera: {camera}; allowed: {allowed}")
|
|
136
|
+
if _state.sentry is None:
|
|
137
|
+
raise HTTPException(400, "No recording loaded")
|
|
138
|
+
if camera not in _state.sentry.cameras:
|
|
139
|
+
raise HTTPException(400, f"Camera not found: {camera}")
|
|
140
|
+
_state.camera_ui_values[camera] = _state.sentry.cameras[camera].extrinsics()
|
|
141
|
+
return {"camera": camera, "values": _state.camera_ui_values[camera]}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.get("/api/load_recording")
|
|
145
|
+
def load_recording(path: str = Query(...)) -> Dict[str, Any]:
|
|
146
|
+
"""Load a recording directory into server state."""
|
|
147
|
+
try:
|
|
148
|
+
_apply_recording(path)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise HTTPException(400, str(e)) from e
|
|
151
|
+
return {"status": "loaded", "path": path}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.get("/api/browse_root")
|
|
155
|
+
def browse_root() -> Dict[str, Any]:
|
|
156
|
+
"""Return the initial directory for the file browser."""
|
|
157
|
+
root = _state.recordings_root or Path("~").expanduser().resolve()
|
|
158
|
+
return {"path": str(root)}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.get("/api/browse")
|
|
162
|
+
def browse(path: str = Query(...)) -> Dict[str, Any]:
|
|
163
|
+
"""List subdirectories of path, flagging valid recordings."""
|
|
164
|
+
base = Path(path).expanduser().resolve()
|
|
165
|
+
if not base.is_dir():
|
|
166
|
+
raise HTTPException(400, f"Not a directory: {path}")
|
|
167
|
+
if _state.recordings_root is not None:
|
|
168
|
+
try:
|
|
169
|
+
base.relative_to(_state.recordings_root)
|
|
170
|
+
except ValueError as e:
|
|
171
|
+
raise HTTPException(
|
|
172
|
+
403, "Access outside recordings root is not allowed"
|
|
173
|
+
) from e
|
|
174
|
+
entries = sorted(
|
|
175
|
+
(
|
|
176
|
+
{
|
|
177
|
+
"name": e.name,
|
|
178
|
+
"is_recording": (e / "POWER_Const" / "sentry_sensors.json").exists(),
|
|
179
|
+
}
|
|
180
|
+
for e in base.iterdir()
|
|
181
|
+
if e.is_dir()
|
|
182
|
+
),
|
|
183
|
+
key=lambda e: (not e["is_recording"], e["name"]),
|
|
184
|
+
)
|
|
185
|
+
root = _state.recordings_root or base.anchor
|
|
186
|
+
return {
|
|
187
|
+
"path": str(base),
|
|
188
|
+
"parent": str(base.parent),
|
|
189
|
+
"root": str(root),
|
|
190
|
+
"entries": entries,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.get("/api/sensors_json")
|
|
195
|
+
def get_sensors_json() -> Dict[str, Any]:
|
|
196
|
+
"""Return the current on-disk sentry_sensors.json and pending UI values."""
|
|
197
|
+
if _state.recording_path is None:
|
|
198
|
+
raise HTTPException(400, "No recording loaded")
|
|
199
|
+
return {
|
|
200
|
+
"data": read_sensors_json(_state.recording_path),
|
|
201
|
+
"pending": _state.camera_ui_values,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.get("/api/save_sensors_json")
|
|
206
|
+
def save_sensors_json() -> Dict[str, str]:
|
|
207
|
+
"""Write pending UI values to sentry_sensors.json."""
|
|
208
|
+
if _state.recording_path is None:
|
|
209
|
+
raise HTTPException(400, "No recording loaded")
|
|
210
|
+
if _state.dry_run:
|
|
211
|
+
for camera, values in _state.camera_ui_values.items():
|
|
212
|
+
print(
|
|
213
|
+
f"[dry-run] {camera}: "
|
|
214
|
+
f"pitch={values['pitch']:.4f} "
|
|
215
|
+
f"yaw={values['yaw']:.4f} "
|
|
216
|
+
f"roll={values['roll']:.4f}"
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
update_sensors_json(_state.recording_path, _state.camera_ui_values)
|
|
220
|
+
return {"status": "dry-run (see terminal)" if _state.dry_run else "saved"}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def configure_server(
|
|
224
|
+
recording_path: Optional[str] = None,
|
|
225
|
+
dry_run: bool = False,
|
|
226
|
+
reference_camera: Optional[str] = None,
|
|
227
|
+
recordings_root: Optional[str] = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Apply startup configuration to server state. Raises SystemExit for bad inputs."""
|
|
230
|
+
_state.dry_run = dry_run
|
|
231
|
+
if recordings_root is not None:
|
|
232
|
+
_state.recordings_root = Path(recordings_root).expanduser().resolve()
|
|
233
|
+
if reference_camera is not None:
|
|
234
|
+
if reference_camera not in CAMERA_PATTERNS:
|
|
235
|
+
raise SystemExit(
|
|
236
|
+
f"calibration: unknown reference {reference_camera!r}; "
|
|
237
|
+
f"expected one of: {sorted(CAMERA_PATTERNS)}"
|
|
238
|
+
)
|
|
239
|
+
_state.reference_camera = reference_camera
|
|
240
|
+
if recording_path is not None:
|
|
241
|
+
_apply_recording(recording_path)
|
|
242
|
+
print(f"Loaded recording: {recording_path}")
|
|
243
|
+
print(f"Reference (master) camera: {_state.reference_camera}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_web_server(
|
|
247
|
+
recording_path: Optional[str] = None,
|
|
248
|
+
port: int = 8323,
|
|
249
|
+
host: str = "0.0.0.0",
|
|
250
|
+
dry_run: bool = False,
|
|
251
|
+
reference_camera: Optional[str] = None,
|
|
252
|
+
recordings_root: Optional[str] = None,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Start the calibration web server, optionally with a pre-loaded recording."""
|
|
255
|
+
configure_server(
|
|
256
|
+
recording_path=recording_path,
|
|
257
|
+
dry_run=dry_run,
|
|
258
|
+
reference_camera=reference_camera,
|
|
259
|
+
recordings_root=recordings_root,
|
|
260
|
+
)
|
|
261
|
+
open_url = (
|
|
262
|
+
f"http://127.0.0.1:{port}" if host == "0.0.0.0" else f"http://{host}:{port}"
|
|
263
|
+
)
|
|
264
|
+
print(f"Calibration server listening on {host}:{port} — open {open_url}")
|
|
265
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
Binary file
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* SEA.AI design tokens — DARK (default) + LIGHT */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
/* Neutral surfaces */
|
|
5
|
+
--surface-neutral-1: #101214;
|
|
6
|
+
--surface-neutral-2: #181b1e;
|
|
7
|
+
--surface-neutral-3: #202428;
|
|
8
|
+
--surface-neutral-4: #292e33;
|
|
9
|
+
--surface-neutral-5: #31373d;
|
|
10
|
+
|
|
11
|
+
/* Semantic surfaces */
|
|
12
|
+
--surface-primary-3: #0a67c2;
|
|
13
|
+
--surface-danger-3: #c20a20;
|
|
14
|
+
--surface-warning-3: #f1b80d;
|
|
15
|
+
--surface-success-3: #2b9e6e;
|
|
16
|
+
|
|
17
|
+
/* Content on neutral backgrounds */
|
|
18
|
+
--text-1: #e8eaed;
|
|
19
|
+
--text-2: #8a919a;
|
|
20
|
+
--text-3: #565e69;
|
|
21
|
+
|
|
22
|
+
/* Content on colored surfaces */
|
|
23
|
+
--content-primary-2: #cfe5fc;
|
|
24
|
+
--content-danger-1: #fa9ea9;
|
|
25
|
+
--content-warning-1: #fae39e;
|
|
26
|
+
--content-success-1: #a7f3d0;
|
|
27
|
+
|
|
28
|
+
/* Focus / accent */
|
|
29
|
+
--accent-primary-2: #0a67c2;
|
|
30
|
+
--accent-danger-2: #c20a20;
|
|
31
|
+
--stroke-normal: 1.5px;
|
|
32
|
+
|
|
33
|
+
/* JSON syntax (neutral semantics: structure vs data) */
|
|
34
|
+
--json-string: #8ecf8a;
|
|
35
|
+
--json-number: #c5a9f5;
|
|
36
|
+
|
|
37
|
+
/* Shape */
|
|
38
|
+
--radius-s: 4px;
|
|
39
|
+
--radius-m: 8px;
|
|
40
|
+
--radius-l: 16px;
|
|
41
|
+
|
|
42
|
+
/* Spacing */
|
|
43
|
+
--space-component-xs: 4px;
|
|
44
|
+
--space-component-s: 8px;
|
|
45
|
+
--space-component-m: 12px;
|
|
46
|
+
--space-component-l: 16px;
|
|
47
|
+
--space-component-2xl: 24px;
|
|
48
|
+
--space-component-3xl: 32px;
|
|
49
|
+
|
|
50
|
+
/* Legacy aliases (components, inline) */
|
|
51
|
+
--surface-1: var(--surface-neutral-1);
|
|
52
|
+
--surface-2: var(--surface-neutral-2);
|
|
53
|
+
--surface-3: var(--surface-neutral-3);
|
|
54
|
+
--surface-4: var(--surface-neutral-4);
|
|
55
|
+
--surface-5: var(--surface-neutral-5);
|
|
56
|
+
--primary: var(--surface-primary-3);
|
|
57
|
+
--primary-hover: #0856a3;
|
|
58
|
+
--danger: var(--surface-danger-3);
|
|
59
|
+
--warning: var(--surface-warning-3);
|
|
60
|
+
--success: var(--surface-success-3);
|
|
61
|
+
--content-primary: var(--content-primary-2);
|
|
62
|
+
--content-danger: var(--content-danger-1);
|
|
63
|
+
--content-warning: var(--content-warning-1);
|
|
64
|
+
--content-success: var(--content-success-1);
|
|
65
|
+
--space-xs: var(--space-component-xs);
|
|
66
|
+
--space-s: var(--space-component-s);
|
|
67
|
+
--space-m: var(--space-component-m);
|
|
68
|
+
--space-l: var(--space-component-l);
|
|
69
|
+
--space-2xl: var(--space-component-2xl);
|
|
70
|
+
--space-3xl: var(--space-component-3xl);
|
|
71
|
+
--button-height: 48px;
|
|
72
|
+
--input-height: 40px;
|
|
73
|
+
--theme-button-height: 30px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[data-theme="light"] {
|
|
77
|
+
--surface-neutral-1: #b9bdc1;
|
|
78
|
+
--surface-neutral-2: #cbced1;
|
|
79
|
+
--surface-neutral-3: #dcdee0;
|
|
80
|
+
--surface-neutral-4: #eeeff0;
|
|
81
|
+
--surface-neutral-5: #ffffff;
|
|
82
|
+
|
|
83
|
+
--surface-primary-3: #0a67c2;
|
|
84
|
+
--surface-danger-3: #c20a20;
|
|
85
|
+
--surface-warning-3: #f1b80d;
|
|
86
|
+
--surface-success-3: #1a7a4f;
|
|
87
|
+
|
|
88
|
+
--text-1: #101214;
|
|
89
|
+
--text-2: #31373d;
|
|
90
|
+
--text-3: #565e69;
|
|
91
|
+
|
|
92
|
+
/* On colored surfaces: same as DARK per design spec */
|
|
93
|
+
--content-primary-2: #cfe5fc;
|
|
94
|
+
--content-danger-1: #fa9ea9;
|
|
95
|
+
--content-warning-1: #fae39e;
|
|
96
|
+
--content-success-1: #a7f3d0;
|
|
97
|
+
|
|
98
|
+
--json-string: #2d7a3a;
|
|
99
|
+
--json-number: #7b5ab5;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
*,
|
|
103
|
+
*::before,
|
|
104
|
+
*::after {
|
|
105
|
+
box-sizing: border-box;
|
|
106
|
+
margin: 0;
|
|
107
|
+
padding: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Typography — Saira Condensed; card-* uppercase for chrome */
|
|
111
|
+
body {
|
|
112
|
+
font-family: "Saira Condensed", "Arial Narrow", Arial, sans-serif;
|
|
113
|
+
background: var(--surface-neutral-3);
|
|
114
|
+
color: var(--text-1);
|
|
115
|
+
min-height: 100vh;
|
|
116
|
+
-webkit-font-smoothing: antialiased;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.card-label-s {
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
font-size: 16px;
|
|
122
|
+
line-height: 24px;
|
|
123
|
+
letter-spacing: 0;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.card-title-s {
|
|
128
|
+
font-weight: 400;
|
|
129
|
+
font-size: 20px;
|
|
130
|
+
line-height: 28px;
|
|
131
|
+
letter-spacing: 0.02em;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.main-body-xs {
|
|
136
|
+
font-weight: 400;
|
|
137
|
+
font-size: 12px;
|
|
138
|
+
line-height: 20px;
|
|
139
|
+
letter-spacing: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.element-button-m {
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
font-size: 16px;
|
|
145
|
+
line-height: 24px;
|
|
146
|
+
letter-spacing: 0.015em;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
::-webkit-scrollbar {
|
|
150
|
+
width: 4px;
|
|
151
|
+
}
|
|
152
|
+
::-webkit-scrollbar-track {
|
|
153
|
+
background: transparent;
|
|
154
|
+
}
|
|
155
|
+
::-webkit-scrollbar-thumb {
|
|
156
|
+
background: var(--surface-neutral-5);
|
|
157
|
+
border-radius: 2px;
|
|
158
|
+
}
|
|
159
|
+
::-webkit-scrollbar-thumb:hover {
|
|
160
|
+
background: var(--text-3);
|
|
161
|
+
}
|