x4d-devkit 0.4.0__tar.gz → 0.5.0__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.4.0/x4d_devkit.egg-info → x4d_devkit-0.5.0}/PKG-INFO +1 -1
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/pyproject.toml +1 -1
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_coordinate_frames.py +31 -30
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/converters/nuscenes.py +44 -12
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/loader.py +16 -12
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/models.py +12 -8
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/detection.py +9 -16
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/validator.py +13 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0/x4d_devkit.egg-info}/PKG-INFO +1 -1
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/LICENSE +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/README.md +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/setup.cfg +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_fusion.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_loader.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_matching.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_models.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_nuscenes_converter.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_token.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_transform.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_validation.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/fusion.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/SOURCES.txt +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/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.5.0"
|
|
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"
|
|
@@ -154,7 +154,9 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
154
154
|
pts.tofile(str(lidar_dir / "frame0.bin"))
|
|
155
155
|
pts.tofile(str(lidar_dir / "frame1.bin"))
|
|
156
156
|
|
|
157
|
-
# -- annotation.json --
|
|
157
|
+
# -- annotation.json (v0.5: per-sample ego frame) --
|
|
158
|
+
# s-0 ego at origin; s-1 ego at (10, 0, 0). Same world pos (5,3,0) and (10,3,0)
|
|
159
|
+
# for the two frames maps to ego (5,3,0) and (0,3,0) respectively.
|
|
158
160
|
(clip_dir / "annotation.json").write_text(json.dumps([
|
|
159
161
|
{
|
|
160
162
|
"annotation_token": "ann-0",
|
|
@@ -180,7 +182,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
180
182
|
"instance_token": "inst-0",
|
|
181
183
|
"category": "car",
|
|
182
184
|
"bbox_3d": {
|
|
183
|
-
"translation": {"x":
|
|
185
|
+
"translation": {"x": 0.0, "y": 3.0, "z": 0.0},
|
|
184
186
|
"size": {"length": 4.5, "width": 2.0, "height": 1.5},
|
|
185
187
|
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.0, "qw": 1.0},
|
|
186
188
|
},
|
|
@@ -277,54 +279,53 @@ class TestLoadPointCloudFrames:
|
|
|
277
279
|
# ---------------------------------------------------------------------------
|
|
278
280
|
|
|
279
281
|
class TestAnnotationsForSampleFrames:
|
|
280
|
-
def
|
|
281
|
-
"""Default frame should return annotations in
|
|
282
|
+
def test_ego_frame_default(self, loader):
|
|
283
|
+
"""Default frame should return annotations in per-sample ego coordinates (v0.5 native)."""
|
|
282
284
|
anns = loader.annotations_for_sample("s-0")
|
|
283
285
|
assert len(anns) == 1
|
|
284
286
|
np.testing.assert_allclose(anns[0].translation, [5.0, 3.0, 0.0])
|
|
285
287
|
|
|
286
|
-
def
|
|
287
|
-
"""Explicit frame='
|
|
288
|
+
def test_ego_frame_explicit(self, loader):
|
|
289
|
+
"""Explicit frame='ego' should match default."""
|
|
288
290
|
anns_default = loader.annotations_for_sample("s-0")
|
|
289
|
-
|
|
290
|
-
assert len(anns_default) == len(
|
|
291
|
-
np.testing.assert_allclose(anns_default[0].translation,
|
|
291
|
+
anns_ego = loader.annotations_for_sample("s-0", frame="ego")
|
|
292
|
+
assert len(anns_default) == len(anns_ego)
|
|
293
|
+
np.testing.assert_allclose(anns_default[0].translation, anns_ego[0].translation)
|
|
292
294
|
|
|
293
|
-
def
|
|
294
|
-
"""frame='
|
|
295
|
-
anns = loader.annotations_for_sample("s-0", frame="
|
|
296
|
-
# ego at origin ->
|
|
295
|
+
def test_world_frame_first_sample(self, loader):
|
|
296
|
+
"""frame='world' for first sample (ego at origin): same as ego."""
|
|
297
|
+
anns = loader.annotations_for_sample("s-0", frame="world")
|
|
298
|
+
# ego at origin -> ego-to-world is identity
|
|
297
299
|
np.testing.assert_allclose(anns[0].translation, [5.0, 3.0, 0.0], atol=1e-5)
|
|
298
300
|
|
|
299
|
-
def
|
|
300
|
-
"""frame='
|
|
301
|
-
anns = loader.annotations_for_sample("s-1", frame="
|
|
302
|
-
#
|
|
303
|
-
np.testing.assert_allclose(anns[0].translation, [
|
|
301
|
+
def test_world_frame_second_sample(self, loader):
|
|
302
|
+
"""frame='world' for second sample (ego at x=10): adds ego offset."""
|
|
303
|
+
anns = loader.annotations_for_sample("s-1", frame="world")
|
|
304
|
+
# ego frame (0,3,0), ego at (10,0,0) -> world (10,3,0)
|
|
305
|
+
np.testing.assert_allclose(anns[0].translation, [10.0, 3.0, 0.0], atol=1e-5)
|
|
304
306
|
|
|
305
|
-
def
|
|
306
|
-
"""
|
|
307
|
+
def test_ego_frame_velocity_unchanged(self, loader):
|
|
308
|
+
"""Native (ego) frame: velocity returned as stored."""
|
|
307
309
|
anns = loader.annotations_for_sample("s-0", frame="ego")
|
|
308
|
-
# ego rotation is identity, so velocity unchanged
|
|
309
310
|
np.testing.assert_allclose(anns[0].velocity, [5.0, 0.0, 0.0], atol=1e-5)
|
|
310
311
|
|
|
311
|
-
def
|
|
312
|
-
"""Non-pose fields should be preserved."""
|
|
313
|
-
anns_world = loader.annotations_for_sample("s-0", frame="world")
|
|
312
|
+
def test_world_frame_preserves_other_fields(self, loader):
|
|
313
|
+
"""Non-pose fields should be preserved across frame transforms."""
|
|
314
314
|
anns_ego = loader.annotations_for_sample("s-0", frame="ego")
|
|
315
|
+
anns_world = loader.annotations_for_sample("s-0", frame="world")
|
|
315
316
|
assert anns_ego[0].token == anns_world[0].token
|
|
316
317
|
assert anns_ego[0].category == anns_world[0].category
|
|
317
318
|
np.testing.assert_allclose(anns_ego[0].size, anns_world[0].size)
|
|
318
319
|
assert anns_ego[0].num_lidar_pts == anns_world[0].num_lidar_pts
|
|
319
320
|
|
|
320
|
-
def
|
|
321
|
+
def test_world_frame_returns_new_objects(self, loader):
|
|
321
322
|
"""Transformed annotations should be new objects, not mutated originals."""
|
|
322
|
-
anns_world = loader.annotations_for_sample("s-1", frame="world")
|
|
323
323
|
anns_ego = loader.annotations_for_sample("s-1", frame="ego")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
anns_world = loader.annotations_for_sample("s-1", frame="world")
|
|
325
|
+
# Originals (ego, native) should be unmodified
|
|
326
|
+
np.testing.assert_allclose(anns_ego[0].translation, [0.0, 3.0, 0.0])
|
|
327
|
+
# World should be different
|
|
328
|
+
assert not np.allclose(anns_world[0].translation, anns_ego[0].translation, atol=0.1)
|
|
328
329
|
|
|
329
330
|
def test_sensor_frame_raises(self, loader):
|
|
330
331
|
with pytest.raises(ValueError, match="sensor frame"):
|
|
@@ -380,13 +380,36 @@ class NuScenesConverter:
|
|
|
380
380
|
return None
|
|
381
381
|
return {"vx": float(vel[0]), "vy": float(vel[1]), "vz": float(vel[2])}
|
|
382
382
|
|
|
383
|
+
def _get_lidar_ego_pose_for_sample(self, sample: dict) -> tuple[np.ndarray, list]:
|
|
384
|
+
"""Return (R_global, t_global) of the LIDAR_TOP keyframe ego_pose for a sample.
|
|
385
|
+
|
|
386
|
+
v0.5 convention: the ego frame for an annotation = the ego_pose of the
|
|
387
|
+
sample's LiDAR keyframe sample_data.
|
|
388
|
+
"""
|
|
389
|
+
lidar_sd_token = sample["data"].get("LIDAR_TOP")
|
|
390
|
+
if lidar_sd_token is None:
|
|
391
|
+
# Fallback to whichever target-channel sd has the earliest ts.
|
|
392
|
+
earliest_ts = None
|
|
393
|
+
earliest_ep = None
|
|
394
|
+
for channel, sd_token in sample["data"].items():
|
|
395
|
+
if channel not in TARGET_CHANNELS:
|
|
396
|
+
continue
|
|
397
|
+
sd = self.nusc.get("sample_data", sd_token)
|
|
398
|
+
ep = self.nusc.get("ego_pose", sd["ego_pose_token"])
|
|
399
|
+
if earliest_ts is None or ep["timestamp"] < earliest_ts:
|
|
400
|
+
earliest_ts = ep["timestamp"]
|
|
401
|
+
earliest_ep = ep
|
|
402
|
+
ep = earliest_ep
|
|
403
|
+
else:
|
|
404
|
+
sd = self.nusc.get("sample_data", lidar_sd_token)
|
|
405
|
+
ep = self.nusc.get("ego_pose", sd["ego_pose_token"])
|
|
406
|
+
R = quat_to_rotation_matrix(ep["rotation"])
|
|
407
|
+
return R, ep["translation"]
|
|
408
|
+
|
|
383
409
|
def _extract_annotations(
|
|
384
410
|
self, scene: dict, clip_id: str
|
|
385
411
|
) -> tuple[list[dict], list[dict]]:
|
|
386
|
-
"""Extract annotations and instances for the scene, in
|
|
387
|
-
R_first, t_first = self._get_first_ego_pose_transform(scene)
|
|
388
|
-
R_inv, t_inv = invert_se3(R_first, t_first)
|
|
389
|
-
|
|
412
|
+
"""Extract annotations and instances for the scene, in **per-sample ego frame** (v0.5)."""
|
|
390
413
|
annotations: list[dict] = []
|
|
391
414
|
instance_anns: dict[str, list[dict]] = {}
|
|
392
415
|
|
|
@@ -403,6 +426,10 @@ class NuScenesConverter:
|
|
|
403
426
|
sample = self.nusc.get("sample", sample_token)
|
|
404
427
|
new_sample_token = sample_token_map[sample_token]
|
|
405
428
|
|
|
429
|
+
# Per-sample ego_pose (LIDAR_TOP keyframe) — annotation frame.
|
|
430
|
+
R_ego_global, t_ego_global = self._get_lidar_ego_pose_for_sample(sample)
|
|
431
|
+
R_inv, t_inv = invert_se3(R_ego_global, t_ego_global)
|
|
432
|
+
|
|
406
433
|
for ann_token in sample["anns"]:
|
|
407
434
|
ann = self.nusc.get("sample_annotation", ann_token)
|
|
408
435
|
nusc_instance_token = ann["instance_token"]
|
|
@@ -410,12 +437,13 @@ class NuScenesConverter:
|
|
|
410
437
|
self.nusc.get("instance", nusc_instance_token)["category_token"]
|
|
411
438
|
]["name"]
|
|
412
439
|
|
|
440
|
+
# Global → ego (this sample's ego)
|
|
413
441
|
t_global = np.asarray(ann["translation"])
|
|
414
|
-
|
|
442
|
+
t_ego = apply_se3(R_inv, t_inv, t_global)
|
|
415
443
|
|
|
416
444
|
R_ann_global = quat_to_rotation_matrix(ann["rotation"])
|
|
417
|
-
|
|
418
|
-
|
|
445
|
+
R_ann_ego = R_inv @ R_ann_global
|
|
446
|
+
q_ann_ego = rotation_matrix_to_quat(R_ann_ego)
|
|
419
447
|
|
|
420
448
|
attributes = [
|
|
421
449
|
self._attribute_by_token[at]["name"]
|
|
@@ -426,12 +454,16 @@ class NuScenesConverter:
|
|
|
426
454
|
int(ann["visibility_token"]) if ann["visibility_token"] else None
|
|
427
455
|
)
|
|
428
456
|
|
|
429
|
-
#
|
|
457
|
+
# Velocity: nuScenes computes globally; rotate into this sample's ego.
|
|
430
458
|
vel_global = self._compute_velocity(ann_token)
|
|
431
459
|
if vel_global is not None:
|
|
432
460
|
vel_vec = np.array([vel_global["vx"], vel_global["vy"], vel_global["vz"]])
|
|
433
|
-
|
|
434
|
-
velocity = {
|
|
461
|
+
vel_ego = R_inv @ vel_vec
|
|
462
|
+
velocity = {
|
|
463
|
+
"vx": float(vel_ego[0]),
|
|
464
|
+
"vy": float(vel_ego[1]),
|
|
465
|
+
"vz": float(vel_ego[2]),
|
|
466
|
+
}
|
|
435
467
|
else:
|
|
436
468
|
velocity = None
|
|
437
469
|
|
|
@@ -439,9 +471,9 @@ class NuScenesConverter:
|
|
|
439
471
|
"sample_token": new_sample_token,
|
|
440
472
|
"category": category,
|
|
441
473
|
"bbox_3d": {
|
|
442
|
-
"translation": translation_to_named(
|
|
474
|
+
"translation": translation_to_named(t_ego.tolist()),
|
|
443
475
|
"size": size_wlh_to_named(ann["size"]),
|
|
444
|
-
"rotation": quat_wxyz_to_named(
|
|
476
|
+
"rotation": quat_wxyz_to_named(q_ann_ego),
|
|
445
477
|
},
|
|
446
478
|
"num_lidar_pts": ann["num_lidar_pts"],
|
|
447
479
|
"visibility": visibility,
|
|
@@ -250,7 +250,10 @@ class ClipLoader:
|
|
|
250
250
|
|
|
251
251
|
@property
|
|
252
252
|
def annotations(self) -> list[Annotation]:
|
|
253
|
-
"""All annotations in
|
|
253
|
+
"""All annotations in their **native frame** (ego since v0.5; world for v0.4 clips).
|
|
254
|
+
|
|
255
|
+
Use ``annotations_for_sample(token, frame=...)`` to request a specific frame.
|
|
256
|
+
"""
|
|
254
257
|
return self._annotations
|
|
255
258
|
|
|
256
259
|
@property
|
|
@@ -297,16 +300,17 @@ class ClipLoader:
|
|
|
297
300
|
def annotations_for_sample(
|
|
298
301
|
self,
|
|
299
302
|
sample_token: str,
|
|
300
|
-
frame: FrameType = "
|
|
303
|
+
frame: FrameType = "ego",
|
|
301
304
|
) -> list[Annotation]:
|
|
302
305
|
"""All annotations for a given sample.
|
|
303
306
|
|
|
304
307
|
Args:
|
|
305
308
|
sample_token: The sample token.
|
|
306
309
|
frame: Target coordinate frame.
|
|
307
|
-
- "
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
- "ego" (default, v0.5 native): no transform — annotations are
|
|
311
|
+
stored in the per-sample ego frame.
|
|
312
|
+
- "world": transform from ego to world using the ego_pose of
|
|
313
|
+
the LiDAR sample_data for this sample (or first sample_data
|
|
310
314
|
if no LiDAR).
|
|
311
315
|
|
|
312
316
|
Returns:
|
|
@@ -318,12 +322,12 @@ class ClipLoader:
|
|
|
318
322
|
raise ValueError("Annotations cannot be transformed to sensor frame")
|
|
319
323
|
|
|
320
324
|
anns = self._ann_by_sample.get(sample_token, [])
|
|
321
|
-
if frame == "
|
|
325
|
+
if frame == "ego" or not anns:
|
|
322
326
|
return anns
|
|
323
327
|
|
|
324
|
-
# frame == "
|
|
325
|
-
|
|
326
|
-
return [self._transform_annotation(a,
|
|
328
|
+
# frame == "world": transform per-sample-ego → world via ego_pose.
|
|
329
|
+
world_from_ego = self._get_world_from_ego_for_sample(sample_token)
|
|
330
|
+
return [self._transform_annotation(a, world_from_ego) for a in anns]
|
|
327
331
|
|
|
328
332
|
def load_point_cloud(
|
|
329
333
|
self,
|
|
@@ -422,8 +426,8 @@ class ClipLoader:
|
|
|
422
426
|
self._transform_chain = TransformChain(self)
|
|
423
427
|
return self._transform_chain
|
|
424
428
|
|
|
425
|
-
def
|
|
426
|
-
"""Get the
|
|
429
|
+
def _get_world_from_ego_for_sample(self, sample_token: str) -> Transform:
|
|
430
|
+
"""Get the world_from_ego transform for a sample.
|
|
427
431
|
|
|
428
432
|
Uses the ego_pose from the LiDAR sample_data, or the first sample_data
|
|
429
433
|
if no LiDAR channel exists.
|
|
@@ -439,7 +443,7 @@ class ClipLoader:
|
|
|
439
443
|
sds[0],
|
|
440
444
|
)
|
|
441
445
|
chain = self._get_transform_chain()
|
|
442
|
-
return chain.get_world_from_ego(sd.ego_pose_token)
|
|
446
|
+
return chain.get_world_from_ego(sd.ego_pose_token)
|
|
443
447
|
|
|
444
448
|
@staticmethod
|
|
445
449
|
def _transform_annotation(ann: Annotation, T: Transform) -> Annotation:
|
|
@@ -154,23 +154,27 @@ class SampleData:
|
|
|
154
154
|
|
|
155
155
|
@dataclass(frozen=True)
|
|
156
156
|
class Annotation:
|
|
157
|
-
"""A 3D bounding box annotation in
|
|
157
|
+
"""A 3D bounding box annotation in **per-sample ego frame** (v0.5).
|
|
158
158
|
|
|
159
|
-
Annotations are natively stored in the
|
|
160
|
-
(
|
|
161
|
-
to transform to
|
|
159
|
+
Annotations are natively stored in the ego frame of the sample they belong
|
|
160
|
+
to (the ego_pose of the sample's LiDAR keyframe). Use
|
|
161
|
+
``ClipLoader.annotations_for_sample(..., frame="world")`` to transform to
|
|
162
|
+
the clip's local world frame when needed (rare — typically only for
|
|
163
|
+
cross-frame trajectory visualization).
|
|
162
164
|
|
|
163
165
|
Attributes:
|
|
164
166
|
token: Unique annotation token.
|
|
165
167
|
sample_token: Token of the sample this annotation belongs to.
|
|
166
168
|
instance_token: Token of the tracked object instance.
|
|
167
169
|
category: Object category string (e.g. "car", "pedestrian").
|
|
168
|
-
translation: (3,) float64 array — box center [x, y, z] in
|
|
170
|
+
translation: (3,) float64 array — box center [x, y, z] in this sample's ego frame.
|
|
169
171
|
size: (3,) float64 array — box dimensions [length, width, height] in meters.
|
|
170
|
-
rotation: (4,) float64 array — box orientation as quaternion [qx, qy, qz, qw]
|
|
171
|
-
|
|
172
|
+
rotation: (4,) float64 array — box orientation as quaternion [qx, qy, qz, qw],
|
|
173
|
+
relative to this sample's ego frame.
|
|
174
|
+
num_lidar_pts: Number of LiDAR points inside this box (real-counted, ≥ 0).
|
|
172
175
|
visibility: Visibility level as string ("1" to "4", or "" if unknown).
|
|
173
|
-
velocity: (3,) float64 array — velocity [vx, vy, vz] in m/s,
|
|
176
|
+
velocity: (3,) float64 array — velocity [vx, vy, vz] in m/s, **ego frame**
|
|
177
|
+
(forward = +x, left = +y), or None.
|
|
174
178
|
attributes: List of attribute strings (e.g. ["vehicle.moving"]).
|
|
175
179
|
prev: Token of previous annotation of same instance, or "" if first.
|
|
176
180
|
next: Token of next annotation of same instance, or "" if last.
|
|
@@ -223,33 +223,26 @@ def _extract_boxes(
|
|
|
223
223
|
config: DetectionConfig,
|
|
224
224
|
max_boxes: int | None = None,
|
|
225
225
|
) -> dict[str, list[dict]]:
|
|
226
|
-
"""Extract annotation boxes grouped by sample_token, with distance filtering.
|
|
227
|
-
|
|
226
|
+
"""Extract annotation boxes grouped by sample_token, with distance filtering.
|
|
227
|
+
|
|
228
|
+
v0.5: annotations are in per-sample ego frame, so distance from ego is
|
|
229
|
+
simply ``||ann.translation[:2]||`` — no subtraction of ego_pose required.
|
|
230
|
+
"""
|
|
228
231
|
category_map = config.category_map or {}
|
|
229
232
|
class_names = config.class_names
|
|
230
233
|
class_range = config.class_range
|
|
231
234
|
|
|
232
|
-
# Map sample_token → ego position (for distance filtering)
|
|
233
|
-
sample_ego_pos: dict[str, np.ndarray] = {}
|
|
234
|
-
for sd in clip.sample_data:
|
|
235
|
-
if sd.sample_token not in sample_ego_pos and sd.is_keyframe:
|
|
236
|
-
ep = ep_by_token.get(sd.ego_pose_token)
|
|
237
|
-
if ep is not None:
|
|
238
|
-
sample_ego_pos[sd.sample_token] = ep.translation
|
|
239
|
-
|
|
240
235
|
result: dict[str, list[dict]] = {}
|
|
241
236
|
for sample in clip.samples:
|
|
242
|
-
|
|
243
|
-
anns = clip.annotations_for_sample(sample.token)
|
|
237
|
+
anns = clip.annotations_for_sample(sample.token) # default = ego in v0.5
|
|
244
238
|
boxes = []
|
|
245
239
|
for ann in anns:
|
|
246
240
|
det_name = map_category(ann.category, category_map, class_names)
|
|
247
241
|
if det_name is None or det_name not in class_range:
|
|
248
242
|
continue
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
continue
|
|
243
|
+
dist = float(np.linalg.norm(ann.translation[:2]))
|
|
244
|
+
if dist > class_range[det_name]:
|
|
245
|
+
continue
|
|
253
246
|
boxes.append({
|
|
254
247
|
"translation": ann.translation,
|
|
255
248
|
"size": ann.size,
|
|
@@ -218,6 +218,19 @@ def validate_clip(clip_dir: str | Path) -> ValidationReport:
|
|
|
218
218
|
)
|
|
219
219
|
break
|
|
220
220
|
|
|
221
|
+
# num_lidar_pts must be a non-negative integer (v0.5+: real-computed,
|
|
222
|
+
# 0 means "no points in box", not a default placeholder).
|
|
223
|
+
npts = ann.get("num_lidar_pts")
|
|
224
|
+
if not isinstance(npts, int) or isinstance(npts, bool) or npts < 0:
|
|
225
|
+
issues.append(
|
|
226
|
+
ValidationIssue(
|
|
227
|
+
"annotation_num_lidar_pts",
|
|
228
|
+
"error",
|
|
229
|
+
f"Invalid num_lidar_pts={npts!r} (must be int >= 0)",
|
|
230
|
+
ann.get("annotation_token", ""),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
221
234
|
# Sample token reference
|
|
222
235
|
if ann.get("sample_token") not in sample_tokens:
|
|
223
236
|
issues.append(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|