x4d-devkit 0.12.0__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.12.0 → x4d_devkit-0.14.3}/PKG-INFO +29 -1
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/README.md +28 -0
- {x4d_devkit-0.12.0 → 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.12.0 → x4d_devkit-0.14.3}/tests/test_coordinate_frames.py +41 -41
- x4d_devkit-0.14.3/tests/test_loader.py +254 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_nuscenes_converter.py +24 -23
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_transform_chain.py +7 -7
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_v06_schema.py +2 -2
- x4d_devkit-0.14.3/tests/test_v10_schema.py +238 -0
- x4d_devkit-0.14.3/tests/test_validation.py +421 -0
- x4d_devkit-0.14.3/x4d_devkit/cli.py +280 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/client.py +16 -1
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/projects.py +23 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/converters/nuscenes.py +14 -12
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/loader.py +61 -8
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/models.py +17 -13
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/validator.py +96 -57
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/PKG-INFO +29 -1
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- x4d_devkit-0.12.0/tests/test_loader.py +0 -118
- x4d_devkit-0.12.0/tests/test_validation.py +0 -202
- x4d_devkit-0.12.0/x4d_devkit/cli.py +0 -131
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/LICENSE +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/setup.cfg +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_box.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_fusion.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_matching.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_models.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_token.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_transform.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_v06_api_additions.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/fusion.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.12.0 → 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.
|
|
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.
|
|
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
|
|