x4d-devkit 0.53.0rc2__tar.gz → 0.54.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/pyproject.toml +1 -1
  3. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_schema_migration.py +45 -0
  4. x4d_devkit-0.54.0rc1/tests/test_image_source.py +237 -0
  5. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_sdk_api.py +1 -1
  6. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/cli.py +61 -0
  7. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_clip.py +34 -0
  8. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_schema_migration.py +10 -3
  9. x4d_devkit-0.54.0rc1/x4d_devkit/image_source.py +490 -0
  10. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/PKG-INFO +1 -1
  11. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  12. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/LICENSE +0 -0
  13. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/README.md +0 -0
  14. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/setup.cfg +0 -0
  15. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_accounts.py +0 -0
  16. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_annotation_contract.py +0 -0
  17. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_annotation_document_hash.py +0 -0
  18. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_annotation_reference.py +0 -0
  19. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_assets_client.py +0 -0
  20. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_boundary.py +0 -0
  21. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_box.py +0 -0
  22. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_build_info.py +0 -0
  23. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_category_audit.py +0 -0
  24. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_clean_basemap.py +0 -0
  25. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_clean_basemap_client.py +0 -0
  26. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_cli_auth.py +0 -0
  27. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_client_json_contract.py +0 -0
  28. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_client_projects.py +0 -0
  29. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_clip_archives.py +0 -0
  30. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_contract_validation.py +0 -0
  31. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_coordinate_frames.py +0 -0
  32. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_current_state.py +0 -0
  33. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
  34. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_dataset_contract.py +0 -0
  35. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_dataset_identity.py +0 -0
  36. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_detection_eval.py +0 -0
  37. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_evaluate.py +0 -0
  38. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_export_bundle.py +0 -0
  39. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_clip.py +0 -0
  40. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_conditional_rules.py +0 -0
  41. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_enum_set.py +0 -0
  42. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_export.py +0 -0
  43. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_geometry.py +0 -0
  44. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_groups.py +0 -0
  45. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_preannotation.py +0 -0
  46. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_reference.py +0 -0
  47. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_review.py +0 -0
  48. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_schema_registry_client.py +0 -0
  49. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_schema_upgrade.py +0 -0
  50. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_image_work_session.py +0 -0
  51. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_inference_input.py +0 -0
  52. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_inference_schemas.py +0 -0
  53. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_inference_service.py +0 -0
  54. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_label_schemas.py +0 -0
  55. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_lidar_materialization.py +0 -0
  56. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_loader.py +0 -0
  57. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_matching.py +0 -0
  58. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_metrics.py +0 -0
  59. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_models.py +0 -0
  60. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_parse_equivalence.py +0 -0
  61. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_platform_pose.py +0 -0
  62. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_preannotation_client.py +0 -0
  63. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_project_discovery.py +0 -0
  64. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_reconstruction_client.py +0 -0
  65. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_release_gate.py +0 -0
  66. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_roadmark_preview.py +0 -0
  67. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
  68. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_semantic_scene_client.py +0 -0
  69. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_semantic_scene_contract.py +0 -0
  70. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_semantic_scene_independent.py +0 -0
  71. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
  72. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_source_attributes.py +0 -0
  73. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_time_contract.py +0 -0
  74. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_token.py +0 -0
  75. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_tracking_eval.py +0 -0
  76. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_training_identity.py +0 -0
  77. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_transform.py +0 -0
  78. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_transform_chain.py +0 -0
  79. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_validation.py +0 -0
  80. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_vector_map.py +0 -0
  81. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_vector_map_geometry.py +0 -0
  82. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_vector_map_session.py +0 -0
  83. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_work_session.py +0 -0
  84. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/tests/test_work_session_validation_reuse.py +0 -0
  85. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/__init__.py +0 -0
  86. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/_image_contract.py +0 -0
  87. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/accounts.py +0 -0
  88. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/annotation_contract.py +0 -0
  89. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/annotation_reference.py +0 -0
  90. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/audit.py +0 -0
  91. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/build_info.py +0 -0
  92. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/clean_basemap.py +0 -0
  93. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/__init__.py +0 -0
  94. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/_origin.py +0 -0
  95. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/accounts.py +0 -0
  96. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/annotation_reference.py +0 -0
  97. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/assets.py +0 -0
  98. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/bags.py +0 -0
  99. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  100. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  101. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/clean_basemap.py +0 -0
  102. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/client.py +0 -0
  103. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  104. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/clips.py +0 -0
  105. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  106. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_clips.py +0 -0
  107. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_exports.py +0 -0
  108. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_preannotations.py +0 -0
  109. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_review.py +0 -0
  110. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_schemas.py +0 -0
  111. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/image_work_sessions.py +0 -0
  112. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  113. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/preannotation.py +0 -0
  114. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/projects.py +0 -0
  115. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/reconstruction.py +0 -0
  116. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
  117. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  118. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/services.py +0 -0
  119. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  120. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  121. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  122. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/core/__init__.py +0 -0
  123. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/core/loader.py +0 -0
  124. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/core/models.py +0 -0
  125. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/core/token.py +0 -0
  126. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/core/transform.py +0 -0
  127. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/current_state.py +0 -0
  128. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/dataset_contract.py +0 -0
  129. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  130. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  131. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/api.py +0 -0
  132. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/box.py +0 -0
  133. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/detection.py +0 -0
  134. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/matching.py +0 -0
  135. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  136. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/eval/tracking.py +0 -0
  137. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/export_bundle.py +0 -0
  138. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_export.py +0 -0
  139. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_geometry.py +0 -0
  140. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_groups.py +0 -0
  141. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_preannotation.py +0 -0
  142. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_reference.py +0 -0
  143. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_review.py +0 -0
  144. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_schema.py +0 -0
  145. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_schema_upgrade.py +0 -0
  146. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/image_work_session.py +0 -0
  147. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  148. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/input.py +0 -0
  149. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/models.py +0 -0
  150. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  151. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/service.py +0 -0
  152. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  153. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/json_contract.py +0 -0
  154. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/label_schema.py +0 -0
  155. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
  156. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/materialize.py +0 -0
  157. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/project_discovery.py +0 -0
  158. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/roadmark_preview.py +0 -0
  159. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  160. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
  161. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/time_contract.py +0 -0
  162. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/training_identity.py +0 -0
  163. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  164. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/validation/contract.py +0 -0
  165. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
  166. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/validation/report.py +0 -0
  167. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/validation/validator.py +0 -0
  168. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/vector_map.py +0 -0
  169. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/vector_map_geometry.py +0 -0
  170. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit/work_session.py +0 -0
  171. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  172. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  173. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
  174. {x4d_devkit-0.53.0rc2 → x4d_devkit-0.54.0rc1}/x4d_devkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.53.0rc2
