x4d-devkit 0.40.0__tar.gz → 0.41.0rc2__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.0rc2}/PKG-INFO +1 -1
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/pyproject.toml +1 -1
- x4d_devkit-0.41.0rc2/tests/test_annotation_reference.py +165 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_preannotation_client.py +102 -5
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_sdk_api.py +1 -1
- x4d_devkit-0.41.0rc2/x4d_devkit/annotation_reference.py +304 -0
- x4d_devkit-0.41.0rc2/x4d_devkit/client/annotation_reference.py +131 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/preannotation.py +104 -7
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/reconstruction.py +113 -16
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/LICENSE +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/README.md +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/setup.cfg +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_box.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_loader.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_matching.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_token.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_transform.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_validation.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc2}/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.0rc2"
|
|
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()
|
|
@@ -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)
|
|
@@ -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.0rc2"
|
|
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
|