x4d-devkit 0.6.0__tar.gz → 0.7.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 (47) hide show
  1. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/pyproject.toml +1 -1
  3. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_coordinate_frames.py +5 -2
  4. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_models.py +79 -2
  5. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/__init__.py +4 -0
  6. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/core/__init__.py +4 -0
  7. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/core/loader.py +12 -3
  8. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/core/models.py +97 -6
  9. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
  10. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/LICENSE +0 -0
  11. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/README.md +0 -0
  12. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/setup.cfg +0 -0
  13. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_detection_eval.py +0 -0
  14. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_fusion.py +0 -0
  15. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_loader.py +0 -0
  16. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_matching.py +0 -0
  17. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_metrics.py +0 -0
  18. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_nuscenes_converter.py +0 -0
  19. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_token.py +0 -0
  20. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_transform.py +0 -0
  21. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_transform_chain.py +0 -0
  22. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_v06_api_additions.py +0 -0
  23. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/tests/test_validation.py +0 -0
  24. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/cli.py +0 -0
  25. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/__init__.py +0 -0
  26. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/checkpoints.py +0 -0
  27. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/client.py +0 -0
  28. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/clips.py +0 -0
  29. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/projects.py +0 -0
  30. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/client/test_sets.py +0 -0
  31. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/converters/__init__.py +0 -0
  32. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/converters/nuscenes.py +0 -0
  33. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/core/token.py +0 -0
  34. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/core/transform.py +0 -0
  35. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/eval/__init__.py +0 -0
  36. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/eval/detection.py +0 -0
  37. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/eval/matching.py +0 -0
  38. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/eval/metrics.py +0 -0
  39. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/fusion.py +0 -0
  40. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/validation/__init__.py +0 -0
  41. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/validation/report.py +0 -0
  42. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit/validation/validator.py +0 -0
  43. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit.egg-info/SOURCES.txt +0 -0
  44. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  45. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  46. {x4d_devkit-0.6.0 → x4d_devkit-0.7.0}/x4d_devkit.egg-info/requires.txt +0 -0
  47. {x4d_devkit-0.6.0 → x4d_devkit-0.7.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.6.0
3
+ Version: 0.7.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.6.0"
7
+ version = "0.7.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"
@@ -38,11 +38,14 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
38
38
  "sensors": {
39
39
  "LIDAR_TOP": {
40
40
  "modality": "lidar",
41
- "point_format": {"fields": ["x", "y", "z", "intensity"]}
41
+ "point_format": {
42
+ "fields": ["x", "y", "z", "intensity"],
43
+ "types": ["float32"] * 4,
44
+ "bytes_per_point": 16,
45
+ },
42
46
  },
43
47
  "CAM_FRONT": {
44
48
  "modality": "camera",
45
- "point_format": None
46
49
  }
47
50
  }
48
51
  }))
@@ -9,6 +9,8 @@ from x4d_devkit.core.models import (
9
9
  SampleData,
10
10
  Annotation,
11
11
  Instance,
12
+ PointFormat,
13
+ SensorInfo,
12
14
  )
13
15
 
14
16
 
@@ -16,17 +18,21 @@ class TestClipMeta:
16
18
  def test_construction(self):
17
19
  meta = ClipMeta(
18
20
  clip_id="20180724_n015_scene-0061_000",
19
- schema_version="0.1",
21
+ schema_version="0.5",
20
22
  session_id="n015-2018-07-24-11-22-45+0800",
21
23
  vehicle_id="n015",
22
24
  capture_time_utc="2018-07-24T00:00:00Z",
23
25
  duration_s=19.5,
24
26
  num_keyframes=39,
25
27
  num_sweeps=305,
26
- sensors={"CAM_FRONT": {"modality": "camera", "resolution": [1600, 900]}},
28
+ sensors={
29
+ "CAM_FRONT": SensorInfo(modality="camera", resolution=(1600, 900)),
30
+ },
27
31
  )
28
32
  assert meta.clip_id == "20180724_n015_scene-0061_000"
29
33
  assert meta.num_keyframes == 39
34
+ assert meta.sensors["CAM_FRONT"].modality == "camera"
35
+ assert meta.sensors["CAM_FRONT"].resolution == (1600, 900)
30
36
 
