x4d-devkit 0.14.2__tar.gz → 0.14.3__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.
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/PKG-INFO +29 -1
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/README.md +28 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/pyproject.toml +1 -1
- x4d_devkit-0.14.3/tests/test_cli_auth.py +62 -0
- x4d_devkit-0.14.3/tests/test_client_projects.py +42 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_coordinate_frames.py +41 -41
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_loader.py +8 -7
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_nuscenes_converter.py +24 -23
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_transform_chain.py +7 -7
- x4d_devkit-0.14.3/x4d_devkit/cli.py +280 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/client.py +16 -1
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/projects.py +23 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/converters/nuscenes.py +3 -2
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/PKG-INFO +29 -1
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/SOURCES.txt +2 -0
- x4d_devkit-0.14.2/x4d_devkit/cli.py +0 -131
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/LICENSE +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/setup.cfg +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_box.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_fusion.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_matching.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_models.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_token.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_transform.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v06_api_additions.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v06_schema.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v10_schema.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_validation.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/fusion.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: x4d-devkit
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.3
|
|
4
4
|
Summary: X-4D dataset format SDK — load, validate, evaluate, and convert autonomous driving datasets
|
|
5
5
|
Author: windzu
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -50,6 +50,34 @@ pip install x4d-devkit[client]
|
|
|
50
50
|
|
|
51
51
|
## Quick Start
|
|
52
52
|
|
|
53
|
+
### Connect to an X-4D platform
|
|
54
|
+
|
|
55
|
+
The CLI can authenticate with a bearer token passed explicitly, through
|
|
56
|
+
environment variables, or from the user config written by `x4d login`.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
x4d --base-url http://host:8000 --api-key <token> projects list
|
|
60
|
+
x4d --base-url http://host:8000 --api-key <token> clips list --project-id 1
|
|
61
|
+
x4d --base-url http://host:8000 --api-key <token> clips download --clip-ids clip_a,clip_b --output /data/x4d/clips
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export X4D_BASE_URL=http://host:8000
|
|
66
|
+
export X4D_API_KEY=<token>
|
|
67
|
+
x4d projects list
|
|
68
|
+
x4d clips list --project-name nuscenes-mini --has-archive true --format json
|
|
69
|
+
x4d clips download --project-name nuscenes-mini --output /data/x4d/nuscenes-mini/clips --workers 8
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
x4d --base-url http://host:8000 login --username <user> --password <password>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`login` stores `base_url` and the returned access token in
|
|
77
|
+
`~/.config/x4d/config.toml`. Archive downloads go through the platform file
|
|
78
|
+
proxy and extract standard X4D clip directories; training containers do not
|
|
79
|
+
need database or MinIO access.
|
|
80
|
+
|
|
53
81
|
### Load a clip
|
|
54
82
|
|
|
55
83
|
```python
|
|
@@ -20,6 +20,34 @@ pip install x4d-devkit[client]
|
|
|
20
20
|
|
|
21
21
|
## Quick Start
|
|
22
22
|
|
|
23
|
+
### Connect to an X-4D platform
|
|
24
|
+
|
|
25
|
+
The CLI can authenticate with a bearer token passed explicitly, through
|
|
26
|
+
environment variables, or from the user config written by `x4d login`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
x4d --base-url http://host:8000 --api-key <token> projects list
|
|
30
|
+
x4d --base-url http://host:8000 --api-key <token> clips list --project-id 1
|
|
31
|
+
x4d --base-url http://host:8000 --api-key <token> clips download --clip-ids clip_a,clip_b --output /data/x4d/clips
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export X4D_BASE_URL=http://host:8000
|
|
36
|
+
export X4D_API_KEY=<token>
|
|
37
|
+
x4d projects list
|
|
38
|
+
x4d clips list --project-name nuscenes-mini --has-archive true --format json
|
|
39
|
+
x4d clips download --project-name nuscenes-mini --output /data/x4d/nuscenes-mini/clips --workers 8
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
x4d --base-url http://host:8000 login --username <user> --password <password>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`login` stores `base_url` and the returned access token in
|
|
47
|
+
`~/.config/x4d/config.toml`. Archive downloads go through the platform file
|
|
48
|
+
proxy and extract standard X4D clip directories; training containers do not
|
|
49
|
+
need database or MinIO access.
|
|
50
|
+
|
|
23
51
|
### Load a clip
|
|
24
52
|
|
|
25
53
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.14.
|
|
7
|
+
version = "0.14.3"
|
|
8
8
|
description = "X-4D dataset format SDK — load, validate, evaluate, and convert autonomous driving datasets"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from x4d_devkit import cli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_config_round_trip(tmp_path):
|
|
9
|
+
path = tmp_path / "config.toml"
|
|
10
|
+
cli.save_config(
|
|
11
|
+
{"base_url": "http://host:8000", "api_key": 'tok"en\\value'},
|
|
12
|
+
path=path,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
assert cli.load_config(path) == {
|
|
16
|
+
"base_url": "http://host:8000",
|
|
17
|
+
"api_key": 'tok"en\\value',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_resolve_prefers_explicit_over_env_and_config(tmp_path, monkeypatch):
|
|
22
|
+
path = tmp_path / "config.toml"
|
|
23
|
+
cli.save_config({"base_url": "http://config", "api_key": "config-token"}, path)
|
|
24
|
+
monkeypatch.setenv(cli.CONFIG_ENV, str(path))
|
|
25
|
+
monkeypatch.setenv(cli.BASE_URL_ENV, "http://env")
|
|
26
|
+
monkeypatch.setenv(cli.API_KEY_ENV, "env-token")
|
|
27
|
+
|
|
28
|
+
args = argparse.Namespace(base_url="http://explicit", api_key="explicit-token", token=None)
|
|
29
|
+
|
|
30
|
+
assert cli.resolve_base_url(args) == "http://explicit"
|
|
31
|
+
assert cli.resolve_api_key(args) == "explicit-token"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_resolve_prefers_env_over_config(tmp_path, monkeypatch):
|
|
35
|
+
path = tmp_path / "config.toml"
|
|
36
|
+
cli.save_config({"base_url": "http://config", "api_key": "config-token"}, path)
|
|
37
|
+
monkeypatch.setenv(cli.CONFIG_ENV, str(path))
|
|
38
|
+
monkeypatch.setenv(cli.BASE_URL_ENV, "http://env")
|
|
39
|
+
monkeypatch.setenv(cli.API_KEY_ENV, "env-token")
|
|
40
|
+
|
|
41
|
+
args = argparse.Namespace(base_url=None, api_key=None, token=None)
|
|
42
|
+
|
|
43
|
+
assert cli.resolve_base_url(args) == "http://env"
|
|
44
|
+
assert cli.resolve_api_key(args) == "env-token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_resolve_reads_config_when_env_absent(tmp_path, monkeypatch):
|
|
48
|
+
path = tmp_path / "config.toml"
|
|
49
|
+
cli.save_config({"base_url": "http://config", "api_key": "config-token"}, path)
|
|
50
|
+
monkeypatch.setenv(cli.CONFIG_ENV, str(path))
|
|
51
|
+
monkeypatch.delenv(cli.BASE_URL_ENV, raising=False)
|
|
52
|
+
monkeypatch.delenv(cli.API_KEY_ENV, raising=False)
|
|
53
|
+
|
|
54
|
+
args = argparse.Namespace(base_url=None, api_key=None, token=None)
|
|
55
|
+
|
|
56
|
+
assert cli.resolve_base_url(args) == "http://config"
|
|
57
|
+
assert cli.resolve_api_key(args) == "config-token"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_parse_bool():
|
|
61
|
+
assert cli.parse_bool("true") is True
|
|
62
|
+
assert cli.parse_bool("0") is False
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from x4d_devkit.client.projects import ProjectsAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeClient:
|
|
9
|
+
def __init__(self, pages):
|
|
10
|
+
self.pages = pages
|
|
11
|
+
self.calls = []
|
|
12
|
+
|
|
13
|
+
def get(self, path, params=None):
|
|
14
|
+
assert path == "/projects"
|
|
15
|
+
self.calls.append(params)
|
|
16
|
+
return self.pages[params["page"] - 1]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_get_project_by_exact_name():
|
|
20
|
+
api = ProjectsAPI(
|
|
21
|
+
FakeClient(
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
"items": [{"id": 1, "name": "nuscenes"}, {"id": 2, "name": "nuscenes-mini"}],
|
|
25
|
+
"total": 2,
|
|
26
|
+
"page": 1,
|
|
27
|
+
"page_size": 100,
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert api.get_by_name("nuscenes-mini")["id"] == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_get_project_by_name_requires_match():
|
|
37
|
+
api = ProjectsAPI(
|
|
38
|
+
FakeClient([{"items": [], "total": 0, "page": 1, "page_size": 100}])
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
with pytest.raises(ValueError, match="Project not found"):
|
|
42
|
+
api.get_by_name("missing")
|
|
@@ -42,7 +42,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
42
42
|
"keyframe_count": 2,
|
|
43
43
|
"sweep_count": 0,
|
|
44
44
|
"sensors": {
|
|
45
|
-
"
|
|
45
|
+
"lidar_top": {
|
|
46
46
|
"modality": "lidar",
|
|
47
47
|
"point_format": {
|
|
48
48
|
"fields": ["x", "y", "z", "intensity"],
|
|
@@ -50,7 +50,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
50
50
|
"bytes_per_point": 16,
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
|
-
"
|
|
53
|
+
"cam_front": {
|
|
54
54
|
"modality": "camera",
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -62,7 +62,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
62
62
|
(clip_dir / "calibrated_sensor.json").write_text(json.dumps([
|
|
63
63
|
{
|
|
64
64
|
"calibrated_sensor_token": "cs-lidar",
|
|
65
|
-
"channel": "
|
|
65
|
+
"channel": "lidar_top",
|
|
66
66
|
"translation": {"x": 0.0, "y": 0.0, "z": 1.0},
|
|
67
67
|
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.0, "qw": 1.0},
|
|
68
68
|
"intrinsics": None,
|
|
@@ -70,7 +70,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"calibrated_sensor_token": "cs-cam",
|
|
73
|
-
"channel": "
|
|
73
|
+
"channel": "cam_front",
|
|
74
74
|
"translation": {"x": 0.5, "y": 0.0, "z": 0.8},
|
|
75
75
|
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.0, "qw": 1.0},
|
|
76
76
|
"intrinsics": {"camera_model": "pinhole",
|
|
@@ -117,16 +117,16 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
117
117
|
]))
|
|
118
118
|
|
|
119
119
|
# -- sample_data.json --
|
|
120
|
-
lidar_dir = clip_dir / "
|
|
120
|
+
lidar_dir = clip_dir / "lidar_top"
|
|
121
121
|
lidar_dir.mkdir()
|
|
122
122
|
|
|
123
123
|
(clip_dir / "sample_data.json").write_text(json.dumps([
|
|
124
124
|
{
|
|
125
125
|
"sample_data_token": "sd-lidar-0",
|
|
126
126
|
"sample_token": "s-0",
|
|
127
|
-
"channel": "
|
|
127
|
+
"channel": "lidar_top",
|
|
128
128
|
"timestamp_us": 1000000,
|
|
129
|
-
"file_path": "
|
|
129
|
+
"file_path": "lidar_top/frame0.bin",
|
|
130
130
|
"ego_pose_token": "ep-0",
|
|
131
131
|
"calibrated_sensor_token": "cs-lidar",
|
|
132
132
|
"is_keyframe": True,
|
|
@@ -138,9 +138,9 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
138
138
|
{
|
|
139
139
|
"sample_data_token": "sd-lidar-1",
|
|
140
140
|
"sample_token": "s-1",
|
|
141
|
-
"channel": "
|
|
141
|
+
"channel": "lidar_top",
|
|
142
142
|
"timestamp_us": 1500000,
|
|
143
|
-
"file_path": "
|
|
143
|
+
"file_path": "lidar_top/frame1.bin",
|
|
144
144
|
"ego_pose_token": "ep-1",
|
|
145
145
|
"calibrated_sensor_token": "cs-lidar",
|
|
146
146
|
"is_keyframe": True,
|
|
@@ -242,9 +242,9 @@ class TestFrameIdHelpers:
|
|
|
242
242
|
assert loader.annotation_frame_id == "base_link"
|
|
243
243
|
|
|
244
244
|
def test_sensor_frame_id_returns_channel_frame(self, loader):
|
|
245
|
-
sd = loader.sample_data_for_channel("
|
|
245
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
246
246
|
# v0.2 has no per-sensor frame_id, falls back to channel name.
|
|
247
|
-
assert loader.sensor_frame_id(sd) == "
|
|
247
|
+
assert loader.sensor_frame_id(sd) == "lidar_top"
|
|
248
248
|
|
|
249
249
|
def test_clip_world_constant_is_string_literal(self):
|
|
250
250
|
# The spec pins this name. Any consumer can pass it as a regular frame.
|
|
@@ -259,62 +259,62 @@ class TestFrameIdHelpers:
|
|
|
259
259
|
class TestLoadPointCloudFrames:
|
|
260
260
|
def test_no_frame_returns_raw_sensor(self, loader):
|
|
261
261
|
"""Default (no frame=) returns the raw sensor data unchanged."""
|
|
262
|
-
sd = loader.sample_data_for_channel("
|
|
262
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
263
263
|
pts = loader.load_point_cloud(sd)
|
|
264
264
|
assert pts.shape == (4, 4)
|
|
265
265
|
np.testing.assert_allclose(pts[0, :3], [1.0, 0.0, 0.0], atol=1e-6)
|
|
266
266
|
|
|
267
267
|
def test_frame_equal_to_sensor_frame_is_noop(self, loader):
|
|
268
268
|
"""Passing the sensor's own frame_id matches the no-frame default."""
|
|
269
|
-
sd = loader.sample_data_for_channel("
|
|
269
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
270
270
|
pts_default = loader.load_point_cloud(sd)
|
|
271
271
|
pts_explicit = loader.load_point_cloud(sd, frame=loader.sensor_frame_id(sd))
|
|
272
272
|
np.testing.assert_array_equal(pts_default, pts_explicit)
|
|
273
273
|
|
|
274
274
|
def test_frame_equal_to_ego_applies_calibration(self, loader):
|
|
275
|
-
"""frame=ego_pose_frame_id applies sensor-to-ego (z+1 for
|
|
276
|
-
sd = loader.sample_data_for_channel("
|
|
275
|
+
"""frame=ego_pose_frame_id applies sensor-to-ego (z+1 for lidar_top)."""
|
|
276
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
277
277
|
pts = loader.load_point_cloud(sd, frame=loader.ego_pose_frame_id)
|
|
278
278
|
np.testing.assert_allclose(pts[0, :3], [1.0, 0.0, 1.0], atol=1e-5)
|
|
279
279
|
np.testing.assert_allclose(pts[2, :3], [0.0, 0.0, 2.0], atol=1e-5)
|
|
280
280
|
|
|
281
281
|
def test_frame_clip_world_first_ego(self, loader):
|
|
282
282
|
"""frame='clip_world' for the first ego pose (at origin) ≡ ego frame."""
|
|
283
|
-
sd = loader.sample_data_for_channel("
|
|
283
|
+
sd = loader.sample_data_for_channel("lidar_top")[0] # ep-0 at origin
|
|
284
284
|
pts_ego = loader.load_point_cloud(sd, frame=loader.ego_pose_frame_id)
|
|
285
285
|
pts_world = loader.load_point_cloud(sd, frame=CLIP_WORLD)
|
|
286
286
|
np.testing.assert_allclose(pts_world[:, :3], pts_ego[:, :3], atol=1e-5)
|
|
287
287
|
|
|
288
288
|
def test_frame_clip_world_second_ego(self, loader):
|
|
289
289
|
"""frame='clip_world' for the second ego pose adds the ego offset."""
|
|
290
|
-
sd = loader.sample_data_for_channel("
|
|
290
|
+
sd = loader.sample_data_for_channel("lidar_top")[1] # ep-1 at x=10
|
|
291
291
|
pts = loader.load_point_cloud(sd, frame=CLIP_WORLD)
|
|
292
292
|
np.testing.assert_allclose(pts[0, :3], [11.0, 0.0, 1.0], atol=1e-5)
|
|
293
293
|
|
|
294
294
|
def test_intensity_preserved(self, loader):
|
|
295
295
|
"""Non-xyz columns are passed through unchanged."""
|
|
296
|
-
sd = loader.sample_data_for_channel("
|
|
296
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
297
297
|
pts = loader.load_point_cloud(sd, frame=CLIP_WORLD)
|
|
298
298
|
np.testing.assert_allclose(pts[:, 3], 0.5, atol=1e-6)
|
|
299
299
|
|
|
300
300
|
def test_magic_string_world_rejected(self, loader):
|
|
301
301
|
"""The legacy magic string 'world' must be rejected with a hint."""
|
|
302
|
-
sd = loader.sample_data_for_channel("
|
|
302
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
303
303
|
with pytest.raises(ValueError, match=r"clip_world"):
|
|
304
304
|
loader.load_point_cloud(sd, frame="world")
|
|
305
305
|
|
|
306
306
|
def test_magic_string_ego_rejected(self, loader):
|
|
307
|
-
sd = loader.sample_data_for_channel("
|
|
307
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
308
308
|
with pytest.raises(ValueError, match=r"ego_pose_frame_id"):
|
|
309
309
|
loader.load_point_cloud(sd, frame="ego")
|
|
310
310
|
|
|
311
311
|
def test_magic_string_sensor_rejected(self, loader):
|
|
312
|
-
sd = loader.sample_data_for_channel("
|
|
312
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
313
313
|
with pytest.raises(ValueError, match=r"sensor_frame_id"):
|
|
314
314
|
loader.load_point_cloud(sd, frame="sensor")
|
|
315
315
|
|
|
316
316
|
def test_unknown_frame_id_raises_keyerror(self, loader):
|
|
317
|
-
sd = loader.sample_data_for_channel("
|
|
317
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
318
318
|
with pytest.raises(KeyError, match=r"not_a_real_frame"):
|
|
319
319
|
loader.load_point_cloud(sd, frame="not_a_real_frame")
|
|
320
320
|
|
|
@@ -367,9 +367,9 @@ class TestAnnotationsForSampleFrames:
|
|
|
367
367
|
|
|
368
368
|
def test_frame_sensor_id_transforms_to_that_sensor(self, loader):
|
|
369
369
|
"""Passing a sensor's frame_id transforms annotations into that sensor's frame."""
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
anns = loader.annotations_for_sample("s-0", frame="
|
|
370
|
+
# lidar_top is at (0,0,1) in ego frame. Annotation at ego (5,3,0) →
|
|
371
|
+
# lidar_top frame: subtract z=1 → (5,3,-1).
|
|
372
|
+
anns = loader.annotations_for_sample("s-0", frame="lidar_top")
|
|
373
373
|
np.testing.assert_allclose(anns[0].translation, [5.0, 3.0, -1.0], atol=1e-5)
|
|
374
374
|
|
|
375
375
|
def test_magic_string_sensor_rejected(self, loader):
|
|
@@ -399,51 +399,51 @@ class TestAnnotationsForSampleFrames:
|
|
|
399
399
|
|
|
400
400
|
class TestGetTransform:
|
|
401
401
|
def test_identity_same_frame(self, loader):
|
|
402
|
-
T = loader.get_transform("
|
|
402
|
+
T = loader.get_transform("lidar_top", "lidar_top")
|
|
403
403
|
np.testing.assert_allclose(T.as_matrix, np.eye(4), atol=1e-10)
|
|
404
404
|
|
|
405
405
|
def test_static_sensor_to_ego(self, loader):
|
|
406
406
|
# Static-only: no sd needed.
|
|
407
|
-
T = loader.get_transform("
|
|
407
|
+
T = loader.get_transform("lidar_top", loader.ego_pose_frame_id)
|
|
408
408
|
np.testing.assert_allclose(T.translation, [0.0, 0.0, 1.0], atol=1e-10)
|
|
409
409
|
|
|
410
410
|
def test_static_sensor_to_sensor(self, loader):
|
|
411
|
-
#
|
|
411
|
+
# lidar_top at (0,0,1) and cam_front at (0.5,0,0.8) in ego frame.
|
|
412
412
|
# T_CAM_from_LIDAR = T_CAM_from_ego @ T_ego_from_LIDAR
|
|
413
413
|
# = (-0.5, 0, -0.8) ∘ (0, 0, +1) = (-0.5, 0, +0.2)
|
|
414
|
-
T = loader.get_transform("
|
|
414
|
+
T = loader.get_transform("lidar_top", "cam_front")
|
|
415
415
|
np.testing.assert_allclose(T.translation, [-0.5, 0.0, 0.2], atol=1e-10)
|
|
416
416
|
|
|
417
417
|
def test_dynamic_sensor_to_clip_world_requires_sd(self, loader):
|
|
418
|
-
sd = loader.sample_data_for_channel("
|
|
419
|
-
T = loader.get_transform("
|
|
418
|
+
sd = loader.sample_data_for_channel("lidar_top")[1] # ego at x=10
|
|
419
|
+
T = loader.get_transform("lidar_top", CLIP_WORLD, sd=sd)
|
|
420
420
|
# sensor → ego (z+1) → world (x+10) → (10, 0, 1)
|
|
421
421
|
np.testing.assert_allclose(T.translation, [10.0, 0.0, 1.0], atol=1e-10)
|
|
422
422
|
|
|
423
423
|
def test_clip_world_without_sd_raises(self, loader):
|
|
424
424
|
with pytest.raises(ValueError, match=r"clip_world"):
|
|
425
|
-
loader.get_transform("
|
|
425
|
+
loader.get_transform("lidar_top", CLIP_WORLD)
|
|
426
426
|
|
|
427
427
|
def test_round_trip_through_clip_world(self, loader):
|
|
428
|
-
sd = loader.sample_data_for_channel("
|
|
429
|
-
T_fwd = loader.get_transform("
|
|
430
|
-
T_rev = loader.get_transform(CLIP_WORLD, "
|
|
428
|
+
sd = loader.sample_data_for_channel("lidar_top")[1]
|
|
429
|
+
T_fwd = loader.get_transform("lidar_top", CLIP_WORLD, sd=sd)
|
|
430
|
+
T_rev = loader.get_transform(CLIP_WORLD, "lidar_top", sd=sd)
|
|
431
431
|
np.testing.assert_allclose((T_fwd @ T_rev).as_matrix, np.eye(4), atol=1e-10)
|
|
432
432
|
|
|
433
433
|
def test_magic_string_rejected(self, loader):
|
|
434
|
-
sd = loader.sample_data_for_channel("
|
|
434
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
435
435
|
with pytest.raises(ValueError, match=r"clip_world"):
|
|
436
436
|
loader.get_transform("sensor", "world", sd=sd)
|
|
437
437
|
|
|
438
438
|
def test_unknown_frame_id_raises_keyerror(self, loader):
|
|
439
439
|
with pytest.raises(KeyError, match=r"not_a_real_frame"):
|
|
440
|
-
loader.get_transform("
|
|
440
|
+
loader.get_transform("lidar_top", "not_a_real_frame")
|
|
441
441
|
|
|
442
442
|
def test_consistency_with_load_point_cloud(self, loader):
|
|
443
|
-
sd = loader.sample_data_for_channel("
|
|
443
|
+
sd = loader.sample_data_for_channel("lidar_top")[1]
|
|
444
444
|
pts_sensor = loader.load_point_cloud(sd)
|
|
445
445
|
pts_world = loader.load_point_cloud(sd, frame=CLIP_WORLD)
|
|
446
|
-
T = loader.get_transform("
|
|
446
|
+
T = loader.get_transform("lidar_top", CLIP_WORLD, sd=sd)
|
|
447
447
|
pts_manual = T.apply(pts_sensor[:, :3].astype(np.float64)).astype(np.float32)
|
|
448
448
|
np.testing.assert_allclose(pts_world[:, :3], pts_manual, atol=1e-5)
|
|
449
449
|
|
|
@@ -476,11 +476,11 @@ class TestCoordinateFramesIntegration:
|
|
|
476
476
|
def test_point_cloud_round_trip(self, nuscenes_loader):
|
|
477
477
|
"""sensor → clip_world → sensor recovers original points."""
|
|
478
478
|
sd = next(
|
|
479
|
-
s for s in nuscenes_loader.sample_data_for_channel("
|
|
479
|
+
s for s in nuscenes_loader.sample_data_for_channel("lidar_top") if s.is_keyframe
|
|
480
480
|
)
|
|
481
481
|
pts_sensor = nuscenes_loader.load_point_cloud(sd)
|
|
482
482
|
pts_world = nuscenes_loader.load_point_cloud(sd, frame=CLIP_WORLD)
|
|
483
|
-
T_back = nuscenes_loader.get_transform(CLIP_WORLD, "
|
|
483
|
+
T_back = nuscenes_loader.get_transform(CLIP_WORLD, "lidar_top", sd=sd)
|
|
484
484
|
recovered = T_back.apply(pts_world[:, :3].astype(np.float64)).astype(np.float32)
|
|
485
485
|
np.testing.assert_allclose(recovered, pts_sensor[:, :3], atol=1e-4)
|
|
486
486
|
|
|
@@ -53,10 +53,11 @@ class TestClipLoader:
|
|
|
53
53
|
|
|
54
54
|
def test_calibrated_sensors(self, clip_dir):
|
|
55
55
|
loader = ClipLoader(clip_dir)
|
|
56
|
-
assert len(loader.calibrated_sensors) ==
|
|
56
|
+
assert len(loader.calibrated_sensors) == 8
|
|
57
57
|
channels = {cs.channel for cs in loader.calibrated_sensors}
|
|
58
|
-
assert "
|
|
59
|
-
assert "
|
|
58
|
+
assert "base_link" in channels
|
|
59
|
+
assert "cam_front" in channels
|
|
60
|
+
assert "lidar_top" in channels
|
|
60
61
|
|
|
61
62
|
def test_annotations_and_instances(self, clip_dir):
|
|
62
63
|
loader = ClipLoader(clip_dir)
|
|
@@ -78,9 +79,9 @@ class TestClipLoader:
|
|
|
78
79
|
|
|
79
80
|
def test_sample_data_for_channel(self, clip_dir):
|
|
80
81
|
loader = ClipLoader(clip_dir)
|
|
81
|
-
cam_data = loader.sample_data_for_channel("
|
|
82
|
+
cam_data = loader.sample_data_for_channel("cam_front")
|
|
82
83
|
assert len(cam_data) > 0
|
|
83
|
-
assert all(sd.channel == "
|
|
84
|
+
assert all(sd.channel == "cam_front" for sd in cam_data)
|
|
84
85
|
timestamps = [sd.timestamp_us for sd in cam_data]
|
|
85
86
|
assert timestamps == sorted(timestamps)
|
|
86
87
|
|
|
@@ -101,7 +102,7 @@ class TestClipLoader:
|
|
|
101
102
|
|
|
102
103
|
def test_load_point_cloud(self, clip_dir):
|
|
103
104
|
loader = ClipLoader(clip_dir)
|
|
104
|
-
lidar_data = loader.sample_data_for_channel("
|
|
105
|
+
lidar_data = loader.sample_data_for_channel("lidar_top")
|
|
105
106
|
kf = next(sd for sd in lidar_data if sd.is_keyframe)
|
|
106
107
|
pc = loader.load_point_cloud(kf)
|
|
107
108
|
assert isinstance(pc, np.ndarray)
|
|
@@ -111,7 +112,7 @@ class TestClipLoader:
|
|
|
111
112
|
|
|
112
113
|
def test_load_image_path(self, clip_dir):
|
|
113
114
|
loader = ClipLoader(clip_dir)
|
|
114
|
-
cam_data = loader.sample_data_for_channel("
|
|
115
|
+
cam_data = loader.sample_data_for_channel("cam_front")
|
|
115
116
|
kf = next(sd for sd in cam_data if sd.is_keyframe)
|
|
116
117
|
path = loader.load_image_path(kf)
|
|
117
118
|
assert path.is_file()
|
|
@@ -160,26 +160,27 @@ class TestNuScenesConverterExtraction:
|
|
|
160
160
|
assert clip_id == "20180724_n015_scene-0061_000"
|
|
161
161
|
assert meta["clip_id"] == clip_id
|
|
162
162
|
assert meta["vehicle_id"] == "n015"
|
|
163
|
-
assert "
|
|
164
|
-
assert "
|
|
165
|
-
assert "
|
|
163
|
+
assert "lidar_top" in meta["sensors"]
|
|
164
|
+
assert "cam_front" in meta["sensors"]
|
|
165
|
+
assert "radar_front" not in meta["sensors"]
|
|
166
166
|
assert len(meta["sensors"]) == 7 # 6 cam + 1 lidar
|
|
167
|
-
assert meta["sensors"]["
|
|
168
|
-
assert meta["sensors"]["
|
|
169
|
-
assert meta["schema_version"] == "0.
|
|
167
|
+
assert meta["sensors"]["lidar_top"]["modality"] == "lidar"
|
|
168
|
+
assert meta["sensors"]["cam_front"]["modality"] == "camera"
|
|
169
|
+
assert meta["schema_version"] == "0.10"
|
|
170
170
|
|
|
171
171
|
def test_extract_calibrated_sensors(self, converter):
|
|
172
172
|
scene = converter.nusc.scene[0]
|
|
173
173
|
clip_id, _ = converter._extract_metadata(scene)
|
|
174
174
|
cs_records = converter._extract_calibrated_sensors(scene, clip_id)
|
|
175
|
-
assert len(cs_records) ==
|
|
175
|
+
assert len(cs_records) == 8 # base_link root + 6 cameras + 1 lidar
|
|
176
176
|
channels = {r["channel"] for r in cs_records}
|
|
177
|
-
assert "
|
|
178
|
-
assert "
|
|
179
|
-
assert "
|
|
177
|
+
assert "base_link" in channels
|
|
178
|
+
assert "cam_front" in channels
|
|
179
|
+
assert "lidar_top" in channels
|
|
180
|
+
assert "radar_front" not in channels
|
|
180
181
|
# Check token is deterministic
|
|
181
|
-
cam_front = next(r for r in cs_records if r["channel"] == "
|
|
182
|
-
expected_token = generate_token(clip_id, "cs:
|
|
182
|
+
cam_front = next(r for r in cs_records if r["channel"] == "cam_front")
|
|
183
|
+
expected_token = generate_token(clip_id, "cs:cam_front")
|
|
183
184
|
assert cam_front["calibrated_sensor_token"] == expected_token
|
|
184
185
|
# Check named fields
|
|
185
186
|
assert "x" in cam_front["translation"]
|
|
@@ -281,9 +282,9 @@ class TestNuScenesConverterSamples:
|
|
|
281
282
|
|
|
282
283
|
# Check channels — only target channels
|
|
283
284
|
channels = {sd["channel"] for sd in sample_data_records}
|
|
284
|
-
assert "
|
|
285
|
-
assert "
|
|
286
|
-
assert "
|
|
285
|
+
assert "cam_front" in channels
|
|
286
|
+
assert "lidar_top" in channels
|
|
287
|
+
assert "radar_front" not in channels
|
|
287
288
|
|
|
288
289
|
# Check file_path format
|
|
289
290
|
for sd in sample_data_records:
|
|
@@ -303,7 +304,7 @@ class TestNuScenesConverterSamples:
|
|
|
303
304
|
sample_data_records = converter._extract_sample_data(scene, clip_id, ep_old_to_new)
|
|
304
305
|
|
|
305
306
|
sd_by_token = {sd["sample_data_token"]: sd for sd in sample_data_records}
|
|
306
|
-
cam_front_sds = [sd for sd in sample_data_records if sd["channel"] == "
|
|
307
|
+
cam_front_sds = [sd for sd in sample_data_records if sd["channel"] == "cam_front"]
|
|
307
308
|
cam_front_sds.sort(key=lambda sd: sd["timestamp_us"])
|
|
308
309
|
# First should have prev=None, last next=None
|
|
309
310
|
assert cam_front_sds[0]["prev"] is None
|
|
@@ -354,21 +355,21 @@ class TestNuScenesConverterFullScene:
|
|
|
354
355
|
assert len(data) > 0, f"{name} is empty"
|
|
355
356
|
|
|
356
357
|
# Check sensor data directories
|
|
357
|
-
assert (clip_dir / "samples" / "
|
|
358
|
-
assert (clip_dir / "samples" / "
|
|
359
|
-
assert not (clip_dir / "samples" / "
|
|
358
|
+
assert (clip_dir / "samples" / "lidar_top").is_dir()
|
|
359
|
+
assert (clip_dir / "samples" / "cam_front").is_dir()
|
|
360
|
+
assert not (clip_dir / "samples" / "radar_front").exists()
|
|
360
361
|
|
|
361
362
|
# Check sensor files exist and are non-empty
|
|
362
|
-
lidar_files = list((clip_dir / "samples" / "
|
|
363
|
+
lidar_files = list((clip_dir / "samples" / "lidar_top").glob("*.bin"))
|
|
363
364
|
assert len(lidar_files) > 0
|
|
364
365
|
assert lidar_files[0].stat().st_size > 0
|
|
365
366
|
|
|
366
|
-
cam_files = list((clip_dir / "samples" / "
|
|
367
|
+
cam_files = list((clip_dir / "samples" / "cam_front").glob("*.jpg"))
|
|
367
368
|
assert len(cam_files) > 0
|
|
368
369
|
|
|
369
370
|
# Check sweeps exist
|
|
370
|
-
assert (clip_dir / "sweeps" / "
|
|
371
|
-
sweep_files = list((clip_dir / "sweeps" / "
|
|
371
|
+
assert (clip_dir / "sweeps" / "lidar_top").is_dir()
|
|
372
|
+
sweep_files = list((clip_dir / "sweeps" / "lidar_top").glob("*.bin"))
|
|
372
373
|
assert len(sweep_files) > 0
|
|
373
374
|
|
|
374
375
|
def test_sample_data_files_all_exist(self, converter):
|
|
@@ -31,14 +31,14 @@ def loader(tmp_path_factory):
|
|
|
31
31
|
class TestTransformChain:
|
|
32
32
|
def test_ego_from_sensor_lidar(self, loader):
|
|
33
33
|
chain = TransformChain(loader)
|
|
34
|
-
T = chain.get_ego_from_sensor("
|
|
34
|
+
T = chain.get_ego_from_sensor("lidar_top")
|
|
35
35
|
assert isinstance(T, Transform)
|
|
36
|
-
#
|
|
36
|
+
# lidar_top has extrinsic offset from base_link, should not be identity
|
|
37
37
|
assert not np.allclose(T.translation, [0, 0, 0], atol=1e-3)
|
|
38
38
|
|
|
39
39
|
def test_ego_from_sensor_camera(self, loader):
|
|
40
40
|
chain = TransformChain(loader)
|
|
41
|
-
T = chain.get_ego_from_sensor("
|
|
41
|
+
T = chain.get_ego_from_sensor("cam_front")
|
|
42
42
|
assert isinstance(T, Transform)
|
|
43
43
|
|
|
44
44
|
def test_world_from_ego_first_frame_near_identity(self, loader):
|
|
@@ -52,7 +52,7 @@ class TestTransformChain:
|
|
|
52
52
|
def test_world_from_sensor_composition(self, loader):
|
|
53
53
|
"""world_from_sensor == world_from_ego @ ego_from_sensor."""
|
|
54
54
|
chain = TransformChain(loader)
|
|
55
|
-
lidar_data = loader.sample_data_for_channel("
|
|
55
|
+
lidar_data = loader.sample_data_for_channel("lidar_top")
|
|
56
56
|
sd = next(s for s in lidar_data if s.is_keyframe)
|
|
57
57
|
|
|
58
58
|
T_direct = chain.get_world_from_sensor(sd)
|
|
@@ -66,7 +66,7 @@ class TestTransformChain:
|
|
|
66
66
|
def test_point_cloud_round_trip(self, loader):
|
|
67
67
|
"""sensor -> world -> sensor round-trip should recover original points."""
|
|
68
68
|
chain = TransformChain(loader)
|
|
69
|
-
lidar_data = loader.sample_data_for_channel("
|
|
69
|
+
lidar_data = loader.sample_data_for_channel("lidar_top")
|
|
70
70
|
sd = next(s for s in lidar_data if s.is_keyframe)
|
|
71
71
|
pc = loader.load_point_cloud(sd)
|
|
72
72
|
xyz = pc[:, :3]
|
|
@@ -78,11 +78,11 @@ class TestTransformChain:
|
|
|
78
78
|
def test_transform_points_to_world_changes_coords(self, loader):
|
|
79
79
|
"""Points in world coords should differ from sensor coords."""
|
|
80
80
|
chain = TransformChain(loader)
|
|
81
|
-
lidar_data = loader.sample_data_for_channel("
|
|
81
|
+
lidar_data = loader.sample_data_for_channel("lidar_top")
|
|
82
82
|
sd = next(s for s in lidar_data if s.is_keyframe)
|
|
83
83
|
pc = loader.load_point_cloud(sd)
|
|
84
84
|
xyz = pc[:10, :3]
|
|
85
85
|
|
|
86
86
|
world_pts = chain.transform_points_to_world(xyz, sd)
|
|
87
|
-
# Not identical (
|
|
87
|
+
# Not identical (lidar_top has extrinsic offset)
|
|
88
88
|
assert not np.allclose(world_pts, xyz, atol=1e-3)
|