x4d-devkit 0.2.2__tar.gz → 0.3.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.2.2/x4d_devkit.egg-info → x4d_devkit-0.3.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/pyproject.toml +1 -1
  3. x4d_devkit-0.3.0/tests/test_fusion.py +134 -0
  4. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/__init__.py +4 -0
  5. x4d_devkit-0.3.0/x4d_devkit/fusion.py +104 -0
  6. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0/x4d_devkit.egg-info}/PKG-INFO +1 -1
  7. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  8. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/LICENSE +0 -0
  9. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/README.md +0 -0
  10. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/setup.cfg +0 -0
  11. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_coordinate_frames.py +0 -0
  12. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_detection_eval.py +0 -0
  13. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_loader.py +0 -0
  14. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_matching.py +0 -0
  15. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_metrics.py +0 -0
  16. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_models.py +0 -0
  17. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_nuscenes_converter.py +0 -0
  18. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_token.py +0 -0
  19. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_transform.py +0 -0
  20. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_transform_chain.py +0 -0
  21. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/tests/test_validation.py +0 -0
  22. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/cli.py +0 -0
  23. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/__init__.py +0 -0
  24. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/checkpoints.py +0 -0
  25. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/client.py +0 -0
  26. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/clips.py +0 -0
  27. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/projects.py +0 -0
  28. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/client/test_sets.py +0 -0
  29. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/converters/__init__.py +0 -0
  30. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/converters/nuscenes.py +0 -0
  31. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/core/__init__.py +0 -0
  32. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/core/loader.py +0 -0
  33. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/core/models.py +0 -0
  34. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/core/token.py +0 -0
  35. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/core/transform.py +0 -0
  36. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/eval/__init__.py +0 -0
  37. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/eval/detection.py +0 -0
  38. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/eval/matching.py +0 -0
  39. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/eval/metrics.py +0 -0
  40. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/validation/__init__.py +0 -0
  41. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/validation/report.py +0 -0
  42. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit/validation/validator.py +0 -0
  43. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  44. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  45. {x4d_devkit-0.2.2 → x4d_devkit-0.3.0}/x4d_devkit.egg-info/requires.txt +0 -0
  46. {x4d_devkit-0.2.2 → x4d_devkit-0.3.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.2.2
3
+ Version: 0.3.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.2.2"
7
+ version = "0.3.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"
@@ -0,0 +1,134 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from x4d_devkit.fusion import (
5
+ LIDAR_FUSED_DTYPE,
6
+ FusionSource,
7
+ fuse_pointclouds,
8
+ )
9
+
10
+
11
+ def _identity_pose():
12
+ m = np.eye(4, dtype=np.float64)
13
+ return m
14
+
15
+
16
+ def test_fused_dtype_is_5_float32_fields():
17
+ assert LIDAR_FUSED_DTYPE.itemsize == 20
18
+ assert LIDAR_FUSED_DTYPE.names == ("x", "y", "z", "intensity", "t")
19
+ for name in LIDAR_FUSED_DTYPE.names:
20
+ assert LIDAR_FUSED_DTYPE.fields[name][0] == np.dtype("<f4")
21
+
22
+
23
+ def test_single_lidar_identity_pose_no_per_point_ts():
24
+ pts = np.array(
25
+ [(1.0, 2.0, 3.0, 0.5),
26
+ (4.0, 5.0, 6.0, 0.9)],
27
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4")],
28
+ )
29
+ out = fuse_pointclouds(
30
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(), per_point_ts_field=None)],
31
+ frame_ref_time_us=0,
32
+ )
33
+ assert out.dtype == LIDAR_FUSED_DTYPE
34
+ assert out.shape == (2,)
35
+ np.testing.assert_array_equal(out["x"], [1.0, 4.0])
36
+ np.testing.assert_allclose(out["intensity"], [0.5, 0.9], rtol=1e-6)
37
+ np.testing.assert_array_equal(out["t"], [0.0, 0.0])
38
+
39
+
40
+ def test_multi_lidar_concat_with_per_lidar_pose():
41
+ pts_a = np.array([(1.0, 0.0, 0.0, 0.1)],
42
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4")])
43
+ pts_b = np.array([(0.0, 1.0, 0.0, 0.2)],
44
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4")])
45
+ pose_b = np.eye(4, dtype=np.float64); pose_b[0, 3] = 5.0 # shift +5 in x
46
+ out = fuse_pointclouds(
47
+ sources=[
48
+ FusionSource(points=pts_a, lidar_to_ego=_identity_pose(), per_point_ts_field=None),
49
+ FusionSource(points=pts_b, lidar_to_ego=pose_b, per_point_ts_field=None),
50
+ ],
51
+ frame_ref_time_us=0,
52
+ )
53
+ assert out.shape == (2,)
54
+ np.testing.assert_allclose(out["x"], [1.0, 5.0])
55
+ np.testing.assert_allclose(out["y"], [0.0, 1.0])
56
+
57
+
58
+ def test_per_point_ts_offset_is_seconds_relative_to_frame_ref():
59
+ # Points at +1ms and +2ms vs frame ref
60
+ pts = np.array(
61
+ [(1.0, 0.0, 0.0, 0.0, 1_000_000_000),
62
+ (2.0, 0.0, 0.0, 0.0, 1_000_001_000)],
63
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4"), ("timestamp", "<u8")],
64
+ )
65
+ out = fuse_pointclouds(
66
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(),
67
+ per_point_ts_field="timestamp", per_point_ts_unit="ns")],
68
+ frame_ref_time_us=1_000_000, # 1ms after epoch
69
+ )
70
+ np.testing.assert_allclose(out["t"], [0.0, 1e-6], atol=1e-9)
71
+
72
+
73
+ def test_missing_intensity_raises():
74
+ pts = np.array([(1.0, 2.0, 3.0)],
75
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4")])
76
+ with pytest.raises(ValueError, match="intensity"):
77
+ fuse_pointclouds(
78
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(), per_point_ts_field=None)],
79
+ frame_ref_time_us=0,
80
+ )
81
+
82
+
83
+ def test_per_point_ts_offset_microseconds_unit():
84
+ # Points at +1ms vs frame ref, given in microseconds
85
+ pts = np.array(
86
+ [(1.0, 0.0, 0.0, 0.0, 1_000_000),
87
+ (2.0, 0.0, 0.0, 0.0, 1_001_000)],
88
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4"), ("timestamp", "<u8")],
89
+ )
90
+ out = fuse_pointclouds(
91
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(),
92
+ per_point_ts_field="timestamp", per_point_ts_unit="us")],
93
+ frame_ref_time_us=1_000_000,
94
+ )
95
+ np.testing.assert_allclose(out["t"], [0.0, 1e-3], atol=1e-9)
96
+
97
+
98
+ def test_unsupported_ts_unit_raises():
99
+ pts = np.array(
100
+ [(1.0, 0.0, 0.0, 0.0, 0)],
101
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4"), ("timestamp", "<u8")],
102
+ )
103
+ with pytest.raises(ValueError, match="unsupported per_point_ts_unit"):
104
+ fuse_pointclouds(
105
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(),
106
+ per_point_ts_field="timestamp", per_point_ts_unit="ms")],
107
+ frame_ref_time_us=0,
108
+ )
109
+
110
+
111
+ def test_unstructured_array_raises():
112
+ pts = np.zeros((3, 4), dtype=np.float32) # plain (N, 4) — not structured
113
+ with pytest.raises(ValueError, match="structured numpy array"):
114
+ fuse_pointclouds(
115
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(), per_point_ts_field=None)],
116
+ frame_ref_time_us=0,
117
+ )
118
+
119
+
120
+ def test_dtype_round_trip_via_tofile(tmp_path):
121
+ pts = np.array(
122
+ [(1.5, -2.5, 3.5, 0.25),
123
+ (10.0, 20.0, 30.0, 0.5)],
124
+ dtype=[("x", "<f4"), ("y", "<f4"), ("z", "<f4"), ("intensity", "<f4")],
125
+ )
126
+ out = fuse_pointclouds(
127
+ sources=[FusionSource(points=pts, lidar_to_ego=_identity_pose(), per_point_ts_field=None)],
128
+ frame_ref_time_us=0,
129
+ )
130
+ path = tmp_path / "test.bin"
131
+ out.tofile(str(path))
132
+ assert path.stat().st_size == 2 * 20 # 2 points * 20 bytes/point
133
+ roundtrip = np.fromfile(str(path), dtype=LIDAR_FUSED_DTYPE)
134
+ np.testing.assert_array_equal(roundtrip, out)
@@ -21,10 +21,14 @@ from x4d_devkit.eval import (
21
21
  nusc_detection_config,
22
22
  )