31
37
  def test_frozen(self):
32
38
  meta = ClipMeta(
@@ -153,3 +159,74 @@ class TestInstance:
153
159
  first_annotation_token="a1", last_annotation_token="a5",
154
160
  )
155
161
  assert inst.num_annotations == 5
162
+
163
+
164
+ class TestPointFormat:
165
+ def test_construction_normalises_to_tuples(self):
166
+ pf = PointFormat(
167
+ fields=["x", "y", "z", "intensity", "time"],
168
+ types=["float32"] * 5,
169
+ bytes_per_point=20,
170
+ )
171
+ assert isinstance(pf.fields, tuple)
172
+ assert isinstance(pf.types, tuple)
173
+ assert pf.num_fields == 5
174
+
175
+ def test_mismatched_lengths_rejected(self):
176
+ with pytest.raises(ValueError, match="must have equal length"):
177
+ PointFormat(fields=["x", "y", "z"], types=["float32", "float32"], bytes_per_point=12)
178
+
179
+ def test_round_trip_via_dict(self):
180
+ d = {
181
+ "fields": ["x", "y", "z", "intensity", "time"],
182
+ "types": ["float32"] * 5,
183
+ "bytes_per_point": 20,
184
+ }
185
+ pf = PointFormat.from_dict(d)
186
+ assert pf.to_dict() == d
187
+
188
+
189
+ class TestSensorInfo:
190
+ def test_lidar_with_point_format(self):
191
+ d = {
192
+ "modality": "lidar",
193
+ "point_format": {
194
+ "fields": ["x", "y", "z", "intensity", "time"],
195
+ "types": ["float32"] * 5,
196
+ "bytes_per_point": 20,
197
+ },
198
+ }
199
+ si = SensorInfo.from_dict(d)
200
+ assert si.modality == "lidar"
201
+ assert si.point_format is not None
202
+ assert si.point_format.num_fields == 5
203
+ assert si.resolution is None
204
+ assert si.to_dict() == d
205
+
206
+ def test_camera_with_resolution(self):
207
+ d = {"modality": "camera", "resolution": [1600, 900]}
208
+ si = SensorInfo.from_dict(d)
209
+ assert si.modality == "camera"
210
+ assert si.point_format is None
211
+ assert si.resolution == (1600, 900)
212
+ assert si.to_dict() == d
213
+
214
+ def test_extras_round_trip(self):
215
+ """Unknown keys (e.g. producer-specific extras) survive a round-trip."""
216
+ d = {
217
+ "modality": "lidar",
218
+ "point_format": {
219
+ "fields": ["x", "y", "z"],
220
+ "types": ["float32", "float32", "float32"],
221
+ "bytes_per_point": 12,
222
+ },
223
+ "fused_from": ["lidar_top", "lidar_left"],
224
+ }
225
+ si = SensorInfo.from_dict(d)
226
+ assert si.extras == {"fused_from": ["lidar_top", "lidar_left"]}
227
+ out = si.to_dict()
228
+ assert out["fused_from"] == ["lidar_top", "lidar_left"]
229
+
230
+ def test_camera_no_point_format_in_dict(self):
231
+ si = SensorInfo.from_dict({"modality": "camera"})
232
+ assert si.point_format is None
@@ -15,9 +15,11 @@ from x4d_devkit.core import (
15
15
  ClipMeta,
16
16
  EgoPose,
17
17
  Instance,
18
+ PointFormat,
18
19
  Sample,
19
20
  SampleData,
20
21
  SchemaVersionMismatch,
22
+ SensorInfo,
21
23
  Transform,
22
24
  TransformChain,
23
25
  generate_token,
@@ -47,9 +49,11 @@ __all__ = [
47
49
  "DetectionResult",
48
50
  "EgoPose",
49
51
  "Instance",
52
+ "PointFormat",
50
53
  "Sample",
51
54
  "SampleData",
52
55
  "SchemaVersionMismatch",
56
+ "SensorInfo",
53
57
  "Transform",
54
58
  "TransformChain",
55
59
  "ValidationReport",
@@ -7,8 +7,10 @@ from x4d_devkit.core.models import (
7
7
  ClipMeta,
8
8
  EgoPose,
9
9
  Instance,
10
+ PointFormat,
10
11
  Sample,
11
12
  SampleData,
13
+ SensorInfo,
12
14
  )
13
15
  from x4d_devkit.core.token import generate_token
14
16
  from x4d_devkit.core.transform import Transform, TransformChain
@@ -20,9 +22,11 @@ __all__ = [
20
22
  "ClipMeta",
21
23
  "EgoPose",
22
24
  "Instance",
25
+ "PointFormat",
23
26
  "Sample",
24
27
  "SampleData",
25
28
  "SchemaVersionMismatch",
29
+ "SensorInfo",
26
30
  "Transform",
27
31
  "TransformChain",
28
32
  "generate_token",
@@ -17,6 +17,7 @@ from x4d_devkit.core.models import (
17
17
  SampleData,
18
18
  Annotation,
19
19
  Instance,
20
+ SensorInfo,
20
21
  )
21
22
  from x4d_devkit.core.transform import Transform, TransformChain, rotation_matrix_to_quat
22
23
 
@@ -122,6 +123,10 @@ class ClipLoader:
122
123
 
123
124
  def _load_meta(self) -> ClipMeta:
124
125
  d = self._read_json("meta.json")
126
+ sensors = {
127
+ channel: SensorInfo.from_dict(raw)
128
+ for channel, raw in (d.get("sensors") or {}).items()
129
+ }
125
130
  return ClipMeta(
126
131
  clip_id=d["clip_id"],
127
132
  schema_version=d["schema_version"],
@@ -131,7 +136,7 @@ class ClipLoader:
131
136
  duration_s=d["duration_s"],
132
137
  num_keyframes=d["keyframe_count"],
133
138
  num_sweeps=d["sweep_count"],
134
- sensors=d["sensors"],
139
+ sensors=sensors,
135
140
  )
136
141
 
137
142
  def _load_calibrated_sensors(self) -> list[CalibratedSensor]:
@@ -400,8 +405,12 @@ class ClipLoader:
400
405
  raise ValueError(f"Invalid frame '{frame}', must be one of {_VALID_FRAMES}")
401
406
 
402
407
  sensor_info = self._meta.sensors[sd.channel]
403
- pf = sensor_info["point_format"]
404
- num_fields = len(pf["fields"])
408
+ if sensor_info.point_format is None:
409
+ raise ValueError(
410
+ f"channel '{sd.channel}' has no point_format in meta.json — "
411
+ f"cannot load as a point cloud (modality={sensor_info.modality})"
412
+ )
413
+ num_fields = sensor_info.point_format.num_fields
405
414
 
406
415
  path = self._clip_dir / sd.file_path
407
416
  raw = np.fromfile(str(path), dtype=np.float32)
@@ -12,28 +12,119 @@ Coordinate conventions:
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- from dataclasses import dataclass
15
+ from dataclasses import dataclass, field
16
16
  from typing import Any
17
17
 
18
18
  import numpy as np
19
19
 
20
20
 
21
+ @dataclass(frozen=True)
22
+ class PointFormat:
23
+ """Binary layout of a LiDAR point cloud .bin file.
24
+
25
+ Each point is laid out as ``len(fields)`` consecutive scalars of the
26
+ matching numpy dtype (``types[i]`` for ``fields[i]``). ``bytes_per_point``
27
+ is the on-disk stride in bytes.
28
+
29
+ Attributes:
30
+ fields: Per-component field names, e.g. ``["x", "y", "z", "intensity", "time"]``.
31
+ types: Per-component numpy dtype names, e.g. ``["float32", ...]``.
32
+ bytes_per_point: Total bytes per point on disk (== sum of dtype sizes).
33
+ """
34
+
35
+ fields: tuple[str, ...]
36
+ types: tuple[str, ...]
37
+ bytes_per_point: int
38
+
39
+ def __post_init__(self) -> None:
40
+ # Normalise sequences → immutable tuples for free hashability.
41
+ object.__setattr__(self, "fields", tuple(self.fields))
42
+ object.__setattr__(self, "types", tuple(self.types))
43
+ if len(self.fields) != len(self.types):
44
+ raise ValueError(
45
+ f"PointFormat: fields ({len(self.fields)}) and types "
46
+ f"({len(self.types)}) must have equal length"
47
+ )
48
+
49
+ @classmethod
50
+ def from_dict(cls, d: dict[str, Any]) -> PointFormat:
51
+ return cls(
52
+ fields=tuple(d["fields"]),
53
+ types=tuple(d["types"]),
54
+ bytes_per_point=int(d["bytes_per_point"]),
55
+ )
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ return {
59
+ "fields": list(self.fields),
60
+ "types": list(self.types),
61
+ "bytes_per_point": self.bytes_per_point,
62
+ }
63
+
64
+ @property
65
+ def num_fields(self) -> int:
66
+ return len(self.fields)
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class SensorInfo:
71
+ """Per-channel sensor metadata embedded in ``meta.json``.
72
+
73
+ Attributes:
74
+ modality: ``"lidar"`` or ``"camera"``.
75
+ point_format: Binary point layout (LiDAR only; ``None`` for cameras).
76
+ resolution: ``(width, height)`` in pixels (cameras only; ``None`` for LiDAR).
77
+ extras: Any additional fields the producer wrote into the sensor dict.
78
+ Preserved verbatim so ``to_dict()`` round-trips unknown keys.
79
+ """
80
+
81
+ modality: str
82
+ point_format: PointFormat | None = None
83
+ resolution: tuple[int, int] | None = None
84
+ extras: dict[str, Any] = field(default_factory=dict)
85
+
86
+ @classmethod
87
+ def from_dict(cls, d: dict[str, Any]) -> SensorInfo:
88
+ modality = d["modality"]
89
+ pf_raw = d.get("point_format")
90
+ point_format = PointFormat.from_dict(pf_raw) if pf_raw is not None else None
91
+ res_raw = d.get("resolution")
92
+ resolution = (int(res_raw[0]), int(res_raw[1])) if res_raw is not None else None
93
+ known = {"modality", "point_format", "resolution"}
94
+ extras = {k: v for k, v in d.items() if k not in known}
95
+ return cls(
96
+ modality=modality,
97
+ point_format=point_format,
98
+ resolution=resolution,
99
+ extras=extras,
100
+ )
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ out: dict[str, Any] = {"modality": self.modality}
104
+ if self.point_format is not None:
105
+ out["point_format"] = self.point_format.to_dict()
106
+ if self.resolution is not None:
107
+ out["resolution"] = list(self.resolution)
108
+ out.update(self.extras)
109
+ return out
110
+
111
+
21
112
  @dataclass(frozen=True)
22
113
  class ClipMeta:
23
114
  """Metadata for an X4D clip (from meta.json).
24
115
 
25
116
  Attributes:
26
117
  clip_id: Unique clip identifier (e.g. "20180724_n015_scene-0061_000").
27
- schema_version: X4D format version (e.g. "0.2").
118
+ schema_version: X4D format version (e.g. "0.5").
28
119
  session_id: Recording session identifier.
29
120
  vehicle_id: Vehicle identifier.
30
121
  capture_time_utc: Capture timestamp in ISO 8601 format.
31
122
  duration_s: Clip duration in seconds.
32
123
  num_keyframes: Number of keyframes (annotated frames).
33
124
  num_sweeps: Number of sweep frames (non-annotated intermediate frames).
34
- sensors: Sensor metadata dict, keyed by channel name. Each value
35
- contains ``modality`` ("lidar"/"camera") and ``point_format``
36
- (for LiDAR channels, describes binary file field layout).
125
+ sensors: Per-channel sensor metadata, keyed by channel name. Each
126
+ value is a ``SensorInfo`` carrying modality + (for LiDAR) the
127
+ binary ``PointFormat`` describing the .bin file layout.
37
128
  """
38
129
 
39
130
  clip_id: str
@@ -44,7 +135,7 @@ class ClipMeta:
44
135
  duration_s: float
45
136
  num_keyframes: int
46
137
  num_sweeps: int
47
- sensors: dict[str, dict[str, Any]]
138
+ sensors: dict[str, SensorInfo]
48
139
 
49
140
 
50
141
  @dataclass(frozen=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.6.0
3
+ Version: 0.7.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