x4d-devkit 0.2.0__tar.gz → 0.2.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.
Files changed (46) hide show
  1. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/PKG-INFO +27 -7
  2. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/README.md +26 -6
  3. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/pyproject.toml +1 -1
  4. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/__init__.py +2 -0
  5. x4d_devkit-0.2.1/x4d_devkit/client/__init__.py +13 -0
  6. x4d_devkit-0.2.1/x4d_devkit/client/checkpoints.py +64 -0
  7. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/client/client.py +23 -6
  8. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/client/clips.py +52 -8
  9. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/client/projects.py +11 -0
  10. x4d_devkit-0.2.1/x4d_devkit/client/test_sets.py +63 -0
  11. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/core/loader.py +64 -5
  12. x4d_devkit-0.2.1/x4d_devkit/core/models.py +215 -0
  13. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/core/transform.py +111 -14
  14. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/eval/detection.py +40 -1
  15. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/validation/report.py +20 -2
  16. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/validation/validator.py +25 -1
  17. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit.egg-info/PKG-INFO +27 -7
  18. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  19. x4d_devkit-0.2.0/x4d_devkit/client/__init__.py +0 -3
  20. x4d_devkit-0.2.0/x4d_devkit/core/models.py +0 -106
  21. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/LICENSE +0 -0
  22. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/setup.cfg +0 -0
  23. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_coordinate_frames.py +0 -0
  24. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_detection_eval.py +0 -0
  25. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_loader.py +0 -0
  26. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_matching.py +0 -0
  27. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_metrics.py +0 -0
  28. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_models.py +0 -0
  29. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_nuscenes_converter.py +0 -0
  30. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_token.py +0 -0
  31. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_transform.py +0 -0
  32. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_transform_chain.py +0 -0
  33. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/tests/test_validation.py +0 -0
  34. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/cli.py +0 -0
  35. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/converters/__init__.py +0 -0
  36. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/converters/nuscenes.py +0 -0
  37. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/core/__init__.py +0 -0
  38. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/core/token.py +0 -0
  39. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/eval/__init__.py +0 -0
  40. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/eval/matching.py +0 -0
  41. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/eval/metrics.py +0 -0
  42. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit/validation/__init__.py +0 -0
  43. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  44. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  45. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/x4d_devkit.egg-info/requires.txt +0 -0
  46. {x4d_devkit-0.2.0 → x4d_devkit-0.2.1}/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.0
3
+ Version: 0.2.1
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
@@ -57,13 +57,33 @@ pip install x4d-devkit[client]
57
57
  ```python
58
58
  from x4d_devkit import ClipLoader
59
59
 
60
- clip = ClipLoader("/path/to/clip")
61
- print(clip.meta)
60
+ loader = ClipLoader("/path/to/clip")
61
+ print(loader.meta)
62
62
 
63
- for sample in clip.samples:
64
- # Access sensor data
65
- for sd in clip.sample_data_by_sample(sample.token):
66
- print(sd.channel, sd.filename)
63
+ for sample in loader.samples:
64
+ for sd in loader.sample_data_for_sample(sample.token):
65
+ print(sd.channel, sd.file_path)
66
+ ```
67
+
68
+ ### Coordinate frame transforms
69
+
70
+ Point clouds and annotations can be loaded in different coordinate frames:
71
+
72
+ ```python
73
+ loader = ClipLoader("/path/to/clip")
74
+ sd = loader.sample_data_for_channel("LIDAR_TOP")[0]
75
+
76
+ # Load point cloud in different frames
77
+ pts_sensor = loader.load_point_cloud(sd, frame="sensor") # raw (default)
78
+ pts_ego = loader.load_point_cloud(sd, frame="ego") # sensor → ego
79
+ pts_world = loader.load_point_cloud(sd, frame="world") # sensor → world
80
+
81
+ # Get annotations in ego frame (for training)
82
+ anns_ego = loader.annotations_for_sample(sample.token, frame="ego")
83
+
84
+ # Get the transform matrix directly
85
+ T = loader.get_transform(sd, from_frame="sensor", to_frame="world")
86
+ pts_world = T.apply(pts_sensor[:, :3]) # or use T.as_matrix for 4x4
67
87
  ```
