x4d-devkit 0.2.0__tar.gz → 0.2.2__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.2.0 → x4d_devkit-0.2.2}/PKG-INFO +27 -7
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/README.md +26 -6
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/pyproject.toml +1 -1
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_coordinate_frames.py +1 -1
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/__init__.py +2 -0
- x4d_devkit-0.2.2/x4d_devkit/client/__init__.py +13 -0
- x4d_devkit-0.2.2/x4d_devkit/client/checkpoints.py +64 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/client/client.py +23 -6
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/client/clips.py +52 -8
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/client/projects.py +11 -0
- x4d_devkit-0.2.2/x4d_devkit/client/test_sets.py +63 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/core/loader.py +64 -5
- x4d_devkit-0.2.2/x4d_devkit/core/models.py +215 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/core/transform.py +111 -14
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/eval/detection.py +40 -1
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/validation/report.py +20 -2
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/validation/validator.py +25 -1
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit.egg-info/PKG-INFO +27 -7
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit.egg-info/SOURCES.txt +2 -0
- x4d_devkit-0.2.0/x4d_devkit/client/__init__.py +0 -3
- x4d_devkit-0.2.0/x4d_devkit/core/models.py +0 -106
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/LICENSE +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/setup.cfg +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_loader.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_matching.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_models.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_nuscenes_converter.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_token.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_transform.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/tests/test_validation.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/converters/nuscenes.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.2.0 → x4d_devkit-0.2.2}/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.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
-
|
|
61
|
-
print(
|
|
60
|
+
loader = ClipLoader("/path/to/clip")
|
|
61
|
+
print(loader.meta)
|
|
62
62
|
|
|
63
|
-
for sample in
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
29
|
-
print(
|
|
28
|
+
loader = ClipLoader("/path/to/clip")
|
|
29
|
+
print(loader.meta)
|
|
30
30
|
|
|
31
|
-
for sample in
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
7
|
+
version = "0.2.2"
|
|
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"
|
|
@@ -28,7 +28,7 @@ def _make_synthetic_clip(tmp_path: Path) -> Path:
|
|
|
28
28
|
# -- meta.json --
|
|
29
29
|
(clip_dir / "meta.json").write_text(json.dumps({
|
|
30
30
|
"clip_id": "test-clip",
|
|
31
|
-
"schema_version": "0.
|
|
31
|
+
"schema_version": "0.2",
|
|
32
32
|
"session_id": "sess-001",
|
|
33
33
|
"vehicle_id": "veh-001",
|
|
34
34
|
"capture_time_utc": "2026-01-01T00:00:00Z",
|
|
@@ -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.
|
|
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://
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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=
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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 ---
|