3
+ Version: 0.54.0rc1
4
4
  Summary: Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation
5
5
  Author: windzu
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.53.0rc2"
7
+ version = "0.54.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"
@@ -281,3 +281,48 @@ def test_typed_client_verifies_the_exact_preview_and_receipt():
281
281
  with pytest.raises(ValueError, match="another request"):
282
282
  client.image_schemas.commit_migration(request)
283
283
  assert build_image_schema_migration_request(parsed, "k").to_dict()["preview"] == preview
284
+
285
+
286
+ def box_object(attributes=None):
287
+ return {
288
+ "id": "box_a",
289
+ "image_id": "image_a",
290
+ "view_id": "objects",
291
+ "category_id": "box",
292
+ "geometry": {"geometry_type": "box_2d", "box": {"x_min": 10, "y_min": 20, "x_max": 40, "y_max": 60}},
293
+ "attributes": attributes or {},
294
+ "attribute_ranges": [],
295
+ }
296
+
297
+
298
+ def test_box_objects_migrate_alongside_lines():
299
+ clip = clip_payload()
300
+ clip["image_annotation"]["objects"] = [line_object(), box_object()]
301
+ clip["identity"] = build_image_identity(clip)
302
+ candidate, _ = image_schema_migration_candidate(
303
+ clip, target(clip), mapping(VISIBLE_TO_RANGE, DISCARD_STYLE)
304
+ )
305
+ assert candidate.to_dict()["image_annotation"]["objects"][1] == box_object()
306
+
307
+
308
+ def test_a_box_value_cannot_become_a_range():
309
+ clip = clip_payload()
310
+ body = deepcopy(clip["label_schema"]["body"])
311
+ body["categories"][1]["attributes"] = [
312
+ {"id": "flag", "display_name": "Flag", "type": "boolean", "required": False, "scopes": ["object"], "values": []}
313
+ ]
314
+ clip["label_schema"] = snapshot(body)
315
+ clip["image_annotation"]["objects"] = [box_object({"flag": True})]
316
+ clip["identity"] = build_image_identity(clip)
317
+ new = deepcopy(body)
318
+ new["version"] = "2.0.0"
319
+ new["categories"][1]["attributes"] = []
320
+ rules = mapping(
321
+ {
322
+ "match": {"category_id": "box", "scope": "object", "attribute_id": "flag", "value": True},
323
+ "action": {"kind": "range", "attribute_id": "flag", "value": True},
324
+ }
325
+ )
326
+ with pytest.raises(ImageSchemaMigrationConflict) as caught:
327
+ image_schema_migration_candidate(clip, snapshot(new), rules)
328
+ assert [row["code"] for row in caught.value.conflicts] == ["mapped_value_invalid"]
@@ -0,0 +1,237 @@
1
+ import copy
2
+ import hashlib
3
+ import json
4
+ import struct
5
+ import subprocess
6
+ import sys
7
+ import zlib
8
+
9
+ import pytest
10
+
11
+ from x4d_devkit._image_contract import ImageContractError, fingerprint
12
+ from x4d_devkit.image_clip import build_image_identity, parse_image_clip
13
+ from x4d_devkit.image_source import (
14
+ IMAGE_SOURCE_BUNDLE_VERSION,
15
+ IMAGE_SOURCE_PROVENANCE_VERSION,
16
+ build_image_source_provenance,
17
+ bundle_calibrations,
18
+ bundles_from_images,
19
+ image_dimensions,
20
+ parse_image_source_bundle,
21
+ read_image_source_bundle,
22
+ synthetic_undistorted_pinhole,
23
+ time_ns_dict,
24
+ )
25
+ from tests.image_fixtures import clip_payload
26
+
27
+
28
+ def jpeg(width, height, extra=b""):
29
+ # SOI, an APP0 segment, then a baseline SOF0 frame header.
30
+ app0 = b"\xff\xe0" + struct.pack(">H", 16) + b"JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
31
+ sof = b"\xff\xc0" + struct.pack(">HBHHB", 11, 8, height, width, 1) + b"\x01\x11\x00"
32
+ return b"\xff\xd8" + app0 + sof + extra + b"\xff\xd9"
33
+
34
+
35
+ def png(width, height):
36
+ ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
37
+ chunk = struct.pack(">I", len(ihdr)) + b"IHDR" + ihdr + struct.pack(">I", zlib.crc32(b"IHDR" + ihdr))
38
+ return b"\x89PNG\r\n\x1a\n" + chunk
39
+
40
+
41
+ def manifest(images, *, channel="cam_a", width=8, height=6):
42
+ calibration = synthetic_undistorted_pinhole(width=width, height=height, frame_id="camera", parent_frame="base")
43
+ samples = []
44
+ for ns, data in images:
45
+ samples.append({"timestamp": time_ns_dict(ns), "images": {channel: {
46
+ "timestamp": time_ns_dict(ns), "path": f"images/{channel}/{ns}.jpg", "size": len(data),
47
+ "sha256": hashlib.sha256(data).hexdigest(), "width": width, "height": height, "encoding": "jpeg"}}})
48
+ return {
49
+ "contract_version": IMAGE_SOURCE_BUNDLE_VERSION,
50
+ "bundle_id": "bundle_a",
51
+ "origin": {"label": "vendor delivery", "attributes": {"batch": "1104"}},
52
+ "timestamp_source": "filename",
53
+ "channels": [{"channel": channel, "calibration": calibration, "calibration_source": "synthetic"}],
54
+ "reference_channel": channel,
55
+ "sync_window_ns": 0,
56
+ "samples": samples,
57
+ }
58
+
59
+
60
+ def write_bundle(root, value, media):
61
+ root.mkdir()
62
+ (root / "manifest.json").write_text(json.dumps(value))
63
+ for path, data in media.items():
64
+ (root / path).parent.mkdir(parents=True, exist_ok=True)
65
+ (root / path).write_bytes(data)
66
+
67
+
68
+ def test_image_dimensions_read_jpeg_and_png_headers():
69
+ assert image_dimensions(jpeg(1920, 1536)) == ("jpeg", 1920, 1536)
70
+ assert image_dimensions(png(5, 7)) == ("png", 5, 7)
71
+ with pytest.raises(ImageContractError):
72
+ image_dimensions(b"GIF89a")
73
+ with pytest.raises(ImageContractError):
74
+ image_dimensions(b"\xff\xd8\xff\xd9")
75
+
76
+
77
+ def test_synthetic_calibration_closes_the_native_calibration_grammar():
78
+ value = synthetic_undistorted_pinhole(width=1920, height=1536, frame_id="cam", parent_frame="base_link")
79
+ assert value["intrinsics"] == {"model": "pinhole", "fx": 1920.0, "fy": 1920.0, "cx": 960.0, "cy": 768.0}
80
+ assert value["distortion"] == {"model": "plumb_bob", "coefficients": [0.0] * 5}
81
+ assert value["camera_info"]["frame_id"] == value["extrinsics"]["child_frame"] == "cam"
82
+
83
+
84
+ def test_bundle_grammar_is_closed_and_ordered():
85
+ data = jpeg(8, 6)
86
+ bundle = parse_image_source_bundle(manifest([(10, data), (20, data)]))
87
+ assert len(bundle.images) == 2 and bundle.fingerprint.startswith("sha256:")
88
+
89
+ for mutate in (
90
+ lambda v: v.update(extra=1),
91
+ lambda v: v.update(contract_version="x4d-image-source-bundle-v0"),
92
+ lambda v: v["samples"].reverse(),
93
+ lambda v: v["samples"][0]["images"]["cam_a"].update(path="images/cam_a/11.jpg"),
94
+ lambda v: v["samples"][0]["images"]["cam_a"].update(width=9),
95
+ lambda v: v["channels"][0].update(calibration_source="guessed"),
96
+ lambda v: v["channels"][0]["calibration"].update(id="calibration_cam_a"),
97
+ lambda v: v.update(sync_window_ns=5),
98
+ lambda v: v.update(timestamp_source="exif"),
99
+ lambda v: v.update(reference_channel="cam_b"),
100
+ lambda v: v.update(samples=[]),
101
+ ):
102
+ value = manifest([(10, data), (20, data)])
103
+ mutate(value)
104
+ with pytest.raises(ImageContractError):
105
+ parse_image_source_bundle(value)
106
+
107
+
108
+ def test_multi_camera_bundle_uses_reference_time_and_window():
109
+ data = jpeg(8, 6)
110
+ value = manifest([(1_000, data)])
111
+ second = copy.deepcopy(value["channels"][0])
112
+ second["channel"] = "cam_b"
113
+ value["channels"].append(second)
114
+ value["sync_window_ns"] = 50
115
+ value["samples"][0]["images"]["cam_b"] = {**value["samples"][0]["images"]["cam_a"],
116
+ "timestamp": time_ns_dict(1_040), "path": "images/cam_b/1040.jpg"}
117
+ parse_image_source_bundle(value)
118
+ value["samples"][0]["images"]["cam_b"].update(timestamp=time_ns_dict(1_060), path="images/cam_b/1060.jpg")
119
+ with pytest.raises(ImageContractError, match="synchronization window"):
120
+ parse_image_source_bundle(value)
121
+
122
+
123
+ def test_directory_validation_checks_file_set_and_every_byte(tmp_path):
124
+ data = jpeg(8, 6)
125
+ value = manifest([(10, data)])
126
+ write_bundle(tmp_path / "ok", value, {"images/cam_a/10.jpg": data})
127
+ assert read_image_source_bundle(tmp_path / "ok").to_dict() == value
128
+
129
+ write_bundle(tmp_path / "extra", value, {"images/cam_a/10.jpg": data, "notes.txt": b"x"})
130
+ with pytest.raises(ImageContractError, match="file set"):
131
+ read_image_source_bundle(tmp_path / "extra")
132
+
133
+ tampered = jpeg(8, 6, b"\x00")
134
+ write_bundle(tmp_path / "tampered", value, {"images/cam_a/10.jpg": tampered})
135
+ with pytest.raises(ImageContractError, match="size"):
136
+ read_image_source_bundle(tmp_path / "tampered")
137
+ assert read_image_source_bundle(tmp_path / "tampered", verify_media=False)
138
+
139
+ resized = jpeg(9, 6)
140
+ value_resized = manifest([(10, resized)])
141
+ write_bundle(tmp_path / "resized", value_resized, {"images/cam_a/10.jpg": resized})
142
+ value_resized["samples"][0]["images"]["cam_a"]["sha256"] = hashlib.sha256(resized).hexdigest()
143
+ with pytest.raises(ImageContractError, match="dimensions"):
144
+ read_image_source_bundle(tmp_path / "resized")
145
+
146
+
147
+ def test_converter_splits_by_gap_and_limit_with_explicit_synthetic_calibration(tmp_path):
148
+ source = tmp_path / "images"
149
+ source.mkdir()
150
+ for ns in (1_000_000_000, 2_000_000_000, 3_000_000_000, 100_000_000_000):
151
+ (source / f"{ns}.jpg").write_bytes(jpeg(8, 6))
152
+ bundles = bundles_from_images(source, tmp_path / "out", channel="cam_a", origin_label="loose images",
153
+ synthetic_pinhole=True, split_gap_s=60, max_images=2)
154
+ assert [len(row.images) for row in bundles] == [2, 1, 1]
155
+ first = bundles[0].to_dict()
156
+ assert first["timestamp_source"] == "filename"
157
+ assert first["channels"][0]["calibration_source"] == "synthetic"
158
+ for row in bundles:
159
+ assert read_image_source_bundle(tmp_path / "out" / row.to_dict()["bundle_id"]).to_dict() == row.to_dict()
160
+
161
+ with pytest.raises(ImageContractError):
162
+ bundles_from_images(source, tmp_path / "none", channel="cam_a", origin_label="x")
163
+ (source / "readme.txt").write_text("x")
164
+ with pytest.raises(ImageContractError):
165
+ bundles_from_images(source, tmp_path / "bad", channel="cam_a", origin_label="x", synthetic_pinhole=True)
166
+
167
+
168
+ def provenance_clip():
169
+ data = jpeg(1920, 1536)
170
+ value = manifest([(10_000_000_020, data)], width=1920, height=1536)
171
+ value["channels"][0]["calibration"]["extrinsics"]["child_frame"] = "camera"
172
+ bundle = parse_image_source_bundle(value)
173
+ clip = clip_payload()
174
+ provenance = build_image_source_provenance(bundle)
175
+ provenance["calibrations"][0]["calibration_id"] = "calibration_a"
176
+ clip["meta"]["source_provenance"] = provenance
177
+ clip["meta"]["source_bindings"][0]["source_fingerprint"] = provenance["bundle_fingerprint"]
178
+ return clip, bundle
179
+
180
+
181
+ def test_absent_provenance_keeps_existing_clips_and_identity_unchanged():
182
+ value = clip_payload()
183
+ before = copy.deepcopy(value["identity"])
184
+ clip = parse_image_clip(value)
185
+ assert clip.source_provenance is None and clip.source_kind == "bag_admission"
186
+ assert clip.calibration_is_measured("calibration_a") is True
187
+ assert build_image_identity(value) == before
188
+
189
+
190
+ def test_provenance_fragment_marks_synthetic_calibration_and_binds_the_bundle():
191
+ value, bundle = provenance_clip()
192
+ value["identity"] = build_image_identity(value)
193
+ clip = parse_image_clip(value)
194
+ assert clip.source_kind == "external_bundle"
195
+ assert clip.source_provenance["contract_version"] == IMAGE_SOURCE_PROVENANCE_VERSION
196
+ assert clip.source_provenance["bundle_fingerprint"] == fingerprint(bundle.to_dict())
197
+ assert clip.calibration_is_measured("calibration_a") is False
198
+ with pytest.raises(KeyError):
199
+ clip.calibration_is_measured("missing")
200
+ assert value["identity"]["source_revision"] != clip_payload()["identity"]["source_revision"]
201
+
202
+ for mutate in (
203
+ lambda v: v["meta"]["source_provenance"].update(kind="bag"),
204
+ lambda v: v["meta"]["source_provenance"]["calibrations"][0].update(source="unknown"),
205
+ lambda v: v["meta"]["source_provenance"].update(calibrations=[]),
206
+ lambda v: v["meta"]["source_bindings"][0].update(source_fingerprint="sha256:" + "9" * 64),
207
+ lambda v: v["meta"]["source_provenance"].update(extra=True),
208
+ ):
209
+ broken = copy.deepcopy(value)
210
+ mutate(broken)
211
+ broken["identity"] = build_image_identity(broken)
212
+ with pytest.raises(ImageContractError):
213
+ parse_image_clip(broken)
214
+
215
+
216
+ def test_bundle_calibrations_are_native_rows():
217
+ bundle = parse_image_source_bundle(manifest([(10, jpeg(8, 6))]))
218
+ rows = bundle_calibrations(bundle)
219
+ assert rows[0]["id"] == "calibration_cam_a" and rows[0]["channel"] == "cam_a"
220
+
221
+
222
+ def test_cli_converts_and_validates(tmp_path):
223
+ source = tmp_path / "images"
224
+ source.mkdir()
225
+ (source / "5000000000.jpg").write_bytes(jpeg(8, 6))
226
+
227
+ def run(*args):
228
+ return subprocess.run([sys.executable, "-m", "x4d_devkit.cli", *args], capture_output=True, text=True)
229
+
230
+ built = run("image-source", "from-images", str(source), "--out", str(tmp_path / "out"),
231
+ "--channel", "cam_a", "--origin-label", "loose", "--synthetic-pinhole")
232
+ assert built.returncode == 0, built.stderr
233
+ path = json.loads(built.stdout)["bundles"][0]["path"]
234
+ checked = run("image-source", "validate", path)
235
+ assert checked.returncode == 0 and json.loads(checked.stdout)["image_count"] == 1
236
+ (tmp_path / "out" / json.loads(built.stdout)["bundles"][0]["bundle_id"] / "x.txt").write_text("x")
237
+ assert run("image-source", "validate", path).returncode == 1
@@ -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.53.0rc2"
14
+ assert x4d_devkit.__version__ == "0.54.0rc1"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -426,6 +426,45 @@ def cmd_validate_image_export(args):
426
426
  print_json({"ok": True, "summary": summary})