68
88
 
69
89
  ### Validate a clip
@@ -25,13 +25,33 @@ pip install x4d-devkit[client]
25
25
  ```python
26
26
  from x4d_devkit import ClipLoader
27
27
 
28
- clip = ClipLoader("/path/to/clip")
29
- print(clip.meta)
28
+ loader = ClipLoader("/path/to/clip")
29
+ print(loader.meta)
30
30
 
31
- for sample in clip.samples:
32
- # Access sensor data
33
- for sd in clip.sample_data_by_sample(sample.token):
34
- print(sd.channel, sd.filename)
31
+ for sample in loader.samples:
32
+ for sd in loader.sample_data_for_sample(sample.token):
33
+ print(sd.channel, sd.file_path)
34
+ ```
35
+
36
+ ### Coordinate frame transforms
37
+
38
+ Point clouds and annotations can be loaded in different coordinate frames:
39
+
40
+ ```python
41
+ loader = ClipLoader("/path/to/clip")
42
+ sd = loader.sample_data_for_channel("LIDAR_TOP")[0]
43
+
44
+ # Load point cloud in different frames
45
+ pts_sensor = loader.load_point_cloud(sd, frame="sensor") # raw (default)
46
+ pts_ego = loader.load_point_cloud(sd, frame="ego") # sensor → ego
47
+ pts_world = loader.load_point_cloud(sd, frame="world") # sensor → world
48
+
49
+ # Get annotations in ego frame (for training)
50
+ anns_ego = loader.annotations_for_sample(sample.token, frame="ego")
51
+
52
+ # Get the transform matrix directly
53
+ T = loader.get_transform(sd, from_frame="sensor", to_frame="world")
54
+ pts_world = T.apply(pts_sensor[:, :3]) # or use T.as_matrix for 4x4
35
55
  ```
36
56
 
37
57
  ### Validate a clip
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.2.0"
7
+ version = "0.2.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"
@@ -1,5 +1,6 @@
1
1
  """X-4D dataset format SDK."""
2
2
 
