x4d-devkit 0.41.0rc1__tar.gz → 0.42.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.
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/PKG-INFO +2 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/pyproject.toml +2 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_preannotation_client.py +102 -5
- x4d_devkit-0.42.0/tests/test_roadmark_preview.py +87 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/preannotation.py +104 -7
- x4d_devkit-0.42.0/x4d_devkit/roadmark_preview.py +219 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/PKG-INFO +2 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/requires.txt +1 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/LICENSE +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/README.md +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/setup.cfg +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_box.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_loader.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_matching.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_token.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_transform.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_validation.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.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.
|
|
3
|
+
Version: 0.42.0
|
|
4
4
|
Summary: Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation
|
|
5
5
|
Author: windzu
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,6 +23,7 @@ Requires-Dist: numpy>=1.24
|
|
|
23
23
|
Requires-Dist: httpx>=0.27
|
|
24
24
|
Requires-Dist: rfc8785==0.1.4
|
|
25
25
|
Requires-Dist: zstandard<1,>=0.23
|
|
26
|
+
Requires-Dist: pyproj<4,>=3.6
|
|
26
27
|
Provides-Extra: inference
|
|
27
28
|
Requires-Dist: pydantic>=2.0; extra == "inference"
|
|
28
29
|
Requires-Dist: fastapi>=0.110; extra == "inference"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.42.0"
|
|
8
8
|
description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"httpx>=0.27",
|
|
30
30
|
"rfc8785==0.1.4",
|
|
31
31
|
"zstandard>=0.23,<1",
|
|
32
|
+
"pyproj>=3.6,<4",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.optional-dependencies]
|
|
@@ -50,7 +50,7 @@ def _service() -> dict:
|
|
|
50
50
|
|
|
51
51
|
def _run(*, status: str = "queued", reused: bool = False) -> dict:
|
|
52
52
|
succeeded = status == "succeeded"
|
|
53
|
-
|
|
53
|
+
result = {
|
|
54
54
|
"run_id": "preann-run-1",
|
|
55
55
|
"request_identity": _digest("a"),
|
|
56
56
|
"project_id": 7,
|
|
@@ -76,7 +76,34 @@ def _run(*, status: str = "queued", reused: bool = False) -> dict:
|
|
|
76
76
|
"created_at": "2026-08-18T08:00:00Z",
|
|
77
77
|
"updated_at": "2026-08-18T08:02:00Z",
|
|
78
78
|
"reused": reused,
|
|
79
|
+
"attempt_number": 1,
|
|
80
|
+
"attempt_count": 1,
|
|
81
|
+
"latest_attempt_id": "preann-run-1",
|
|
82
|
+
"is_latest_attempt": True,
|
|
83
|
+
"retry_of_run_id": None,
|
|
79
84
|
}
|
|
85
|
+
result["attempts"] = [
|
|
86
|
+
{
|
|
87
|
+
key: copy.deepcopy(result[key])
|
|
88
|
+
for key in (
|
|
89
|
+
"run_id",
|
|
90
|
+
"attempt_number",
|
|
91
|
+
"retry_of_run_id",
|
|
92
|
+
"status",
|
|
93
|
+
"progress",
|
|
94
|
+
"error",
|
|
95
|
+
"candidate_id",
|
|
96
|
+
"temporal_workflow_id",
|
|
97
|
+
"actor_id",
|
|
98
|
+
"started_at",
|
|
99
|
+
"completed_at",
|
|
100
|
+
"cancellation_requested_at",
|
|
101
|
+
"created_at",
|
|
102
|
+
"updated_at",
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
return result
|
|
80
107
|
|
|
81
108
|
|
|
82
109
|
def _portable_candidate() -> dict:
|
|
@@ -144,7 +171,10 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
|
|
|
144
171
|
requests.append(request)
|
|
145
172
|
if request.url.path == "/api/v1/preannotation/services":
|
|
146
173
|
return httpx.Response(200, json=[_service()])
|
|
147
|
-
if
|
|
174
|
+
if (
|
|
175
|
+
request.url.path == "/api/v1/preannotation/runs"
|
|
176
|
+
and request.method == "POST"
|
|
177
|
+
):
|
|
148
178
|
return httpx.Response(202, json=_run())
|
|
149
179
|
if request.url.path == "/api/v1/preannotation/runs" and request.method == "GET":
|
|
150
180
|
return httpx.Response(200, json=[_run(status="succeeded")])
|
|
@@ -172,9 +202,7 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
|
|
|
172
202
|
profile_id="centerpoint-track-v1",
|
|
173
203
|
config={"score_threshold": 0.4},
|
|
174
204
|
)
|
|
175
|
-
runs = client.preannotation.list_runs(
|
|
176
|
-
project_id=7, clip_id="clip-1", limit=10
|
|
177
|
-
)
|
|
205
|
+
runs = client.preannotation.list_runs(project_id=7, clip_id="clip-1", limit=10)
|
|
178
206
|
fetched = client.preannotation.get_run("preann-run-1")
|
|
179
207
|
cancellation = client.preannotation.cancel_run("preann-run-1")
|
|
180
208
|
candidate = client.preannotation.get_candidate(_digest("d"))
|
|
@@ -260,3 +288,72 @@ def test_create_run_rejects_non_i_json_config_before_request():
|
|
|
260
288
|
profile_id="centerpoint-track-v1",
|
|
261
289
|
config={"threshold": float("nan")},
|
|
262
290
|
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _retried_run():
|
|
294
|
+
previous = _run(status="failed")["attempts"][0]
|
|
295
|
+
result = _run(status="succeeded")
|
|
296
|
+
result.update(
|
|
297
|
+
run_id="preann-run-2",
|
|
298
|
+
attempt_number=2,
|
|
299
|
+
attempt_count=2,
|
|
300
|
+
latest_attempt_id="preann-run-2",
|
|
301
|
+
retry_of_run_id="preann-run-1",
|
|
302
|
+
)
|
|
303
|
+
result["attempts"] = [
|
|
304
|
+
previous,
|
|
305
|
+
{key: copy.deepcopy(result[key]) for key in previous},
|
|
306
|
+
]
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_retry_history_round_trip_and_historical_run():
|
|
311
|
+
payload = _retried_run()
|
|
312
|
+
parsed = PlatformPreannotationRun.model_validate(payload)
|
|
313
|
+
assert parsed.attempt_count == 2
|
|
314
|
+
assert parsed.attempts[0].status == PlatformPreannotationRunStatus.FAILED
|
|
315
|
+
assert parsed.attempts[1].candidate_id == parsed.candidate_id
|
|
316
|
+
assert (
|
|
317
|
+
PlatformPreannotationRun.model_validate_json(parsed.model_dump_json()) == parsed
|
|
318
|
+
)
|
|
319
|
+
payload.update(payload["attempts"][0])
|
|
320
|
+
payload["is_latest_attempt"] = False
|
|
321
|
+
old = PlatformPreannotationRun.model_validate(payload)
|
|
322
|
+
assert old.run_id == "preann-run-1" and old.latest_attempt_id == "preann-run-2"
|
|
323
|
+
payload["attempts"] = []
|
|
324
|
+
assert PlatformPreannotationRun.model_validate(payload).attempt_count == 2
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.parametrize(
|
|
328
|
+
"field,value",
|
|
329
|
+
[
|
|
330
|
+
("attempt_count", 1),
|
|
331
|
+
("attempt_number", 1),
|
|
332
|
+
("attempt_number", True),
|
|
333
|
+
("latest_attempt_id", "preann-missing"),
|
|
334
|
+
("is_latest_attempt", False),
|
|
335
|
+
("is_latest_attempt", 1),
|
|
336
|
+
("retry_of_run_id", None),
|
|
337
|
+
("attempts", [_run(status="failed")["attempts"][0]]),
|
|
338
|
+
],
|
|
339
|
+
)
|
|
340
|
+
def test_inconsistent_attempt_identity_is_rejected(field, value):
|
|
341
|
+
payload = _retried_run()
|
|
342
|
+
payload[field] = value
|
|
343
|
+
with pytest.raises(ValidationError):
|
|
344
|
+
PlatformPreannotationRun.model_validate(payload)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.parametrize("mutation", ["unknown", "link", "order", "state"])
|
|
348
|
+
def test_malformed_attempt_history_is_rejected(mutation):
|
|
349
|
+
payload = _retried_run()
|
|
350
|
+
if mutation == "unknown":
|
|
351
|
+
payload["attempts"][0]["unknown_field"] = True
|
|
352
|
+
elif mutation == "link":
|
|
353
|
+
payload["attempts"][1]["retry_of_run_id"] = "another-run"
|
|
354
|
+
elif mutation == "order":
|
|
355
|
+
payload["attempts"].reverse()
|
|
356
|
+
else:
|
|
357
|
+
payload["attempts"][1]["progress"]["stage"] = "different-state"
|
|
358
|
+
with pytest.raises(ValidationError):
|
|
359
|
+
PlatformPreannotationRun.model_validate(payload)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from x4d_devkit.roadmark_preview import preview_roadmark
|
|
8
|
+
|
|
9
|
+
BIG = 2024121315242816837
|
|
10
|
+
LINE = [[3, 0, 4, 1], [3.00001, 0.00001, 5, 1]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def source():
|
|
14
|
+
return {"valid": True, "centerline": [
|
|
15
|
+
{"class": "centerline", "id": BIG, "points": LINE, "orientation": True,
|
|
16
|
+
"successor": [BIG + 1], "predecessor": [], "left_lane": [BIG + 2]},
|
|
17
|
+
{"class": "centerline", "id": BIG + 1, "points": LINE,
|
|
18
|
+
"successor": [], "predecessor": [BIG]}],
|
|
19
|
+
"lane": [{"class": "lane", "id": BIG + 2, "points": LINE, "dashtype": "dash"}],
|
|
20
|
+
"special_points": [{"class": "special_points", "id": 1, "points": [[3, 0, 8]], "type": "type"}],
|
|
21
|
+
"arrow": [{"class": "arrow", "id": 2, "subclass": "straight", "vertices": [],
|
|
22
|
+
"points": [[3, 0, 2], [3.00001, 0, 2], [3.00001, 0.00001, 2], [3, 0.00001, 2]]}],
|
|
23
|
+
"map_labeling_area": [{"points": [[[3, 0, 2], [3.00001, 0, 2], [3, 0.00001, 2], [3, 0, 2]]]}]}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def preview(payload, matrix=None):
|
|
27
|
+
if matrix is None:
|
|
28
|
+
matrix = [[1, 0, 0, -500000], [0, 1, 0, 0], [0, 0, 1, -1], [0, 0, 0, 1]]
|
|
29
|
+
return preview_roadmark(json.dumps(payload), source_crs="EPSG:4326", metric_crs="EPSG:32631",
|
|
30
|
+
clip_world_from_metric=matrix)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_exact_source_ids_graph_and_uninterpreted_values():
|
|
34
|
+
payload = source()
|
|
35
|
+
original = copy.deepcopy(payload)
|
|
36
|
+
view = preview(payload)
|
|
37
|
+
assert payload == original
|
|
38
|
+
assert view["features"][0]["id"] == str(BIG)
|
|
39
|
+
assert view["features"][1]["id"] == str(BIG + 1)
|
|
40
|
+
assert view["relations"] == [
|
|
41
|
+
{"type": "successor", "from_id": str(BIG), "to_id": str(BIG + 1)},
|
|
42
|
+
{"type": "left_lane", "from_id": str(BIG), "to_id": str(BIG + 2)}]
|
|
43
|
+
feature = view["features"][0]
|
|
44
|
+
assert feature["properties"]["orientation"] is True
|
|
45
|
+
assert feature["properties"]["source_point_extra"] == [[1], [1]]
|
|
46
|
+
assert view["features"][3]["properties"]["type"] == "type"
|
|
47
|
+
assert view["coverage"][0]["completeness"] == "unknown"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_horizontal_projection_axis_order_height_and_rigid_transform():
|
|
51
|
+
view = preview(source())
|
|
52
|
+
point = view["features"][0]["geometry"]["points"][0]
|
|
53
|
+
np.testing.assert_allclose(point, [0, 0, 3], atol=1e-8)
|
|
54
|
+
assert view["features"][4]["geometry"]["rings"][0][0] == view["features"][4]["geometry"]["rings"][0][-1]
|
|
55
|
+
assert view["source"]["vertical_datum"] == "unspecified"
|
|
56
|
+
# Rotation acts on height too; never reduce the transform to yaw/XY.
|
|
57
|
+
matrix = [[1, 0, 0, -500000], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
|
|
58
|
+
np.testing.assert_allclose(preview(source(), matrix)["features"][0]["geometry"]["points"][0], [0, -4, 0], atol=1e-8)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("change", [
|
|
62
|
+
lambda p: p["centerline"][0].update(id=float(BIG)),
|
|
63
|
+
lambda p: p["centerline"][0].update(successor=[0]),
|
|
64
|
+
lambda p: p["centerline"][1].update(predecessor=[]),
|
|
65
|
+
lambda p: p["centerline"][0].update(left_lane=[BIG + 1]),
|
|
66
|
+
lambda p: p["centerline"][0].update(points=[[float("nan"), 0, 0], [3, 1, 0]]),
|
|
67
|
+
lambda p: p["centerline"][0].update(points=[[3, 0, 0], [3, 0, 0]]),
|
|
68
|
+
lambda p: p["lane"][0].update(id=BIG),
|
|
69
|
+
lambda p: p.update(unknown_layer=[]),
|
|
70
|
+
lambda p: p.update(deceleration_zone=[{}]),
|
|
71
|
+
])
|
|
72
|
+
def test_invalid_source_is_not_repaired(change):
|
|
73
|
+
payload = source()
|
|
74
|
+
change(payload)
|
|
75
|
+
with pytest.raises(ValueError):
|
|
76
|
+
preview(payload)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_no_implicit_crs_or_transform_and_duplicate_keys_rejected():
|
|
80
|
+
args = dict(source_crs="EPSG:4326", metric_crs="EPSG:32631", clip_world_from_metric=np.eye(4).tolist())
|
|
81
|
+
with pytest.raises(ValueError, match="Duplicate"):
|
|
82
|
+
preview_roadmark('{"valid":true,"valid":false}', **args)
|
|
83
|
+
with pytest.raises(ValueError, match="SE\\(3\\)"):
|
|
84
|
+
preview(source(), (np.eye(4) * 2).tolist())
|
|
85
|
+
with pytest.raises(ValueError, match="metres"):
|
|
86
|
+
preview_roadmark(json.dumps(source()), **{**args, "metric_crs": "EPSG:4326"})
|
|
87
|
+
assert preview({"valid": False})["bounds"] is None
|
|
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def test_package_version_is_current_release():
|
|
14
|
-
assert x4d_devkit.__version__ == "0.
|
|
14
|
+
assert x4d_devkit.__version__ == "0.42.0"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -86,7 +86,53 @@ class PlatformPreannotationService(_ControlPlaneModel):
|
|
|
86
86
|
return value
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
class
|
|
89
|
+
class PlatformPreannotationRunAttempt(_ControlPlaneModel):
|
|
90
|
+
"""One immutable attempt identity and its current execution state."""
|
|
91
|
+
|
|
92
|
+
run_id: str
|
|
93
|
+
attempt_number: int = Field(ge=1, strict=True)
|
|
94
|
+
retry_of_run_id: str | None
|
|
95
|
+
status: PlatformPreannotationRunStatus
|
|
96
|
+
progress: PreannotationRunProgress
|
|
97
|
+
error: PreannotationRunError | None
|
|
98
|
+
candidate_id: str | None
|
|
99
|
+
temporal_workflow_id: str
|
|
100
|
+
actor_id: int | None = Field(gt=0, strict=True)
|
|
101
|
+
started_at: datetime | None
|
|
102
|
+
completed_at: datetime | None
|
|
103
|
+
cancellation_requested_at: datetime | None
|
|
104
|
+
created_at: datetime
|
|
105
|
+
updated_at: datetime
|
|
106
|
+
|
|
107
|
+
@field_validator("run_id", "retry_of_run_id", mode="before")
|
|
108
|
+
@classmethod
|
|
109
|
+
def validate_attempt_id(cls, value: Any, info: Any) -> str | None:
|
|
110
|
+
if value is None and info.field_name == "retry_of_run_id":
|
|
111
|
+
return None
|
|
112
|
+
return _protocol_token(value, path="preannotation attempt identifier")
|
|
113
|
+
|
|
114
|
+
@field_validator("candidate_id", mode="before")
|
|
115
|
+
@classmethod
|
|
116
|
+
def validate_attempt_candidate(cls, value: Any) -> str | None:
|
|
117
|
+
return None if value is None else _sha256(value, path="attempt.candidate_id")
|
|
118
|
+
|
|
119
|
+
@field_validator("temporal_workflow_id", mode="before")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_attempt_workflow(cls, value: Any) -> str:
|
|
122
|
+
return _canonical_string(value, path="attempt.temporal_workflow_id")
|
|
123
|
+
|
|
124
|
+
@model_validator(mode="after")
|
|
125
|
+
def validate_attempt_result(self) -> "PlatformPreannotationRunAttempt":
|
|
126
|
+
if (self.status == PlatformPreannotationRunStatus.SUCCEEDED) != (
|
|
127
|
+
self.candidate_id is not None
|
|
128
|
+
):
|
|
129
|
+
raise ValueError("only succeeded attempts must contain candidate_id")
|
|
130
|
+
if (self.attempt_number == 1) != (self.retry_of_run_id is None):
|
|
131
|
+
raise ValueError("only the first attempt has no retry predecessor")
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PlatformPreannotationRun(PlatformPreannotationRunAttempt):
|
|
90
136
|
"""One durable X-4D preannotation run."""
|
|
91
137
|
|
|
92
138
|
run_id: str
|
|
@@ -99,6 +145,10 @@ class PlatformPreannotationRun(_ControlPlaneModel):
|
|
|
99
145
|
profile_id: str
|
|
100
146
|
pipeline_identity: str
|
|
101
147
|
checkpoint_identity: str
|
|
148
|
+
attempt_count: int = Field(ge=1, strict=True)
|
|
149
|
+
latest_attempt_id: str
|
|
150
|
+
is_latest_attempt: bool = Field(strict=True)
|
|
151
|
+
attempts: list[PlatformPreannotationRunAttempt]
|
|
102
152
|
status: PlatformPreannotationRunStatus
|
|
103
153
|
progress: PreannotationRunProgress
|
|
104
154
|
error: PreannotationRunError | None
|
|
@@ -117,7 +167,9 @@ class PlatformPreannotationRun(_ControlPlaneModel):
|
|
|
117
167
|
def validate_project_id(cls, value: Any) -> int:
|
|
118
168
|
return _positive_int(value, path="preannotation run.project_id")
|
|
119
169
|
|
|
120
|
-
@field_validator(
|
|
170
|
+
@field_validator(
|
|
171
|
+
"run_id", "service_id", "profile_id", "latest_attempt_id", mode="before"
|
|
172
|
+
)
|
|
121
173
|
@classmethod
|
|
122
174
|
def validate_protocol_id(cls, value: Any) -> str:
|
|
123
175
|
return _protocol_token(value, path="preannotation run identifier")
|
|
@@ -169,9 +221,48 @@ class PlatformPreannotationRun(_ControlPlaneModel):
|
|
|
169
221
|
def succeeded_run_has_candidate(self) -> "PlatformPreannotationRun":
|
|
170
222
|
if self.status == PlatformPreannotationRunStatus.SUCCEEDED:
|
|
171
223
|
if self.candidate_id is None:
|
|
172
|
-
raise ValueError(
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"succeeded preannotation run must contain candidate_id"
|
|
226
|
+
)
|
|
173
227
|
elif self.candidate_id is not None:
|
|
174
|
-
raise ValueError(
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"only a succeeded preannotation run may contain candidate_id"
|
|
230
|
+
)
|
|
231
|
+
return self
|
|
232
|
+
|
|
233
|
+
@model_validator(mode="after")
|
|
234
|
+
def validate_attempt_history(self) -> "PlatformPreannotationRun":
|
|
235
|
+
if self.attempt_number > self.attempt_count:
|
|
236
|
+
raise ValueError("attempt_number must not exceed attempt_count")
|
|
237
|
+
if self.is_latest_attempt != (self.attempt_number == self.attempt_count):
|
|
238
|
+
raise ValueError("latest attempt flag disagrees with attempt count")
|
|
239
|
+
if self.is_latest_attempt != (self.run_id == self.latest_attempt_id):
|
|
240
|
+
raise ValueError("latest attempt flag disagrees with run identity")
|
|
241
|
+
# The list endpoint explicitly omits expansion; detail includes history.
|
|
242
|
+
if not self.attempts:
|
|
243
|
+
return self
|
|
244
|
+
if len(self.attempts) != self.attempt_count:
|
|
245
|
+
raise ValueError("attempt_count must match complete attempt history")
|
|
246
|
+
ids = [attempt.run_id for attempt in self.attempts]
|
|
247
|
+
if len(set(ids)) != len(ids) or self.run_id not in ids:
|
|
248
|
+
raise ValueError("attempt history must uniquely contain this run")
|
|
249
|
+
for number, attempt in enumerate(self.attempts, 1):
|
|
250
|
+
predecessor = None if number == 1 else ids[number - 2]
|
|
251
|
+
if (
|
|
252
|
+
attempt.attempt_number != number
|
|
253
|
+
or attempt.retry_of_run_id != predecessor
|
|
254
|
+
):
|
|
255
|
+
raise ValueError(
|
|
256
|
+
"attempt history must be ordered with exact retry links"
|
|
257
|
+
)
|
|
258
|
+
if self.latest_attempt_id != ids[-1] or self.is_latest_attempt != (
|
|
259
|
+
self.run_id == self.latest_attempt_id
|
|
260
|
+
):
|
|
261
|
+
raise ValueError("latest attempt identity disagrees with history")
|
|
262
|
+
current = self.attempts[ids.index(self.run_id)]
|
|
263
|
+
for field in PlatformPreannotationRunAttempt.model_fields:
|
|
264
|
+
if getattr(current, field) != getattr(self, field):
|
|
265
|
+
raise ValueError(f"current attempt disagrees with history: {field}")
|
|
175
266
|
return self
|
|
176
267
|
|
|
177
268
|
|
|
@@ -384,13 +475,17 @@ class PreannotationAPI:
|
|
|
384
475
|
payload = self._get("/preannotation/runs", params=params)
|
|
385
476
|
if type(payload) is not list:
|
|
386
477
|
raise TypeError("preannotation runs response must be a list")
|
|
387
|
-
results = tuple(
|
|
478
|
+
results = tuple(
|
|
479
|
+
PlatformPreannotationRun.model_validate(item) for item in payload
|
|
480
|
+
)
|
|
388
481
|
if any(
|
|
389
482
|
item.project_id != project_id
|
|
390
483
|
or (clip_id is not None and item.clip_id != clip_id)
|
|
391
484
|
for item in results
|
|
392
485
|
):
|
|
393
|
-
raise ValueError(
|
|
486
|
+
raise ValueError(
|
|
487
|
+
"preannotation runs response is outside the requested scope"
|
|
488
|
+
)
|
|
394
489
|
return results
|
|
395
490
|
|
|
396
491
|
def get_run(self, run_id: str) -> PlatformPreannotationRun:
|
|
@@ -401,7 +496,9 @@ class PreannotationAPI:
|
|
|
401
496
|
self._get(f"/preannotation/runs/{run_id}")
|
|
402
497
|
)
|
|
403
498
|
if result.run_id != run_id:
|
|
404
|
-
raise ValueError(
|
|
499
|
+
raise ValueError(
|
|
500
|
+
"preannotation run response identity does not match request"
|
|
501
|
+
)
|
|
405
502
|
return result
|
|
406
503
|
|
|
407
504
|
def cancel_run(self, run_id: str) -> PlatformPreannotationCancellation:
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Explicit RoadMark source inspection; never a native Dataset compatibility reader.
|
|
2
|
+
|
|
3
|
+
The caller supplies the geographic CRS and the rigid metric-to-Clip transform.
|
|
4
|
+
Source layer names and unresolved properties remain source facts. This preview
|
|
5
|
+
does not create native categories, lane segments, or annotation state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from pyproj import CRS, Transformer
|
|
17
|
+
from pyproj.exceptions import ProjError
|
|
18
|
+
|
|
19
|
+
from x4d_devkit.core.transform import Transform
|
|
20
|
+
from x4d_devkit.json_contract import ensure_i_json
|
|
21
|
+
|
|
22
|
+
CONTRACT = "x4d-roadmark-preview-v1"
|
|
23
|
+
SOURCE_LAYERS = {
|
|
24
|
+
"arrow": "polygon", "centerline": "polyline", "crosswalk": "polygon",
|
|
25
|
+
"curb": "polyline", "lane": "polyline", "special_points": "point",
|
|
26
|
+
"stop_line": "polyline",
|
|
27
|
+
}
|
|
28
|
+
_EMPTY_ONLY = {"construction_area", "deceleration_line", "deceleration_zone"}
|
|
29
|
+
_RELATIONS = ("predecessor", "successor", "left_lane", "right_lane")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _source_json(raw: str) -> dict:
|
|
33
|
+
# RoadMark IDs are integers beyond I-JSON's safe range. Decode exactly once,
|
|
34
|
+
# before converting identity fields to strings. Native I-JSON is unchanged.
|
|
35
|
+
def members(pairs):
|
|
36
|
+
result = {}
|
|
37
|
+
for key, value in pairs:
|
|
38
|
+
if key in result:
|
|
39
|
+
raise ValueError(f"Duplicate RoadMark field: {key}")
|
|
40
|
+
result[key] = value
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
def constant(value):
|
|
44
|
+
raise ValueError(f"Non-finite RoadMark number: {value}")
|
|
45
|
+
|
|
46
|
+
if type(raw) is not str or len(raw.encode("utf-8")) > 8_000_000:
|
|
47
|
+
raise ValueError("RoadMark input must be UTF-8 text of at most 8 MB")
|
|
48
|
+
payload = json.loads(raw, object_pairs_hook=members, parse_constant=constant)
|
|
49
|
+
if type(payload) is not dict:
|
|
50
|
+
raise ValueError("RoadMark total map must be an object")
|
|
51
|
+
unknown = set(payload) - set(SOURCE_LAYERS) - _EMPTY_ONLY - {"valid", "map_labeling_area"}
|
|
52
|
+
if unknown:
|
|
53
|
+
raise ValueError(f"Unsupported RoadMark layers: {sorted(unknown)}")
|
|
54
|
+
if type(payload.get("valid")) is not bool:
|
|
55
|
+
raise ValueError("RoadMark valid must be a boolean")
|
|
56
|
+
return payload
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _id(value: Any) -> str:
|
|
60
|
+
if type(value) is not int:
|
|
61
|
+
raise ValueError("RoadMark IDs must be JSON integers, not rounded floats")
|
|
62
|
+
return str(value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _property(value: Any) -> Any:
|
|
66
|
+
if type(value) is int and abs(value) > 2**53 - 1:
|
|
67
|
+
return str(value)
|
|
68
|
+
if type(value) is list:
|
|
69
|
+
return [_property(item) for item in value]
|
|
70
|
+
if type(value) is dict:
|
|
71
|
+
return {key: _property(item) for key, item in value.items()}
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def preview_roadmark(
|
|
76
|
+
raw: str, *, source_crs: str, metric_crs: str,
|
|
77
|
+
clip_world_from_metric: list[list[float]],
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Parse one total map into a transient, metre/XYZ, Clip-world read view.
|
|
80
|
+
|
|
81
|
+
Geographic inputs use longitude, latitude, source height. Only horizontal
|
|
82
|
+
CRS projection runs: source Z is preserved before the explicit rigid
|
|
83
|
+
transform. No vertical datum or time correction is guessed. Polygon rings
|
|
84
|
+
must already be closed; source line orientation is never reinterpreted.
|
|
85
|
+
"""
|
|
86
|
+
source = _source_json(raw)
|
|
87
|
+
try:
|
|
88
|
+
geographic, metric = CRS(source_crs), CRS(metric_crs)
|
|
89
|
+
except ProjError as exc:
|
|
90
|
+
raise ValueError(f"Invalid source/metric CRS: {exc}") from exc
|
|
91
|
+
if not geographic.is_geographic or len(geographic.axis_info) != 2:
|
|
92
|
+
raise ValueError("source_crs must be an explicit two-dimensional geographic CRS")
|
|
93
|
+
if not metric.is_projected or len(metric.axis_info) != 2 or any(
|
|
94
|
+
axis.unit_conversion_factor != 1 for axis in metric.axis_info
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("metric_crs must be an explicit projected CRS in metres")
|
|
97
|
+
matrix = np.asarray(clip_world_from_metric)
|
|
98
|
+
if (matrix.shape != (4, 4) or matrix.dtype.kind not in "fiu"
|
|
99
|
+
or not np.isfinite(matrix).all()
|
|
100
|
+
or not np.allclose(matrix[3], [0, 0, 0, 1], atol=1e-9, rtol=0)
|
|
101
|
+
or not np.allclose(matrix[:3, :3].T @ matrix[:3, :3], np.eye(3), atol=1e-8, rtol=0)
|
|
102
|
+
or not np.isclose(np.linalg.det(matrix[:3, :3]), 1, atol=1e-8, rtol=0)):
|
|
103
|
+
raise ValueError("clip_world_from_metric must be a finite SE(3) matrix")
|
|
104
|
+
rigid = Transform(matrix[:3, :3], matrix[:3, 3])
|
|
105
|
+
try:
|
|
106
|
+
projector = Transformer.from_crs(geographic, metric, always_xy=True, allow_ballpark=False)
|
|
107
|
+
except ProjError as exc:
|
|
108
|
+
raise ValueError(f"No supported horizontal projection: {exc}") from exc
|
|
109
|
+
all_points: list[list[float]] = []
|
|
110
|
+
|
|
111
|
+
def points(rows: Any, kind: str) -> list[list[float]]:
|
|
112
|
+
if type(rows) is not list or not rows:
|
|
113
|
+
raise ValueError("Geometry must contain points")
|
|
114
|
+
for row in rows:
|
|
115
|
+
if (type(row) is not list or len(row) not in (3, 4)
|
|
116
|
+
or any(type(n) not in (int, float) or not math.isfinite(n) for n in row)
|
|
117
|
+
or abs(row[0]) > 180 or abs(row[1]) > 90):
|
|
118
|
+
raise ValueError("RoadMark points must be finite longitude/latitude/height[/source extra]")
|
|
119
|
+
minimum = {"point": 1, "polyline": 2, "polygon": 3}[kind]
|
|
120
|
+
if len({tuple(row[:3]) for row in rows}) < minimum or (kind == "point" and len(rows) != 1):
|
|
121
|
+
raise ValueError(f"Invalid {kind} vertex count")
|
|
122
|
+
if kind == "polygon" and rows[0][:3] != rows[-1][:3]:
|
|
123
|
+
raise ValueError("RoadMark polygon rings must be explicitly closed")
|
|
124
|
+
xyz = np.array([row[:3] for row in rows], dtype=float)
|
|
125
|
+
try:
|
|
126
|
+
xyz[:, 0], xyz[:, 1] = projector.transform(xyz[:, 0], xyz[:, 1], errcheck=True)
|
|
127
|
+
except ProjError as exc:
|
|
128
|
+
raise ValueError(f"Source coordinates cannot be projected: {exc}") from exc
|
|
129
|
+
converted = rigid.apply(xyz)
|
|
130
|
+
if not np.isfinite(converted).all():
|
|
131
|
+
raise ValueError("Non-finite projected geometry")
|
|
132
|
+
result = converted.tolist()
|
|
133
|
+
all_points.extend(result)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
features, relations, coverage = [], [], []
|
|
137
|
+
records: dict[str, dict] = {}
|
|
138
|
+
for layer, rows in source.items():
|
|
139
|
+
if layer == "valid":
|
|
140
|
+
continue
|
|
141
|
+
if type(rows) is not list:
|
|
142
|
+
raise ValueError(f"RoadMark {layer} must be an array")
|
|
143
|
+
if layer in _EMPTY_ONLY:
|
|
144
|
+
if rows:
|
|
145
|
+
raise ValueError(f"Non-empty {layer} has no verified source geometry mapping")
|
|
146
|
+
continue
|
|
147
|
+
if layer == "map_labeling_area":
|
|
148
|
+
for area in rows:
|
|
149
|
+
if (type(area) is not dict or set(area) != {"points"}
|
|
150
|
+
or type(area["points"]) is not list or len(area["points"]) != 1):
|
|
151
|
+
raise ValueError("Invalid RoadMark map_labeling_area")
|
|
152
|
+
coverage.append({"geometry": {"type": "polygon", "rings": [
|
|
153
|
+
points(ring, "polygon") for ring in area["points"]]}, "completeness": "unknown"})
|
|
154
|
+
continue
|
|
155
|
+
for row in rows:
|
|
156
|
+
if type(row) is not dict or row.get("class") != layer:
|
|
157
|
+
raise ValueError(f"RoadMark class must match layer {layer}")
|
|
158
|
+
identity = _id(row.get("id"))
|
|
159
|
+
if identity in records:
|
|
160
|
+
raise ValueError(f"Duplicate RoadMark ID: {identity}")
|
|
161
|
+
records[identity] = row
|
|
162
|
+
kind = SOURCE_LAYERS[layer]
|
|
163
|
+
if layer == "arrow":
|
|
164
|
+
# This source uses four ordered corners for the marked region,
|
|
165
|
+
# not a closed silhouette of the painted arrow itself.
|
|
166
|
+
converted = points(row.get("points"), "polyline")
|
|
167
|
+
if len(converted) != 4 or len({tuple(p) for p in converted}) != 4:
|
|
168
|
+
raise ValueError("RoadMark arrow regions require four distinct corners")
|
|
169
|
+
converted = [*converted, converted[0]]
|
|
170
|
+
else:
|
|
171
|
+
converted = points(row.get("points"), kind)
|
|
172
|
+
geometry = {"type": kind}
|
|
173
|
+
geometry.update({"point": converted[0]} if kind == "point" else
|
|
174
|
+
{"rings": [converted]} if kind == "polygon" else {"points": converted})
|
|
175
|
+
properties = {key: _property(value) for key, value in row.items()
|
|
176
|
+
if key not in {"id", "class", "points", *_RELATIONS}}
|
|
177
|
+
if any(len(point) == 4 for point in row["points"]):
|
|
178
|
+
properties["source_point_extra"] = [point[3:] for point in row["points"]]
|
|
179
|
+
features.append({"id": identity, "layer": layer, "geometry": geometry, "properties": properties})
|
|
180
|
+
for identity, row in records.items():
|
|
181
|
+
for name in _RELATIONS:
|
|
182
|
+
refs = row.get(name, [])
|
|
183
|
+
if type(refs) is not list:
|
|
184
|
+
raise ValueError(f"RoadMark {name} must be an array")
|
|
185
|
+
targets = [_id(value) for value in refs]
|
|
186
|
+
if len(set(targets)) != len(targets):
|
|
187
|
+
raise ValueError(f"Duplicate {name} relation on {identity}")
|
|
188
|
+
for target in targets:
|
|
189
|
+
other = records.get(target)
|
|
190
|
+
if other is None:
|
|
191
|
+
raise ValueError(f"Dangling RoadMark reference: {identity} -> {target}")
|
|
192
|
+
if name in ("left_lane", "right_lane"):
|
|
193
|
+
if row["class"] != "centerline" or other["class"] != "lane":
|
|
194
|
+
raise ValueError("RoadMark boundary references must connect centerline to lane")
|
|
195
|
+
else:
|
|
196
|
+
reverse = "predecessor" if name == "successor" else "successor"
|
|
197
|
+
if (row["class"] != other["class"] or type(other.get(reverse, [])) is not list
|
|
198
|
+
or int(identity) not in other.get(reverse, [])):
|
|
199
|
+
raise ValueError("RoadMark predecessor/successor must be reciprocal within a source layer")
|
|
200
|
+
if name != "predecessor":
|
|
201
|
+
relations.append({"type": name, "from_id": identity, "to_id": target})
|
|
202
|
+
bounds = [np.min(all_points, axis=0).tolist(), np.max(all_points, axis=0).tolist()] if all_points else None
|
|
203
|
+
result = {
|
|
204
|
+
"contract_version": CONTRACT, "frame_id": "clip_world", "features": features,
|
|
205
|
+
"relations": relations, "coverage": coverage, "bounds": bounds,
|
|
206
|
+
"source": {"format": "roadmark-lb-all", "sha256": hashlib.sha256(raw.encode("utf-8")).hexdigest(),
|
|
207
|
+
"valid": source["valid"], "source_crs": geographic.to_string(), "metric_crs": metric.to_string(),
|
|
208
|
+
"clip_world_from_metric": matrix.tolist(), "vertical_datum": "unspecified"},
|
|
209
|
+
"limitations": ["Source layers and relation names are not native Label Schema categories.",
|
|
210
|
+
"Source height is retained; the vertical datum is unspecified.",
|
|
211
|
+
"Source orientation, extra point values and special-point types remain uninterpreted.",
|
|
212
|
+
"Coverage completeness is unknown; source valid does not establish completeness.",
|
|
213
|
+
"No per-frame visibility or pose-time correction is inferred."],
|
|
214
|
+
}
|
|
215
|
+
ensure_i_json(result)
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = ["CONTRACT", "preview_roadmark"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: x4d-devkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.42.0
|
|
4
4
|
Summary: Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation
|
|
5
5
|
Author: windzu
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,6 +23,7 @@ Requires-Dist: numpy>=1.24
|
|
|
23
23
|
Requires-Dist: httpx>=0.27
|
|
24
24
|
Requires-Dist: rfc8785==0.1.4
|
|
25
25
|
Requires-Dist: zstandard<1,>=0.23
|
|
26
|
+
Requires-Dist: pyproj<4,>=3.6
|
|
26
27
|
Provides-Extra: inference
|
|
27
28
|
Requires-Dist: pydantic>=2.0; extra == "inference"
|
|
28
29
|
Requires-Dist: fastapi>=0.110; extra == "inference"
|
|
@@ -34,6 +34,7 @@ tests/test_platform_pose.py
|
|
|
34
34
|
tests/test_preannotation_client.py
|
|
35
35
|
tests/test_reconstruction_client.py
|
|
36
36
|
tests/test_release_gate.py
|
|
37
|
+
tests/test_roadmark_preview.py
|
|
37
38
|
tests/test_sdk_api.py
|
|
38
39
|
tests/test_semantic_scene_authoring.py
|
|
39
40
|
tests/test_semantic_scene_client.py
|
|
@@ -63,6 +64,7 @@ x4d_devkit/json_contract.py
|
|
|
63
64
|
x4d_devkit/label_schema.py
|
|
64
65
|
x4d_devkit/lidar_materialization.py
|
|
65
66
|
x4d_devkit/materialize.py
|
|
67
|
+
x4d_devkit/roadmark_preview.py
|
|
66
68
|
x4d_devkit/semantic_scene_authoring.py
|
|
67
69
|
x4d_devkit/semantic_scene_contract.py
|
|
68
70
|
x4d_devkit/time_contract.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_current_state_identity_consumers_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|