x4d-devkit 0.8.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {x4d_devkit-0.8.0/x4d_devkit.egg-info → x4d_devkit-0.11.1}/PKG-INFO +1 -1
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/pyproject.toml +1 -1
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_coordinate_frames.py +1 -1
- x4d_devkit-0.11.1/tests/test_v06_schema.py +414 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/converters/nuscenes.py +89 -28
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/loader.py +115 -19
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/models.py +38 -13
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/transform.py +112 -0
- x4d_devkit-0.11.1/x4d_devkit/validation/validator.py +463 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1/x4d_devkit.egg-info}/PKG-INFO +1 -1
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/SOURCES.txt +1 -0
- x4d_devkit-0.8.0/x4d_devkit/validation/validator.py +0 -273
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/LICENSE +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/README.md +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/setup.cfg +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_box.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_fusion.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_loader.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_matching.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_models.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_nuscenes_converter.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_token.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_transform.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_v06_api_additions.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_validation.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/fusion.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -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.11.1"
|
|
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"
|
|
@@ -331,7 +331,7 @@ class TestAnnotationsForSampleFrames:
|
|
|
331
331
|
assert not np.allclose(anns_world[0].translation, anns_ego[0].translation, atol=0.1)
|
|
332
332
|
|
|
333
333
|
def test_sensor_frame_raises(self, loader):
|
|
334
|
-
with pytest.raises(ValueError, match="
|
|
334
|
+
with pytest.raises(ValueError, match="ambiguous"):
|
|
335
335
|
loader.annotations_for_sample("s-0", frame="sensor")
|
|
336
336
|
|
|
337
337
|
def test_invalid_frame_raises(self, loader):
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Tests for v0.6 schema additions: annotation_frame_id + per-sensor frame_id.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- SensorInfo.frame_id round-trip
|
|
5
|
+
- ClipMeta.annotation_frame_id round-trip
|
|
6
|
+
- ClipLoader.annotation_frame_id property (explicit + back-compat default)
|
|
7
|
+
- ClipLoader.frame_id_for_channel
|
|
8
|
+
- annotations_for_sample(frame=<arbitrary frame_id>)
|
|
9
|
+
- TransformChain.get_ego_from_frame_id
|
|
10
|
+
- validate_clip schema 0.6 strict checks
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from x4d_devkit import ClipLoader, validate_clip
|
|
23
|
+
from x4d_devkit.core.models import ClipMeta, PointFormat, SensorInfo
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _q_from_yaw(yaw: float) -> dict[str, float]:
|
|
27
|
+
return {"qx": 0.0, "qy": 0.0, "qz": math.sin(yaw / 2), "qw": math.cos(yaw / 2)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _write_clip(
|
|
31
|
+
clip_dir: Path,
|
|
32
|
+
*,
|
|
33
|
+
schema_version: str,
|
|
34
|
+
annotation_frame_id: str | None,
|
|
35
|
+
sensors_meta: dict,
|
|
36
|
+
calibrated: list[dict],
|
|
37
|
+
samples: list[dict] | None = None,
|
|
38
|
+
sample_data: list[dict] | None = None,
|
|
39
|
+
annotations: list[dict] | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
clip_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
meta = {
|
|
43
|
+
"clip_id": "test-v06",
|
|
44
|
+
"schema_version": schema_version,
|
|
45
|
+
"session_id": "s",
|
|
46
|
+
"vehicle_id": "v",
|
|
47
|
+
"capture_time_utc": "2026-05-06T00:00:00Z",
|
|
48
|
+
"duration_s": 0.0,
|
|
49
|
+
"keyframe_count": len(samples or []),
|
|
50
|
+
"sweep_count": 0,
|
|
51
|
+
"sensors": sensors_meta,
|
|
52
|
+
}
|
|
53
|
+
if annotation_frame_id is not None:
|
|
54
|
+
meta["annotation_frame_id"] = annotation_frame_id
|
|
55
|
+
(clip_dir / "meta.json").write_text(json.dumps(meta))
|
|
56
|
+
(clip_dir / "calibrated_sensor.json").write_text(json.dumps(calibrated))
|
|
57
|
+
(clip_dir / "ego_pose.json").write_text(json.dumps([{
|
|
58
|
+
"ego_pose_token": "ep-0",
|
|
59
|
+
"timestamp_us": 0,
|
|
60
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
61
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
62
|
+
}]))
|
|
63
|
+
(clip_dir / "sample.json").write_text(json.dumps(samples or [{
|
|
64
|
+
"sample_token": "s-0", "timestamp_us": 0, "is_keyframe": True,
|
|
65
|
+
"prev": "", "next": "",
|
|
66
|
+
}]))
|
|
67
|
+
(clip_dir / "sample_data.json").write_text(json.dumps(sample_data or [{
|
|
68
|
+
"sample_data_token": "sd-0",
|
|
69
|
+
"sample_token": "s-0",
|
|
70
|
+
"channel": calibrated[0]["channel"],
|
|
71
|
+
"timestamp_us": 0,
|
|
72
|
+
"file_path": "samples/x/0.bin",
|
|
73
|
+
"ego_pose_token": "ep-0",
|
|
74
|
+
"calibrated_sensor_token": calibrated[0]["calibrated_sensor_token"],
|
|
75
|
+
"is_keyframe": True,
|
|
76
|
+
"width": None, "height": None,
|
|
77
|
+
"prev": "", "next": "",
|
|
78
|
+
}]))
|
|
79
|
+
(clip_dir / "annotation.json").write_text(json.dumps(annotations or []))
|
|
80
|
+
(clip_dir / "instance.json").write_text(json.dumps([]))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --- Model round-trip ---
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_sensor_info_frame_id_round_trip():
|
|
87
|
+
info = SensorInfo.from_dict({"modality": "lidar", "frame_id": "LIDAR_TOP"})
|
|
88
|
+
assert info.frame_id == "LIDAR_TOP"
|
|
89
|
+
assert info.to_dict()["frame_id"] == "LIDAR_TOP"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_sensor_info_no_frame_id_omits_key_in_to_dict():
|
|
93
|
+
info = SensorInfo.from_dict({"modality": "camera"})
|
|
94
|
+
assert info.frame_id is None
|
|
95
|
+
assert "frame_id" not in info.to_dict()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_clip_meta_annotation_frame_id_default_none():
|
|
99
|
+
meta = ClipMeta(
|
|
100
|
+
clip_id="x", schema_version="0.5", session_id="s", vehicle_id="v",
|
|
101
|
+
capture_time_utc="t", duration_s=0.0, num_keyframes=0, num_sweeps=0,
|
|
102
|
+
sensors={},
|
|
103
|
+
)
|
|
104
|
+
assert meta.annotation_frame_id is None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- Loader: annotation_frame_id property ---
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_loader_annotation_frame_id_explicit(tmp_path):
|
|
111
|
+
_write_clip(
|
|
112
|
+
tmp_path,
|
|
113
|
+
schema_version="0.6",
|
|
114
|
+
annotation_frame_id="LIDAR_TOP",
|
|
115
|
+
sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
|
|
116
|
+
"point_format": {"fields": ["x", "y", "z"],
|
|
117
|
+
"types": ["float32"] * 3,
|
|
118
|
+
"bytes_per_point": 12}}},
|
|
119
|
+
calibrated=[{
|
|
120
|
+
"calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
|
|
121
|
+
"translation": {"x": 0, "y": 0, "z": 1.5},
|
|
122
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
123
|
+
"intrinsics": None, "distortion": None,
|
|
124
|
+
}],
|
|
125
|
+
)
|
|
126
|
+
loader = ClipLoader(tmp_path)
|
|
127
|
+
assert loader.annotation_frame_id == "LIDAR_TOP"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_loader_annotation_frame_id_back_compat_default(tmp_path):
|
|
131
|
+
"""Pre-v0.6 clip with no annotation_frame_id falls back to base_link."""
|
|
132
|
+
_write_clip(
|
|
133
|
+
tmp_path,
|
|
134
|
+
schema_version="0.5",
|
|
135
|
+
annotation_frame_id=None,
|
|
136
|
+
sensors_meta={"lidar": {"modality": "lidar"}},
|
|
137
|
+
calibrated=[{
|
|
138
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
139
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
140
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
141
|
+
"intrinsics": None, "distortion": None,
|
|
142
|
+
}],
|
|
143
|
+
)
|
|
144
|
+
loader = ClipLoader(tmp_path)
|
|
145
|
+
assert loader.annotation_frame_id == "base_link"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_loader_frame_id_for_channel_explicit(tmp_path):
|
|
149
|
+
_write_clip(
|
|
150
|
+
tmp_path,
|
|
151
|
+
schema_version="0.6",
|
|
152
|
+
annotation_frame_id="base_link",
|
|
153
|
+
sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
|
|
154
|
+
calibrated=[{
|
|
155
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
156
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
157
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
158
|
+
"intrinsics": None, "distortion": None,
|
|
159
|
+
}],
|
|
160
|
+
)
|
|
161
|
+
loader = ClipLoader(tmp_path)
|
|
162
|
+
assert loader.frame_id_for_channel("lidar") == "base_link"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_loader_frame_id_for_channel_fallback_to_channel_name(tmp_path):
|
|
166
|
+
"""Pre-v0.6 clip falls back to channel ≡ frame_id convention."""
|
|
167
|
+
_write_clip(
|
|
168
|
+
tmp_path,
|
|
169
|
+
schema_version="0.5",
|
|
170
|
+
annotation_frame_id=None,
|
|
171
|
+
sensors_meta={"LIDAR_TOP": {"modality": "lidar"}},
|
|
172
|
+
calibrated=[{
|
|
173
|
+
"calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
|
|
174
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
175
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
176
|
+
"intrinsics": None, "distortion": None,
|
|
177
|
+
}],
|
|
178
|
+
)
|
|
179
|
+
loader = ClipLoader(tmp_path)
|
|
180
|
+
assert loader.frame_id_for_channel("LIDAR_TOP") == "LIDAR_TOP"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --- Loader: annotations_for_sample with arbitrary frame_id ---
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_annotations_for_sample_in_storage_frame_no_op(tmp_path):
|
|
187
|
+
"""Requesting the storage frame returns annotations unchanged."""
|
|
188
|
+
ann = {
|
|
189
|
+
"annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
|
|
190
|
+
"category": "car",
|
|
191
|
+
"bbox_3d": {
|
|
192
|
+
"translation": {"x": 5.0, "y": 0.0, "z": 0.0},
|
|
193
|
+
"size": {"length": 4.0, "width": 2.0, "height": 1.5},
|
|
194
|
+
"rotation": _q_from_yaw(0.0),
|
|
195
|
+
},
|
|
196
|
+
"num_lidar_pts": 100, "visibility": "4", "velocity": None,
|
|
197
|
+
"attributes": [], "prev": "", "next": "", "score": None,
|
|
198
|
+
}
|
|
199
|
+
_write_clip(
|
|
200
|
+
tmp_path,
|
|
201
|
+
schema_version="0.6",
|
|
202
|
+
annotation_frame_id="LIDAR_TOP",
|
|
203
|
+
sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
|
|
204
|
+
"point_format": {"fields": ["x", "y", "z"],
|
|
205
|
+
"types": ["float32"] * 3,
|
|
206
|
+
"bytes_per_point": 12}}},
|
|
207
|
+
calibrated=[{
|
|
208
|
+
"calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
|
|
209
|
+
"translation": {"x": 0, "y": 0, "z": 1.5},
|
|
210
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
211
|
+
"intrinsics": None, "distortion": None,
|
|
212
|
+
}],
|
|
213
|
+
annotations=[ann],
|
|
214
|
+
)
|
|
215
|
+
loader = ClipLoader(tmp_path)
|
|
216
|
+
[a] = loader.annotations_for_sample("s-0", frame="LIDAR_TOP")
|
|
217
|
+
np.testing.assert_allclose(a.translation, [5.0, 0.0, 0.0])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_annotations_for_sample_lidar_top_to_base_link(tmp_path):
|
|
221
|
+
"""Annotation in LIDAR_TOP with z-offset 1.5 → base_link adds 1.5 to z."""
|
|
222
|
+
ann = {
|
|
223
|
+
"annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
|
|
224
|
+
"category": "car",
|
|
225
|
+
"bbox_3d": {
|
|
226
|
+
"translation": {"x": 5.0, "y": 0.0, "z": 0.0},
|
|
227
|
+
"size": {"length": 4.0, "width": 2.0, "height": 1.5},
|
|
228
|
+
"rotation": _q_from_yaw(0.0),
|
|
229
|
+
},
|
|
230
|
+
"num_lidar_pts": 100, "visibility": "4", "velocity": None,
|
|
231
|
+
"attributes": [], "prev": "", "next": "", "score": None,
|
|
232
|
+
}
|
|
233
|
+
_write_clip(
|
|
234
|
+
tmp_path,
|
|
235
|
+
schema_version="0.6",
|
|
236
|
+
annotation_frame_id="LIDAR_TOP",
|
|
237
|
+
sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
|
|
238
|
+
"point_format": {"fields": ["x", "y", "z"],
|
|
239
|
+
"types": ["float32"] * 3,
|
|
240
|
+
"bytes_per_point": 12}}},
|
|
241
|
+
calibrated=[{
|
|
242
|
+
"calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
|
|
243
|
+
"translation": {"x": 0, "y": 0, "z": 1.5},
|
|
244
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
245
|
+
"intrinsics": None, "distortion": None,
|
|
246
|
+
}],
|
|
247
|
+
annotations=[ann],
|
|
248
|
+
)
|
|
249
|
+
loader = ClipLoader(tmp_path)
|
|
250
|
+
[a] = loader.annotations_for_sample("s-0", frame="base_link")
|
|
251
|
+
np.testing.assert_allclose(a.translation, [5.0, 0.0, 1.5])
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_annotations_for_sample_unknown_frame_id_raises(tmp_path):
|
|
255
|
+
_write_clip(
|
|
256
|
+
tmp_path,
|
|
257
|
+
schema_version="0.6",
|
|
258
|
+
annotation_frame_id="base_link",
|
|
259
|
+
sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
|
|
260
|
+
calibrated=[{
|
|
261
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
262
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
263
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
264
|
+
"intrinsics": None, "distortion": None,
|
|
265
|
+
}],
|
|
266
|
+
annotations=[{
|
|
267
|
+
"annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
|
|
268
|
+
"category": "car",
|
|
269
|
+
"bbox_3d": {"translation": {"x": 0, "y": 0, "z": 0},
|
|
270
|
+
"size": {"length": 1, "width": 1, "height": 1},
|
|
271
|
+
"rotation": _q_from_yaw(0.0)},
|
|
272
|
+
"num_lidar_pts": 0, "visibility": "", "velocity": None,
|
|
273
|
+
"attributes": [], "prev": "", "next": "", "score": None,
|
|
274
|
+
}],
|
|
275
|
+
)
|
|
276
|
+
loader = ClipLoader(tmp_path)
|
|
277
|
+
with pytest.raises(ValueError, match="Invalid frame"):
|
|
278
|
+
loader.annotations_for_sample("s-0", frame="never_exists_frame")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# --- TransformChain.get_ego_from_frame_id ---
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_transform_chain_ego_from_frame_id_base_link_is_identity(tmp_path):
|
|
285
|
+
_write_clip(
|
|
286
|
+
tmp_path,
|
|
287
|
+
schema_version="0.6",
|
|
288
|
+
annotation_frame_id="base_link",
|
|
289
|
+
sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
|
|
290
|
+
calibrated=[{
|
|
291
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
292
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
293
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
294
|
+
"intrinsics": None, "distortion": None,
|
|
295
|
+
}],
|
|
296
|
+
)
|
|
297
|
+
loader = ClipLoader(tmp_path)
|
|
298
|
+
from x4d_devkit.core.transform import TransformChain
|
|
299
|
+
chain = TransformChain(loader)
|
|
300
|
+
T = chain.get_ego_from_frame_id("base_link")
|
|
301
|
+
np.testing.assert_allclose(T.translation, [0, 0, 0])
|
|
302
|
+
np.testing.assert_allclose(T.rotation, np.eye(3))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_transform_chain_ego_from_frame_id_resolves_via_sensor_frame_id(tmp_path):
|
|
306
|
+
"""When a sensor declares frame_id != channel, lookup uses frame_id."""
|
|
307
|
+
_write_clip(
|
|
308
|
+
tmp_path,
|
|
309
|
+
schema_version="0.6",
|
|
310
|
+
annotation_frame_id="base_link",
|
|
311
|
+
sensors_meta={"recon_pcd": {"modality": "lidar", "frame_id": "camera-front"}},
|
|
312
|
+
calibrated=[{
|
|
313
|
+
"calibrated_sensor_token": "cs-0", "channel": "recon_pcd",
|
|
314
|
+
"translation": {"x": 1.0, "y": 0, "z": 0},
|
|
315
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
316
|
+
"intrinsics": None, "distortion": None,
|
|
317
|
+
}],
|
|
318
|
+
)
|
|
319
|
+
loader = ClipLoader(tmp_path)
|
|
320
|
+
from x4d_devkit.core.transform import TransformChain
|
|
321
|
+
chain = TransformChain(loader)
|
|
322
|
+
T = chain.get_ego_from_frame_id("camera-front")
|
|
323
|
+
np.testing.assert_allclose(T.translation, [1.0, 0, 0])
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# --- Validator: schema 0.6 strict checks ---
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_validate_v06_missing_annotation_frame_id_errors(tmp_path):
|
|
330
|
+
_write_clip(
|
|
331
|
+
tmp_path,
|
|
332
|
+
schema_version="0.6",
|
|
333
|
+
annotation_frame_id=None, # missing on purpose
|
|
334
|
+
sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
|
|
335
|
+
calibrated=[{
|
|
336
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
337
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
338
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
339
|
+
"intrinsics": None, "distortion": None,
|
|
340
|
+
}],
|
|
341
|
+
)
|
|
342
|
+
report = validate_clip(tmp_path)
|
|
343
|
+
assert any(i.check == "annotation_frame_id" for i in report.errors)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_validate_v06_missing_sensor_frame_id_errors(tmp_path):
|
|
347
|
+
_write_clip(
|
|
348
|
+
tmp_path,
|
|
349
|
+
schema_version="0.6",
|
|
350
|
+
annotation_frame_id="base_link",
|
|
351
|
+
sensors_meta={"lidar": {"modality": "lidar"}}, # no frame_id
|
|
352
|
+
calibrated=[{
|
|
353
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
354
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
355
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
356
|
+
"intrinsics": None, "distortion": None,
|
|
357
|
+
}],
|
|
358
|
+
)
|
|
359
|
+
report = validate_clip(tmp_path)
|
|
360
|
+
assert any(i.check == "sensor_frame_id" for i in report.errors)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_validate_v06_unresolvable_annotation_frame_errors(tmp_path):
|
|
364
|
+
_write_clip(
|
|
365
|
+
tmp_path,
|
|
366
|
+
schema_version="0.6",
|
|
367
|
+
annotation_frame_id="non_existent_frame",
|
|
368
|
+
sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
|
|
369
|
+
calibrated=[{
|
|
370
|
+
"calibrated_sensor_token": "cs-0", "channel": "lidar",
|
|
371
|
+
"translation": {"x": 0, "y": 0, "z": 0},
|
|
372
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
373
|
+
"intrinsics": None, "distortion": None,
|
|
374
|
+
}],
|
|
375
|
+
)
|
|
376
|
+
report = validate_clip(tmp_path)
|
|
377
|
+
msgs = [i.message for i in report.errors if i.check == "annotation_frame_id"]
|
|
378
|
+
assert any("does not resolve" in m for m in msgs)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_validate_v06_passes_with_lidar_top_annotation_frame(tmp_path):
|
|
382
|
+
"""Scenario 1 (nuScenes-style): annotation_frame_id == sensor's own frame_id."""
|
|
383
|
+
ann = {
|
|
384
|
+
"annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
|
|
385
|
+
"category": "car",
|
|
386
|
+
"bbox_3d": {"translation": {"x": 5, "y": 0, "z": 0},
|
|
387
|
+
"size": {"length": 4, "width": 2, "height": 1.5},
|
|
388
|
+
"rotation": _q_from_yaw(0.0)},
|
|
389
|
+
"num_lidar_pts": 50, "visibility": "4", "velocity": None,
|
|
390
|
+
"attributes": [], "prev": "", "next": "", "score": None,
|
|
391
|
+
}
|
|
392
|
+
_write_clip(
|
|
393
|
+
tmp_path,
|
|
394
|
+
schema_version="0.6",
|
|
395
|
+
annotation_frame_id="LIDAR_TOP",
|
|
396
|
+
sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
|
|
397
|
+
"point_format": {"fields": ["x", "y", "z", "intensity", "t"],
|
|
398
|
+
"types": ["float32"] * 5,
|
|
399
|
+
"bytes_per_point": 20}}},
|
|
400
|
+
calibrated=[{
|
|
401
|
+
"calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
|
|
402
|
+
"translation": {"x": 0, "y": 0, "z": 1.5},
|
|
403
|
+
"rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
|
|
404
|
+
"intrinsics": None, "distortion": None,
|
|
405
|
+
}],
|
|
406
|
+
annotations=[ann],
|
|
407
|
+
)
|
|
408
|
+
pcd_path = tmp_path / "samples/x/0.bin"
|
|
409
|
+
pcd_path.parent.mkdir(parents=True, exist_ok=True)
|
|
410
|
+
np.zeros(5, dtype=np.float32).tofile(pcd_path)
|
|
411
|
+
|
|
412
|
+
report = validate_clip(tmp_path)
|
|
413
|
+
schema_errors = [i for i in report.errors if i.check in ("annotation_frame_id", "sensor_frame_id")]
|
|
414
|
+
assert schema_errors == [], f"unexpected schema errors: {schema_errors}"
|