x4d-devkit 0.41.0rc2__tar.gz → 0.42.0rc1__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 (123) hide show
  1. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/PKG-INFO +2 -1
  2. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/pyproject.toml +2 -1
  3. x4d_devkit-0.42.0rc1/tests/test_roadmark_preview.py +87 -0
  4. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_sdk_api.py +1 -1
  5. x4d_devkit-0.42.0rc1/x4d_devkit/roadmark_preview.py +219 -0
  6. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit.egg-info/PKG-INFO +2 -1
  7. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  8. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit.egg-info/requires.txt +1 -0
  9. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/LICENSE +0 -0
  10. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/README.md +0 -0
  11. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/setup.cfg +0 -0
  12. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_annotation_contract.py +0 -0
  13. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_annotation_reference.py +0 -0
  14. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_assets_client.py +0 -0
  15. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_boundary.py +0 -0
  16. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_box.py +0 -0
  17. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_build_info.py +0 -0
  18. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_cli_auth.py +0 -0
  19. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_client_json_contract.py +0 -0
  20. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_client_projects.py +0 -0
  21. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_clip_archives.py +0 -0
  22. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_contract_validation.py +0 -0
  23. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_coordinate_frames.py +0 -0
  24. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_current_state.py +0 -0
  25. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
  26. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_dataset_contract.py +0 -0
  27. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_dataset_identity.py +0 -0
  28. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_detection_eval.py +0 -0
  29. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_evaluate.py +0 -0
  30. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_export_bundle.py +0 -0
  31. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_image_review.py +0 -0
  32. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_inference_input.py +0 -0
  33. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_inference_schemas.py +0 -0
  34. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_inference_service.py +0 -0
  35. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_label_schemas.py +0 -0
  36. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_lidar_materialization.py +0 -0
  37. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_loader.py +0 -0
  38. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_matching.py +0 -0
  39. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_metrics.py +0 -0
  40. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_models.py +0 -0
  41. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_platform_pose.py +0 -0
  42. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_preannotation_client.py +0 -0
  43. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_reconstruction_client.py +0 -0
  44. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_release_gate.py +0 -0
  45. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
  46. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_semantic_scene_client.py +0 -0
  47. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_semantic_scene_contract.py +0 -0
  48. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
  49. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_time_contract.py +0 -0
  50. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_token.py +0 -0
  51. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_tracking_eval.py +0 -0
  52. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_training_identity.py +0 -0
  53. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_transform.py +0 -0
  54. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_transform_chain.py +0 -0
  55. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_validation.py +0 -0
  56. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/tests/test_work_session.py +0 -0
  57. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/__init__.py +0 -0
  58. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/annotation_contract.py +0 -0
  59. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/annotation_reference.py +0 -0
  60. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/build_info.py +0 -0
  61. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/cli.py +0 -0
  62. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/__init__.py +0 -0
  63. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/_origin.py +0 -0
  64. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/annotation_reference.py +0 -0
  65. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/assets.py +0 -0
  66. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/bags.py +0 -0
  67. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  68. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  69. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/client.py +0 -0
  70. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  71. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/clips.py +0 -0
  72. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  73. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/image_review.py +0 -0
  74. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  75. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/preannotation.py +0 -0
  76. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/projects.py +0 -0
  77. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/reconstruction.py +0 -0
  78. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
  79. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  80. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/services.py +0 -0
  81. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  82. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  83. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  84. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/core/__init__.py +0 -0
  85. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/core/loader.py +0 -0
  86. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/core/models.py +0 -0
  87. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/core/token.py +0 -0
  88. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/core/transform.py +0 -0
  89. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/current_state.py +0 -0
  90. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/dataset_contract.py +0 -0
  91. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  92. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  93. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/api.py +0 -0
  94. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/box.py +0 -0
  95. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/detection.py +0 -0
  96. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/matching.py +0 -0
  97. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  98. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/eval/tracking.py +0 -0
  99. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/export_bundle.py +0 -0
  100. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/image_review.py +0 -0
  101. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  102. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/input.py +0 -0
  103. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/models.py +0 -0
  104. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  105. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/service.py +0 -0
  106. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  107. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/json_contract.py +0 -0
  108. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/label_schema.py +0 -0
  109. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
  110. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/materialize.py +0 -0
  111. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  112. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
  113. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/time_contract.py +0 -0
  114. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/training_identity.py +0 -0
  115. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  116. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/validation/contract.py +0 -0
  117. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
  118. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/validation/report.py +0 -0
  119. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/validation/validator.py +0 -0
  120. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit/work_session.py +0 -0
  121. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  122. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  123. {x4d_devkit-0.41.0rc2 → x4d_devkit-0.42.0rc1}/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.41.0rc2
3
+ Version: 0.42.0rc1
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.41.0rc2"
7
+ version = "0.42.0rc1"
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]
@@ -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.41.0rc2"
14
+ assert x4d_devkit.__version__ == "0.42.0rc1"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -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.41.0rc2
3
+ Version: 0.42.0rc1
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
@@ -2,6 +2,7 @@ numpy>=1.24
2
2
  httpx>=0.27
3
3
  rfc8785==0.1.4
4
4
  zstandard<1,>=0.23
5
+ pyproj<4,>=3.6
5
6
 
6
7
  [dev]
7
8
  pytest>=7.0
File without changes
File without changes
File without changes