x4d-devkit 0.40.0__tar.gz → 0.41.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.
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/PKG-INFO +1 -1
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/pyproject.toml +1 -1
- x4d_devkit-0.41.0rc1/tests/test_annotation_reference.py +165 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_sdk_api.py +1 -1
- x4d_devkit-0.41.0rc1/x4d_devkit/annotation_reference.py +304 -0
- x4d_devkit-0.41.0rc1/x4d_devkit/client/annotation_reference.py +131 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/reconstruction.py +113 -16
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/LICENSE +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/README.md +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/setup.cfg +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_box.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_loader.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_matching.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_token.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_transform.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_validation.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -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.41.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"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import gzip
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from x4d_devkit import X4DClient
|
|
10
|
+
from x4d_devkit.annotation_reference import (
|
|
11
|
+
ANNOTATION_REFERENCE_OPERATION,
|
|
12
|
+
ANNOTATION_REFERENCE_SCHEMA,
|
|
13
|
+
GEOMETRY_IDENTITY_FIELDS,
|
|
14
|
+
decode_annotation_reference_chunk,
|
|
15
|
+
parse_annotation_reference_manifest,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fixture():
|
|
20
|
+
xyz = np.array([[1.5, -2, 3.25], [4, 0, 5]], dtype="<f4")
|
|
21
|
+
payload = gzip.compress(xyz.tobytes(), mtime=0)
|
|
22
|
+
chunk = dict(
|
|
23
|
+
id="0",
|
|
24
|
+
uri="/api/v1/files/clips/c/generated/reconstruction/annotation_reference/a/0.bin.gz",
|
|
25
|
+
sha256="sha256:" + hashlib.sha256(payload).hexdigest(),
|
|
26
|
+
size_bytes=len(payload),
|
|
27
|
+
point_count=2,
|
|
28
|
+
bounds=[xyz.min(0).tolist(), xyz.max(0).tolist()],
|
|
29
|
+
)
|
|
30
|
+
overview = dict(
|
|
31
|
+
chunk, id="overview", uri=chunk["uri"].replace("0.bin", "overview.bin")
|
|
32
|
+
)
|
|
33
|
+
manifest = dict(
|
|
34
|
+
schema_version=ANNOTATION_REFERENCE_SCHEMA,
|
|
35
|
+
coordinate_frame="clip_world",
|
|
36
|
+
point_format="xyz-f32-le-gzip",
|
|
37
|
+
clip_id="c",
|
|
38
|
+
dependency_identity="sha256:" + "a" * 64,
|
|
39
|
+
input_identity={
|
|
40
|
+
name: "sha256:" + "b" * 64 for name in GEOMETRY_IDENTITY_FIELDS
|
|
41
|
+
},
|
|
42
|
+
profile=dict(
|
|
43
|
+
identity="annotation_reference@1",
|
|
44
|
+
algorithm="annotation_reference@1.0.0",
|
|
45
|
+
lidar_channels=["arbitrary-scanner"],
|
|
46
|
+
voxel_size_m=0.03,
|
|
47
|
+
overview_voxel_size_m=0.3,
|
|
48
|
+
min_range_m=2.0,
|
|
49
|
+
max_range_m=60.0,
|
|
50
|
+
point_deskew=False,
|
|
51
|
+
foreground_policy="retain_all",
|
|
52
|
+
),
|
|
53
|
+
point_count=2,
|
|
54
|
+
bounds=chunk["bounds"],
|
|
55
|
+
chunks=[chunk],
|
|
56
|
+
overview=overview,
|
|
57
|
+
)
|
|
58
|
+
return manifest, payload, xyz.tobytes()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse(payload, **kwargs):
|
|
62
|
+
return parse_annotation_reference_manifest(
|
|
63
|
+
payload, api_url="http://platform", expected_clip_id="c", **kwargs
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_exact_geometry_roundtrip_and_current_input_binding():
|
|
68
|
+
payload, compressed, points = fixture()
|
|
69
|
+
result = parse(payload, expected_input_identity=payload["input_identity"])
|
|
70
|
+
assert result.to_dict() == payload
|
|
71
|
+
assert decode_annotation_reference_chunk(result.chunks[0], compressed) == points
|
|
72
|
+
stale = dict(payload["input_identity"], pose_revision="sha256:" + "c" * 64)
|
|
73
|
+
with pytest.raises(ValueError, match="current geometry"):
|
|
74
|
+
parse(payload, expected_input_identity=stale)
|
|
75
|
+
with pytest.raises(ValueError, match="integrity"):
|
|
76
|
+
decode_annotation_reference_chunk(result.chunks[0], compressed[:-1] + b"x")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize(
|
|
80
|
+
"edit",
|
|
81
|
+
[
|
|
82
|
+
lambda m: m.update(point_count=3),
|
|
83
|
+
lambda m: m["chunks"].append(copy.deepcopy(m["chunks"][0])),
|
|
84
|
+
lambda m: m["chunks"][0].update(uri="http://untrusted/api/v1/files/x"),
|
|
85
|
+
lambda m: m["chunks"][0].update(
|
|
86
|
+
uri=m["chunks"][0]["uri"].replace("clips/c/", "clips/other/")
|
|
87
|
+
),
|
|
88
|
+
lambda m: m["chunks"][0].update(point_count=1_000_001),
|
|
89
|
+
lambda m: m["chunks"][0].update(bounds=[[0, -2, 3.25], [4, 0, 5]]),
|
|
90
|
+
lambda m: m["input_identity"].update(annotation_revision="sha256:" + "a" * 64),
|
|
91
|
+
lambda m: m["profile"].update(foreground_policy="remove_all_boxes"),
|
|
92
|
+
lambda m: m["profile"].update(point_deskew=True),
|
|
93
|
+
lambda m: m["profile"].update(lidar_channels=["x", "x"]),
|
|
94
|
+
lambda m: m.update(coordinate_frame="sensor"),
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
def test_incompatible_partial_or_cross_clip_reference_is_rejected(edit):
|
|
98
|
+
payload, _, _ = fixture()
|
|
99
|
+
edit(payload)
|
|
100
|
+
with pytest.raises(ValueError):
|
|
101
|
+
parse(payload)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_valid_checksum_cannot_hide_extra_points_or_gzip_members():
|
|
105
|
+
payload, compressed, points = fixture()
|
|
106
|
+
for body in (gzip.compress(points + points), compressed + compressed):
|
|
107
|
+
payload["chunks"][0].update(
|
|
108
|
+
size_bytes=len(body), sha256="sha256:" + hashlib.sha256(body).hexdigest()
|
|
109
|
+
)
|
|
110
|
+
parsed = parse(payload)
|
|
111
|
+
with pytest.raises(ValueError, match="compression"):
|
|
112
|
+
decode_annotation_reference_chunk(parsed.chunks[0], body)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_client_routes_and_validates_task_and_clip_binding():
|
|
116
|
+
manifest, _, _ = fixture()
|
|
117
|
+
state = dict(
|
|
118
|
+
operation=ANNOTATION_REFERENCE_OPERATION,
|
|
119
|
+
project_id=7,
|
|
120
|
+
clip_id="c",
|
|
121
|
+
dependency_identity=manifest["dependency_identity"],
|
|
122
|
+
status="completed",
|
|
123
|
+
task_id="task",
|
|
124
|
+
progress=100.0,
|
|
125
|
+
stage="ready",
|
|
126
|
+
error=None,
|
|
127
|
+
current=True,
|
|
128
|
+
manifest=manifest,
|
|
129
|
+
)
|
|
130
|
+
calls = []
|
|
131
|
+
|
|
132
|
+
def handler(request):
|
|
133
|
+
calls.append(request)
|
|
134
|
+
return httpx.Response(200, json=state)
|
|
135
|
+
|
|
136
|
+
client = X4DClient(api_url="http://platform", token="test")
|
|
137
|
+
client._http.close()
|
|
138
|
+
client._http = httpx.Client(
|
|
139
|
+
base_url="http://platform", transport=httpx.MockTransport(handler)
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
assert client.reconstruction.prepare_annotation_reference(
|
|
143
|
+
project_id=7, clip_id="c"
|
|
144
|
+
).current
|
|
145
|
+
assert (
|
|
146
|
+
client.reconstruction.get_current_annotation_reference(
|
|
147
|
+
project_id=7, clip_id="c"
|
|
148
|
+
).manifest.point_count
|
|
149
|
+
== 2
|
|
150
|
+
)
|
|
151
|
+
assert (
|
|
152
|
+
client.reconstruction.get_annotation_reference_task(
|
|
153
|
+
project_id=7, task_id="task"
|
|
154
|
+
).task_id
|
|
155
|
+
== "task"
|
|
156
|
+
)
|
|
157
|
+
assert calls[0].method == "POST"
|
|
158
|
+
assert calls[1].url.path.endswith("/annotation-reference/current")
|
|
159
|
+
state["project_id"] = 8
|
|
160
|
+
with pytest.raises(ValueError, match="project"):
|
|
161
|
+
client.reconstruction.get_current_annotation_reference(
|
|
162
|
+
project_id=7, clip_id="c"
|
|
163
|
+
)
|
|
164
|
+
finally:
|
|
165
|
+
client.close()
|
|
@@ -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.41.0rc1"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Pure point-cloud evidence bound to one Clip's exact geometry inputs.
|
|
2
|
+
|
|
3
|
+
This optional generated evidence is not a Dataset annotation component.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import copy
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import hashlib
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
import zlib
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import urlsplit
|
|
16
|
+
|
|
17
|
+
from x4d_devkit.client.assets import _validated_file_uri
|
|
18
|
+
from x4d_devkit.json_contract import ensure_i_json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ANNOTATION_REFERENCE_SCHEMA = "x4d-annotation-reference-v1"
|
|
22
|
+
ANNOTATION_REFERENCE_OPERATION = "reconstruction.prepare_annotation_reference"
|
|
23
|
+
GEOMETRY_IDENTITY_FIELDS = (
|
|
24
|
+
"samples_revision",
|
|
25
|
+
"sweeps_revision",
|
|
26
|
+
"calibration_revision",
|
|
27
|
+
"pose_revision",
|
|
28
|
+
)
|
|
29
|
+
_HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _object(value, keys, path):
|
|
33
|
+
if not isinstance(value, dict) or set(value) != set(keys):
|
|
34
|
+
raise ValueError(f"{path} must contain exactly {sorted(keys)}")
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _text(value, path):
|
|
39
|
+
if not isinstance(value, str) or not value or value.strip() != value:
|
|
40
|
+
raise ValueError(f"{path} must be canonical text")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _positive(value, path):
|
|
45
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
46
|
+
raise ValueError(f"{path} must be a positive integer")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _hash(value, path):
|
|
51
|
+
if not isinstance(value, str) or not _HASH.fullmatch(value):
|
|
52
|
+
raise ValueError(f"{path} must be a SHA-256 identity")
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bounds(value, path):
|
|
57
|
+
if not isinstance(value, list) or len(value) != 2:
|
|
58
|
+
raise ValueError(f"{path} must have lower and upper XYZ bounds")
|
|
59
|
+
for row in value:
|
|
60
|
+
if (
|
|
61
|
+
not isinstance(row, list)
|
|
62
|
+
or len(row) != 3
|
|
63
|
+
or any(
|
|
64
|
+
isinstance(v, bool)
|
|
65
|
+
or not isinstance(v, (int, float))
|
|
66
|
+
or not math.isfinite(v)
|
|
67
|
+
for v in row
|
|
68
|
+
)
|
|
69
|
+
):
|
|
70
|
+
raise ValueError(f"{path} must contain finite XYZ coordinates")
|
|
71
|
+
if any(low > high for low, high in zip(*value)):
|
|
72
|
+
raise ValueError(f"{path} is inverted")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class AnnotationReferenceChunk:
|
|
77
|
+
id: str
|
|
78
|
+
uri: str
|
|
79
|
+
sha256: str
|
|
80
|
+
size_bytes: int
|
|
81
|
+
point_count: int
|
|
82
|
+
bounds: tuple[tuple[float, ...], ...]
|
|
83
|
+
|
|
84
|
+
def to_dict(self):
|
|
85
|
+
return dict(
|
|
86
|
+
id=self.id,
|
|
87
|
+
uri=self.uri,
|
|
88
|
+
sha256=self.sha256,
|
|
89
|
+
size_bytes=self.size_bytes,
|
|
90
|
+
point_count=self.point_count,
|
|
91
|
+
bounds=[list(row) for row in self.bounds],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class AnnotationReferenceManifest:
|
|
97
|
+
clip_id: str
|
|
98
|
+
dependency_identity: str
|
|
99
|
+
input_identity: dict[str, str]
|
|
100
|
+
profile: dict[str, Any]
|
|
101
|
+
point_count: int
|
|
102
|
+
bounds: tuple[tuple[float, ...], ...]
|
|
103
|
+
chunks: tuple[AnnotationReferenceChunk, ...]
|
|
104
|
+
overview: AnnotationReferenceChunk
|
|
105
|
+
|
|
106
|
+
def to_dict(self):
|
|
107
|
+
return dict(
|
|
108
|
+
schema_version=ANNOTATION_REFERENCE_SCHEMA,
|
|
109
|
+
coordinate_frame="clip_world",
|
|
110
|
+
point_format="xyz-f32-le-gzip",
|
|
111
|
+
clip_id=self.clip_id,
|
|
112
|
+
dependency_identity=self.dependency_identity,
|
|
113
|
+
input_identity=copy.deepcopy(self.input_identity),
|
|
114
|
+
profile=copy.deepcopy(self.profile),
|
|
115
|
+
point_count=self.point_count,
|
|
116
|
+
bounds=[list(row) for row in self.bounds],
|
|
117
|
+
chunks=[chunk.to_dict() for chunk in self.chunks],
|
|
118
|
+
overview=self.overview.to_dict(),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_annotation_reference_manifest(
|
|
123
|
+
payload: Any,
|
|
124
|
+
*,
|
|
125
|
+
api_url: str,
|
|
126
|
+
expected_clip_id: str,
|
|
127
|
+
expected_dependency_identity: str | None = None,
|
|
128
|
+
expected_input_identity: dict[str, str] | None = None,
|
|
129
|
+
) -> AnnotationReferenceManifest:
|
|
130
|
+
value = _object(
|
|
131
|
+
ensure_i_json(payload),
|
|
132
|
+
{
|
|
133
|
+
"schema_version",
|
|
134
|
+
"coordinate_frame",
|
|
135
|
+
"point_format",
|
|
136
|
+
"clip_id",
|
|
137
|
+
"dependency_identity",
|
|
138
|
+
"input_identity",
|
|
139
|
+
"profile",
|
|
140
|
+
"point_count",
|
|
141
|
+
"bounds",
|
|
142
|
+
"chunks",
|
|
143
|
+
"overview",
|
|
144
|
+
},
|
|
145
|
+
"annotation reference",
|
|
146
|
+
)
|
|
147
|
+
if (
|
|
148
|
+
value["schema_version"] != ANNOTATION_REFERENCE_SCHEMA
|
|
149
|
+
or value["coordinate_frame"] != "clip_world"
|
|
150
|
+
or value["point_format"] != "xyz-f32-le-gzip"
|
|
151
|
+
):
|
|
152
|
+
raise ValueError("Unsupported annotation reference contract")
|
|
153
|
+
if value["clip_id"] != expected_clip_id:
|
|
154
|
+
raise ValueError("Annotation reference belongs to another Clip")
|
|
155
|
+
dependency = _hash(value["dependency_identity"], "dependency_identity")
|
|
156
|
+
if (
|
|
157
|
+
expected_dependency_identity is not None
|
|
158
|
+
and dependency != expected_dependency_identity
|
|
159
|
+
):
|
|
160
|
+
raise ValueError("Annotation reference dependency changed")
|
|
161
|
+
identity = _object(
|
|
162
|
+
value["input_identity"], GEOMETRY_IDENTITY_FIELDS, "input_identity"
|
|
163
|
+
)
|
|
164
|
+
for name, item in identity.items():
|
|
165
|
+
_hash(item, name)
|
|
166
|
+
if expected_input_identity is not None and identity != expected_input_identity:
|
|
167
|
+
raise ValueError("Annotation reference does not match the current geometry")
|
|
168
|
+
profile = _object(
|
|
169
|
+
value["profile"],
|
|
170
|
+
{
|
|
171
|
+
"identity",
|
|
172
|
+
"algorithm",
|
|
173
|
+
"lidar_channels",
|
|
174
|
+
"voxel_size_m",
|
|
175
|
+
"overview_voxel_size_m",
|
|
176
|
+
"min_range_m",
|
|
177
|
+
"max_range_m",
|
|
178
|
+
"point_deskew",
|
|
179
|
+
"foreground_policy",
|
|
180
|
+
},
|
|
181
|
+
"profile",
|
|
182
|
+
)
|
|
183
|
+
_text(profile["identity"], "profile.identity")
|
|
184
|
+
_text(profile["algorithm"], "profile.algorithm")
|
|
185
|
+
channels = profile["lidar_channels"]
|
|
186
|
+
if not isinstance(channels, list) or not channels:
|
|
187
|
+
raise ValueError("Reference profile requires LiDAR channels")
|
|
188
|
+
for channel in channels:
|
|
189
|
+
_text(channel, "lidar channel")
|
|
190
|
+
if len(set(channels)) != len(channels):
|
|
191
|
+
raise ValueError("Reference profile has duplicate LiDAR channels")
|
|
192
|
+
for key in ("voxel_size_m", "overview_voxel_size_m", "min_range_m", "max_range_m"):
|
|
193
|
+
item = profile[key]
|
|
194
|
+
if (
|
|
195
|
+
isinstance(item, bool)
|
|
196
|
+
or not isinstance(item, (int, float))
|
|
197
|
+
or not math.isfinite(item)
|
|
198
|
+
or item < 0
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(f"Invalid reference {key}")
|
|
201
|
+
if (
|
|
202
|
+
profile["voxel_size_m"] <= 0
|
|
203
|
+
or profile["overview_voxel_size_m"] < profile["voxel_size_m"]
|
|
204
|
+
or profile["max_range_m"] <= profile["min_range_m"]
|
|
205
|
+
):
|
|
206
|
+
raise ValueError("Invalid reference voxel or range ordering")
|
|
207
|
+
if (
|
|
208
|
+
profile["point_deskew"] is not False
|
|
209
|
+
or profile["foreground_policy"] != "retain_all"
|
|
210
|
+
):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
"Reference v1 requires declared rigid scans and retained foreground"
|
|
213
|
+
)
|
|
214
|
+
count = _positive(value["point_count"], "point_count")
|
|
215
|
+
_bounds(value["bounds"], "bounds")
|
|
216
|
+
|
|
217
|
+
def chunk(item):
|
|
218
|
+
item = _object(
|
|
219
|
+
item,
|
|
220
|
+
{"id", "uri", "sha256", "size_bytes", "point_count", "bounds"},
|
|
221
|
+
"chunk",
|
|
222
|
+
)
|
|
223
|
+
_text(item["id"], "chunk.id")
|
|
224
|
+
uri = _validated_file_uri(item["uri"], api_url=api_url)
|
|
225
|
+
prefix = f"/api/v1/files/clips/{expected_clip_id}/generated/reconstruction/annotation_reference/"
|
|
226
|
+
if not urlsplit(uri).path.startswith(prefix):
|
|
227
|
+
raise ValueError("Reference chunk is outside its Clip evidence namespace")
|
|
228
|
+
_hash(item["sha256"], "chunk.sha256")
|
|
229
|
+
_positive(item["size_bytes"], "chunk.size_bytes")
|
|
230
|
+
n = _positive(item["point_count"], "chunk.point_count")
|
|
231
|
+
if n > 1_000_000 or item["size_bytes"] > n * 12 + 65536:
|
|
232
|
+
raise ValueError("Reference chunk exceeds bounded transfer size")
|
|
233
|
+
_bounds(item["bounds"], "chunk.bounds")
|
|
234
|
+
if any(
|
|
235
|
+
item["bounds"][0][a] < value["bounds"][0][a]
|
|
236
|
+
or item["bounds"][1][a] > value["bounds"][1][a]
|
|
237
|
+
for a in range(3)
|
|
238
|
+
):
|
|
239
|
+
raise ValueError("Chunk lies outside reference bounds")
|
|
240
|
+
return AnnotationReferenceChunk(
|
|
241
|
+
item["id"],
|
|
242
|
+
uri,
|
|
243
|
+
item["sha256"],
|
|
244
|
+
item["size_bytes"],
|
|
245
|
+
n,
|
|
246
|
+
tuple(tuple(map(float, row)) for row in item["bounds"]),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if not isinstance(value["chunks"], list) or not value["chunks"]:
|
|
250
|
+
raise ValueError("Reference must declare its complete fine chunks")
|
|
251
|
+
chunks = tuple(chunk(item) for item in value["chunks"])
|
|
252
|
+
overview = chunk(value["overview"])
|
|
253
|
+
all_chunks = (*chunks, overview)
|
|
254
|
+
if (
|
|
255
|
+
len({c.id for c in all_chunks}) != len(all_chunks)
|
|
256
|
+
or len({c.uri for c in all_chunks}) != len(all_chunks)
|
|
257
|
+
or sum(c.point_count for c in chunks) != count
|
|
258
|
+
or overview.point_count > count
|
|
259
|
+
):
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"Reference chunk identities or complete point count are inconsistent"
|
|
262
|
+
)
|
|
263
|
+
return AnnotationReferenceManifest(
|
|
264
|
+
expected_clip_id,
|
|
265
|
+
dependency,
|
|
266
|
+
copy.deepcopy(identity),
|
|
267
|
+
copy.deepcopy(profile),
|
|
268
|
+
count,
|
|
269
|
+
tuple(tuple(map(float, row)) for row in value["bounds"]),
|
|
270
|
+
chunks,
|
|
271
|
+
overview,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def decode_annotation_reference_chunk(
|
|
276
|
+
chunk: AnnotationReferenceChunk, payload: bytes
|
|
277
|
+
) -> bytes:
|
|
278
|
+
"""Verify compressed bytes and bound decompression before exposing XYZ."""
|
|
279
|
+
if (
|
|
280
|
+
len(payload) != chunk.size_bytes
|
|
281
|
+
or "sha256:" + hashlib.sha256(payload).hexdigest() != chunk.sha256
|
|
282
|
+
):
|
|
283
|
+
raise ValueError("Annotation reference chunk integrity mismatch")
|
|
284
|
+
decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
|
|
285
|
+
points = decoder.decompress(payload, chunk.point_count * 12 + 1)
|
|
286
|
+
if (
|
|
287
|
+
len(points) != chunk.point_count * 12
|
|
288
|
+
or not decoder.eof
|
|
289
|
+
or decoder.unused_data
|
|
290
|
+
or decoder.unconsumed_tail
|
|
291
|
+
):
|
|
292
|
+
raise ValueError(
|
|
293
|
+
"Annotation reference chunk point count or compression mismatch"
|
|
294
|
+
)
|
|
295
|
+
import numpy as np
|
|
296
|
+
|
|
297
|
+
xyz = np.frombuffer(points, dtype="<f4").reshape(-1, 3)
|
|
298
|
+
if (
|
|
299
|
+
not np.isfinite(xyz).all()
|
|
300
|
+
or not np.array_equal(xyz.min(axis=0), chunk.bounds[0])
|
|
301
|
+
or not np.array_equal(xyz.max(axis=0), chunk.bounds[1])
|
|
302
|
+
):
|
|
303
|
+
raise ValueError("Annotation reference chunk geometry disagrees with manifest")
|
|
304
|
+
return points
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Typed reconstruction reference states without annotation write authority."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from x4d_devkit.annotation_reference import (
|
|
10
|
+
ANNOTATION_REFERENCE_OPERATION,
|
|
11
|
+
AnnotationReferenceManifest,
|
|
12
|
+
_hash,
|
|
13
|
+
_object,
|
|
14
|
+
_positive,
|
|
15
|
+
_text,
|
|
16
|
+
parse_annotation_reference_manifest,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class AnnotationReferenceState:
|
|
22
|
+
project_id: int
|
|
23
|
+
clip_id: str
|
|
24
|
+
dependency_identity: str
|
|
25
|
+
status: str
|
|
26
|
+
task_id: str | None
|
|
27
|
+
progress: float
|
|
28
|
+
stage: str | None
|
|
29
|
+
error: dict[str, Any] | None
|
|
30
|
+
current: bool
|
|
31
|
+
manifest: AnnotationReferenceManifest | None
|
|
32
|
+
|
|
33
|
+
def to_dict(self):
|
|
34
|
+
return dict(
|
|
35
|
+
operation=ANNOTATION_REFERENCE_OPERATION,
|
|
36
|
+
project_id=self.project_id,
|
|
37
|
+
clip_id=self.clip_id,
|
|
38
|
+
dependency_identity=self.dependency_identity,
|
|
39
|
+
status=self.status,
|
|
40
|
+
task_id=self.task_id,
|
|
41
|
+
progress=self.progress,
|
|
42
|
+
stage=self.stage,
|
|
43
|
+
error=copy.deepcopy(self.error),
|
|
44
|
+
current=self.current,
|
|
45
|
+
manifest=self.manifest.to_dict() if self.manifest else None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_annotation_reference_state(
|
|
50
|
+
payload, *, api_url, project_id, clip_id=None, task_id=None
|
|
51
|
+
):
|
|
52
|
+
value = _object(
|
|
53
|
+
payload,
|
|
54
|
+
{
|
|
55
|
+
"operation",
|
|
56
|
+
"project_id",
|
|
57
|
+
"clip_id",
|
|
58
|
+
"dependency_identity",
|
|
59
|
+
"status",
|
|
60
|
+
"task_id",
|
|
61
|
+
"progress",
|
|
62
|
+
"stage",
|
|
63
|
+
"error",
|
|
64
|
+
"current",
|
|
65
|
+
"manifest",
|
|
66
|
+
},
|
|
67
|
+
"reference state",
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
value["operation"] != ANNOTATION_REFERENCE_OPERATION
|
|
71
|
+
or value["project_id"] != project_id
|
|
72
|
+
):
|
|
73
|
+
raise ValueError("Reference response belongs to another operation or project")
|
|
74
|
+
_positive(value["project_id"], "project_id")
|
|
75
|
+
_text(value["clip_id"], "clip_id")
|
|
76
|
+
_hash(value["dependency_identity"], "dependency_identity")
|
|
77
|
+
if clip_id is not None and value["clip_id"] != clip_id:
|
|
78
|
+
raise ValueError("Reference response belongs to another Clip")
|
|
79
|
+
if task_id is not None and value["task_id"] != task_id:
|
|
80
|
+
raise ValueError("Reference response belongs to another task")
|
|
81
|
+
if value["status"] not in {
|
|
82
|
+
"unavailable",
|
|
83
|
+
"queued",
|
|
84
|
+
"accepted",
|
|
85
|
+
"running",
|
|
86
|
+
"completed",
|
|
87
|
+
"failed",
|
|
88
|
+
"cancelled",
|
|
89
|
+
}:
|
|
90
|
+
raise ValueError("Unsupported reference task status")
|
|
91
|
+
if value["task_id"] is not None:
|
|
92
|
+
_text(value["task_id"], "task_id")
|
|
93
|
+
if value["stage"] is not None:
|
|
94
|
+
_text(value["stage"], "stage")
|
|
95
|
+
if type(value["current"]) is not bool:
|
|
96
|
+
raise ValueError("Reference current must be boolean")
|
|
97
|
+
progress = value["progress"]
|
|
98
|
+
if (
|
|
99
|
+
isinstance(progress, bool)
|
|
100
|
+
or not isinstance(progress, (int, float))
|
|
101
|
+
or not 0 <= progress <= 100
|
|
102
|
+
):
|
|
103
|
+
raise ValueError("Invalid reference progress")
|
|
104
|
+
if value["error"] is not None and not isinstance(value["error"], dict):
|
|
105
|
+
raise ValueError("Reference error must be structured")
|
|
106
|
+
manifest = (
|
|
107
|
+
None
|
|
108
|
+
if value["manifest"] is None
|
|
109
|
+
else parse_annotation_reference_manifest(
|
|
110
|
+
value["manifest"],
|
|
111
|
+
api_url=api_url,
|
|
112
|
+
expected_clip_id=value["clip_id"],
|
|
113
|
+
expected_dependency_identity=value["dependency_identity"],
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if value["current"] != (manifest is not None) or (
|
|
117
|
+
manifest and value["status"] != "completed"
|
|
118
|
+
):
|
|
119
|
+
raise ValueError("Reference ready state is inconsistent")
|
|
120
|
+
return AnnotationReferenceState(
|
|
121
|
+
project_id,
|
|
122
|
+
value["clip_id"],
|
|
123
|
+
value["dependency_identity"],
|
|
124
|
+
value["status"],
|
|
125
|
+
value["task_id"],
|
|
126
|
+
float(progress),
|
|
127
|
+
value["stage"],
|
|
128
|
+
copy.deepcopy(value["error"]),
|
|
129
|
+
value["current"],
|
|
130
|
+
manifest,
|
|
131
|
+
)
|