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.
Files changed (46) hide show
  1. {x4d_devkit-0.4.0/x4d_devkit.egg-info → x4d_devkit-0.5.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/pyproject.toml +1 -1
  3. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_coordinate_frames.py +31 -30
  4. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/converters/nuscenes.py +44 -12
  5. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/loader.py +16 -12
  6. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/models.py +12 -8
  7. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/detection.py +9 -16
  8. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/validator.py +13 -0
  9. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0/x4d_devkit.egg-info}/PKG-INFO +1 -1
  10. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/LICENSE +0 -0
  11. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/README.md +0 -0
  12. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/setup.cfg +0 -0
  13. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_detection_eval.py +0 -0
  14. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_fusion.py +0 -0
  15. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_loader.py +0 -0
  16. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_matching.py +0 -0
  17. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_metrics.py +0 -0
  18. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_models.py +0 -0
  19. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_nuscenes_converter.py +0 -0
  20. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_token.py +0 -0
  21. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_transform.py +0 -0
  22. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_transform_chain.py +0 -0
  23. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/tests/test_validation.py +0 -0
  24. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/__init__.py +0 -0
  25. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/cli.py +0 -0
  26. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/__init__.py +0 -0
  27. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/checkpoints.py +0 -0
  28. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/client.py +0 -0
  29. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/clips.py +0 -0
  30. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/projects.py +0 -0
  31. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/client/test_sets.py +0 -0
  32. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/converters/__init__.py +0 -0
  33. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/__init__.py +0 -0
  34. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/token.py +0 -0
  35. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/core/transform.py +0 -0
  36. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/__init__.py +0 -0
  37. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/matching.py +0 -0
  38. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/eval/metrics.py +0 -0
  39. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/fusion.py +0 -0
  40. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/__init__.py +0 -0
  41. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit/validation/report.py +0 -0
  42. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/SOURCES.txt +0 -0
  43. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  44. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  45. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/x4d_devkit.egg-info/requires.txt +0 -0
  46. {x4d_devkit-0.4.0 → x4d_devkit-0.5.0}/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.4.0
3
+ Version: 0.5.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.4.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": 10.0, "y": 3.0, "z": 0.0},
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 test_world_frame_default(self, loader):
281
- """Default frame should return annotations in world coordinates."""
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 test_world_frame_explicit(self, loader):
287
- """Explicit frame='world' should match default."""
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
- anns_world = loader.annotations_for_sample("s-0", frame="world")
290
- assert len(anns_default) == len(anns_world)
291
- np.testing.assert_allclose(anns_default[0].translation, anns_world[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 test_ego_frame_first_sample(self, loader):
294
- """frame='ego' for first sample (ego at origin): same as world."""
295
- anns = loader.annotations_for_sample("s-0", frame="ego")
296
- # ego at origin -> world-to-ego is identity
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 test_ego_frame_second_sample(self, loader):
300
- """frame='ego' for second sample (ego at x=10): subtracts ego offset."""
301
- anns = loader.annotations_for_sample("s-1", frame="ego")
302
- # annotation at world (10,3,0), ego at (10,0,0) -> ego frame (0,3,0)
303
- np.testing.assert_allclose(anns[0].translation, [0.0, 3.0, 0.0], atol=1e-5)
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 test_ego_frame_velocity_rotated(self, loader):
306
- """Velocity should be rotated (not translated) to ego frame."""
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 test_ego_frame_preserves_other_fields(self, loader):
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 test_ego_frame_returns_new_objects(self, loader):
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
- # Originals should be unmodified
325
- np.testing.assert_allclose(anns_world[0].translation, [10.0, 3.0, 0.0])
326
- # Ego should be different
327
- assert not np.allclose(anns_ego[0].translation, anns_world[0].translation, atol=0.1)
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 local coords."""
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
- t_local = apply_se3(R_inv, t_inv, t_global)
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
- R_ann_local = R_inv @ R_ann_global
418
- q_ann_local = rotation_matrix_to_quat(R_ann_local)
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
- # Compute velocity in global coords, then rotate to local
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
- vel_local = R_inv @ vel_vec
434
- velocity = {"vx": float(vel_local[0]), "vy": float(vel_local[1]), "vz": float(vel_local[2])}
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(t_local.tolist()),
474
+ "translation": translation_to_named(t_ego.tolist()),
443
475
  "size": size_wlh_to_named(ann["size"]),
444
- "rotation": quat_wxyz_to_named(q_ann_local),
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 world frame."""
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 = "world",
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
- - "world" (default): native annotation frame, no transform.
308
- - "ego": transform from world to ego frame using the ego_pose
309
- of the LiDAR sample_data for this sample (or first sample_data
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 == "world" or not anns:
325
+ if frame == "ego" or not anns:
322
326
  return anns
323
327
 
324
- # frame == "ego": need ego_pose for this sample
325
- ego_from_world = self._get_ego_from_world_for_sample(sample_token)
326
- return [self._transform_annotation(a, ego_from_world) for a in anns]
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 _get_ego_from_world_for_sample(self, sample_token: str) -> Transform:
426
- """Get the inverse ego_pose transform for a sample.
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).inverse
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 world frame.
157
+ """A 3D bounding box annotation in **per-sample ego frame** (v0.5).
158
158
 
159
- Annotations are natively stored in the clip's local world frame
160
- (first ego pose origin). Use ``ClipLoader.annotations_for_sample(..., frame="ego")``
161
- to transform to ego frame.
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 world frame.
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
- num_lidar_pts: Number of LiDAR points inside this box.
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, or None.
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
- ep_by_token = {ep.token: ep for ep in clip.ego_poses}
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
- ego_pos = sample_ego_pos.get(sample.token)
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
- if ego_pos is not None:
250
- dist = float(np.linalg.norm(ann.translation[:2] - ego_pos[:2]))
251
- if dist > class_range[det_name]:
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.4.0
3
+ Version: 0.5.0
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
File without changes
File without changes
File without changes
File without changes