viobench 0.0.1__tar.gz
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.
- viobench-0.0.1/PKG-INFO +58 -0
- viobench-0.0.1/README.md +23 -0
- viobench-0.0.1/pyproject.toml +44 -0
- viobench-0.0.1/setup.cfg +4 -0
- viobench-0.0.1/tests/test_datasets.py +330 -0
- viobench-0.0.1/tests/test_debug.py +142 -0
- viobench-0.0.1/tests/test_dnn.py +71 -0
- viobench-0.0.1/tests/test_evaluation.py +168 -0
- viobench-0.0.1/tests/test_fuel.py +164 -0
- viobench-0.0.1/tests/test_generic_host.py +60 -0
- viobench-0.0.1/tests/test_metrics.py +116 -0
- viobench-0.0.1/tests/test_openvins.py +222 -0
- viobench-0.0.1/tests/test_openvins_math.py +27 -0
- viobench-0.0.1/tests/test_orbslam3.py +164 -0
- viobench-0.0.1/tests/test_packaging.py +44 -0
- viobench-0.0.1/tests/test_regularize.py +101 -0
- viobench-0.0.1/tests/test_sandbox.py +375 -0
- viobench-0.0.1/tests/test_server.py +72 -0
- viobench-0.0.1/tests/test_video.py +168 -0
- viobench-0.0.1/tests/test_vins_marginalization.py +26 -0
- viobench-0.0.1/tests/test_vins_mono.py +120 -0
- viobench-0.0.1/tests/test_workspace.py +53 -0
- viobench-0.0.1/viobench/__init__.py +26 -0
- viobench-0.0.1/viobench/__main__.py +5 -0
- viobench-0.0.1/viobench/solution/examples/native/cpp_minimal/CMakeLists.txt +12 -0
- viobench-0.0.1/viobench/solution/examples/native/cpp_minimal/README.md +10 -0
- viobench-0.0.1/viobench/solution/examples/native/cpp_minimal/solution.cpp +97 -0
- viobench-0.0.1/viobench/solution/examples/python/minimal/solution.py +29 -0
- viobench-0.0.1/viobench/solution/runtime/native/README.md +22 -0
- viobench-0.0.1/viobench/solution/runtime/native/build.sh +18 -0
- viobench-0.0.1/viobench/solution/runtime/native/viobench_abi.h +51 -0
- viobench-0.0.1/viobench/solution/runtime/native/viobench_debug_main.c +136 -0
- viobench-0.0.1/viobench/solution/runtime/native/viobench_reactor.cmake +24 -0
- viobench-0.0.1/viobench/solution/runtime/python/README.md +55 -0
- viobench-0.0.1/viobench/solution/runtime/python/build.sh +9 -0
- viobench-0.0.1/viobench/solution/runtime/python/bundler/__init__.py +10 -0
- viobench-0.0.1/viobench/solution/runtime/python/bundler/bundle.py +159 -0
- viobench-0.0.1/viobench/solution/runtime/python/bundler/embed.py +75 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/build-wasm.sh +137 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/patches/numpy.patch +99 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/patches/opencv.patch +54 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/python.sh +5 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/scripts/collect_numpy.py +33 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/scripts/freeze.py +38 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/scripts/link.py +69 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/smoke_vo.py +44 -0
- viobench-0.0.1/viobench/solution/runtime/python/cpython/test.sh +39 -0
- viobench-0.0.1/viobench/solution/runtime/python/guest/viobench_guest.py +167 -0
- viobench-0.0.1/viobench/solution/runtime/python/reactor.c +160 -0
- viobench-0.0.1/viobench/solution/toolchain/download_dependencies.sh +113 -0
- viobench-0.0.1/viobench/solution/workspace.py +86 -0
- viobench-0.0.1/viobench/tools/__init__.py +12 -0
- viobench-0.0.1/viobench/tools/abi/__init__.py +39 -0
- viobench-0.0.1/viobench/tools/abi/config.py +73 -0
- viobench-0.0.1/viobench/tools/abi/module.py +50 -0
- viobench-0.0.1/viobench/tools/abi/wasm_binary.py +224 -0
- viobench-0.0.1/viobench/tools/cli.py +334 -0
- viobench-0.0.1/viobench/tools/config.json +15 -0
- viobench-0.0.1/viobench/tools/datasets/__init__.py +11 -0
- viobench-0.0.1/viobench/tools/datasets/episode.py +220 -0
- viobench-0.0.1/viobench/tools/datasets/feed.py +34 -0
- viobench-0.0.1/viobench/tools/datasets/generate/__init__.py +1 -0
- viobench-0.0.1/viobench/tools/datasets/generate/hyperdrone_world.h.in +50 -0
- viobench-0.0.1/viobench/tools/datasets/generate/simulation.py +396 -0
- viobench-0.0.1/viobench/tools/datasets/regularize.py +87 -0
- viobench-0.0.1/viobench/tools/datasets/storage.py +288 -0
- viobench-0.0.1/viobench/tools/geometry.py +105 -0
- viobench-0.0.1/viobench/tools/runner/__init__.py +9 -0
- viobench-0.0.1/viobench/tools/runner/check.py +52 -0
- viobench-0.0.1/viobench/tools/runner/direct.py +157 -0
- viobench-0.0.1/viobench/tools/runner/engine.py +62 -0
- viobench-0.0.1/viobench/tools/runner/leaderboard.py +94 -0
- viobench-0.0.1/viobench/tools/runner/metrics.py +136 -0
- viobench-0.0.1/viobench/tools/runner/run.py +188 -0
- viobench-0.0.1/viobench/tools/runner/sandbox.py +263 -0
- viobench-0.0.1/viobench/tools/runner/score.py +59 -0
- viobench-0.0.1/viobench/tools/runner/video.py +104 -0
- viobench-0.0.1/viobench/tools/server/__init__.py +1 -0
- viobench-0.0.1/viobench/tools/server/app.py +128 -0
- viobench-0.0.1/viobench/tools/server/config.py +45 -0
- viobench-0.0.1/viobench/tools/server/store.py +159 -0
- viobench-0.0.1/viobench/tools/server/teams.py +30 -0
- viobench-0.0.1/viobench/tools/server/ui/app.js +461 -0
- viobench-0.0.1/viobench/tools/server/ui/index.html +76 -0
- viobench-0.0.1/viobench/tools/server/ui/style.css +101 -0
- viobench-0.0.1/viobench/tools/server/worker.py +86 -0
- viobench-0.0.1/viobench.egg-info/PKG-INFO +58 -0
- viobench-0.0.1/viobench.egg-info/SOURCES.txt +90 -0
- viobench-0.0.1/viobench.egg-info/dependency_links.txt +1 -0
- viobench-0.0.1/viobench.egg-info/entry_points.txt +2 -0
- viobench-0.0.1/viobench.egg-info/requires.txt +32 -0
- viobench-0.0.1/viobench.egg-info/top_level.txt +1 -0
viobench-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viobench
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Compute-aware relative state estimation challenge: run and score solution .wasm modules, episode datasets, submission server, and the recipes to build a solution
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: wasmtime==50.0.1
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Provides-Extra: data
|
|
11
|
+
Requires-Dist: opencv-python-headless>=4.9; extra == "data"
|
|
12
|
+
Requires-Dist: pyyaml>=6; extra == "data"
|
|
13
|
+
Provides-Extra: sim
|
|
14
|
+
Requires-Dist: opencv-python-headless>=4.9; extra == "sim"
|
|
15
|
+
Requires-Dist: pyyaml>=6; extra == "sim"
|
|
16
|
+
Requires-Dist: hyperdrone==0.1.10; extra == "sim"
|
|
17
|
+
Provides-Extra: train
|
|
18
|
+
Requires-Dist: torch; extra == "train"
|
|
19
|
+
Requires-Dist: onnx; extra == "train"
|
|
20
|
+
Provides-Extra: server
|
|
21
|
+
Requires-Dist: fastapi; extra == "server"
|
|
22
|
+
Requires-Dist: uvicorn; extra == "server"
|
|
23
|
+
Requires-Dist: python-multipart; extra == "server"
|
|
24
|
+
Requires-Dist: tzdata; extra == "server"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: opencv-python-headless>=4.9; extra == "dev"
|
|
28
|
+
Requires-Dist: pyyaml>=6; extra == "dev"
|
|
29
|
+
Requires-Dist: torch; extra == "dev"
|
|
30
|
+
Requires-Dist: onnx; extra == "dev"
|
|
31
|
+
Requires-Dist: fastapi; extra == "dev"
|
|
32
|
+
Requires-Dist: python-multipart; extra == "dev"
|
|
33
|
+
Requires-Dist: tzdata; extra == "dev"
|
|
34
|
+
Requires-Dist: httpx; extra == "dev"
|
|
35
|
+
|
|
36
|
+
# viobench: compute-aware relative state estimation
|
|
37
|
+
|
|
38
|
+
Estimate a flying camera's pose relative to its first frame, and its twist, from camera frames and IMU data. A submission is one
|
|
39
|
+
WebAssembly module; the evaluator meters its compute deterministically (Wasmtime fuel) and places it on an
|
|
40
|
+
accuracy-vs-compute curve.
|
|
41
|
+
|
|
42
|
+
| | |
|
|
43
|
+
|---|---|
|
|
44
|
+
| [`viobench/tools/`](viobench/tools/) | host side: `abi`, `datasets`, `runner`, `server`, `cli` |
|
|
45
|
+
| [`viobench/solution/`](viobench/solution/) | guest side: toolchain, runtimes, examples; what `viobench init` copies into a workspace |
|
|
46
|
+
| [`docs/`](docs/) | [TASK](docs/TASK.md), [STUDENT_GUIDE](docs/STUDENT_GUIDE.md), [DEBUGGING](docs/DEBUGGING.md), [ABI](docs/ABI.md), [RUNTIME](docs/RUNTIME.md) |
|
|
47
|
+
| [`tests/`](tests/) | pytest suite |
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m venv .venv && . .venv/bin/activate && pip install -e ".[dev,data]"
|
|
51
|
+
viobench build-runtime # viobench/solution/build/runtime/dist/runtime.wasm, 10–15 min once
|
|
52
|
+
pytest
|
|
53
|
+
viobench bundle viobench/solution/examples/python/klt_imu -o klt.wasm && viobench check klt.wasm
|
|
54
|
+
viobench evaluate klt.wasm data/val -o report-klt
|
|
55
|
+
viobench leaderboard report-* --plot leaderboard.png
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`viobench --help` lists all commands. Evaluation needs only wasmtime: no Docker, no GPU.
|
viobench-0.0.1/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# viobench: compute-aware relative state estimation
|
|
2
|
+
|
|
3
|
+
Estimate a flying camera's pose relative to its first frame, and its twist, from camera frames and IMU data. A submission is one
|
|
4
|
+
WebAssembly module; the evaluator meters its compute deterministically (Wasmtime fuel) and places it on an
|
|
5
|
+
accuracy-vs-compute curve.
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [`viobench/tools/`](viobench/tools/) | host side: `abi`, `datasets`, `runner`, `server`, `cli` |
|
|
10
|
+
| [`viobench/solution/`](viobench/solution/) | guest side: toolchain, runtimes, examples; what `viobench init` copies into a workspace |
|
|
11
|
+
| [`docs/`](docs/) | [TASK](docs/TASK.md), [STUDENT_GUIDE](docs/STUDENT_GUIDE.md), [DEBUGGING](docs/DEBUGGING.md), [ABI](docs/ABI.md), [RUNTIME](docs/RUNTIME.md) |
|
|
12
|
+
| [`tests/`](tests/) | pytest suite |
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python -m venv .venv && . .venv/bin/activate && pip install -e ".[dev,data]"
|
|
16
|
+
viobench build-runtime # viobench/solution/build/runtime/dist/runtime.wasm, 10–15 min once
|
|
17
|
+
pytest
|
|
18
|
+
viobench bundle viobench/solution/examples/python/klt_imu -o klt.wasm && viobench check klt.wasm
|
|
19
|
+
viobench evaluate klt.wasm data/val -o report-klt
|
|
20
|
+
viobench leaderboard report-* --plot leaderboard.png
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`viobench --help` lists all commands. Evaluation needs only wasmtime: no Docker, no GPU.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "viobench"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Compute-aware relative state estimation challenge: run and score solution .wasm modules, episode datasets, submission server, and the recipes to build a solution"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"wasmtime==50.0.1",
|
|
14
|
+
"numpy>=1.26",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
data = ["opencv-python-headless>=4.9", "pyyaml>=6"]
|
|
19
|
+
sim = ["opencv-python-headless>=4.9", "pyyaml>=6", "hyperdrone==0.1.10"]
|
|
20
|
+
train = ["torch", "onnx"]
|
|
21
|
+
server = ["fastapi", "uvicorn", "python-multipart", "tzdata"] # tzdata: the quota's Pacific-time days where the OS has no zoneinfo
|
|
22
|
+
dev = ["pytest", "opencv-python-headless>=4.9", "pyyaml>=6", "torch", "onnx", "fastapi", "python-multipart", "tzdata", "httpx"] # httpx: the server tests' client
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
viobench = "viobench.tools.cli:main"
|
|
26
|
+
|
|
27
|
+
# viobench.tools is the host side; viobench.solution is the guest side and consists of files, not modules, except
|
|
28
|
+
# workspace.py and the Python runtime's bundler, reached through implicit namespace packages (no __init__.py in solution/).
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["viobench", "viobench.tools*", "viobench.solution", "viobench.solution.runtime", "viobench.solution.runtime.python", "viobench.solution.runtime.python.bundler"]
|
|
31
|
+
namespaces = true
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
"viobench.tools.datasets.generate" = ["*.h.in"]
|
|
35
|
+
"viobench.tools" = ["config.json"]
|
|
36
|
+
"viobench.tools.server" = ["ui/*"]
|
|
37
|
+
# the build recipe and examples that `viobench init` copies into new workspaces
|
|
38
|
+
"viobench.solution" = [
|
|
39
|
+
"toolchain/*", "runtime/native/*", "runtime/python/*", "runtime/python/guest/*", "runtime/python/cpython/*",
|
|
40
|
+
"runtime/python/cpython/patches/*", "runtime/python/cpython/scripts/*", "examples/python/minimal/*", "examples/native/cpp_minimal/*",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
viobench-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import zipfile
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from viobench.tools.abi.config import make_config
|
|
8
|
+
from viobench.tools.datasets.episode import Episode, FrameSequence, interpolate_poses
|
|
9
|
+
from viobench.tools.datasets.storage import (
|
|
10
|
+
episode_file,
|
|
11
|
+
episodes,
|
|
12
|
+
list_episodes,
|
|
13
|
+
read_episode,
|
|
14
|
+
write_episode,
|
|
15
|
+
)
|
|
16
|
+
from viobench.tools.geometry import poses_to_matrices, quaternion_to_matrix
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def synthetic_episode(n_frames=8, rate=20.0, imu_rate=200.0, width=32, height=24, channels=1, lead_in=0.0):
|
|
20
|
+
"""lead_in: seconds of IMU (and ground truth) recorded before the first frame."""
|
|
21
|
+
rng = np.random.default_rng(0)
|
|
22
|
+
config = make_config(width=width, height=height, channels=channels, frame_rate_hz=rate, num_frames=n_frames)
|
|
23
|
+
T_body_camera = np.asarray(config["imu"]["T_imu_cam"]).reshape(4, 4)
|
|
24
|
+
duration = n_frames / rate
|
|
25
|
+
gt_t = np.arange(-lead_in, duration + 1 / imu_rate, 1 / imu_rate)
|
|
26
|
+
# smooth trajectory: circle with yaw
|
|
27
|
+
angle = 0.5 * gt_t
|
|
28
|
+
position = np.column_stack([np.cos(angle), np.sin(angle), 0.1 * gt_t])
|
|
29
|
+
quaternion = np.column_stack([np.zeros_like(angle), np.zeros_like(angle), np.sin(angle / 2), np.cos(angle / 2)])
|
|
30
|
+
ground_truth = np.column_stack([gt_t, position, quaternion])
|
|
31
|
+
imu_t = gt_t[1:]
|
|
32
|
+
imu = np.column_stack([imu_t, rng.normal(0, 0.1, (len(imu_t), 3)) + [0, 0, 9.81], rng.normal(0, 0.01, (len(imu_t), 3))])
|
|
33
|
+
frame_times = (np.arange(n_frames) + 1) / rate
|
|
34
|
+
frames = rng.integers(0, 256, (n_frames, height, width) + (() if channels == 1 else (channels,)), dtype=np.uint8)
|
|
35
|
+
return Episode("synthetic", config, frame_times, frames, imu, ground_truth, T_body_camera, {"condition": "test"})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_episode_windows_and_ground_truth():
|
|
39
|
+
episode = synthetic_episode()
|
|
40
|
+
episode.check_consistency()
|
|
41
|
+
windows = [imu for _, _, imu in episode.steps()]
|
|
42
|
+
assert len(windows) == 8
|
|
43
|
+
assert all(len(w) == 10 for w in windows[1:]) # 200 Hz / 20 Hz
|
|
44
|
+
assert windows[0][-1, 0] <= episode.frame_times[0] and all(w[0, 0] > t_prev for w, t_prev in zip(windows[1:], episode.frame_times[:-1]))
|
|
45
|
+
relative = episode.ground_truth_relative()
|
|
46
|
+
np.testing.assert_allclose(relative[0], [0, 0, 0, 0, 0, 0, 1], atol=1e-12)
|
|
47
|
+
# consistency with a direct computation at frame 3
|
|
48
|
+
T_wc = poses_to_matrices(episode.camera_poses_world())
|
|
49
|
+
expected = np.linalg.inv(T_wc[0]) @ T_wc[3]
|
|
50
|
+
np.testing.assert_allclose(poses_to_matrices(relative)[3], expected, atol=1e-12)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_ground_truth_twist_of_an_offset_camera():
|
|
54
|
+
"""The synthetic body circles at 0.5 rad/s (yaw = angle), climbing at 0.1 m/s; the camera sits off the body origin, rotated."""
|
|
55
|
+
import dataclasses
|
|
56
|
+
|
|
57
|
+
episode = synthetic_episode()
|
|
58
|
+
R_bc = quaternion_to_matrix([0.1, -0.2, 0.3, 0.9])
|
|
59
|
+
T_body_camera = np.eye(4)
|
|
60
|
+
T_body_camera[:3, :3], T_body_camera[:3, 3] = R_bc, [0.05, -0.02, 0.1]
|
|
61
|
+
episode = dataclasses.replace(episode, T_body_camera=T_body_camera)
|
|
62
|
+
angle = 0.5 * episode.frame_times
|
|
63
|
+
velocity_world = np.column_stack([-0.5 * np.sin(angle), 0.5 * np.cos(angle), np.full_like(angle, 0.1)])
|
|
64
|
+
R_wb = quaternion_to_matrix(np.column_stack([np.zeros_like(angle), np.zeros_like(angle), np.sin(angle / 2), np.cos(angle / 2)]))
|
|
65
|
+
omega_body = np.array([0.0, 0.0, 0.5])
|
|
66
|
+
velocity_body = np.einsum("nji,nj->ni", R_wb, velocity_world) + np.cross(omega_body, T_body_camera[:3, 3]) # camera centre: v + w x r
|
|
67
|
+
expected = np.column_stack([velocity_body @ R_bc, np.tile(R_bc.T @ omega_body, (len(angle), 1))])
|
|
68
|
+
twist = episode.ground_truth_twist()
|
|
69
|
+
np.testing.assert_allclose(twist[:-1], expected[:-1], atol=1e-5)
|
|
70
|
+
np.testing.assert_allclose(twist[-1], expected[-1], atol=2e-3) # the ground truth ends at the last frame: one-sided there
|
|
71
|
+
state = episode.ground_truth_state()
|
|
72
|
+
assert state.shape == (8, 13)
|
|
73
|
+
np.testing.assert_array_equal(state[:, :7], episode.ground_truth_relative())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_first_imu_window_is_one_frame_interval():
|
|
77
|
+
episode = synthetic_episode(lead_in=1.5) # a recording may hold IMU from long before its first frame, simulated episodes barely
|
|
78
|
+
first = episode.imu_window(0)
|
|
79
|
+
assert len(first) == 10 and first[0, 0] > episode.frame_times[0] - episode.frame_interval
|
|
80
|
+
np.testing.assert_array_equal(episode.delivered_imu(), np.concatenate([imu for _, _, imu in episode.steps()]))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_interpolation_matches_samples():
|
|
84
|
+
times = np.array([0.0, 1.0, 2.0])
|
|
85
|
+
poses = np.array([[0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, np.sin(0.5), np.cos(0.5)], [2, 0, 0, 0, 0, np.sin(1.0), np.cos(1.0)]])
|
|
86
|
+
mid = interpolate_poses(times, poses, [0.5])[0]
|
|
87
|
+
assert mid[0] == pytest.approx(0.5)
|
|
88
|
+
np.testing.assert_allclose(quaternion_to_matrix(mid[3:]), quaternion_to_matrix([0, 0, np.sin(0.25), np.cos(0.25)]), atol=1e-12)
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
interpolate_poses(times, poses, [2.5])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _pngs(path):
|
|
94
|
+
"""The PNG members of an episode archive, in archive order (frame order)."""
|
|
95
|
+
with zipfile.ZipFile(path) as archive:
|
|
96
|
+
return [archive.read(name) for name in archive.namelist() if name.endswith(".png")]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_write_read_roundtrip(tmp_path):
|
|
100
|
+
episode = synthetic_episode()
|
|
101
|
+
path = write_episode(episode, tmp_path / "ep0.zip", origin_ns=1_400_000_000_000_000_000)
|
|
102
|
+
assert list_episodes(tmp_path) == [path] and list_episodes(path) == [path]
|
|
103
|
+
loaded = read_episode(path)
|
|
104
|
+
loaded.check_consistency()
|
|
105
|
+
np.testing.assert_array_equal(loaded.frames, episode.frames)
|
|
106
|
+
np.testing.assert_allclose(loaded.frame_times, episode.frame_times, atol=1e-9)
|
|
107
|
+
np.testing.assert_allclose(loaded.imu, episode.imu, rtol=1e-6, atol=1e-9)
|
|
108
|
+
np.testing.assert_allclose(loaded.ground_truth, episode.ground_truth, rtol=1e-6, atol=1e-9)
|
|
109
|
+
np.testing.assert_allclose(loaded.T_body_camera, episode.T_body_camera)
|
|
110
|
+
assert loaded.config == episode.config and loaded.meta["condition"] == "test" and loaded.meta["dataset"] == tmp_path.name
|
|
111
|
+
assert loaded.meta["origin_ns"] == 1_400_000_000_000_000_000 and loaded.meta["path"] == str(path)
|
|
112
|
+
with zipfile.ZipFile(path) as archive:
|
|
113
|
+
names = archive.namelist()
|
|
114
|
+
assert names[0] == "ep0/episode.json" and "ep0/mav0/cam0/sensor.yaml" in names and "ep0/mav0/imu0/sensor.yaml" in names
|
|
115
|
+
assert all(info.compress_type == zipfile.ZIP_STORED for info in archive.infolist())
|
|
116
|
+
# the same episode is the same archive, byte for byte: the sets are content-addressed
|
|
117
|
+
assert write_episode(episode, tmp_path / "again" / "ep0.zip", origin_ns=1_400_000_000_000_000_000).read_bytes() == path.read_bytes()
|
|
118
|
+
assert write_episode(loaded, tmp_path / "resaved" / "ep0.zip").read_bytes() == path.read_bytes() # keeps the origin, copies the PNGs
|
|
119
|
+
with pytest.raises(ValueError):
|
|
120
|
+
write_episode(episode, tmp_path / "ep0")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_unzipped_episode_reads_the_same(tmp_path):
|
|
124
|
+
episode = synthetic_episode(n_frames=4, width=16, height=12, channels=3)
|
|
125
|
+
path = write_episode(episode, tmp_path / "set" / "ep.zip", attachments={"trajectory.mp4": b"not really a video"})
|
|
126
|
+
assert episode_file(path, "trajectory.mp4") == b"not really a video" and episode_file(path, "notes.txt") is None
|
|
127
|
+
with zipfile.ZipFile(path) as archive:
|
|
128
|
+
archive.extractall(tmp_path / "unzipped")
|
|
129
|
+
directory = tmp_path / "unzipped" / "ep"
|
|
130
|
+
assert list_episodes(tmp_path / "unzipped") == [directory] and (directory / "mav0" / "cam0" / "data.csv").is_file()
|
|
131
|
+
unzipped = read_episode(directory)
|
|
132
|
+
np.testing.assert_array_equal(np.asarray(unzipped.frames), episode.frames)
|
|
133
|
+
assert unzipped.meta["dataset"] == "unzipped" and episode_file(directory, "trajectory.mp4") == b"not really a video"
|
|
134
|
+
assert [e.meta["dataset"] for e in episodes([tmp_path / "set", directory])] == ["set", "unzipped"]
|
|
135
|
+
repacked = write_episode(unzipped, tmp_path / "repacked" / "ep.zip", attachments={"trajectory.mp4": directory / "trajectory.mp4"})
|
|
136
|
+
assert repacked.read_bytes() == path.read_bytes()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize("channels", [1, 3])
|
|
140
|
+
def test_write_episode_copies_existing_pngs(tmp_path, channels):
|
|
141
|
+
episode = synthetic_episode(n_frames=4, width=16, height=12, channels=channels)
|
|
142
|
+
source = read_episode(write_episode(episode, tmp_path / "src.zip"))
|
|
143
|
+
assert isinstance(source.frames, FrameSequence) and source.frames.encoded is not None
|
|
144
|
+
copy = write_episode(source, tmp_path / "dst.zip")
|
|
145
|
+
assert _pngs(copy) == _pngs(tmp_path / "src.zip")
|
|
146
|
+
np.testing.assert_array_equal(np.asarray(read_episode(copy).frames), episode.frames)
|
|
147
|
+
modified = read_episode(copy, frames="memory")
|
|
148
|
+
modified.frames[:] = 0
|
|
149
|
+
write_episode(modified, copy)
|
|
150
|
+
np.testing.assert_array_equal(np.asarray(source.frames), episode.frames)
|
|
151
|
+
assert not np.asarray(read_episode(copy).frames).any()
|
|
152
|
+
write_episode(source, copy)
|
|
153
|
+
np.testing.assert_array_equal(np.asarray(read_episode(copy).frames), episode.frames)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize("origin_ns", [0, 1_400_000_000_000_000_000])
|
|
157
|
+
def test_write_episode_back_to_its_own_archive(tmp_path, origin_ns):
|
|
158
|
+
episode = synthetic_episode(n_frames=4, width=16, height=12)
|
|
159
|
+
path = write_episode(episode, tmp_path / "episode.zip", origin_ns=origin_ns)
|
|
160
|
+
original_pngs = _pngs(path)
|
|
161
|
+
loaded = read_episode(path) # streams its frames from the archive that is about to be replaced
|
|
162
|
+
loaded.meta["condition"] = "corrected"
|
|
163
|
+
write_episode(loaded, path)
|
|
164
|
+
assert _pngs(path) == original_pngs
|
|
165
|
+
saved = read_episode(path)
|
|
166
|
+
assert saved.meta["condition"] == "corrected" and saved.meta["origin_ns"] == origin_ns
|
|
167
|
+
np.testing.assert_array_equal(np.asarray(saved.frames), episode.frames)
|
|
168
|
+
np.testing.assert_array_equal(np.asarray(loaded.frames), episode.frames) # the open reader still sees the archive it opened
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_write_episode_exports_spool_and_survives_cleanup(tmp_path):
|
|
172
|
+
from viobench.tools.datasets.generate.simulation import Spool, spooled_frames
|
|
173
|
+
from viobench.tools.datasets.storage import encode_png
|
|
174
|
+
|
|
175
|
+
episode = synthetic_episode(n_frames=4, width=16, height=12, channels=3) # the simulator spools RGB
|
|
176
|
+
expected = episode.frames.copy()
|
|
177
|
+
spool = Spool(tmp_path / "spool")
|
|
178
|
+
spool_path = spool.path
|
|
179
|
+
paths = [spool_path / f"{index}.png" for index in range(len(expected))]
|
|
180
|
+
for png, frame in zip(paths, expected):
|
|
181
|
+
png.write_bytes(encode_png(frame))
|
|
182
|
+
spooled = [png.read_bytes() for png in paths]
|
|
183
|
+
episode.frames = spooled_frames(paths, expected.shape[1:], spool)
|
|
184
|
+
mapped = episode.frames.map(np.copy)
|
|
185
|
+
assert mapped.encoded is None
|
|
186
|
+
del mapped, spool
|
|
187
|
+
destination = write_episode(episode, tmp_path / "dst.zip")
|
|
188
|
+
assert _pngs(destination) == spooled # copied byte for byte, not re-encoded
|
|
189
|
+
write_episode(episode, destination) # exporting again replaces the archive
|
|
190
|
+
np.testing.assert_array_equal(np.asarray(episode.frames), expected)
|
|
191
|
+
del episode
|
|
192
|
+
gc.collect()
|
|
193
|
+
assert not spool_path.exists()
|
|
194
|
+
np.testing.assert_array_equal(np.asarray(read_episode(destination).frames), expected)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.parametrize("frames", ["lazy", "memory"])
|
|
198
|
+
def test_failed_frame_write_preserves_destination(tmp_path, monkeypatch, frames):
|
|
199
|
+
from viobench.tools.datasets import storage
|
|
200
|
+
|
|
201
|
+
episode = synthetic_episode(n_frames=4, width=16, height=12)
|
|
202
|
+
source = read_episode(write_episode(episode, tmp_path / "src.zip"), frames=frames)
|
|
203
|
+
episode.frames[:] = 0
|
|
204
|
+
destination = write_episode(episode, tmp_path / "dst.zip")
|
|
205
|
+
before = destination.read_bytes()
|
|
206
|
+
|
|
207
|
+
def fail(*args):
|
|
208
|
+
raise OSError("frame unavailable")
|
|
209
|
+
|
|
210
|
+
if frames == "lazy":
|
|
211
|
+
source.frames.encoded = fail # the stored PNG cannot be read
|
|
212
|
+
else:
|
|
213
|
+
monkeypatch.setattr(storage, "encode_png", fail) # the frame cannot be encoded
|
|
214
|
+
with pytest.raises(OSError, match="frame unavailable"):
|
|
215
|
+
write_episode(source, destination)
|
|
216
|
+
assert destination.read_bytes() == before
|
|
217
|
+
assert sorted(path.name for path in tmp_path.iterdir()) == ["dst.zip", "src.zip"] # no partial archive left behind
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_simulation_spool_lives_as_long_as_its_frames(tmp_path):
|
|
221
|
+
from viobench.tools.datasets.generate.simulation import Spool, spooled_frames
|
|
222
|
+
|
|
223
|
+
spool = Spool(tmp_path / "spool")
|
|
224
|
+
path = spool.path
|
|
225
|
+
assert path.is_dir() and path.parent == tmp_path / "spool"
|
|
226
|
+
frames = spooled_frames([], (2, 2, 3), spool)
|
|
227
|
+
del spool
|
|
228
|
+
gc.collect()
|
|
229
|
+
assert path.is_dir(), "the frame sequence keeps the spool alive"
|
|
230
|
+
del frames
|
|
231
|
+
gc.collect()
|
|
232
|
+
assert not path.exists(), "the last reference removes the directory"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_sim_config_scales_with_duration(tmp_path, monkeypatch):
|
|
236
|
+
from viobench.tools.datasets.generate.simulation import SimConfig
|
|
237
|
+
|
|
238
|
+
monkeypatch.setenv("VIOBENCH_CACHE", str(tmp_path))
|
|
239
|
+
minute = SimConfig()
|
|
240
|
+
assert minute.duration_seconds == 60.0 and minute.step_count(200.0) == 12000 and minute.num_waypoints == 48
|
|
241
|
+
assert SimConfig(duration_seconds=10).num_waypoints == 8 and SimConfig(duration_seconds=10, waypoints=5).num_waypoints == 5
|
|
242
|
+
header = minute.render_header(200.0).read_text()
|
|
243
|
+
assert "NUM_WAYPOINTS = 48;" in header and "INITIALIZATION_HOLD_STEPS = 200;" in header and "@" not in header
|
|
244
|
+
assert SimConfig(duration_seconds=30).render_header(200.0) != minute.render_header(200.0) # a different route length is a different JIT key
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_simulate_runs_scene_by_scene_and_names_episodes_by_order(tmp_path, monkeypatch):
|
|
249
|
+
"""Rounds of sim.instances parallel flights per scene: crashed flights are dropped, the surplus of the last round is
|
|
250
|
+
discarded, survivors are named {name}-{scene}-{index:04d} by order within the scene, and a scene whose flights
|
|
251
|
+
keep crashing is given up with an error."""
|
|
252
|
+
import sys
|
|
253
|
+
import types
|
|
254
|
+
|
|
255
|
+
from viobench.tools.datasets.generate import simulation
|
|
256
|
+
|
|
257
|
+
monkeypatch.setenv("VIOBENCH_CACHE", str(tmp_path))
|
|
258
|
+
environments = []
|
|
259
|
+
|
|
260
|
+
class FakeHyperDrone:
|
|
261
|
+
def __init__(self, references, config=None, seed=0, parameters=None):
|
|
262
|
+
self.reference, self.seed, self.dt, self.closed = references[0], seed, 0.005, False
|
|
263
|
+
environments.append(self)
|
|
264
|
+
|
|
265
|
+
def close(self):
|
|
266
|
+
self.closed = True
|
|
267
|
+
|
|
268
|
+
monkeypatch.setitem(sys.modules, "hyperdrone", types.SimpleNamespace(HyperDrone=FakeHyperDrone, Config=lambda **kwargs: kwargs))
|
|
269
|
+
crashes = {("B", 7, 0)} # (scene, seed, instance) flights that crash; every flight in scene C crashes
|
|
270
|
+
rounds = []
|
|
271
|
+
|
|
272
|
+
def fake_round(env, sim, seed, scene_name, camera, name, log, progress=(0, None)):
|
|
273
|
+
rounds.append((scene_name, seed, progress))
|
|
274
|
+
for instance in range(sim.instances):
|
|
275
|
+
if (scene_name, seed, instance) in crashes or scene_name == "C":
|
|
276
|
+
continue
|
|
277
|
+
episode = synthetic_episode(n_frames=2)
|
|
278
|
+
episode.name = episode.config["episode"]["name"] = f"{name}-{scene_name}-{seed:05d}-{instance:02d}" # the provisional name
|
|
279
|
+
episode.meta.update(scene=scene_name, seed=seed, instance=instance)
|
|
280
|
+
yield episode
|
|
281
|
+
|
|
282
|
+
monkeypatch.setattr(simulation, "_run_batch", fake_round)
|
|
283
|
+
sim = simulation.SimConfig(instances=2)
|
|
284
|
+
|
|
285
|
+
episodes = list(simulation.simulate(sim, {"A": "ref-a", "B": "ref-b"}, 3, seed=5, name="sim"))
|
|
286
|
+
assert [episode.name for episode in episodes] == ["sim-A-0000", "sim-A-0001", "sim-A-0002", "sim-B-0000", "sim-B-0001", "sim-B-0002"]
|
|
287
|
+
assert all(episode.config["episode"]["name"] == episode.name for episode in episodes)
|
|
288
|
+
# A's second round has one surplus flight, B's first round one crashed flight: neither is an episode
|
|
289
|
+
assert [(episode.meta["seed"], episode.meta["instance"]) for episode in episodes] == [(5, 0), (5, 1), (6, 0), (7, 1), (8, 0), (8, 1)]
|
|
290
|
+
assert rounds == [("A", 5, (0, 3)), ("A", 6, (2, 3)), ("B", 7, (0, 3)), ("B", 8, (1, 3))]
|
|
291
|
+
assert [(env.reference, env.seed) for env in environments] == [("ref-a", 5), ("ref-b", 7)]
|
|
292
|
+
assert all(env.closed for env in environments)
|
|
293
|
+
|
|
294
|
+
generator = simulation.simulate(sim, {"A": "ref-a", "C": "ref-c"}, 1, seed=0)
|
|
295
|
+
assert next(generator).name == "sim-A-0000"
|
|
296
|
+
with pytest.raises(RuntimeError, match=r"C: 0 of 1 episodes after 4 flights \(4 crashed\)"):
|
|
297
|
+
next(generator)
|
|
298
|
+
assert environments[-1].reference == "ref-c" and environments[-1].closed
|
|
299
|
+
with pytest.raises(ValueError):
|
|
300
|
+
list(simulation.simulate(sim, {"A": "ref-a"}, 0))
|
|
301
|
+
|
|
302
|
+
def test_adapt_always_delivers_rgb():
|
|
303
|
+
episode = synthetic_episode(n_frames=3, width=320, height=240) # a grayscale-only set
|
|
304
|
+
config, frames = episode.adapt()
|
|
305
|
+
assert frames.shape == (3, 240, 320, 3)
|
|
306
|
+
for channel in range(3):
|
|
307
|
+
np.testing.assert_array_equal(frames[..., channel], episode.frames) # replicated, and nothing announces it
|
|
308
|
+
assert set(config) == {"width", "height", "fx", "fy", "cx", "cy", "T_cam_imu"}
|
|
309
|
+
np.testing.assert_allclose(np.asarray(config["T_cam_imu"]) @ episode.T_body_camera, np.eye(4), atol=1e-12)
|
|
310
|
+
rgb = synthetic_episode(n_frames=3, width=320, height=240, channels=3)
|
|
311
|
+
assert rgb.adapt()[1] is rgb.frames # RGB at the stored resolution passes through untouched
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_adapt_rescales_frames_and_intrinsics():
|
|
315
|
+
episode = synthetic_episode(n_frames=2, width=640, height=480)
|
|
316
|
+
config, frames = episode.adapt(resolution=(320, 240))
|
|
317
|
+
assert frames.shape == (2, 240, 320, 3) and (config["width"], config["height"]) == (320, 240)
|
|
318
|
+
assert config["fx"] == pytest.approx(episode.config["camera"]["fx"] / 2)
|
|
319
|
+
assert config["cx"] == pytest.approx((episode.config["camera"]["cx"] + 0.5) / 2 - 0.5)
|
|
320
|
+
# a frame-centre ray keeps its direction: the principal point stays at the image centre
|
|
321
|
+
assert config["cx"] == pytest.approx(320 / 2 - 0.5) and config["cy"] == pytest.approx(240 / 2 - 0.5)
|
|
322
|
+
small = synthetic_episode(n_frames=2, width=320, height=240)
|
|
323
|
+
config, frames = small.adapt(resolution=(640, 480))
|
|
324
|
+
assert frames.shape == (2, 480, 640, 3) and (config["width"], config["height"]) == (640, 480)
|
|
325
|
+
assert config["fx"] == pytest.approx(small.config["camera"]["fx"] * 2)
|
|
326
|
+
assert config["cx"] == pytest.approx((small.config["camera"]["cx"] + 0.5) * 2 - 0.5)
|
|
327
|
+
with pytest.raises(ValueError, match="resolution must be one of"):
|
|
328
|
+
episode.adapt(resolution=(64, 48))
|
|
329
|
+
with pytest.raises(ValueError, match="sets must be stored at"):
|
|
330
|
+
synthetic_episode(n_frames=2, width=64, height=48).adapt(resolution=(320, 240))
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The out-of-sandbox debugging facilities: `viobench debug` (Python, in-process), `viobench feed` + the native host
|
|
2
|
+
`debug` executable, `viobench score`."""
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from viobench.tools.cli import main as cli
|
|
12
|
+
from viobench.tools.datasets.feed import write_feed
|
|
13
|
+
from viobench.tools.datasets.storage import write_episode
|
|
14
|
+
from viobench.tools.runner import direct
|
|
15
|
+
from viobench.tools.runner.score import load_estimates, score
|
|
16
|
+
from tests.test_datasets import synthetic_episode
|
|
17
|
+
|
|
18
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
19
|
+
EXAMPLES = REPO / "viobench" / "solution" / "examples"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_direct_run_matches_the_sandbox(engine, runtime_path, tmp_path):
|
|
23
|
+
"""dead_reckoning on the host and inside the runtime give the same poses (same code, same inputs, host numpy)."""
|
|
24
|
+
from viobench.solution.runtime.python.bundler.bundle import bundle
|
|
25
|
+
from viobench.tools.runner.run import evaluate
|
|
26
|
+
|
|
27
|
+
episode = synthetic_episode(n_frames=30, width=320, height=240) # 1.5 s: dead reckoning calibrates on the first 0.5 s (identity), then estimates
|
|
28
|
+
dataset = tmp_path / "set"
|
|
29
|
+
write_episode(episode, dataset / f"{episode.name}.zip")
|
|
30
|
+
solution = direct.PythonSolution(EXAMPLES / "python" / "dead_reckoning")
|
|
31
|
+
assert solution.resolution == (320, 240)
|
|
32
|
+
[(_, estimates, metrics)] = direct.run(solution, [dataset], output_dir=tmp_path / "direct", log=lambda *a: None)
|
|
33
|
+
assert estimates.shape == (30, 13) and metrics.held == 0
|
|
34
|
+
np.testing.assert_array_equal(estimates[0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0])
|
|
35
|
+
assert np.abs(estimates[-1, 7:]).max() > 0 # dead reckoning reports its twist once it has calibrated
|
|
36
|
+
written = load_estimates(tmp_path / "direct" / "set" / episode.name / "estimate.tum", episode.frame_times)
|
|
37
|
+
np.testing.assert_allclose(written, estimates, atol=1e-8)
|
|
38
|
+
|
|
39
|
+
wasm = tmp_path / "dr.wasm"
|
|
40
|
+
bundle(EXAMPLES / "python" / "dead_reckoning", wasm, runtime=runtime_path)
|
|
41
|
+
evaluate(wasm, [dataset], output_dir=tmp_path / "report", engine=engine, log=lambda *a: None)
|
|
42
|
+
sandboxed = load_estimates(tmp_path / "report" / "logs" / "set" / episode.name / "estimate.tum", episode.frame_times)
|
|
43
|
+
np.testing.assert_allclose(estimates, sandboxed, atol=1e-6)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_direct_frame_width_and_host_solution_bundle(solution_dir, tmp_path):
|
|
47
|
+
"""frame_width picks the resolution; solution.py may re-export a class from elsewhere; data files next to the code come through `solution_bundle`."""
|
|
48
|
+
directory = solution_dir(
|
|
49
|
+
"from estimator import Est as Solution\n",
|
|
50
|
+
{
|
|
51
|
+
"estimator.py": (
|
|
52
|
+
"import numpy as np\nimport solution_bundle as bundle\n"
|
|
53
|
+
"class Est:\n"
|
|
54
|
+
" frame_width = 640\n"
|
|
55
|
+
" def __init__(self, config):\n"
|
|
56
|
+
" assert config['width'] == 640 and 'channels' not in config\n"
|
|
57
|
+
" self.offset = np.load(bundle.open('data/offset.npy'))\n"
|
|
58
|
+
" assert bundle.exists('data/offset.npy') and bundle.listdir('data') == ['offset.npy'] and bundle.read_text('note.txt') == 'hi'\n"
|
|
59
|
+
" def step(self, t, frame, imu):\n"
|
|
60
|
+
" assert frame.shape == (480, 640, 3)\n"
|
|
61
|
+
" return np.concatenate([self.offset, [0, 0, 0, 1], np.zeros(6)])\n"
|
|
62
|
+
),
|
|
63
|
+
"note.txt": "hi",
|
|
64
|
+
},
|
|
65
|
+
name="vga",
|
|
66
|
+
)
|
|
67
|
+
import io
|
|
68
|
+
|
|
69
|
+
buffer = io.BytesIO()
|
|
70
|
+
np.save(buffer, np.array([1.0, 2.0, 3.0]))
|
|
71
|
+
(directory / "data").mkdir()
|
|
72
|
+
(directory / "data" / "offset.npy").write_bytes(buffer.getvalue())
|
|
73
|
+
solution = direct.PythonSolution(directory)
|
|
74
|
+
assert solution.resolution == (640, 480)
|
|
75
|
+
episode = synthetic_episode(n_frames=3, width=640, height=480) # grayscale: delivered as RGB like the sandbox does
|
|
76
|
+
steps = list(direct.trace(solution, episode))
|
|
77
|
+
assert len(steps) == 3 and steps[0].frame.shape == (480, 640, 3)
|
|
78
|
+
np.testing.assert_allclose(steps[-1].pose[:3], [1.0, 2.0, 3.0])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_direct_cli(tmp_path, capsys):
|
|
82
|
+
episode = synthetic_episode(n_frames=4, width=320, height=240)
|
|
83
|
+
write_episode(episode, tmp_path / "set" / f"{episode.name}.zip")
|
|
84
|
+
assert cli(["debug", str(EXAMPLES / "python" / "minimal"), str(tmp_path / "set"), "-o", str(tmp_path / "out")]) == 0
|
|
85
|
+
assert "ATE" in capsys.readouterr().out
|
|
86
|
+
assert (tmp_path / "out" / "set" / episode.name / "estimate.tum").is_file()
|
|
87
|
+
assert (tmp_path / "out" / "set" / episode.name / "twist.txt").is_file()
|
|
88
|
+
from tests.test_video import read_video
|
|
89
|
+
|
|
90
|
+
fps, frames = read_video(tmp_path / "out" / "set" / episode.name / "trajectory.mp4")
|
|
91
|
+
assert fps == pytest.approx(20.0) and len(frames) == episode.num_frames
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_feed_and_score(tmp_path, capsys):
|
|
95
|
+
episode = synthetic_episode(n_frames=6, width=640, height=480, lead_in=1.0)
|
|
96
|
+
write_episode(episode, tmp_path / "set" / f"{episode.name}.zip")
|
|
97
|
+
feed = write_feed(episode, tmp_path / "feed", resolution=(320, 240))
|
|
98
|
+
assert (feed / "shape.txt").read_text().split() == ["6", "240", "320", "3", "60"] # IMU: six windows of one frame interval
|
|
99
|
+
frames = np.fromfile(feed / "frames.u8", dtype=np.uint8).reshape(6, 240, 320, 3)
|
|
100
|
+
assert frames.std() > 0
|
|
101
|
+
config = json.loads((feed / "config.json").read_text())
|
|
102
|
+
assert config["width"] == 320 and "channels" not in config
|
|
103
|
+
imu = np.fromfile(feed / "imu.f64").reshape(-1, 7)
|
|
104
|
+
np.testing.assert_allclose(imu, episode.delivered_imu())
|
|
105
|
+
gt = np.fromfile(feed / "gt.f64").reshape(6, 7)
|
|
106
|
+
np.testing.assert_allclose(gt, episode.ground_truth_relative())
|
|
107
|
+
# a perfect estimate scores zero; a frame missing from the files repeats the previous pose (identity for the first)
|
|
108
|
+
tum = tmp_path / "perfect.tum"
|
|
109
|
+
np.savetxt(tum, np.column_stack([episode.frame_times, gt]), fmt="%.12g")
|
|
110
|
+
np.savetxt(tmp_path / "twist.txt", np.column_stack([episode.frame_times, episode.ground_truth_twist()]), fmt="%.12g")
|
|
111
|
+
metrics = score(tmp_path / "set" / f"{episode.name}.zip", tum)
|
|
112
|
+
assert metrics.ate_rmse < 1e-9 and metrics.velocity_rmse < 1e-6 and metrics.held == 0
|
|
113
|
+
assert "ATE 0.000" in capsys.readouterr().out
|
|
114
|
+
rows = np.column_stack([episode.frame_times, gt])[1:] # first frame missing
|
|
115
|
+
np.savetxt(tum, rows, fmt="%.12g")
|
|
116
|
+
assert np.isnan(load_estimates(tum, episode.frame_times)[0, :7]).all()
|
|
117
|
+
assert score(tmp_path / "set" / f"{episode.name}.zip", tum, log=lambda *a: None).held == 1
|
|
118
|
+
assert cli(["score", str(tmp_path / "set" / f"{episode.name}.zip"), str(tum)]) == 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.skipif(shutil.which("cmake") is None or (shutil.which("cc") is None and shutil.which("gcc") is None and shutil.which("clang") is None),
|
|
122
|
+
reason="needs cmake and a host C/C++ compiler")
|
|
123
|
+
def test_native_host_debug_build(tmp_path):
|
|
124
|
+
"""cpp_minimal configured for the host builds `debug`, which replays a feed and writes identity poses."""
|
|
125
|
+
source = EXAMPLES / "native" / "cpp_minimal"
|
|
126
|
+
build = tmp_path / "build-host"
|
|
127
|
+
configure = subprocess.run(["cmake", "-S", str(source), "-B", str(build)], capture_output=True, text=True)
|
|
128
|
+
assert configure.returncode == 0, configure.stderr
|
|
129
|
+
assert "debug" in configure.stdout
|
|
130
|
+
compile_ = subprocess.run(["cmake", "--build", str(build)], capture_output=True, text=True)
|
|
131
|
+
assert compile_.returncode == 0, compile_.stderr
|
|
132
|
+
episode = synthetic_episode(n_frames=5, width=320, height=240)
|
|
133
|
+
write_episode(episode, tmp_path / "set" / f"{episode.name}.zip")
|
|
134
|
+
feed = write_feed(episode, tmp_path / "feed")
|
|
135
|
+
tum = tmp_path / "estimate.tum"
|
|
136
|
+
run = subprocess.run([str(build / "debug"), str(feed), str(tum)], capture_output=True, text=True)
|
|
137
|
+
assert run.returncode == 0, run.stderr
|
|
138
|
+
assert "5/5 frames ok" in run.stdout
|
|
139
|
+
estimates = load_estimates(tum, episode.frame_times)
|
|
140
|
+
np.testing.assert_allclose(estimates, np.tile([0, 0, 0, 0, 0, 0, 1.0, 0, 0, 0, 0, 0, 0], (5, 1)))
|
|
141
|
+
metrics = score(tmp_path / "set" / f"{episode.name}.zip", tum, log=lambda *a: None)
|
|
142
|
+
assert metrics.held == 0
|