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.
Files changed (56) hide show
  1. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/PKG-INFO +29 -1
  2. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/README.md +28 -0
  3. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/pyproject.toml +1 -1
  4. x4d_devkit-0.14.3/tests/test_cli_auth.py +62 -0
  5. x4d_devkit-0.14.3/tests/test_client_projects.py +42 -0
  6. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_coordinate_frames.py +41 -41
  7. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_loader.py +8 -7
  8. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_nuscenes_converter.py +24 -23
  9. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_transform_chain.py +7 -7
  10. x4d_devkit-0.14.3/x4d_devkit/cli.py +280 -0
  11. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/client.py +16 -1
  12. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/projects.py +23 -0
  13. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/converters/nuscenes.py +3 -2
  14. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/PKG-INFO +29 -1
  15. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  16. x4d_devkit-0.14.2/x4d_devkit/cli.py +0 -131
  17. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/LICENSE +0 -0
  18. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/setup.cfg +0 -0
  19. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_box.py +0 -0
  20. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_detection_eval.py +0 -0
  21. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_evaluate.py +0 -0
  22. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_fusion.py +0 -0
  23. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_matching.py +0 -0
  24. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_metrics.py +0 -0
  25. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_models.py +0 -0
  26. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_token.py +0 -0
  27. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_transform.py +0 -0
  28. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v06_api_additions.py +0 -0
  29. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v06_schema.py +0 -0
  30. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_v10_schema.py +0 -0
  31. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/tests/test_validation.py +0 -0
  32. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/__init__.py +0 -0
  33. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/__init__.py +0 -0
  34. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/checkpoints.py +0 -0
  35. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/clips.py +0 -0
  36. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/client/test_sets.py +0 -0
  37. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/converters/__init__.py +0 -0
  38. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/__init__.py +0 -0
  39. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/loader.py +0 -0
  40. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/models.py +0 -0
  41. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/token.py +0 -0
  42. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/core/transform.py +0 -0
  43. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/__init__.py +0 -0
  44. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/api.py +0 -0
  45. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/box.py +0 -0
  46. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/detection.py +0 -0
  47. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/matching.py +0 -0
  48. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/eval/metrics.py +0 -0
  49. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/fusion.py +0 -0
  50. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/__init__.py +0 -0
  51. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/report.py +0 -0
  52. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit/validation/validator.py +0 -0
  53. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  54. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/entry_points.txt +0 -0
  55. {x4d_devkit-0.14.2 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/requires.txt +0 -0
  56. {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.2
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.2"
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
- "LIDAR_TOP": {
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
- "CAM_FRONT": {
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": "LIDAR_TOP",
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": "CAM_FRONT",
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 / "LIDAR_TOP"
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": "LIDAR_TOP",
127
+ "channel": "lidar_top",
128
128
  "timestamp_us": 1000000,
129
- "file_path": "LIDAR_TOP/frame0.bin",
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": "LIDAR_TOP",
141
+ "channel": "lidar_top",
142
142
  "timestamp_us": 1500000,
143
- "file_path": "LIDAR_TOP/frame1.bin",
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("LIDAR_TOP")[0]
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) == "LIDAR_TOP"
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("LIDAR_TOP")[0]
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("LIDAR_TOP")[0]
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 LIDAR_TOP)."""
276
- sd = loader.sample_data_for_channel("LIDAR_TOP")[0]
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("LIDAR_TOP")[0] # ep-0 at origin
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("LIDAR_TOP")[1] # ep-1 at x=10
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("LIDAR_TOP")[0]
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("LIDAR_TOP")[0]
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("LIDAR_TOP")[0]
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("LIDAR_TOP")[0]
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("LIDAR_TOP")[0]
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
- # 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")
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("LIDAR_TOP", "LIDAR_TOP")
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("LIDAR_TOP", loader.ego_pose_frame_id)
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
- # LIDAR_TOP at (0,0,1) and CAM_FRONT at (0.5,0,0.8) in ego frame.
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("LIDAR_TOP", "CAM_FRONT")
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("LIDAR_TOP")[1] # ego at x=10
419
- T = loader.get_transform("LIDAR_TOP", CLIP_WORLD, sd=sd)
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("LIDAR_TOP", CLIP_WORLD)
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("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)
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("LIDAR_TOP")[0]
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("LIDAR_TOP", "not_a_real_frame")
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("LIDAR_TOP")[1]
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("LIDAR_TOP", CLIP_WORLD, sd=sd)
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("LIDAR_TOP") if s.is_keyframe
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, "LIDAR_TOP", sd=sd)
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) == 7
56
+ assert len(loader.calibrated_sensors) == 8
57
57
  channels = {cs.channel for cs in loader.calibrated_sensors}
58
- assert "CAM_FRONT" in channels
59
- assert "LIDAR_TOP" in channels
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("CAM_FRONT")
82
+ cam_data = loader.sample_data_for_channel("cam_front")
82
83
  assert len(cam_data) > 0
83
- assert all(sd.channel == "CAM_FRONT" for sd in cam_data)
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("LIDAR_TOP")
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("CAM_FRONT")
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 "LIDAR_TOP" in meta["sensors"]
164
- assert "CAM_FRONT" in meta["sensors"]
165
- assert "RADAR_FRONT" not in meta["sensors"]
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"]["LIDAR_TOP"]["modality"] == "lidar"
168
- assert meta["sensors"]["CAM_FRONT"]["modality"] == "camera"
169
- assert meta["schema_version"] == "0.1"
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) == 7
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 "CAM_FRONT" in channels
178
- assert "LIDAR_TOP" in channels
179
- assert "RADAR_FRONT" not in channels
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"] == "CAM_FRONT")
182
- expected_token = generate_token(clip_id, "cs:CAM_FRONT")
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 "CAM_FRONT" in channels
285
- assert "LIDAR_TOP" in channels
286
- assert "RADAR_FRONT" not in channels
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"] == "CAM_FRONT"]
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" / "LIDAR_TOP").is_dir()
358
- assert (clip_dir / "samples" / "CAM_FRONT").is_dir()
359
- assert not (clip_dir / "samples" / "RADAR_FRONT").exists()
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" / "LIDAR_TOP").glob("*.bin"))
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" / "CAM_FRONT").glob("*.jpg"))
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" / "LIDAR_TOP").is_dir()
371
- sweep_files = list((clip_dir / "sweeps" / "LIDAR_TOP").glob("*.bin"))
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("LIDAR_TOP")
34
+ T = chain.get_ego_from_sensor("lidar_top")
35
35
  assert isinstance(T, Transform)
36
- # LIDAR_TOP has extrinsic offset from base_link, should not be identity
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("CAM_FRONT")
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("LIDAR_TOP")
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("LIDAR_TOP")
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("LIDAR_TOP")
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 (LIDAR_TOP has extrinsic offset)
87
+ # Not identical (lidar_top has extrinsic offset)
88
88
  assert not np.allclose(world_pts, xyz, atol=1e-3)