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.
Files changed (58) hide show
  1. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/PKG-INFO +29 -1
  2. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/README.md +28 -0
  3. {x4d_devkit-0.12.0 → 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.12.0 → x4d_devkit-0.14.3}/tests/test_coordinate_frames.py +41 -41
  7. x4d_devkit-0.14.3/tests/test_loader.py +254 -0
  8. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_nuscenes_converter.py +24 -23
  9. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_transform_chain.py +7 -7
  10. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_v06_schema.py +2 -2
  11. x4d_devkit-0.14.3/tests/test_v10_schema.py +238 -0
  12. x4d_devkit-0.14.3/tests/test_validation.py +421 -0
  13. x4d_devkit-0.14.3/x4d_devkit/cli.py +280 -0
  14. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/client.py +16 -1
  15. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/projects.py +23 -0
  16. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/converters/nuscenes.py +14 -12
  17. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/loader.py +61 -8
  18. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/models.py +17 -13
  19. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/validator.py +96 -57
  20. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/PKG-INFO +29 -1
  21. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/SOURCES.txt +3 -0
  22. x4d_devkit-0.12.0/tests/test_loader.py +0 -118
  23. x4d_devkit-0.12.0/tests/test_validation.py +0 -202
  24. x4d_devkit-0.12.0/x4d_devkit/cli.py +0 -131
  25. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/LICENSE +0 -0
  26. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/setup.cfg +0 -0
  27. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_box.py +0 -0
  28. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_detection_eval.py +0 -0
  29. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_evaluate.py +0 -0
  30. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_fusion.py +0 -0
  31. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_matching.py +0 -0
  32. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_metrics.py +0 -0
  33. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_models.py +0 -0
  34. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_token.py +0 -0
  35. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_transform.py +0 -0
  36. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/tests/test_v06_api_additions.py +0 -0
  37. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/__init__.py +0 -0
  38. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/__init__.py +0 -0
  39. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/checkpoints.py +0 -0
  40. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/clips.py +0 -0
  41. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/client/test_sets.py +0 -0
  42. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/converters/__init__.py +0 -0
  43. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/__init__.py +0 -0
  44. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/token.py +0 -0
  45. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/core/transform.py +0 -0
  46. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/__init__.py +0 -0
  47. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/api.py +0 -0
  48. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/box.py +0 -0
  49. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/detection.py +0 -0
  50. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/matching.py +0 -0
  51. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/eval/metrics.py +0 -0
  52. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/fusion.py +0 -0
  53. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/__init__.py +0 -0
  54. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit/validation/report.py +0 -0
  55. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  56. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/entry_points.txt +0 -0
  57. {x4d_devkit-0.12.0 → x4d_devkit-0.14.3}/x4d_devkit.egg-info/requires.txt +0 -0
  58. {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.12.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.12.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
- "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