427
427
 
428
428
 
429
+ def cmd_image_source_validate(args):
430
+ from x4d_devkit._image_contract import ImageContractError
431
+ from x4d_devkit.image_source import read_image_source_bundle
432
+
433
+ try:
434
+ bundle = read_image_source_bundle(args.path, verify_media=not args.metadata_only)
435
+ except (OSError, ValueError, ImageContractError) as exc:
436
+ print_json({"ok": False, "error": str(exc)})
437
+ raise SystemExit(1) from exc
438
+ value = bundle.to_dict()
439
+ print_json({"ok": True, "bundle_id": value["bundle_id"], "bundle_fingerprint": bundle.fingerprint,
440
+ "sample_count": len(value["samples"]), "image_count": len(bundle.images),
441
+ "bytes": sum(row["size"] for row in bundle.images), "media_verified": not args.metadata_only})
442
+
443
+
444
+ def cmd_image_source_from_images(args):
445
+ from x4d_devkit._image_contract import ImageContractError
446
+ from x4d_devkit.image_source import bundles_from_images
447
+
448
+ calibration = None
449
+ try:
450
+ if args.calibration:
451
+ from x4d_devkit.json_contract import parse_i_json
452
+
453
+ calibration = parse_i_json(Path(args.calibration).read_bytes(), path=args.calibration)
454
+ bundles = bundles_from_images(
455
+ args.images_dir, args.out, channel=args.channel, origin_label=args.origin_label,
456
+ calibration=calibration, synthetic_pinhole=args.synthetic_pinhole, focal_px=args.focal_px,
457
+ frame_id=args.frame_id, parent_frame=args.parent_frame, split_gap_s=args.split_gap_s,
458
+ max_images=args.max_images, bundle_prefix=args.bundle_prefix,
459
+ )
460
+ except (OSError, ValueError, ImageContractError) as exc:
461
+ print_json({"ok": False, "error": str(exc)})
462
+ raise SystemExit(1) from exc
463
+ print_json({"ok": True, "bundles": [
464
+ {"bundle_id": row.to_dict()["bundle_id"], "path": str(Path(args.out) / row.to_dict()["bundle_id"]),
465
+ "bundle_fingerprint": row.fingerprint, "image_count": len(row.images)} for row in bundles]})
466
+
467
+
429
468
  def cmd_validate(args):
