x4d-devkit 0.52.0rc1__tar.gz → 0.52.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.52.0rc1 → x4d_devkit-0.52.0rc2}/PKG-INFO +1 -1
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/pyproject.toml +1 -1
- x4d_devkit-0.52.0rc2/tests/test_image_conditional_rules.py +328 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_clip.py +97 -1
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_schema.py +136 -3
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/SOURCES.txt +1 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/LICENSE +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/README.md +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/setup.cfg +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_accounts.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_document_hash.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_box.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_enum_set.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_groups.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_schema_upgrade.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_loader.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_matching.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_models.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_parse_equivalence.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_project_discovery.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_token.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_transform.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_validation.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/accounts.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/accounts.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_schemas.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_groups.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_schema_upgrade.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/project_discovery.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.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.52.
|
|
7
|
+
version = "0.52.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,328 @@
|
|
|
1
|
+
"""Closed conditional attribute rules, English terms and description coverage (#220)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from x4d_devkit._image_contract import ImageContractError, fingerprint
|
|
10
|
+
from x4d_devkit.image_clip import build_image_identity, parse_image_clip
|
|
11
|
+
from x4d_devkit.image_schema import parse_image_schema_body
|
|
12
|
+
from tests.image_fixtures import clip_payload, schema_body
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def enum(attribute_id, values, **extra):
|
|
16
|
+
return {
|
|
17
|
+
"id": attribute_id,
|
|
18
|
+
"display_name": attribute_id,
|
|
19
|
+
"type": "enum",
|
|
20
|
+
"required": False,
|
|
21
|
+
"scopes": ["object"],
|
|
22
|
+
"values": [{"id": value, "display_name": value} for value in values],
|
|
23
|
+
**extra,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def boolean(attribute_id):
|
|
28
|
+
return {
|
|
29
|
+
"id": attribute_id,
|
|
30
|
+
"display_name": attribute_id,
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"required": False,
|
|
33
|
+
"scopes": ["object"],
|
|
34
|
+
"values": [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def directions():
|
|
39
|
+
return {
|
|
40
|
+
"id": "occlusion_directions",
|
|
41
|
+
"display_name": "occlusion directions",
|
|
42
|
+
"type": "enum_set",
|
|
43
|
+
"required": False,
|
|
44
|
+
"scopes": ["object"],
|
|
45
|
+
"values": [
|
|
46
|
+
{"id": side, "display_name": side} for side in ("top", "bottom", "left", "right")
|
|
47
|
+
],
|
|
48
|
+
"min_items": 1,
|
|
49
|
+
"max_items": 4,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
SIGNAL_RULES = [
|
|
54
|
+
{
|
|
55
|
+
"id": "head_facts",
|
|
56
|
+
"attributes": ["head_type", "occlusion"],
|
|
57
|
+
"required": ["head_type", "occlusion"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "arrow",
|
|
61
|
+
"when": {"attribute": "head_type", "equals": "arrow"},
|
|
62
|
+
"attributes": ["arrow_direction"],
|
|
63
|
+
"required": ["arrow_direction"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "damaged",
|
|
67
|
+
"when": {"attribute": "head_type", "in": ["countdown", "countdown_damaged"]},
|
|
68
|
+
"attributes": ["digit_unclear"],
|
|
69
|
+
"required": ["digit_unclear"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "digit_countdown",
|
|
73
|
+
"when": {"attribute": "head_type", "equals": "countdown"},
|
|
74
|
+
"attributes": ["digit"],
|
|
75
|
+
"required": ["digit"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "digit_damaged_known",
|
|
79
|
+
"when": {
|
|
80
|
+
"all": [
|
|
81
|
+
{"attribute": "head_type", "equals": "countdown_damaged"},
|
|
82
|
+
{"attribute": "digit_unclear", "equals": False},
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
"attributes": ["digit"],
|
|
86
|
+
"required": ["digit"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "digit_optional_when_unclear",
|
|
90
|
+
"when": {
|
|
91
|
+
"all": [
|
|
92
|
+
{"attribute": "head_type", "equals": "countdown_damaged"},
|
|
93
|
+
{"attribute": "digit_unclear", "equals": True},
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"attributes": ["digit"],
|
|
97
|
+
"required": [],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "occluded",
|
|
101
|
+
"when": {"attribute": "occlusion", "equals": True},
|
|
102
|
+
"attributes": ["occlusion_directions", "occlusion_ratio"],
|
|
103
|
+
"required": ["occlusion_directions", "occlusion_ratio"],
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def signal_category(rules=None):
|
|
109
|
+
return {
|
|
110
|
+
"id": "box",
|
|
111
|
+
"display_name": "Signal head",
|
|
112
|
+
"geometry_type": "box_2d",
|
|
113
|
+
"color": "#112233",
|
|
114
|
+
"attributes": [
|
|
115
|
+
enum("head_type", ["round", "arrow", "countdown", "countdown_damaged"]),
|
|
116
|
+
enum("arrow_direction", ["up", "left"]),
|
|
117
|
+
enum("digit", [str(digit) for digit in range(10)]),
|
|
118
|
+
boolean("digit_unclear"),
|
|
119
|
+
boolean("occlusion"),
|
|
120
|
+
directions(),
|
|
121
|
+
enum("occlusion_ratio", ["low", "medium", "high"]),
|
|
122
|
+
],
|
|
123
|
+
"rules": copy.deepcopy(SIGNAL_RULES if rules is None else rules),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def body(category=None, description_fields=None):
|
|
128
|
+
value = schema_body()
|
|
129
|
+
value["categories"][1] = category or signal_category()
|
|
130
|
+
if description_fields is not None:
|
|
131
|
+
value["task_views"][1]["category_ids"] = ["box"]
|
|
132
|
+
value["task_views"][1]["description_fields"] = description_fields
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def box(object_id, attributes):
|
|
137
|
+
return {
|
|
138
|
+
"id": object_id,
|
|
139
|
+
"image_id": "image_a",
|
|
140
|
+
"view_id": "objects",
|
|
141
|
+
"category_id": "box",
|
|
142
|
+
"geometry": {
|
|
143
|
+
"geometry_type": "box_2d",
|
|
144
|
+
"box": {"x_min": 10, "y_min": 20, "x_max": 30, "y_max": 40},
|
|
145
|
+
},
|
|
146
|
+
"attributes": attributes,
|
|
147
|
+
"attribute_ranges": [],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def clip(schema, objects, *, objects_state="complete", descriptions=(), text_state="unprocessed"):
|
|
152
|
+
value = clip_payload()
|
|
153
|
+
value["label_schema"]["body"] = schema
|
|
154
|
+
value["label_schema"]["identity"]["fingerprint"] = fingerprint(schema)
|
|
155
|
+
value["image_annotation"]["objects"] = objects
|
|
156
|
+
value["image_annotation"]["descriptions"] = list(descriptions)
|
|
157
|
+
states = {"objects": objects_state, "semantics": text_state}
|
|
158
|
+
for row in value["image_annotation"]["completion"]:
|
|
159
|
+
row["state"] = states[row["view_id"]]
|
|
160
|
+
value["identity"] = build_image_identity(value)
|
|
161
|
+
return parse_image_clip(value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def head(**attributes):
|
|
165
|
+
return box("head_a", {"occlusion": False, **attributes})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.parametrize(
|
|
169
|
+
"attributes",
|
|
170
|
+
[
|
|
171
|
+
{"head_type": "round"},
|
|
172
|
+
{"head_type": "arrow", "arrow_direction": "left"},
|
|
173
|
+
{"head_type": "countdown", "digit_unclear": False, "digit": "4"},
|
|
174
|
+
{"head_type": "countdown_damaged", "digit_unclear": True},
|
|
175
|
+
{"head_type": "countdown_damaged", "digit_unclear": True, "digit": "9"},
|
|
176
|
+
{"head_type": "countdown_damaged", "digit_unclear": False, "digit": "9"},
|
|
177
|
+
{
|
|
178
|
+
"head_type": "round",
|
|
179
|
+
"occlusion": True,
|
|
180
|
+
"occlusion_directions": ["top", "left"],
|
|
181
|
+
"occlusion_ratio": "medium",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
)
|
|
185
|
+
def test_completed_heads_satisfy_conditional_rules(attributes):
|
|
186
|
+
clip(body(), [head(**attributes)])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.parametrize(
|
|
190
|
+
("attributes", "code", "attribute"),
|
|
191
|
+
[
|
|
192
|
+
({}, "attribute_required_on_completion", "head_type"),
|
|
193
|
+
({"head_type": "arrow"}, "attribute_required_on_completion", "arrow_direction"),
|
|
194
|
+
({"head_type": "round", "arrow_direction": "up"}, "attribute_not_applicable", "arrow_direction"),
|
|
195
|
+
({"head_type": "countdown", "digit_unclear": False}, "attribute_required_on_completion", "digit"),
|
|
196
|
+
({"head_type": "countdown_damaged", "digit_unclear": False}, "attribute_required_on_completion", "digit"),
|
|
197
|
+
({"head_type": "round", "digit": "3"}, "attribute_not_applicable", "digit"),
|
|
198
|
+
({"head_type": "round", "occlusion": True, "occlusion_ratio": "low"}, "attribute_required_on_completion", "occlusion_directions"),
|
|
199
|
+
({"head_type": "round", "occlusion_directions": ["top"]}, "attribute_not_applicable", "occlusion_directions"),
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
def test_completed_heads_reject_missing_or_leftover_facts(attributes, code, attribute):
|
|
203
|
+
with pytest.raises(ImageContractError) as error:
|
|
204
|
+
clip(body(), [head(**attributes)])
|
|
205
|
+
assert error.value.code == code
|
|
206
|
+
assert error.value.path == f"image_annotation.objects.head_a.attributes.{attribute}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_drafts_may_pause_with_missing_or_leftover_facts_but_values_stay_typed():
|
|
210
|
+
clip(body(), [head(head_type="round", arrow_direction="up")], objects_state="unprocessed")
|
|
211
|
+
clip(body(), [box("head_a", {})], objects_state="unprocessed")
|
|
212
|
+
with pytest.raises(ImageContractError):
|
|
213
|
+
clip(body(), [head(head_type="round", arrow_direction="sideways")], objects_state="unprocessed")
|
|
214
|
+
with pytest.raises(ImageContractError): # enum_set bounds apply to drafts too
|
|
215
|
+
clip(
|
|
216
|
+
body(),
|
|
217
|
+
[head(head_type="round", occlusion=True, occlusion_directions=[])],
|
|
218
|
+
objects_state="unprocessed",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_rules_change_the_schema_fingerprint():
|
|
223
|
+
plain = parse_image_schema_body(body(signal_category(rules=SIGNAL_RULES[:1])))
|
|
224
|
+
full = parse_image_schema_body(body())
|
|
225
|
+
assert plain.identity.fingerprint != full.identity.fingerprint
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _rule(**change):
|
|
229
|
+
rule = copy.deepcopy(SIGNAL_RULES[1])
|
|
230
|
+
rule.update(change)
|
|
231
|
+
return rule
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.parametrize(
|
|
235
|
+
"rules",
|
|
236
|
+
[
|
|
237
|
+
[],
|
|
238
|
+
[_rule(attributes=["missing"], required=[])],
|
|
239
|
+
[_rule(required=["head_type"])],
|
|
240
|
+
[_rule(attributes=["arrow_direction", "arrow_direction"])],
|
|
241
|
+
[_rule(when={"attribute": "missing", "equals": "arrow"})],
|
|
242
|
+
[_rule(when={"attribute": "head_type", "equals": "triangle"})],
|
|
243
|
+
[_rule(when={"attribute": "head_type", "equals": True})],
|
|
244
|
+
[_rule(when={"attribute": "occlusion", "equals": "true"})],
|
|
245
|
+
[_rule(when={"attribute": "head_type", "in": []})],
|
|
246
|
+
[_rule(when={"attribute": "head_type", "in": ["arrow", "arrow"]})],
|
|
247
|
+
[_rule(when={"attribute": "head_type", "contains": "arrow"})],
|
|
248
|
+
[_rule(when={"attribute": "occlusion_directions", "equals": "top"})],
|
|
249
|
+
[_rule(when={"attribute": "head_type", "equals": "arrow", "in": ["arrow"]})],
|
|
250
|
+
[_rule(when={"all": [{"any": [{"attribute": "head_type", "equals": "arrow"}]}]})],
|
|
251
|
+
[_rule(when={"all": []})],
|
|
252
|
+
[_rule(when="head_type == 'arrow'")],
|
|
253
|
+
[_rule(), _rule()],
|
|
254
|
+
[
|
|
255
|
+
_rule(id="a", when={"attribute": "digit", "equals": "1"}, attributes=["arrow_direction"]),
|
|
256
|
+
_rule(id="b", when={"attribute": "arrow_direction", "equals": "up"}, attributes=["digit"], required=[]),
|
|
257
|
+
],
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
def test_schema_rejects_open_or_inconsistent_rules(rules):
|
|
261
|
+
with pytest.raises(ImageContractError):
|
|
262
|
+
parse_image_schema_body(body(signal_category(rules=rules)))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_governed_attribute_cannot_also_be_always_required():
|
|
266
|
+
category = signal_category()
|
|
267
|
+
category["attributes"][1]["required"] = True
|
|
268
|
+
with pytest.raises(ImageContractError):
|
|
269
|
+
parse_image_schema_body(body(category))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_enum_values_may_declare_english_terms():
|
|
273
|
+
category = signal_category()
|
|
274
|
+
category["attributes"][0]["values"][0]["term"] = {"singular": "round light", "plural": "round lights"}
|
|
275
|
+
category["attributes"][2]["values"][0]["term"] = {"singular": "zero"}
|
|
276
|
+
parse_image_schema_body(body(category))
|
|
277
|
+
for term in ({"singular": " "}, {"plural": "lights"}, {"singular": "x", "plural": ""}, {"singular": "x", "other": "y"}):
|
|
278
|
+
category["attributes"][0]["values"][0]["term"] = term
|
|
279
|
+
with pytest.raises(ImageContractError):
|
|
280
|
+
parse_image_schema_body(body(category))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
TEXT_FIELDS = [
|
|
284
|
+
{"id": "phrase", "display_name": "Phrase", "required": True, "target": "object"},
|
|
285
|
+
{"id": "caption", "display_name": "Caption", "required": True, "target": "image"},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def text(text_id, target, field, value="text"):
|
|
290
|
+
return {
|
|
291
|
+
"id": text_id,
|
|
292
|
+
"image_id": "image_a",
|
|
293
|
+
"view_id": "semantics",
|
|
294
|
+
"target_id": target,
|
|
295
|
+
"fields": {field: value},
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_completed_description_scope_covers_every_object_and_the_image():
|
|
300
|
+
schema = body(description_fields=TEXT_FIELDS)
|
|
301
|
+
objects = [head(head_type="round"), {**head(head_type="round"), "id": "head_b"}]
|
|
302
|
+
full = [
|
|
303
|
+
text("p_a", "head_a", "phrase"),
|
|
304
|
+
text("p_b", "head_b", "phrase"),
|
|
305
|
+
text("caption", None, "caption"),
|
|
306
|
+
]
|
|
307
|
+
clip(schema, objects, descriptions=full, text_state="complete")
|
|
308
|
+
clip(schema, objects, descriptions=full[:1], text_state="unprocessed")
|
|
309
|
+
for missing in range(3):
|
|
310
|
+
partial = full[:missing] + full[missing + 1 :]
|
|
311
|
+
with pytest.raises(ImageContractError) as error:
|
|
312
|
+
clip(schema, objects, descriptions=partial, text_state="complete")
|
|
313
|
+
assert error.value.code == "description_coverage_incomplete"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_description_targets_are_unique_and_fields_match_their_target():
|
|
317
|
+
schema = body(description_fields=TEXT_FIELDS)
|
|
318
|
+
objects = [head(head_type="round")]
|
|
319
|
+
with pytest.raises(ImageContractError): # two phrases for one object
|
|
320
|
+
clip(schema, objects, descriptions=[text("p1", "head_a", "phrase"), text("p2", "head_a", "phrase")])
|
|
321
|
+
with pytest.raises(ImageContractError): # caption field on an object description
|
|
322
|
+
clip(schema, objects, descriptions=[text("p1", "head_a", "caption")])
|
|
323
|
+
with pytest.raises(ImageContractError): # phrase on the image-level description
|
|
324
|
+
clip(schema, objects, descriptions=[text("c", None, "phrase")])
|
|
325
|
+
with pytest.raises(ImageContractError):
|
|
326
|
+
parse_image_schema_body(
|
|
327
|
+
body(description_fields=[{**TEXT_FIELDS[0], "target": "region"}])
|
|
328
|
+
)
|
|
@@ -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.52.
|
|
14
|
+
assert x4d_devkit.__version__ == "0.52.0rc2"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -261,6 +261,72 @@ def _attributes(values: Any, category: dict, scope: str, path: str) -> None:
|
|
|
261
261
|
)
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
def _condition_holds(condition: dict, values: dict) -> bool:
|
|
265
|
+
for combinator, test in (("all", all), ("any", any)):
|
|
266
|
+
if combinator in condition:
|
|
267
|
+
return test(_condition_holds(item, values) for item in condition[combinator])
|
|
268
|
+
name = condition["attribute"]
|
|
269
|
+
if name not in values:
|
|
270
|
+
return False
|
|
271
|
+
if "equals" in condition:
|
|
272
|
+
return values[name] == condition["equals"] and type(values[name]) is type(
|
|
273
|
+
condition["equals"]
|
|
274
|
+
)
|
|
275
|
+
if "in" in condition:
|
|
276
|
+
return values[name] in condition["in"]
|
|
277
|
+
return condition["contains"] in values[name]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _completed_rules(values: dict, category: dict, path: str) -> None:
|
|
281
|
+
"""Completion-time requiredness and applicability from closed Schema rules."""
|
|
282
|
+
|
|
283
|
+
rules = category.get("rules", [])
|
|
284
|
+
governed = {name for rule in rules for name in rule["attributes"]}
|
|
285
|
+
applicable: set[str] = set()
|
|
286
|
+
for rule in rules:
|
|
287
|
+
if "when" not in rule or _condition_holds(rule["when"], values):
|
|
288
|
+
applicable.update(rule["attributes"])
|
|
289
|
+
for name in rule["required"]:
|
|
290
|
+
if name not in values:
|
|
291
|
+
raise ImageContractError(
|
|
292
|
+
code="attribute_required_on_completion",
|
|
293
|
+
message=f"rule {rule['id']} requires this attribute",
|
|
294
|
+
path=path + "." + name,
|
|
295
|
+
)
|
|
296
|
+
for name in sorted(governed - applicable):
|
|
297
|
+
if name in values:
|
|
298
|
+
raise ImageContractError(
|
|
299
|
+
code="attribute_not_applicable",
|
|
300
|
+
message="attribute is not applicable to the completed object",
|
|
301
|
+
path=path + "." + name,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _description_coverage(
|
|
306
|
+
image_id: str, view: dict, objects: dict, described: set
|
|
307
|
+
) -> None:
|
|
308
|
+
targets = {item.get("target") for item in view["description_fields"]}
|
|
309
|
+
path = f"image_annotation.completion.{image_id}.{view['id']}"
|
|
310
|
+
if "object" in targets:
|
|
311
|
+
for name, row in sorted(objects.items()):
|
|
312
|
+
if row["image_id"] != image_id or (
|
|
313
|
+
view["category_ids"] and row["category_id"] not in view["category_ids"]
|
|
314
|
+
):
|
|
315
|
+
continue
|
|
316
|
+
if (view["id"], image_id, name) not in described:
|
|
317
|
+
raise ImageContractError(
|
|
318
|
+
code="description_coverage_incomplete",
|
|
319
|
+
message="every applicable object requires its description",
|
|
320
|
+
path=path + "." + name,
|
|
321
|
+
)
|
|
322
|
+
if "image" in targets and (view["id"], image_id, None) not in described:
|
|
323
|
+
raise ImageContractError(
|
|
324
|
+
code="description_coverage_incomplete",
|
|
325
|
+
message="the completed image requires its image description",
|
|
326
|
+
path=path,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
264
330
|
def validate_image_annotations(
|
|
265
331
|
value: Any, *, clip_id: str, images: dict, schema: dict, view_ids: list[str]
|
|
266
332
|
) -> ImageDocument:
|
|
@@ -363,6 +429,7 @@ def validate_image_annotations(
|
|
|
363
429
|
categories=categories,
|
|
364
430
|
result_ids=set(objects) | set(descriptions),
|
|
365
431
|
)
|
|
432
|
+
described: set[tuple] = set()
|
|
366
433
|
for name, row in descriptions.items():
|
|
367
434
|
path = "image_annotation.descriptions." + name
|
|
368
435
|
fields(row, {"id", "image_id", "view_id", "target_id", "fields"}, path)
|
|
@@ -398,12 +465,25 @@ def validate_image_annotations(
|
|
|
398
465
|
path + ".target_id",
|
|
399
466
|
)
|
|
400
467
|
values = row["fields"]
|
|
401
|
-
|
|
468
|
+
kind = "image" if target is None else "object"
|
|
469
|
+
definitions = {
|
|
470
|
+
item["id"]: item
|
|
471
|
+
for item in view["description_fields"]
|
|
472
|
+
if item.get("target", kind) == kind
|
|
473
|
+
}
|
|
402
474
|
require(
|
|
403
475
|
type(values) is dict and set(values) <= set(definitions),
|
|
404
476
|
"unsupported description fields",
|
|
405
477
|
path + ".fields",
|
|
406
478
|
)
|
|
479
|
+
if any("target" in item for item in view["description_fields"]):
|
|
480
|
+
key = (row["view_id"], row["image_id"], target)
|
|
481
|
+
require(
|
|
482
|
+
key not in described,
|
|
483
|
+
"duplicate description for the same target",
|
|
484
|
+
path + ".target_id",
|
|
485
|
+
)
|
|
486
|
+
described.add(key)
|
|
407
487
|
for key, item in values.items():
|
|
408
488
|
require(
|
|
409
489
|
type(item) is str and bool(item.strip()),
|
|
@@ -454,6 +534,22 @@ def validate_image_annotations(
|
|
|
454
534
|
"completion must cover every applicable image and view",
|
|
455
535
|
"image_annotation.completion",
|
|
456
536
|
)
|
|
537
|
+
complete = {
|
|
538
|
+
(row["image_id"], row["view_id"])
|
|
539
|
+
for row in value["completion"]
|
|
540
|
+
if row["state"] == "complete"
|
|
541
|
+
}
|
|
542
|
+
for name, row in objects.items():
|
|
543
|
+
if (row["image_id"], row["view_id"]) in complete:
|
|
544
|
+
_completed_rules(
|
|
545
|
+
row["attributes"],
|
|
546
|
+
categories[row["category_id"]],
|
|
547
|
+
"image_annotation.objects." + name + ".attributes",
|
|
548
|
+
)
|
|
549
|
+
for image_id, view_id in sorted(complete):
|
|
550
|
+
view = views[view_id]
|
|
551
|
+
if view["kind"] == "descriptions":
|
|
552
|
+
_description_coverage(image_id, view, objects, described)
|
|
457
553
|
return document(value)
|
|
458
554
|
|
|
459
555
|
|
|
@@ -128,6 +128,126 @@ class ImageSchemaRecord(ImageDocument):
|
|
|
128
128
|
return parse_image_schema_body(self.body).task_view_by_id(view_id)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _term(value: Any, path: str) -> None:
|
|
132
|
+
fields(value, {"singular"}, path, optional={"plural"})
|
|
133
|
+
text(value["singular"], path + ".singular")
|
|
134
|
+
if "plural" in value:
|
|
135
|
+
text(value["plural"], path + ".plural")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _predicate(value: Any, attributes: dict, path: str) -> str:
|
|
139
|
+
require(type(value) is dict, "condition must be an object", path)
|
|
140
|
+
operators = [name for name in ("equals", "in", "contains") if name in value]
|
|
141
|
+
require(
|
|
142
|
+
len(operators) == 1 and set(value) == {"attribute", operators[0]},
|
|
143
|
+
"predicate requires attribute and exactly one of equals/in/contains",
|
|
144
|
+
path,
|
|
145
|
+
)
|
|
146
|
+
name, operator = value["attribute"], operators[0]
|
|
147
|
+
require(
|
|
148
|
+
type(name) is str and name in attributes and "object" in attributes[name]["scopes"],
|
|
149
|
+
"condition must reference an object attribute of this category",
|
|
150
|
+
path + ".attribute",
|
|
151
|
+
)
|
|
152
|
+
definition = attributes[name]
|
|
153
|
+
declared = {item["id"] for item in definition["values"]}
|
|
154
|
+
operand = value[operator]
|
|
155
|
+
kind = definition["type"]
|
|
156
|
+
if operator == "equals" and kind == "boolean":
|
|
157
|
+
ok = type(operand) is bool
|
|
158
|
+
elif operator == "equals" and kind == "enum":
|
|
159
|
+
ok = type(operand) is str and operand in declared
|
|
160
|
+
elif operator == "in" and kind == "enum":
|
|
161
|
+
ok = (
|
|
162
|
+
type(operand) is list
|
|
163
|
+
and len(operand) > 0
|
|
164
|
+
and all(type(item) is str and item in declared for item in operand)
|
|
165
|
+
and len(set(operand)) == len(operand)
|
|
166
|
+
)
|
|
167
|
+
elif operator == "contains" and kind == "enum_set":
|
|
168
|
+
ok = type(operand) is str and operand in declared
|
|
169
|
+
else:
|
|
170
|
+
ok = False
|
|
171
|
+
require(
|
|
172
|
+
ok,
|
|
173
|
+
"condition operand does not match the attribute type or values",
|
|
174
|
+
path + "." + operator,
|
|
175
|
+
)
|
|
176
|
+
return name
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _condition(value: Any, attributes: dict, path: str) -> set[str]:
|
|
180
|
+
require(type(value) is dict, "condition must be an object", path)
|
|
181
|
+
for combinator in ("all", "any"):
|
|
182
|
+
if combinator in value:
|
|
183
|
+
require(set(value) == {combinator}, "combinator must be alone", path)
|
|
184
|
+
items = value[combinator]
|
|
185
|
+
require(
|
|
186
|
+
type(items) is list and bool(items),
|
|
187
|
+
"combinator needs predicates",
|
|
188
|
+
path + "." + combinator,
|
|
189
|
+
)
|
|
190
|
+
return {
|
|
191
|
+
_predicate(item, attributes, f"{path}.{combinator}[{index}]")
|
|
192
|
+
for index, item in enumerate(items)
|
|
193
|
+
}
|
|
194
|
+
return {_predicate(value, attributes, path)}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_attribute_rules(value: Any, attributes: dict, path: str) -> None:
|
|
198
|
+
"""Closed conditional applicability/requiredness rules (x4d-devkit #220)."""
|
|
199
|
+
|
|
200
|
+
rules = unique_ids(value, path, nonempty=True)
|
|
201
|
+
depends: dict[str, set[str]] = {}
|
|
202
|
+
for rule_id, rule in rules.items():
|
|
203
|
+
target = f"{path}.{rule_id}"
|
|
204
|
+
fields(rule, {"id", "attributes", "required"}, target, optional={"when"})
|
|
205
|
+
governed = rows(rule["attributes"], target + ".attributes", nonempty=True)
|
|
206
|
+
require(
|
|
207
|
+
all(type(name) is str and name in attributes for name in governed)
|
|
208
|
+
and len(set(governed)) == len(governed),
|
|
209
|
+
"rule attributes must be unique attributes of this category",
|
|
210
|
+
target + ".attributes",
|
|
211
|
+
)
|
|
212
|
+
for name in governed:
|
|
213
|
+
definition = attributes[name]
|
|
214
|
+
require(
|
|
215
|
+
"object" in definition["scopes"] and not definition["required"],
|
|
216
|
+
"governed attributes must be optional object attributes",
|
|
217
|
+
target + ".attributes." + name,
|
|
218
|
+
)
|
|
219
|
+
required = rows(rule["required"], target + ".required")
|
|
220
|
+
require(
|
|
221
|
+
all(type(name) is str for name in required)
|
|
222
|
+
and len(set(required)) == len(required)
|
|
223
|
+
and set(required) <= set(governed),
|
|
224
|
+
"required must be a unique subset of attributes",
|
|
225
|
+
target + ".required",
|
|
226
|
+
)
|
|
227
|
+
reads = (
|
|
228
|
+
_condition(rule["when"], attributes, target + ".when")
|
|
229
|
+
if "when" in rule
|
|
230
|
+
else set()
|
|
231
|
+
)
|
|
232
|
+
for name in governed:
|
|
233
|
+
depends.setdefault(name, set()).update(reads)
|
|
234
|
+
visiting: set[str] = set()
|
|
235
|
+
done: set[str] = set()
|
|
236
|
+
|
|
237
|
+
def visit(name: str) -> None:
|
|
238
|
+
require(name not in visiting, "rule dependencies must be acyclic", path)
|
|
239
|
+
if name in done:
|
|
240
|
+
return
|
|
241
|
+
visiting.add(name)
|
|
242
|
+
for parent in sorted(depends.get(name, ())):
|
|
243
|
+
visit(parent)
|
|
244
|
+
visiting.discard(name)
|
|
245
|
+
done.add(name)
|
|
246
|
+
|
|
247
|
+
for name in sorted(depends):
|
|
248
|
+
visit(name)
|
|
249
|
+
|
|
250
|
+
|
|
131
251
|
def parse_image_schema_body(value: Any) -> ImageSchemaBody:
|
|
132
252
|
canonical(value)
|
|
133
253
|
fields(
|
|
@@ -162,7 +282,7 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
|
|
|
162
282
|
category,
|
|
163
283
|
{"id", "display_name", "geometry_type", "color", "attributes"},
|
|
164
284
|
path,
|
|
165
|
-
optional={"groupable"},
|
|
285
|
+
optional={"groupable", "rules"},
|
|
166
286
|
)
|
|
167
287
|
if "groupable" in category:
|
|
168
288
|
require(
|
|
@@ -233,8 +353,12 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
|
|
|
233
353
|
target + ".values",
|
|
234
354
|
)
|
|
235
355
|
for item in values.values():
|
|
236
|
-
fields(item, {"id", "display_name"}, target + ".values")
|
|
356
|
+
fields(item, {"id", "display_name"}, target + ".values", optional={"term"})
|
|
237
357
|
text(item["display_name"], target + ".values.display_name")
|
|
358
|
+
if "term" in item:
|
|
359
|
+
_term(item["term"], target + ".values." + item["id"] + ".term")
|
|
360
|
+
if "rules" in category:
|
|
361
|
+
parse_attribute_rules(category["rules"], attributes, path + ".rules")
|
|
238
362
|
views = unique_ids(value["task_views"], "schema.task_views")
|
|
239
363
|
used = set()
|
|
240
364
|
for name, view in views.items():
|
|
@@ -275,7 +399,16 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
|
|
|
275
399
|
)
|
|
276
400
|
for item in descriptions.values():
|
|
277
401
|
fields(
|
|
278
|
-
item,
|
|
402
|
+
item,
|
|
403
|
+
{"id", "display_name", "required"},
|
|
404
|
+
path + ".description_fields",
|
|
405
|
+
optional={"target"},
|
|
406
|
+
)
|
|
407
|
+
require(
|
|
408
|
+
"target" not in item
|
|
409
|
+
or (type(item["target"]) is str and item["target"] in {"object", "image"}),
|
|
410
|
+
"description target must be object or image",
|
|
411
|
+
path + ".description_fields.target",
|
|
279
412
|
)
|
|
280
413
|
text(item["display_name"], path + ".description_fields.display_name")
|
|
281
414
|
require(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|