23
23
  from x4d_devkit.validation import validate_clip, ValidationReport
24
+ from x4d_devkit.fusion import LIDAR_FUSED_DTYPE, FusionSource, fuse_pointclouds # noqa: E402,F401
24
25
 
25
26
  __all__ = [
26
27
  "Annotation",
27
28
  "CalibratedSensor",
29
+ "LIDAR_FUSED_DTYPE",
30
+ "FusionSource",
31
+ "fuse_pointclouds",
28
32
  "ClipLoader",
29
33
  "ClipMeta",
30
34
  "DetectionConfig",
@@ -0,0 +1,104 @@
1
+ """Lidar fusion to ego frame.
2
+
3
+ Single-lidar and multi-lidar share one path: each source provides its points
4
+ in lidar frame plus a 4x4 homogeneous lidar->ego transform. Output is a
5
+ contiguous structured array with the canonical 5-field layout.
6
+
7
+ Used by the X-4D ingestion pipeline to write `samples/lidar/<ts>.bin` and
8
+ `sweeps/lidar/<ts>.bin`. Downstream consumers read with
9
+ `np.fromfile(path, dtype=LIDAR_FUSED_DTYPE)`.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+ import numpy as np
17
+
18
+
19
+ LIDAR_FUSED_DTYPE = np.dtype([
20
+ ("x", "<f4"),
21
+ ("y", "<f4"),
22
+ ("z", "<f4"),
23
+ ("intensity", "<f4"),
24
+ ("t", "<f4"),
25
+ ])
26
+
27
+
28
+ @dataclass
29
+ class FusionSource:
30
+ points: np.ndarray # structured array; must include x, y, z, intensity
31
+ lidar_to_ego: np.ndarray # (4, 4) float64 homogeneous
32
+ per_point_ts_field: Optional[str] = None # name of u64/i64 ts field, e.g. "timestamp"
33
+ per_point_ts_unit: str = "ns" # "ns" | "us"
34
+
35
+
36
+ def _intensity_field(names: tuple[str, ...]) -> str:
37
+ for n in names:
38
+ if "intensity" in n.lower():
39
+ return n
40
+ raise ValueError(
41
+ f"point cloud has no field with 'intensity' in its name (got {names!r}); "
42
+ "fusion refuses to silently zero-fill"
43
+ )
44
+
45
+
46
+ def _transform_xyz(xyz: np.ndarray, T: np.ndarray) -> np.ndarray:
47
+ # xyz: (N, 3) float64; T: (4, 4) float64; returns (N, 3) float32
48
+ homog = np.empty((xyz.shape[0], 4), dtype=np.float64)
49
+ homog[:, :3] = xyz
50
+ homog[:, 3] = 1.0
51
+ out = homog @ T.T
52
+ return out[:, :3].astype(np.float32, copy=False)
53
+
54
+
55
+ def fuse_pointclouds(sources: list[FusionSource], frame_ref_time_us: int) -> np.ndarray:
56
+ """Transform each source into ego frame and concat into the 5-field layout.
57
+
58
+ `frame_ref_time_us` is the synced frame timestamp; per-point `t` is computed
59
+ as `(point_ts - frame_ref) / 1e6` (seconds). When a source has no per-point
60
+ timestamp, `t` is filled with 0 (semantics: no motion-comp info available).
61
+ """
62
+ if not sources:
63
+ return np.empty((0,), dtype=LIDAR_FUSED_DTYPE)
64
+
65
+ chunks: list[np.ndarray] = []
66
+ for src in sources:
67
+ pts = src.points
68
+ if pts.dtype.names is None:
69
+ raise ValueError(
70
+ "FusionSource.points must be a structured numpy array with named fields"
71
+ )
72
+ names = pts.dtype.names
73
+ for required in ("x", "y", "z"):
74
+ if required not in names:
75
+ raise ValueError(f"source missing field {required!r}; got {names!r}")
76
+ intensity_name = _intensity_field(names)
77
+
78
+ xyz = np.column_stack([pts["x"], pts["y"], pts["z"]]).astype(np.float64, copy=False)
79
+ ego_xyz = _transform_xyz(xyz, np.asarray(src.lidar_to_ego, dtype=np.float64))
80
+
81
+ intensity = np.asarray(pts[intensity_name], dtype=np.float32)
82
+
83
+ if src.per_point_ts_field and src.per_point_ts_field in names:
84
+ raw_ts = np.asarray(pts[src.per_point_ts_field], dtype=np.int64)
85
+ if src.per_point_ts_unit == "ns":
86
+ ref_ns = int(frame_ref_time_us) * 1000
87
+ t = ((raw_ts - ref_ns).astype(np.float64) / 1e9).astype(np.float32)
88
+ elif src.per_point_ts_unit == "us":
89
+ ref_us = int(frame_ref_time_us)
90
+ t = ((raw_ts - ref_us).astype(np.float64) / 1e6).astype(np.float32)
91
+ else:
92
+ raise ValueError(f"unsupported per_point_ts_unit: {src.per_point_ts_unit}")
93
+ else:
94
+ t = np.zeros((ego_xyz.shape[0],), dtype=np.float32)
95
+
96
+ out = np.empty((ego_xyz.shape[0],), dtype=LIDAR_FUSED_DTYPE)
97
+ out["x"] = ego_xyz[:, 0]
98
+ out["y"] = ego_xyz[:, 1]
99
+ out["z"] = ego_xyz[:, 2]
100
+ out["intensity"] = intensity
101
+ out["t"] = t
102
+ chunks.append(out)
103
+
104
+ return np.concatenate(chunks, axis=0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.2.2
3
+ Version: 0.3.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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  tests/test_coordinate_frames.py
5
5
  tests/test_detection_eval.py
6
+ tests/test_fusion.py
6
7
  tests/test_loader.py
7
8
  tests/test_matching.py
8
9
  tests/test_metrics.py
@@ -14,6 +15,7 @@ tests/test_transform_chain.py
14
15
  tests/test_validation.py
15
16
  x4d_devkit/__init__.py
16
17
  x4d_devkit/cli.py
18
+ x4d_devkit/fusion.py
17
19
  x4d_devkit.egg-info/PKG-INFO
18
20
  x4d_devkit.egg-info/SOURCES.txt
19
21
  x4d_devkit.egg-info/dependency_links.txt
File without changes
File without changes
File without changes
File without changes