430
469
  from x4d_devkit.validation import validate_clip
431
470
 
@@ -772,6 +811,28 @@ def main():
772
811
  p_image_export.add_argument("--max-json-bytes", type=int, default=64 * 1024 * 1024)
773
812
  p_image_export.set_defaults(func=cmd_validate_image_export)
774
813
 
814
+ p_source = sub.add_parser("image-source", help="Platform-neutral image source bundles")
815
+ p_source_sub = p_source.add_subparsers(dest="subcommand")
816
+ p_source_validate = p_source_sub.add_parser("validate", help="Validate a bundle directory and every image byte")
817
+ p_source_validate.add_argument("path")
818
+ p_source_validate.add_argument("--metadata-only", action="store_true", help="Check manifest and file set only")
819
+ p_source_validate.set_defaults(func=cmd_image_source_validate)
820
+ p_source_build = p_source_sub.add_parser("from-images", help="Convert <epoch_ns>.jpg|png files into bundles")
821
+ p_source_build.add_argument("images_dir")
822
+ p_source_build.add_argument("--out", required=True)
823
+ p_source_build.add_argument("--channel", required=True)
824
+ p_source_build.add_argument("--origin-label", required=True)
825
+ group = p_source_build.add_mutually_exclusive_group(required=True)
826
+ group.add_argument("--calibration", help="Declared calibration JSON (calibrated_sensor body without id/channel)")
827
+ group.add_argument("--synthetic-pinhole", action="store_true", help="Explicit undistorted placeholder calibration")
828
+ p_source_build.add_argument("--focal-px", type=float)
829
+ p_source_build.add_argument("--frame-id")
830
+ p_source_build.add_argument("--parent-frame", default="base_link")
831
+ p_source_build.add_argument("--split-gap-s", type=float)
832
+ p_source_build.add_argument("--max-images", type=int, default=2000)
833
+ p_source_build.add_argument("--bundle-prefix")
834
+ p_source_build.set_defaults(func=cmd_image_source_from_images)
835
+
775
836
  p_val = sub.add_parser("validate", help="Validate clip directory")