3
+ from x4d_devkit.client import X4DClient
3
4
  from x4d_devkit.core import (
4
5
  Annotation,
5
6
  CalibratedSensor,
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "Transform",
37
38
  "TransformChain",
38
39
  "ValidationReport",
40
+ "X4DClient",
39
41
  "generate_token",
40
42
  "nusc_detection_config",
41
43
  "validate_clip",
@@ -0,0 +1,13 @@
1
+ from x4d_devkit.client.checkpoints import CheckpointsAPI
2
+ from x4d_devkit.client.client import X4DClient
3
+ from x4d_devkit.client.clips import ClipsAPI
4
+ from x4d_devkit.client.projects import ProjectsAPI
5
+ from x4d_devkit.client.test_sets import TestSetsAPI
6
+
7
+ __all__ = [
8
+ "CheckpointsAPI",
9
+ "ClipsAPI",
10
+ "ProjectsAPI",
11
+ "TestSetsAPI",
12
+ "X4DClient",
13
+ ]
@@ -0,0 +1,64 @@
1
+ """Checkpoints API wrapper — training project registers its trained models
2
+ so the platform's Model Compare view can run inference on them.
3
+
4
+ This is the primary integration point between a training project and the
5
+ platform: after every epoch the training CLI calls ``register(...)`` and
6
+ the checkpoint immediately shows up in Compare's dropdown.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from x4d_devkit.client.client import X4DClient
14
+
15
+
16
+ class CheckpointsAPI:
17
+ def __init__(self, client: "X4DClient"):
18
+ self._client = client
19
+
20
+ def register(
21
+ self,
22
+ project_id: int,
23
+ name: str,
24
+ service_id: str,
25
+ ckpt_uri: str,
26
+ metadata: dict[str, Any] | None = None,
27
+ ) -> dict:
28
+ """Register a trained checkpoint for inference.
29
+
30
+ Idempotent on (project_id, service_id, ckpt_uri) — re-calling with
31
+ the same arguments updates name/metadata instead of creating a dup.
32
+
33
+ Args:
34
+ project_id: X-4D project the checkpoint belongs to.
35
+ name: Human-readable name, e.g. ``"exp_hg1_v1 · epoch 5"``.
36
+ service_id: The ``service_id`` of the training/inference service
37
+ that owns the checkpoint file (must be registered with the
38
+ platform, otherwise inference dispatch will 404).
39
+ ckpt_uri: Local path to the .pth file *on the service container*.
40
+ Remote/S3 URIs are not yet supported.
41
+ metadata: Arbitrary JSONB bag. By convention keys used by the
42
+ inference backend are ``cfg_file``, ``epoch``, ``is_best``,
43
+ ``training_task_id``.
44
+ """
45
+ return self._client.post(
46
+ "/checkpoints",
47
+ json={
48
+ "project_id": project_id,
49
+ "name": name,
50
+ "service_id": service_id,
51
+ "ckpt_uri": ckpt_uri,
52
+ "metadata": metadata or {},
53
+ },
54
+ )
55
+
56
+ def list(self, project_id: int) -> list[dict]:
57
+ resp = self._client.get(f"/projects/{project_id}/checkpoints")
58
+ return resp["items"]
59
+
60
+ def get(self, checkpoint_id: int) -> dict:
61
+ return self._client.get(f"/checkpoints/{checkpoint_id}")
62
+
63
+ def delete(self, checkpoint_id: int) -> None:
64
+ self._client.delete(f"/checkpoints/{checkpoint_id}")
@@ -1,19 +1,27 @@
1
- """X-4D platform API client."""
1
+ """X-4D platform API client.
2
+
3
+ Entry point for training projects to query the platform: list clips (with
4
+ `exclude_test=True` default for safe-by-default training-data access),
5
+ download archives via the file proxy, list test sets, and register trained
6
+ checkpoints for inference.
7
+ """
2
8
  from __future__ import annotations
3
9
 
4
10
  import httpx
5
11
 
6
- from x4d_devkit.client.projects import ProjectsAPI
12
+ from x4d_devkit.client.checkpoints import CheckpointsAPI
7
13
  from x4d_devkit.client.clips import ClipsAPI
14
+ from x4d_devkit.client.projects import ProjectsAPI
15
+ from x4d_devkit.client.test_sets import TestSetsAPI
8
16
 
9
17
 
10
18
  class X4DClient:
11
19
  """Client for the X-4D platform REST API.
12
20
 
13
21
  Args:
14
- base_url: Platform API base URL (e.g. "http://localhost:8000")
22
+ base_url: Platform API base URL (e.g. "http://backend:8000")
15
23
  api_key: Optional API key for authentication (future use)
16
- timeout: Request timeout in seconds
24
+ timeout: Request timeout in seconds (default 30; downloads bypass this)
17
25
  """
18
26
 
19
27
  def __init__(
@@ -32,20 +40,29 @@ class X4DClient:
32
40
  )
33
41
  self.projects = ProjectsAPI(self)
34
42
  self.clips = ClipsAPI(self)
43
+ self.test_sets = TestSetsAPI(self)
44
+ self.checkpoints = CheckpointsAPI(self)
35
45
 
36
46
  def _url(self, path: str) -> str:
37
47
  return f"{self._api_prefix}{path}"
38
48
 
39
- def get(self, path: str, **kwargs) -> dict:
49
+ def get(self, path: str, **kwargs):
40
50
  resp = self._http.get(self._url(path), **kwargs)
41
51
  resp.raise_for_status()
42
52
  return resp.json()
43
53
 
44
- def post(self, path: str, **kwargs) -> dict:
54
+ def post(self, path: str, **kwargs):
45
55
  resp = self._http.post(self._url(path), **kwargs)
46
56
  resp.raise_for_status()
47
57
  return resp.json()
48
58
 
59
+ def delete(self, path: str, **kwargs):
60
+ resp = self._http.delete(self._url(path), **kwargs)
61
+ resp.raise_for_status()
62
+ if resp.content:
63
+ return resp.json()
64
+ return None
65
+
49
66
  def close(self):
50
67
  self._http.close()
51
68
 
@@ -21,8 +21,15 @@ class ClipsAPI:
21
21
  page_size: int = 100,
22
22
  lifecycle: str | None = None,
23
23
  has_archive: bool | None = None,
24
+ exclude_test: bool = True,
24
25
  ) -> dict:
25
- """List clips in a project with optional filters."""
26
+ """List clips in a project with optional filters.
27
+
28
+ ``exclude_test`` defaults to **True** so that training pipelines are
29
+ safe-by-default — a clip that belongs to any test set is hidden from
30
+ this call unless you opt in. Pass ``exclude_test=False`` for admin /
31
+ inspection flows that want the full project inventory.
32
+ """
26
33
  params: dict = {
27
34
  "project_id": project_id,
28
35
  "page": page,
@@ -32,10 +39,15 @@ class ClipsAPI:
32
39
  params["lifecycle"] = lifecycle
33
40
  if has_archive is not None:
34
41
  params["has_archive"] = str(has_archive).lower()
42
+ if exclude_test:
43
+ params["exclude_test"] = "true"
35
44
  return self._client.get("/clips", params=params)
36
45
 
37
46
  def list_all(self, project_id: int, **kwargs) -> list[dict]:
38
- """List ALL clips in a project (handles pagination automatically)."""
47
+ """List ALL clips in a project (handles pagination automatically).
48
+
49
+ Inherits ``exclude_test=True`` default from :meth:`list`.
50
+ """
39
51
  all_items = []
40
52
  page = 1
41
53
  while True:
@@ -64,6 +76,11 @@ class ClipsAPI:
64
76
  ) -> Path:
65
77
  """Download and optionally extract a clip's tar archive.
66
78
 
79
+ Goes through the platform's ``/api/v1/files/{tar_path}`` proxy
80
+ rather than MinIO presigned URLs. This is the only reliable path
81
+ in multi-container deployments — presigned URLs point at the MinIO
82
+ hostname which is not routable from inside a training container.
83
+
67
84
  Args:
68
85
  clip_id: Clip ID to download
69
86
  output_dir: Directory to save/extract to
@@ -75,11 +92,16 @@ class ClipsAPI:
75
92
  output_dir = Path(output_dir)
76
93
  output_dir.mkdir(parents=True, exist_ok=True)
77
94
 
78
- url = self.get_archive_download_url(clip_id)
95
+ # Look up the tar_path from the archive record (always relative,
96
+ # e.g. "clips/abc.tar") and stream through the file proxy.
97
+ info = self.get_archive_status(clip_id)
98
+ tar_path_remote = info.get("tar_path") or f"clips/{clip_id}.tar"
79
99
  tar_path = output_dir / f"{clip_id}.tar"
80
100
 
81
- # Stream download
82
- with self._client._http.stream("GET", url) as resp:
101
+ with self._client._http.stream(
102
+ "GET",
103
+ f"{self._client._api_prefix}/files/{tar_path_remote}",
104
+ ) as resp:
83
105
  resp.raise_for_status()
84
106
  with open(tar_path, "wb") as f:
85
107
  for chunk in resp.iter_bytes(chunk_size=8 * 1024 * 1024):
@@ -87,10 +109,32 @@ class ClipsAPI:
87
109
 
88
110
  if extract:
89
111
  clip_dir = output_dir / clip_id
90
- clip_dir.mkdir(parents=True, exist_ok=True)
112
+ # Extract to a temp dir first and find the real clip dir — tar
113
+ # may or may not wrap its contents in a top-level folder.
114
+ extract_tmp = output_dir / f"_extract_{clip_id}"
115
+ if extract_tmp.exists():
116
+ import shutil
117
+ shutil.rmtree(extract_tmp)
118
+ extract_tmp.mkdir(parents=True)
91
119
  with tarfile.open(tar_path, "r") as tar:
92
- tar.extractall(path=clip_dir)
93
- tar_path.unlink()
120
+ tar.extractall(path=extract_tmp)
121
+ tar_path.unlink(missing_ok=True)
122
+
123
+ meta_files = list(extract_tmp.rglob("meta.json"))
124
+ if meta_files:
125
+ actual = meta_files[0].parent
126
+ if clip_dir.exists():
127
+ import shutil
128
+ shutil.rmtree(clip_dir)
129
+ import shutil
130
+ shutil.move(str(actual), str(clip_dir))
131
+ if extract_tmp.exists() and extract_tmp != clip_dir:
132
+ shutil.rmtree(extract_tmp, ignore_errors=True)
133
+ else:
134
+ import shutil
135
+ if clip_dir.exists():
136
+ shutil.rmtree(clip_dir)
137
+ shutil.move(str(extract_tmp), str(clip_dir))
94
138
  return clip_dir
95
139
 
96
140
  return tar_path
@@ -23,3 +23,14 @@ class ProjectsAPI:
23
23
  def get(self, project_id: int) -> dict:
24
24
  """Get a project by ID."""
25
25
  return self._client.get(f"/projects/{project_id}")
26
+
27
+ def list_annotation_classes(self, project_id: int) -> list[dict]:
28
+ """List the annotation class definitions configured for a project.
29
+
30
+ Each item has ``{id, label, color, shortcut?, default_size?}``. The
31
+ platform returns raw labels as configured — no simplification, no
32
+ mapping. The training project owns its own `class_mapping:` in its
33
+ experiment yaml and maps these to its model's class space.
34
+ """
35
+ resp = self._client.get(f"/projects/{project_id}/annotation-classes")
36
+ return resp.get("classes", [])
@@ -0,0 +1,63 @@
1
+ """Test sets API wrapper — benchmark clip selections managed by the platform.
2
+
3
+ Test sets are the platform's way of claiming "these clips are the benchmark:
4
+ don't train on them, don't let experiments move them". See the platform
5
+ CLAUDE.md section on Benchmark Architecture for the design rationale.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from x4d_devkit.client.client import X4DClient
13
+
14
+
15
+ class TestSetsAPI:
16
+ def __init__(self, client: "X4DClient"):
17
+ self._client = client
18
+
19
+ def create(
20
+ self,
21
+ project_id: int,
22
+ name: str,
23
+ clip_ids: list[str],
24
+ description: str | None = None,
25
+ metadata: dict[str, Any] | None = None,
26
+ ) -> dict:
27
+ """Create or replace a test set.
28
+
29
+ Idempotent on (project_id, name): re-calling with the same name
30
+ replaces the clip_ids list. Treat that as version replacement, not
31
+ in-place mutation — if you want to keep the old list, start a new
32
+ name (`v1` → `v2`).
33
+ """
34
+ return self._client.post(
35
+ "/test-sets",
36
+ json={
37
+ "project_id": project_id,
38
+ "name": name,
39
+ "description": description,
40
+ "clip_ids": clip_ids,
41
+ "metadata": metadata or {},
42
+ },
43
+ )
44
+
45
+ def list(self, project_id: int) -> list[dict]:
46
+ """List all test sets for a project."""
47
+ resp = self._client.get(f"/projects/{project_id}/test-sets")
48
+ return resp["items"]
49
+
50
+ def list_test_clip_ids(self, project_id: int) -> list[str]:
51
+ """Return the union of clip_ids across every test set in the project.
52
+
53
+ A clip is test-marked if it appears in at least one set. Training
54
+ pipelines use this (indirectly via ``clips.list_all(exclude_test=True)``)
55
+ to avoid training on benchmark data.
56
+ """
57
+ return self._client.get(f"/projects/{project_id}/test-clip-ids")
58
+
59
+ def get(self, test_set_id: int) -> dict:
60
+ return self._client.get(f"/test-sets/{test_set_id}")
61
+
62
+ def delete(self, test_set_id: int) -> None:
63
+ self._client.delete(f"/test-sets/{test_set_id}")
@@ -27,8 +27,26 @@ _VALID_FRAMES = {"sensor", "ego", "world"}
27
27
  class ClipLoader:
28
28
  """Load and index an X4D clip directory.
29
29
 
30
- Eagerly parses all JSON metadata into frozen dataclasses.
30
+ Eagerly parses all JSON metadata into frozen dataclasses on construction.
31
31
  Binary sensor data (point clouds, images) is loaded on demand.
32
+
33
+ Args:
34
+ clip_dir: Path to a clip directory containing meta.json and other
35
+ X4D JSON/binary files.
36
+
37
+ Example::
38
+
39
+ loader = ClipLoader("/data/clips/my_clip")
40
+ print(loader.meta.clip_id)
41
+
42
+ # Iterate samples and load point clouds
43
+ for sample in loader.samples:
44
+ for sd in loader.sample_data_for_sample(sample.token):
45
+ if "LIDAR" in sd.channel:
46
+ pts = loader.load_point_cloud(sd, frame="ego")
47
+
48
+ # Get annotations in ego frame for training
49
+ anns = loader.annotations_for_sample(sample.token, frame="ego")
32
50
  """
33
51
 
34
52
  def __init__(self, clip_dir: str | Path) -> None:
@@ -200,46 +218,78 @@ class ClipLoader:
200
218
 
201
219
  @property
202
220
  def clip_dir(self) -> Path:
221
+ """Absolute path to the clip directory."""
203
222
  return self._clip_dir
204
223
 
205
224
  @property
206
225
  def meta(self) -> ClipMeta:
226
+ """Clip metadata (from meta.json)."""
207
227
  return self._meta
208
228
 
209
229
  @property
210
230
  def samples(self) -> list[Sample]:
231
+ """All samples, sorted by timestamp."""
211
232
  return self._samples
212
233
 
213
234
  @property
214
235
  def sample_data(self) -> list[SampleData]:
236
+ """All sample_data records across all channels."""
215
237
  return self._sample_data
216
238
 
217
239
  @property
218
240
  def ego_poses(self) -> list[EgoPose]:
241
+ """All ego poses, sorted by timestamp."""
219
242
  return self._ego_poses
220
243
 
221
244
  @property
222
245
  def calibrated_sensors(self) -> list[CalibratedSensor]:
246
+ """All calibrated sensor records (one per channel)."""
223
247
  return self._calibrated_sensors
224
248
 
225
249
  @property
226
250
  def annotations(self) -> list[Annotation]:
251
+ """All annotations in world frame."""
227
252
  return self._annotations
228
253
 
229
254
  @property
230
255
  def instances(self) -> list[Instance]:
256
+ """All tracked object instances."""
231
257
  return self._instances
232
258
 
233
259
  def get(self, token: str) -> Any:
234
- """O(1) lookup by token across all entity types. Raises KeyError if not found."""
260
+ """O(1) lookup by token across all entity types.
261
+
262
+ Args:
263
+ token: Any entity token (sample, sample_data, annotation, etc.).
264
+
265
+ Returns:
266
+ The entity object matching the token.
267
+
268
+ Raises:
269
+ KeyError: If the token is not found.
270
+ """
235
271
  return self._token_index[token]
236
272
 
237
273
  def sample_data_for_channel(self, channel: str) -> list[SampleData]:
238
- """All sample_data for a channel, sorted by timestamp."""
274
+ """Get all sample_data for a sensor channel, sorted by timestamp.
275
+
276
+ Args:
277
+ channel: Sensor channel name (e.g. "LIDAR_TOP", "CAM_FRONT").
278
+
279
+ Returns:
280
+ List of SampleData records for this channel, time-ordered.
281
+ """
239
282
  return self._sd_by_channel.get(channel, [])
240
283
 
241
284
  def sample_data_for_sample(self, sample_token: str) -> list[SampleData]:
242
- """All sample_data records for a given sample (all channels at that timestamp)."""
285
+ """Get all sample_data records for a sample (all channels at that timestamp).
286
+
287
+ Args:
288
+ sample_token: The sample token.
289
+
290
+ Returns:
291
+ List of SampleData records (one per channel) for this sample.
292
+ """
243
293
  return self._sd_by_sample.get(sample_token, [])
244
294
 
245
295
  def annotations_for_sample(
@@ -350,7 +400,16 @@ class ClipLoader:
350
400
  return (world_from_ego @ ego_from_sensor).inverse
351
401
 
352
402
  def load_image_path(self, sd: SampleData) -> Path:
353
- """Return the absolute path to the image file (does not load pixels)."""
403
+ """Get the absolute path to an image file.
404
+
405
+ Does not load the image — returns the path for use with OpenCV, PIL, etc.
406
+
407
+ Args:
408
+ sd: The SampleData record for a camera channel.
409
+
410
+ Returns:
411
+ Absolute Path to the image file.
412
+ """
354
413
  return self._clip_dir / sd.file_path
355
414
 
356
415
  # --- Private helpers ---