776
837
  p_val.add_argument("path", help="Clip directory or parent directory")
777
838
  p_val.set_defaults(func=cmd_validate)
@@ -588,6 +588,31 @@ class ImageClip(ImageDocument):
588
588
  def images(self) -> tuple[dict, ...]:
589
589
  return tuple(self.to_dict()["sample_data"])
590
590
 
591
+ @property
592
+ def source_provenance(self) -> dict | None:
593
+ """External-source fragment; ``None`` means platform Bag admission."""
594
+
595
+ return self.to_dict()["meta"].get("source_provenance")
596
+
597
+ @property
598
+ def source_kind(self) -> str:
599
+ provenance = self.source_provenance
600
+ return "bag_admission" if provenance is None else provenance["kind"]
601
+
602
+ def calibration_is_measured(self, calibration_id: str) -> bool:
603
+ """False only for an explicitly synthetic calibration."""
604
+
605
+ ids = {row["id"] for row in self.to_dict()["calibrated_sensor"]}
606
+ if calibration_id not in ids:
607
+ raise KeyError(calibration_id)
608
+ provenance = self.source_provenance
609
+ if provenance is None:
610
+ return True
611
+ return next(
612
+ row["source"] for row in provenance["calibrations"]
613
+ if row["calibration_id"] == calibration_id
614
+ ) != "synthetic"
615
+
591
616
 
592
617
  def parse_image_clip(value: Any, *, expected_clip_id: str | None = None) -> ImageClip:
593
618
  canonical(value)
@@ -609,6 +634,7 @@ def parse_image_clip(value: Any, *, expected_clip_id: str | None = None) -> Imag
609
634
  "processing_fingerprint",
610
635
  },
611
636
  "meta",
637
+ optional={"source_provenance"},
612
638
  )
613
639
  require(
614
640
  meta["contract_version"] == IMAGE_CLIP_CONTRACT_VERSION
@@ -679,6 +705,14 @@ def parse_image_clip(value: Any, *, expected_clip_id: str | None = None) -> Imag
679
705
  "calibration channel scope mismatch",
680
706
  "calibrated_sensor",
681
707
  )
708
+ if "source_provenance" in meta:
709
+ from x4d_devkit.image_source import validate_image_source_provenance
710
+
711
+ validate_image_source_provenance(
712
+ meta["source_provenance"],
713
+ calibration_ids=set(calibrations),
714
+ source_bindings=meta["source_bindings"],
715
+ )
682
716
  samples = unique_ids(value["sample"], "sample")
683
717
  images = unique_ids(value["sample_data"], "sample_data")
684
718
  previous = None
@@ -181,7 +181,7 @@ def migrate_image_annotation(annotation, *, target_schema, mapping):
181
181
  changes, removed, conflicts = [], [], []
182
182
  carried = 0
183
183
 
184
- def convert(object_id, category, scope, values, range_id, base_path):
184
+ def convert(object_id, category, scope, values, range_id, base_path, polyline):
185
185
  nonlocal carried
186
186
  output, added = {}, []
187
187
  for attribute_id in sorted(values):
@@ -230,6 +230,11 @@ def migrate_image_annotation(annotation, *, target_schema, mapping):
230
230
  )
231
231
  continue
232
232
  change["after"] = {"attribute_id": action["attribute_id"], "value": action["value"]}
233
+ if action["kind"] == "range" and not polyline:
234
+ conflicts.append(
235
+ _conflict("mapped_value_invalid", f"rule {index}: only a polyline can carry ranges", path, **context)
236
+ )
237
+ continue
233
238
  if action["kind"] == "range":
234
239
  change["range_id"] = _range_id(object_id, index)
235
240
  added.append((change["range_id"], action))
@@ -256,8 +261,9 @@ def migrate_image_annotation(annotation, *, target_schema, mapping):
256
261
  _conflict("object_unrepresentable", "target category geometry differs", path, object_id=object_id)
257
262
  )
258
263
  continue
264
+ polyline = category["geometry_type"] == "polyline_2d"
259
265
  row["attributes"], added = convert(
260
- object_id, category, "object", row["attributes"], None, path + ".attributes"
266
+ object_id, category, "object", row["attributes"], None, path + ".attributes", polyline
261
267
  )
262
268
  ranges = []
263
269
  for range_index, interval in enumerate(row["attribute_ranges"]):
@@ -268,14 +274,15 @@ def migrate_image_annotation(annotation, *, target_schema, mapping):
268
274
  interval["attributes"],
269
275
  interval["range_id"],
270
276
  f"{path}.attribute_ranges[{range_index}].attributes",
277
+ polyline,
271
278
  )
272
279
  assert not nested # range actions are object-scope only
273
280
  if values:
274
281
  ranges.append({**interval, "attributes": values})
275
282
  else:
276
283
  removed.append({"object_id": object_id, "range_id": interval["range_id"]})
277
- vertices = row["geometry"]["vertices"]
278
284
  for range_id, action in added:
285
+ vertices = row["geometry"]["vertices"]
279
286
  ranges.append(
280
287
  {
281
288
  "range_id": range_id,