From 07b0fe12ba06bcb5c22f2d326805af98ffbeec06 Mon Sep 17 00:00:00 2001 From: lioum <38319231+lioum@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:00:59 +0100 Subject: [PATCH 01/49] Adding O2D training specification (#112) # Conflicts: # python_sdk/docs/specifications/index.rst # python_sdk/src/reality_capture/service/job.py --- .../examples/training_o2d_new_default.py | 13 ++++ python_sdk/docs/specifications/index.rst | 3 + .../docs/specifications/training_o2d.rst | 62 +++++++++++++++++++ python_sdk/src/reality_capture/service/job.py | 15 ++--- .../specifications/training.py | 44 +++++++++++++ 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 python_sdk/docs/specifications/examples/training_o2d_new_default.py create mode 100644 python_sdk/docs/specifications/training_o2d.rst create mode 100644 python_sdk/src/reality_capture/specifications/training.py diff --git a/python_sdk/docs/specifications/examples/training_o2d_new_default.py b/python_sdk/docs/specifications/examples/training_o2d_new_default.py new file mode 100644 index 00000000..8feaf809 --- /dev/null +++ b/python_sdk/docs/specifications/examples/training_o2d_new_default.py @@ -0,0 +1,13 @@ +import reality_capture.specifications.training as training + +o2d_inputs = training.TrainingO2DInputs( + scene="401975b7-0c0a-4498-5896-84987921f4bb", +) +o2d_outputs = [ + training.TrainingO2DOutputsCreate.DETECTOR, + training.TrainingO2DOutputsCreate.METRICS, +] +o2d_options = training.TrainingO2DOptions() +o2ds = training.TrainingO2DSpecificationsCreate( + inputs=o2d_inputs, outputs=o2d_outputs, options=o2d_options +) diff --git a/python_sdk/docs/specifications/index.rst b/python_sdk/docs/specifications/index.rst index cb141d23..27f2654f 100644 --- a/python_sdk/docs/specifications/index.rst +++ b/python_sdk/docs/specifications/index.rst @@ -33,6 +33,7 @@ Specifications regroup all the settings used to create jobs with our APIs. eval_s2d eval_s3d eval_sortho + training_o2d Modeling @@ -58,12 +59,14 @@ Analysis * :doc:`/specifications/segmentation3d` uses a point cloud segmentation detector to classify each point of a point cloud and create 3D features. * :doc:`/specifications/change_detection` will take two point clouds or two meshes to to get 3D regions that capture the changes. * :doc:`/specifications/eval_o2d`, :doc:`/specifications/eval_o3d`, :doc:`/specifications/eval_s2d`, :doc:`/specifications/eval_s3d` and :doc:`/specifications/eval_sortho` will compare a prediction to a reference for a specific detection. +* :doc:`/specifications/training_o2d` will train an o2d detector from a ContextScene .. Conversion .. ========== .. * :doc:`/specifications/point_cloud_conversion` will convert point clouds from one format to another. + Utilities ========= diff --git a/python_sdk/docs/specifications/training_o2d.rst b/python_sdk/docs/specifications/training_o2d.rst new file mode 100644 index 00000000..3e3784c1 --- /dev/null +++ b/python_sdk/docs/specifications/training_o2d.rst @@ -0,0 +1,62 @@ +============ +Training O2D +============ + +The *Training O2D* job uses a ContextScene containing Objects2D annotations of pictures to train a new O2D detector. + +.. contents:: Quick access + :local: + :depth: 2 + +Purpose +======= + + This job has the following purposes: + + +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Purpose + - Inputs + - Possible outputs + - Useful options + * - Train new detector on a dataset (ContextScene) + - | *scene*, + - | *detector*, + | *metrics* (optional) + - | *epochs* + | *max_train_split* + + +Examples +======== + +In this example, we will create a specification for submitting a Training O2D job to produce a new O2D detector from a ContextScene. + +.. literalinclude:: examples/training_o2d_new_default.py + :language: Python + +Classes +======= + +.. currentmodule:: reality_capture.specifications.training + +.. autopydantic_model:: TrainingO2DSpecificationsCreate + +.. autoclass:: TrainingO2DOutputsCreate + :show-inheritance: + :members: + :undoc-members: + +.. autopydantic_model:: TrainingO2DSpecifications + +.. autopydantic_model:: TrainingO2DInputs + :model-show-json: False + +.. autopydantic_model:: TrainingO2DOutputs + :model-show-json: False + +.. autopydantic_model:: TrainingO2DOptions + :model-show-json: False diff --git a/python_sdk/src/reality_capture/service/job.py b/python_sdk/src/reality_capture/service/job.py index 3bfdeb6c..e6cb968b 100644 --- a/python_sdk/src/reality_capture/service/job.py +++ b/python_sdk/src/reality_capture/service/job.py @@ -61,7 +61,8 @@ class JobType(Enum): TOUCH_UP_IMPORT = "TouchUpImport" TOUCH_UP_EXPORT = "TouchUpExport" WATER_CONSTRAINTS = "WaterConstraints" - # POINT_CLOUD_CONVERSION = "PointCloudConversion" + # POINT_CLOUD_CONVERSION = "PointCloudConversion" + TRAINING_O2D = "TrainingO2D" class Service(Enum): MODELING = "Modeling" @@ -97,17 +98,17 @@ class JobCreate(BaseModel): type: JobType = Field(description="Type of job.") # TODO : PointCloudConversionSpecificationsCreate, specifications: Union[CalibrationSpecificationsCreate, ChangeDetectionSpecificationsCreate, - ConstraintsSpecificationsCreate, + ConstraintsSpecificationsCreate, EvalO2DSpecificationsCreate, EvalO3DSpecificationsCreate, EvalS2DSpecificationsCreate, EvalS3DSpecificationsCreate, EvalSOrthoSpecificationsCreate, FillImagePropertiesSpecificationsCreate, - GaussianSplatsSpecificationsCreate, ImportPCSpecificationsCreate, + GaussianSplatsSpecificationsCreate, ImportPCSpecificationsCreate, Objects2DSpecificationsCreate, ProductionSpecificationsCreate, ReconstructionSpecificationsCreate, Segmentation2DSpecificationsCreate, Segmentation3DSpecificationsCreate, SegmentationOrthophotoSpecificationsCreate, TilingSpecificationsCreate, TouchUpExportSpecificationsCreate, - TouchUpImportSpecificationsCreate, WaterConstraintsSpecificationsCreate] = ( - Field(description="Specifications aligned with the job type.")) + TouchUpImportSpecifications, WaterConstraintsSpecificationsCreate, + TrainingO2DOutputsCreate] = Field(description="Specifications aligned with the job type.") itwin_id: str = Field(description="iTwin ID, used by the service for finding " "input reality data and uploading output data.", alias="iTwinId") @@ -150,8 +151,8 @@ class Job(BaseModel): ReconstructionSpecifications, Segmentation2DSpecifications, Segmentation3DSpecifications, SegmentationOrthophotoSpecifications, TilingSpecifications, TouchUpExportSpecifications, - TouchUpImportSpecifications, WaterConstraintsSpecifications] = ( - Field(description="Specifications aligned with the job type.")) + TouchUpImportSpecifications, WaterConstraintsSpecifications, + TrainingO2DSpecifications] = Field(description="Specifications aligned with the job type.") @field_validator("specifications", mode="plain") @classmethod diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py new file mode 100644 index 00000000..c070c2cc --- /dev/null +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel, Field +from typing import Optional +from enum import Enum + + +class TrainingO2DInputs(BaseModel): + scene: str = Field( + description="Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." + ) + + +class TrainingO2DOutputs(BaseModel): + detector: str = Field(description="Reality data id of the detector.") + metrics: Optional[str] = Field(None, description="Reality data id of the metrics") + + +class TrainingO2DOptions(BaseModel): + epochs: Optional[int] = Field( + None, description="Number of time to iterate over the entire dataset" + ) + max_train_split: Optional[float] = Field( + None, + alias="maxTrainingSplit", + description="Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics. Set it to 1.0 for no evaluation and use everything for training.", + gt=0.0, + le=1.0, + ) + + +class TrainingO2DOutputsCreate(Enum): + DETECTOR = "detector" + METRICS = "metrics" + + +class TrainingO2DSpecificationsCreate(BaseModel): + inputs: TrainingO2DInputs = Field(description="Inputs") + outputs: list[TrainingO2DOutputsCreate] = Field(description="Outputs") + options: Optional[TrainingO2DOptions] = Field(None, description="Options") + + +class TrainingO2DSpecifications(BaseModel): + inputs: TrainingO2DInputs = Field(description="Inputs") + outputs: TrainingO2DOutputs = Field(description="Outputs") + options: Optional[TrainingO2DOptions] = Field(None, description="Options") From 7649d4be62f066a30fbb77c2810feac4ab0e55f8 Mon Sep 17 00:00:00 2001 From: lioum <38319231+lioum@users.noreply.github.com> Date: Mon, 12 May 2025 16:44:27 +0200 Subject: [PATCH 02/49] Add training S3D specification (#139) # Conflicts: # python_sdk/docs/specifications/index.rst # python_sdk/src/reality_capture/service/job.py --- .../examples/training_s3d_new_default.py | 12 ++++ python_sdk/docs/specifications/index.rst | 2 + .../docs/specifications/training_s3d.rst | 61 +++++++++++++++++++ python_sdk/src/reality_capture/service/job.py | 14 ++++- .../specifications/training.py | 44 +++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 python_sdk/docs/specifications/examples/training_s3d_new_default.py create mode 100644 python_sdk/docs/specifications/training_s3d.rst diff --git a/python_sdk/docs/specifications/examples/training_s3d_new_default.py b/python_sdk/docs/specifications/examples/training_s3d_new_default.py new file mode 100644 index 00000000..fe8466b9 --- /dev/null +++ b/python_sdk/docs/specifications/examples/training_s3d_new_default.py @@ -0,0 +1,12 @@ +import reality_capture.specifications.training as training + +s3d_inputs = training.TrainingS3DInputs( + scene="401975b7-0c0a-4498-5896-84987921f4bb", +) +s3d_outputs = [ + training.TrainingS3DOutputsCreate.DETECTOR, +] +s3d_options = training.TrainingS3DOptions() +s3ds = training.TrainingS3DSpecificationsCreate( + inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options +) diff --git a/python_sdk/docs/specifications/index.rst b/python_sdk/docs/specifications/index.rst index 27f2654f..9dab0f3f 100644 --- a/python_sdk/docs/specifications/index.rst +++ b/python_sdk/docs/specifications/index.rst @@ -34,6 +34,7 @@ Specifications regroup all the settings used to create jobs with our APIs. eval_s3d eval_sortho training_o2d + training_s3d Modeling @@ -60,6 +61,7 @@ Analysis * :doc:`/specifications/change_detection` will take two point clouds or two meshes to to get 3D regions that capture the changes. * :doc:`/specifications/eval_o2d`, :doc:`/specifications/eval_o3d`, :doc:`/specifications/eval_s2d`, :doc:`/specifications/eval_s3d` and :doc:`/specifications/eval_sortho` will compare a prediction to a reference for a specific detection. * :doc:`/specifications/training_o2d` will train an o2d detector from a ContextScene +* :doc:`/specifications/training_s3d` will train an s3d detector from a ContextScene .. Conversion .. ========== diff --git a/python_sdk/docs/specifications/training_s3d.rst b/python_sdk/docs/specifications/training_s3d.rst new file mode 100644 index 00000000..897c8879 --- /dev/null +++ b/python_sdk/docs/specifications/training_s3d.rst @@ -0,0 +1,61 @@ +============ +Training S3D +============ + +The *Training S3D* job uses a ContextScene containing annotated pointclouds to train a new S3D detector. + +.. contents:: Quick access + :local: + :depth: 2 + +Purpose +======= + + This job has the following purposes: + + +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Purpose + - Inputs + - Possible outputs + - Useful options + * - Train new detector on a dataset (ContextScene) + - | *scene*, + - | *detector*, + - | *epochs* + | *max_train_split* + + +Examples +======== + +In this example, we will create a specification for submitting a Training S3D job to produce a new S3D detector from a ContextScene. + +.. literalinclude:: examples/training_s3d_new_default.py + :language: Python + +Classes +======= + +.. currentmodule:: reality_capture.specifications.training + +.. autopydantic_model:: TrainingS3DSpecificationsCreate + +.. autoclass:: TrainingS3DOutputsCreate + :show-inheritance: + :members: + :undoc-members: + +.. autopydantic_model:: TrainingS3DSpecifications + +.. autopydantic_model:: TrainingS3DInputs + :model-show-json: False + +.. autopydantic_model:: TrainingS3DOutputs + :model-show-json: False + +.. autopydantic_model:: TrainingS3DOptions + :model-show-json: False diff --git a/python_sdk/src/reality_capture/service/job.py b/python_sdk/src/reality_capture/service/job.py index e6cb968b..f3fa491e 100644 --- a/python_sdk/src/reality_capture/service/job.py +++ b/python_sdk/src/reality_capture/service/job.py @@ -28,7 +28,8 @@ WaterConstraintsSpecificationsCreate) """from reality_capture.specifications.point_cloud_conversion import (PointCloudConversionSpecificationsCreate, PointCloudConversionSpecifications)""" - +from reality_capture.specifications.training import (TrainingO2DSpecifications, TrainingO2DSpecificationsCreate, + TrainingS3DSpecificationsCreate, TrainingS3DSpecifications) from reality_capture.specifications.gaussian_splats import (GaussianSplatsSpecificationsCreate, GaussianSplatsSpecifications) from reality_capture.specifications.eval_o2d import (EvalO2DSpecificationsCreate, EvalO2DSpecifications) @@ -63,6 +64,7 @@ class JobType(Enum): WATER_CONSTRAINTS = "WaterConstraints" # POINT_CLOUD_CONVERSION = "PointCloudConversion" TRAINING_O2D = "TrainingO2D" + TRAINING_S3D = "TrainingS3D" class Service(Enum): MODELING = "Modeling" @@ -108,7 +110,8 @@ class JobCreate(BaseModel): Segmentation3DSpecificationsCreate, SegmentationOrthophotoSpecificationsCreate, TilingSpecificationsCreate, TouchUpExportSpecificationsCreate, TouchUpImportSpecifications, WaterConstraintsSpecificationsCreate, - TrainingO2DOutputsCreate] = Field(description="Specifications aligned with the job type.") + TrainingO2DSpecificationsCreate, PointCloudConversionSpecificationsCreate, + TrainingS3DSpecificationsCreate] = Field(description="Specifications aligned with the job type.") itwin_id: str = Field(description="iTwin ID, used by the service for finding " "input reality data and uploading output data.", alias="iTwinId") @@ -152,7 +155,8 @@ class Job(BaseModel): Segmentation3DSpecifications, SegmentationOrthophotoSpecifications, TilingSpecifications, TouchUpExportSpecifications, TouchUpImportSpecifications, WaterConstraintsSpecifications, - TrainingO2DSpecifications] = Field(description="Specifications aligned with the job type.") + TrainingO2DSpecifications, TrainingS3DSpecifications, + PointCloudConversionSpecifications] = Field(description="Specifications aligned with the job type.") @field_validator("specifications", mode="plain") @classmethod @@ -203,6 +207,10 @@ def set_specification_validation_model(cls, raw_dict: dict[str, Any], validation specifications = TouchUpImportSpecifications(**raw_dict) elif job_type == JobType.WATER_CONSTRAINTS: specifications = WaterConstraintsSpecifications(**raw_dict) + elif job_type == JobType.TrainingS3D: + specifications = TrainingS3DSpecifications(**raw_dict) + elif job_type == JobType.TrainingO2D: + specifications = TrainingO2DSpecifications(**raw_dict) else: raise ValueError(f"Unsupported job type: {job_type}") diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index c070c2cc..a27f99d8 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -42,3 +42,47 @@ class TrainingO2DSpecifications(BaseModel): inputs: TrainingO2DInputs = Field(description="Inputs") outputs: TrainingO2DOutputs = Field(description="Outputs") options: Optional[TrainingO2DOptions] = Field(None, description="Options") + + +class TrainingS3DInputs(BaseModel): + scene: str = Field( + description="Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." + ) + + +class TrainingS3DOutputs(BaseModel): + detector: str = Field(description="Reality data id of the detector.") + + +class TrainingS3DOptions(BaseModel): + epochs: Optional[int] = Field( + None, description="Number of time to iterate over the entire dataset" + ) + spacing: Optional[float] = Field( + None, + alias="spacing", + description="Spacing of the pointcloud seen by the detector", + ) + max_train_split: Optional[float] = Field( + None, + alias="maxTrainingSplit", + description="Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics. Set it to 1.0 for no evaluation and use everything for training.", + gt=0.0, + le=1.0, + ) + + +class TrainingS3DOutputsCreate(Enum): + DETECTOR = "detector" + + +class TrainingS3DSpecificationsCreate(BaseModel): + inputs: TrainingS3DInputs = Field(description="Inputs") + outputs: list[TrainingS3DOutputsCreate] = Field(description="Outputs") + options: Optional[TrainingS3DOptions] = Field(None, description="Options") + + +class TrainingS3DSpecifications(BaseModel): + inputs: TrainingS3DInputs = Field(description="Inputs") + outputs: TrainingS3DOutputs = Field(description="Outputs") + options: Optional[TrainingS3DOptions] = Field(None, description="Options") From ecd12efb5a28b98926d07f23b6d548d111ca1f00 Mon Sep 17 00:00:00 2001 From: Cyril <5690282+cnovel@users.noreply.github.com> Date: Thu, 15 May 2025 16:12:37 +0200 Subject: [PATCH 03/49] Add support for Computing Buckets (#143) # Conflicts: # python_sdk/docs/service/bucket.rst # python_sdk/docs/service/index.rst # python_sdk/src/reality_capture/service/bucket.py # python_sdk/src/reality_capture/service/service.py # python_sdk/src/reality_capture/specifications/calibration.py # python_sdk/src/reality_capture/specifications/extract_ground.py # python_sdk/src/reality_capture/specifications/production.py # python_sdk/src/reality_capture/specifications/reconstruction.py # python_sdk/src/reality_capture/specifications/segmentation3d.py # python_sdk/tests/data/bucket_get_200.json # python_sdk/tests/test_job.py # python_sdk/tests/test_service_bucket.py --- python_sdk/src/reality_capture/specifications/training.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index a27f99d8..9a1ba813 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -11,7 +11,8 @@ class TrainingO2DInputs(BaseModel): class TrainingO2DOutputs(BaseModel): detector: str = Field(description="Reality data id of the detector.") - metrics: Optional[str] = Field(None, description="Reality data id of the metrics") + metrics: Optional[str] = Field(None, description="Path in the bucket of the training metrics", + pattern=r"^bkt:.+") class TrainingO2DOptions(BaseModel): From 3f2173c2acc48aa2a079b1deb7d62a10d2791358 Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:56:05 +0100 Subject: [PATCH 04/49] Correct Training specifications --- .../specifications/training.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 9a1ba813..3805d0bb 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -46,31 +46,40 @@ class TrainingO2DSpecifications(BaseModel): class TrainingS3DInputs(BaseModel): - scene: str = Field( - description="Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." + segmentations_3d: list[str] = Field( + description="List of 3D models to train on.", + alias="segmentations3D" ) + presets: Optional[list[str]] = Field(default=None, description="List of paths to preset") + detector_name: str = Field(description="Name of the detector to train", alias="detectorName") class TrainingS3DOutputs(BaseModel): - detector: str = Field(description="Reality data id of the detector.") + detector: str = Field(description="Full detector information (name/version)") + + +class Segmentation3DTrainingModel(Enum): + SPLATNET = "SPLATNet" + + +class PointCloudFeature(Enum): + RGB = "RGB" + NORMAL = "NORMAL" + INTENSITY = "INTENSITY" class TrainingS3DOptions(BaseModel): epochs: Optional[int] = Field( - None, description="Number of time to iterate over the entire dataset" + None, description="Number of time to iterate over the entire dataset", ge=1, le=100 ) spacing: Optional[float] = Field( None, - alias="spacing", description="Spacing of the pointcloud seen by the detector", + gt=0 ) - max_train_split: Optional[float] = Field( - None, - alias="maxTrainingSplit", - description="Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics. Set it to 1.0 for no evaluation and use everything for training.", - gt=0.0, - le=1.0, - ) + model: Optional[Segmentation3DTrainingModel] = Field(None, description="Training Model architecture to use") + features: Optional[list[PointCloudFeature]] = Field(None, description="Features to use for the training") + version_name: Optional[str] = Field(None, description="Version name for the newly trained detector") class TrainingS3DOutputsCreate(Enum): From 3d009920848c935745cdb6536749c2bd010da25a Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:03:11 +0100 Subject: [PATCH 05/49] Better description --- python_sdk/src/reality_capture/specifications/training.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 3805d0bb..0c976eee 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -74,12 +74,12 @@ class TrainingS3DOptions(BaseModel): ) spacing: Optional[float] = Field( None, - description="Spacing of the pointcloud seen by the detector", + description="Spacing of the pointcloud seen by the detector (in meters).", gt=0 ) - model: Optional[Segmentation3DTrainingModel] = Field(None, description="Training Model architecture to use") - features: Optional[list[PointCloudFeature]] = Field(None, description="Features to use for the training") - version_name: Optional[str] = Field(None, description="Version name for the newly trained detector") + model: Optional[Segmentation3DTrainingModel] = Field(None, description="Training Model architecture to use.") + features: Optional[list[PointCloudFeature]] = Field(None, description="Features to use for the training.") + version_name: Optional[str] = Field(None, description="Version name for the newly trained detector.") class TrainingS3DOutputsCreate(Enum): From 5687a0c85b1b158d6d0c0ce86e9135e0de7f70a3 Mon Sep 17 00:00:00 2001 From: lioum <38319231+lioum@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:00:03 +0100 Subject: [PATCH 06/49] Add alias for version_name field of TrainingS3DSpecifications (#247) Co-authored-by: Cyril <5690282+cnovel@users.noreply.github.com> Co-authored-by: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Co-authored-by: pierre-seguin-bentley Co-authored-by: Lou Landry <127218677+loulandryatbentley@users.noreply.github.com> Co-authored-by: EHS-BENTLEY-SYSTEMS Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Arun George <11051042+aruniverse@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> # Conflicts: # .github/workflows/python-build-and-test.yml # .github/workflows/typescript-publish-prerelease.yml # .github/workflows/typescript-publish-release.yml # python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py # python_sdk/docs/specifications/examples/convert_pc_specs.py # python_sdk/docs/specifications/index.rst # python_sdk/pyproject.toml # python_sdk/src/reality_capture/service/detectors.py # python_sdk/src/reality_capture/service/estimation.py # python_sdk/src/reality_capture/service/job.py # python_sdk/src/reality_capture/service/service.py # python_sdk/tests/test_detectors.py # python_sdk/tests/test_job.py # typescript/examples/src/example_modeling.ts # typescript/packages/reality-capture/package.json # typescript/packages/reality-capture/src/service/job.ts # typescript/packages/reality-capture/src/service/service.ts # typescript/packages/reality-capture/src/specifications/calibration.ts # typescript/packages/reality-capture/src/specifications/change_detection.ts # typescript/packages/reality-capture/src/specifications/constraints.ts # typescript/packages/reality-capture/src/specifications/gaussian_splats.ts # typescript/packages/reality-capture/src/specifications/import_point_cloud.ts # typescript/packages/reality-capture/src/specifications/production.ts # typescript/packages/reality-capture/src/specifications/reconstruction.ts # typescript/packages/reality-capture/src/specifications/tiling.ts # typescript/packages/reality-capture/src/specifications/water_constraints.ts # typescript/packages/reality-capture/src/tests/service/job.test.ts # typescript/packages/reality-capture/src/tests/service/service.test.ts # typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts --- python_sdk/src/reality_capture/specifications/training.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 0c976eee..03ded915 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -79,7 +79,7 @@ class TrainingS3DOptions(BaseModel): ) model: Optional[Segmentation3DTrainingModel] = Field(None, description="Training Model architecture to use.") features: Optional[list[PointCloudFeature]] = Field(None, description="Features to use for the training.") - version_name: Optional[str] = Field(None, description="Version name for the newly trained detector.") + version_name: Optional[str] = Field(None, description="Version name for the newly trained detector.", alias="versionName") class TrainingS3DOutputsCreate(Enum): From b276e3c7b2b20ee8d79f021c61ae2bed039282ca Mon Sep 17 00:00:00 2001 From: Liliam Jean Baptiste Date: Mon, 26 Jan 2026 11:46:47 +0100 Subject: [PATCH 07/49] Implemented detector handler # Conflicts: # python_sdk/src/reality_capture/service/detectors.py # python_sdk/src/reality_capture/service/job.py # python_sdk/src/reality_capture/service/service.py --- .../reality_capture/service/data_handler.py | 116 ++++++++++++++++- .../src/reality_capture/service/detectors.py | 37 +++++- python_sdk/src/reality_capture/service/job.py | 24 ++-- .../src/reality_capture/service/response.py | 12 +- .../src/reality_capture/service/service.py | 123 ++++++++++++++++-- .../specifications/training.py | 7 +- 6 files changed, 275 insertions(+), 44 deletions(-) diff --git a/python_sdk/src/reality_capture/service/data_handler.py b/python_sdk/src/reality_capture/service/data_handler.py index 1d474bf1..a0baeb15 100644 --- a/python_sdk/src/reality_capture/service/data_handler.py +++ b/python_sdk/src/reality_capture/service/data_handler.py @@ -1,11 +1,16 @@ import os.path -from typing import Optional +import io +import urllib.parse +from typing import Callable, Optional +import requests +import zipfile from reality_capture.service.error import DetailedErrorResponse, DetailedError, Error from reality_capture.service.response import Response -from reality_capture.service.service import RealityCaptureService +from reality_capture.service.service import DetectorMinimal, RealityCaptureService from reality_capture.service.reality_data import RealityDataUpdate, RealityData, ContainerDetails from reality_capture.service.bucket import BucketResponse from azure.storage.blob import ContainerClient +from reality_capture.service.detectors import Detector, DetectorCreate, DetectorVersion, DetectorType from multiprocessing.pool import ThreadPool @@ -169,7 +174,7 @@ def __init__(self, token_factory, **kwargs) -> None: :param token_factory: An object that implements a ``get_token() -> str`` method. :type token_factory: Object - :param \**kwargs: Internal parameters used only for development purposes. + :param \\**kwargs: Internal parameters used only for development purposes. """ self._service = RealityCaptureService(token_factory, **kwargs) self._progress_hook = None @@ -252,7 +257,7 @@ def delete_data(self, reality_data_id, files_to_delete: list[str], return Response(r.status_code, r.error, None) return _DataHandler.delete_data(r.value.links.container_url.href, files_to_delete) - def set_progress_hook(self, hook: Optional) -> None: + def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: """ Set the progress hook. @@ -273,7 +278,7 @@ def __init__(self, token_factory, **kwargs) -> None: :param token_factory: An object that implements a ``get_token() -> str`` method. :type token_factory: Object - :param \**kwargs: Internal parameters used only for development purposes. + :param \\**kwargs: Internal parameters used only for development purposes. """ self._service = RealityCaptureService(token_factory, **kwargs) self._progress_hook = None @@ -336,7 +341,7 @@ def delete_data(self, itwin_id, files_to_delete: list[str]) -> Response[None]: return Response(r.status_code, r.error, None) return _DataHandler.delete_data(r.value.links.container_url.href, files_to_delete) - def set_progress_hook(self, hook: Optional) -> None: + def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: """ Set the progress hook. @@ -344,3 +349,102 @@ def set_progress_hook(self, hook: Optional) -> None: When returning false, the ongoing action will be cancelled. Can be None if no progress hook is needed. """ self._progress_hook = hook + +class DetectorDataHandler: + """ + Class for interacting with detectors + """ + + def __init__(self, token_factory, **kwargs) -> None: + """ + Constructor method + + :param token_factory: An object that implements a ``get_token() -> str`` method. + :type token_factory: Object + :param \\**kwargs: Internal parameters used only for development purposes. + """ + self._service = RealityCaptureService(token_factory, **kwargs) + self._progress_hook = None + + def get_detector(self, detector_name: str) -> Response[Detector]: + return self._service.get_detector(detector_name) + + def get_specific_detector_version(self, detector_name: str, version_number: str) -> Response[DetectorVersion]: + r = self.get_detector(detector_name) + + if r.is_error() or r.value is None: + return Response(r.status_code, r.error, None) + + detector = next((d for d in r.value.versions if d.version == version_number), None) + + return Response(r.status_code, r.error, detector) + + def create_detector(self, + name: str, type: DetectorType, + display_name: Optional[str] = None, + description: Optional[str] = None, + documentation_url: Optional[str] = None + ) -> Response[DetectorMinimal]: + + detector_attributes = DetectorCreate( + name = name, + type = type, + displayName = display_name, + description = description, + documentationUrl = documentation_url + ) + + return self._service.create_detector(detector_attributes) + + def list_available_detectors(self) -> Response[list[DetectorMinimal]]: + return self._service.get_detectors() + + def download_detector_archive(self, detector_name: str, version_number: str) -> Response[io.BytesIO]: + """ + Download detector archive from a detector version. + + :param dst: Destination path of the downloads. + :param detector_src: Source folder to download in the detector, default to root. + :return: A Response[io.BytesIO] containing the error from the service if any. + """ + r = self.get_specific_detector_version(detector_name, version_number) + + if r.is_error(): + return Response(r.status_code, r.error, None) + + assert r.value is not None + detector_version = r.value + assert detector_version.download_url is not None + + response = requests.get(detector_version.download_url, stream=True) + content = io.BytesIO(response.content) + + return Response(r.status_code, r.error, content) + + def delete_detector(self, detector_name: str) -> Response[None]: + """ + Delete a detector and all its versions + + :return: A Response[list[str]] containing either the files in the detector or the error from the service. + """ + return self._service.delete_detector(detector_name) + + def delete_detector_version(self, detector_name: str, version_number: str) -> Response[None]: + """ + Delete a specific version of a detector + + :param itwin_id: iTwin id for finding the detector. + :param files_to_delete: List of files to delete. + :return: A Response[None] containing either the files in the detector or the error from the service. + """ + return self._service.delete_detector_version(detector_name, version_number) + + def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: + """ + Set the progress hook. + + :param hook: Function taking a float as an argument and returning a bool. + When returning false, the ongoing action will be cancelled. Can be None if no progress hook is needed. + """ + self._progress_hook = hook + diff --git a/python_sdk/src/reality_capture/service/detectors.py b/python_sdk/src/reality_capture/service/detectors.py index 2d1fde16..1e91bd2e 100644 --- a/python_sdk/src/reality_capture/service/detectors.py +++ b/python_sdk/src/reality_capture/service/detectors.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field from datetime import datetime +from reality_capture.service.reality_data import URL class DetectorExport(Enum): OBJECTS = "Objects" @@ -11,9 +12,16 @@ class DetectorExport(Enum): LOCATIONS = "Locations" +class DetectorType(Enum): + PHOTO_OBJECT_DETECTOR = "PhotoObjectDetector" + PHOTO_SEGMENTATION_DETECTOR = "PhotoSegmentationDetector" + ORTHOPHOTO_SEGMENTATION_DETECTOR = "OrthophotoSegmentationDetector" + POINT_CLOUD_SEGMENTATION_DETECTOR = "PointCloudSegmentationDetector" + + class Capabilities(BaseModel): labels: list[str] = Field(description="Labels of the detector version.") - exports: list[DetectorExport] = Field(description="Exports of the detector version.") + exports: Optional[list[DetectorExport]] = Field(None, description="Exports of the detector version.") class DetectorStatus(Enum): @@ -31,13 +39,30 @@ class DetectorVersion(BaseModel): creator_id: Optional[str] = Field(None, description="User Id of the version creator.", alias="creatorId") capabilities: Capabilities = Field(description="Capabilities of the version.") +class DetectorCreate(BaseModel): + name: str = Field(description="Name of the detector.") + display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") + description: Optional[str] = Field(None, description="An optional description of the detector.") + type: DetectorType = Field(description="Type of the detector.") + documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") + +class DetectorUpdate(BaseModel): + display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") + description: Optional[str] = Field(None, description="An optional description of the detector.") + documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") + +class DetectorVersionCreate(BaseModel): + version_number: str = Field(description="Version number string", alias="versionNumber") + capabilities: Capabilities = Field(description="Capabilities of the version.") + +class Links(BaseModel): + upload_url: URL = Field(description="URL to upload the detector zip file.", alias="uploadUrl") + complete_url: URL = Field(description="URL to mark the completion of the detector version creation process.", alias="completeUrl") -class DetectorType(Enum): - PHOTO_OBJECT_DETECTOR = "PhotoObjectDetector" - PHOTO_SEGMENTATION_DETECTOR = "PhotoSegmentationDetector" - ORTHOPHOTO_SEGMENTATION_DETECTOR = "OrthophotoSegmentationDetector" - POINT_CLOUD_SEGMENTATION_DETECTOR = "PointCloudSegmentationDetector" +class DetectorVersionCreateResponseLinks(BaseModel): + version: DetectorVersion = Field(description="Created detector version") + links: Links = Field(description="Upload links for the detector version", alias="_links") class DetectorBase(BaseModel): name: str = Field(description="Name of the detector.") diff --git a/python_sdk/src/reality_capture/service/job.py b/python_sdk/src/reality_capture/service/job.py index f3fa491e..a22a1814 100644 --- a/python_sdk/src/reality_capture/service/job.py +++ b/python_sdk/src/reality_capture/service/job.py @@ -62,9 +62,9 @@ class JobType(Enum): TOUCH_UP_IMPORT = "TouchUpImport" TOUCH_UP_EXPORT = "TouchUpExport" WATER_CONSTRAINTS = "WaterConstraints" - # POINT_CLOUD_CONVERSION = "PointCloudConversion" TRAINING_O2D = "TrainingO2D" TRAINING_S3D = "TrainingS3D" + # POINT_CLOUD_CONVERSION = "PointCloudConversion" class Service(Enum): MODELING = "Modeling" @@ -79,7 +79,7 @@ def _get_appropriate_service(jt: JobType): return Service.MODELING if jt in [JobType.OBJECTS_2D, JobType.SEGMENTATION_2D, JobType.SEGMENTATION_3D, JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.EVAL_O2D, JobType.EVAL_O3D, JobType.EVAL_S2D, - JobType.EVAL_S3D, JobType.EVAL_SORTHO]: + JobType.EVAL_S3D, JobType.EVAL_SORTHO, JobType.TRAINING_S3D]: return Service.ANALYSIS # return Service.CONVERSION raise NotImplemented("Other services not yet implemented") @@ -100,18 +100,18 @@ class JobCreate(BaseModel): type: JobType = Field(description="Type of job.") # TODO : PointCloudConversionSpecificationsCreate, specifications: Union[CalibrationSpecificationsCreate, ChangeDetectionSpecificationsCreate, - ConstraintsSpecificationsCreate, + ConstraintsSpecificationsCreate, # PointCloudConversionSpecifications, EvalO2DSpecificationsCreate, EvalO3DSpecificationsCreate, EvalS2DSpecificationsCreate, EvalS3DSpecificationsCreate, EvalSOrthoSpecificationsCreate, FillImagePropertiesSpecificationsCreate, - GaussianSplatsSpecificationsCreate, ImportPCSpecificationsCreate, + GaussianSplatsSpecificationsCreate, ImportPCSpecificationsCreate, Objects2DSpecificationsCreate, ProductionSpecificationsCreate, ReconstructionSpecificationsCreate, Segmentation2DSpecificationsCreate, Segmentation3DSpecificationsCreate, SegmentationOrthophotoSpecificationsCreate, TilingSpecificationsCreate, TouchUpExportSpecificationsCreate, TouchUpImportSpecifications, WaterConstraintsSpecificationsCreate, - TrainingO2DSpecificationsCreate, PointCloudConversionSpecificationsCreate, - TrainingS3DSpecificationsCreate] = Field(description="Specifications aligned with the job type.") + TrainingO2DSpecificationsCreate, TrainingS3DSpecificationsCreate + ] = Field(description="Specifications aligned with the job type.") itwin_id: str = Field(description="iTwin ID, used by the service for finding " "input reality data and uploading output data.", alias="iTwinId") @@ -145,7 +145,7 @@ class Job(BaseModel): user_id: str = Field(description="Identifier of the user that created the job.", alias="userId") # TODO : add PointCloudConversionSpecifications specifications: Union[CalibrationSpecifications, ChangeDetectionSpecifications, - ConstraintsSpecifications, + ConstraintsSpecifications, # PointCloudConversionSpecifications, EvalO2DSpecifications, EvalO3DSpecifications, EvalS2DSpecifications, EvalS3DSpecifications, EvalSOrthoSpecifications, FillImagePropertiesSpecifications, @@ -156,7 +156,7 @@ class Job(BaseModel): TilingSpecifications, TouchUpExportSpecifications, TouchUpImportSpecifications, WaterConstraintsSpecifications, TrainingO2DSpecifications, TrainingS3DSpecifications, - PointCloudConversionSpecifications] = Field(description="Specifications aligned with the job type.") + ] = Field(description="Specifications aligned with the job type.") @field_validator("specifications", mode="plain") @classmethod @@ -207,10 +207,10 @@ def set_specification_validation_model(cls, raw_dict: dict[str, Any], validation specifications = TouchUpImportSpecifications(**raw_dict) elif job_type == JobType.WATER_CONSTRAINTS: specifications = WaterConstraintsSpecifications(**raw_dict) - elif job_type == JobType.TrainingS3D: - specifications = TrainingS3DSpecifications(**raw_dict) - elif job_type == JobType.TrainingO2D: - specifications = TrainingO2DSpecifications(**raw_dict) + elif job_type == JobType.TRAINING_S3D: + specifications = TrainingS3DSpecifications(**raw_dict) + elif job_type == JobType.TRAINING_O2D: + specifications = TrainingO2DSpecifications(**raw_dict) else: raise ValueError(f"Unsupported job type: {job_type}") diff --git a/python_sdk/src/reality_capture/service/response.py b/python_sdk/src/reality_capture/service/response.py index f85ad98f..0b768802 100644 --- a/python_sdk/src/reality_capture/service/response.py +++ b/python_sdk/src/reality_capture/service/response.py @@ -1,11 +1,12 @@ from typing import TypeVar, Generic, Optional +from dataclasses import dataclass from reality_capture.service.error import DetailedErrorResponse T = TypeVar("T") - -class Response(tuple, Generic[T]): +@dataclass +class Response(Generic[T]): """ A tuple containing a Service response. """ @@ -17,13 +18,6 @@ class Response(tuple, Generic[T]): value: Optional[T] "Optional object if the request succeed." - def __new__(cls, status_code: int, error: Optional[DetailedErrorResponse], value: Optional[T]): - self = tuple.__new__(cls, (status_code, error, value)) - self.value = value - self.status_code = status_code - self.error = error - return self - def get_response_status_code(self) -> int: """ Return the HTTP response status_code diff --git a/python_sdk/src/reality_capture/service/service.py b/python_sdk/src/reality_capture/service/service.py index a15425a1..62debe0d 100644 --- a/python_sdk/src/reality_capture/service/service.py +++ b/python_sdk/src/reality_capture/service/service.py @@ -2,7 +2,7 @@ import requests from reality_capture.service.bucket import BucketResponse -from reality_capture.service.detectors import DetectorsMinimalResponse, DetectorResponse +from reality_capture.service.detectors import DetectorCreate, DetectorUpdate, DetectorVersionCreate, DetectorsMinimalResponse, DetectorResponse, Detector, DetectorMinimal, DetectorVersionCreateResponseLinks, Links from reality_capture.service.files import Files from reality_capture.service.response import Response from reality_capture.service.job import JobCreate, Job, Progress, Messages, Service, Jobs @@ -26,7 +26,7 @@ def __init__(self, token_factory, **kwargs) -> None: :param token_factory: An object that implements a ``get_token() -> str`` method. :type token_factory: Object - :param \**kwargs: See below. + :param \\**kwargs: See below. :Keyword Arguments: * *user_agent* (``str``) -- @@ -263,18 +263,18 @@ def get_service_files(self) -> Response[Files]: return Response(status_code=response.status_code, error=DetailedErrorResponse(error=error), value=None) - def get_detectors(self) -> Response[DetectorsMinimalResponse]: + def get_detectors(self) -> Response[list[DetectorMinimal]]: """ Retrieve all available detectors. :return: A Response[DetectorsMinimalResponse] containing either the detector list or the error from the service. """ - response = self._session.get(self._get_correct_url(Service.ANALYSIS) + f"detectors", + response = self._session.get(self._get_correct_url(Service.ANALYSIS) + "detectors", headers=self._get_header_v2()) try: if response.ok: - return Response(status_code=response.status_code, - value=DetectorsMinimalResponse.model_validate(response.json()), error=None) + payload: DetectorsMinimalResponse = DetectorsMinimalResponse.model_validate(response.json()) + return Response(status_code=response.status_code, value=payload.detectors, error=None) return Response(status_code=response.status_code, error=DetailedErrorResponse.model_validate(response.json()), value=None) except (ValidationError, KeyError) as exception: @@ -282,19 +282,122 @@ def get_detectors(self) -> Response[DetectorsMinimalResponse]: return Response(status_code=response.status_code, error=DetailedErrorResponse(error=error), value=None) - def get_detector(self, detector_name: str) -> Response[DetectorResponse]: + def get_detector(self, detector_name: str) -> Response[Detector]: """ Retrieve details of a detector. :return: A Response[DetectorResponse] containing either the detector details or the error from the service. """ + url_encoded_name = urllib.parse.quote(detector_name, safe="") + endpoint_url = self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}" + response = self._session.get(endpoint_url, headers=self._get_header_v2()) + try: + if response.ok: + payload: DetectorResponse = DetectorResponse.model_validate(response.json()) + return Response(status_code=response.status_code, value=payload.detector, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def create_detector(self, attributes: DetectorCreate) -> Response[DetectorMinimal]: + response = self._session.post( + self._get_correct_url(Service.ANALYSIS) + "detectors", + attributes.model_dump_json(by_alias=True, exclude_none=True), + headers=self._get_header_v2() + ) + try: + if response.ok: + payload: DetectorResponse = DetectorResponse.model_validate(response.json()) + return Response(status_code=response.status_code, value=payload.detector, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def create_detector_version(self, detector_name: str, version_attributes: DetectorVersionCreate) -> Response[DetectorVersionCreateResponseLinks]: + + response = self._session.post(self._get_correct_url(Service.ANALYSIS) + f"detectors/{detector_name}/versions", + version_attributes.model_dump_json(by_alias=True, exclude_none=True), + headers=self._get_header_v2()) + + try: + if response.ok: + payload = DetectorVersionCreateResponseLinks.model_validate(response.json()) + return Response(status_code=response.status_code, value=payload, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def delete_detector(self, detector_name: str) -> Response[None]: url_encoded_name = urllib.parse.quote(detector_name, safe="") response = self._session.get(self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}", - headers=self._get_header_v2()) + headers=self._get_header_v2()) try: if response.ok: - return Response(status_code=response.status_code, - value=DetectorResponse.model_validate(response.json()), error=None) + return Response(status_code=response.status_code, value=None, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def delete_detector_version(self, detector_name: str, version_number: str) -> Response[None]: + response = self._session.delete(self._get_correct_url(Service.ANALYSIS) + f"detectors/{detector_name}/versions/{version_number}", + headers=self._get_header_v2()) + try: + if response.ok: + return Response(status_code=response.status_code, value=None, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def modify_detector(self, detector_name: str, detector_attrs: DetectorUpdate) -> Response[None]: + response = self._session.patch(self._get_correct_url(Service.ANALYSIS) + f"detectors/{detector_name}", + headers=self._get_header_v2()) + + try: + if response.ok: + return Response(status_code=response.status_code, value=None, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def publish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: + response = self._session.post(self._get_correct_url(Service.ANALYSIS) + f"detectors/{detector_name}/versions/{version_number}/publish", + headers=self._get_header_v2()) + + try: + if response.ok: + return Response(status_code=response.status_code, value=None, error=None) + return Response(status_code=response.status_code, + error=DetailedErrorResponse.model_validate(response.json()), value=None) + except (ValidationError, KeyError) as exception: + error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) + return Response(status_code=response.status_code, + error=DetailedErrorResponse(error=error), value=None) + + def unpublish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: + response = self._session.post(self._get_correct_url(Service.ANALYSIS) + f"detectors/{detector_name}/versions/{version_number}/unpublish", + headers=self._get_header_v2()) + + try: + if response.ok: + return Response(status_code=response.status_code, value=None, error=None) return Response(status_code=response.status_code, error=DetailedErrorResponse.model_validate(response.json()), value=None) except (ValidationError, KeyError) as exception: diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 03ded915..a1f31a09 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -79,7 +79,12 @@ class TrainingS3DOptions(BaseModel): ) model: Optional[Segmentation3DTrainingModel] = Field(None, description="Training Model architecture to use.") features: Optional[list[PointCloudFeature]] = Field(None, description="Features to use for the training.") - version_name: Optional[str] = Field(None, description="Version name for the newly trained detector.", alias="versionName") + version_number: Optional[str] = Field( + None, + description="String representing the version number for the newly trained detector.", + alias="versionNumber", + pattern=r"\d+(.\d+)?" + ) class TrainingS3DOutputsCreate(Enum): From 729d855a33647d980b5eb2a980d5ef6830466953 Mon Sep 17 00:00:00 2001 From: Liliam Jean Baptiste Date: Wed, 18 Feb 2026 17:06:25 +0100 Subject: [PATCH 08/49] Fixed tests --- python_sdk/tests/test_detectors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python_sdk/tests/test_detectors.py b/python_sdk/tests/test_detectors.py index 89f18876..b0276989 100644 --- a/python_sdk/tests/test_detectors.py +++ b/python_sdk/tests/test_detectors.py @@ -1,7 +1,6 @@ import responses import json import os -from datetime import datetime from reality_capture.service.service import RealityCaptureService @@ -50,7 +49,7 @@ def test_get_detector_200(self): json=payload, status=200) response = self.rcs.get_detector("@bentley/bentley-city-a-s3d") assert not response.is_error() - assert response.value.detector.name == "@bentley/bentley-city-a-s3d" + assert response.value.name == "@bentley/bentley-city-a-s3d" @responses.activate def test_get_detectors_ill_formed(self): @@ -79,4 +78,4 @@ def test_get_detectors_200(self): json=payload, status=200) response = self.rcs.get_detectors() assert not response.is_error() - assert len(response.value.detectors) == 2 \ No newline at end of file + assert len(response.value) == 2 From 9fbb701dc7b379c658e968ee0e85829cf9fe173a Mon Sep 17 00:00:00 2001 From: Liliam Jean Baptiste Date: Wed, 18 Feb 2026 19:56:04 +0100 Subject: [PATCH 09/49] Updated TrainingS3D example --- .../specifications/examples/training_s3d_new_default.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python_sdk/docs/specifications/examples/training_s3d_new_default.py b/python_sdk/docs/specifications/examples/training_s3d_new_default.py index fe8466b9..3650b567 100644 --- a/python_sdk/docs/specifications/examples/training_s3d_new_default.py +++ b/python_sdk/docs/specifications/examples/training_s3d_new_default.py @@ -1,12 +1,15 @@ import reality_capture.specifications.training as training s3d_inputs = training.TrainingS3DInputs( - scene="401975b7-0c0a-4498-5896-84987921f4bb", + segmentations3D=["401975b7-0c0a-4498-5896-84987921f4bb"], + detectorName="example-detector", ) + s3d_outputs = [ training.TrainingS3DOutputsCreate.DETECTOR, ] -s3d_options = training.TrainingS3DOptions() +s3d_options = training.TrainingS3DOptions(epochs=2, spacing=0.2) + s3ds = training.TrainingS3DSpecificationsCreate( inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options ) From 501ce9fef5290a17a32abd359e06922517114b87 Mon Sep 17 00:00:00 2001 From: EHS-BENTLEY-SYSTEMS Date: Mon, 11 May 2026 16:38:44 +0200 Subject: [PATCH 10/49] Remove CRS outputs and segmented as in S3D Specifications (#289) --- .../specifications/examples/eval_s3d_specs.py | 2 +- .../examples/s3d_specs_segment_meshes.py | 2 +- .../examples/s3d_specs_segment_pc.py | 2 +- ...s3d_specs_segment_pc_and_infer_3d_lines.py | 2 +- .../specifications/eval_s3d.py | 6 ++--- .../specifications/segmentation3d.py | 27 +++---------------- python_sdk/tests/test_job.py | 4 +-- .../src/specifications/eval_s3d.ts | 6 ++--- .../src/specifications/segmentation3d.ts | 13 ++------- .../specifications/test_eval_s3d.test.ts | 12 ++++----- .../test_segmentation3d.test.ts | 7 ++--- 11 files changed, 25 insertions(+), 58 deletions(-) diff --git a/python_sdk/docs/specifications/examples/eval_s3d_specs.py b/python_sdk/docs/specifications/examples/eval_s3d_specs.py index 803b317d..f0742d7d 100644 --- a/python_sdk/docs/specifications/examples/eval_s3d_specs.py +++ b/python_sdk/docs/specifications/examples/eval_s3d_specs.py @@ -2,6 +2,6 @@ eval_s3d_inputs = eval_s3d.EvalS3DInputs(reference="587a14fd-305a-474c-b037-26d4ee8829d9", prediction="08342927-859c-4563-a4b8-6c6cfb7d5bb3") -eval_s3d_outputs = [eval_s3d.EvalS3DOutputsCreate.SEGMENTATION3D, eval_s3d.EvalS3DOutputsCreate.SEGMENTED_POINT_CLOUD, +eval_s3d_outputs = [eval_s3d.EvalS3DOutputsCreate.SEGMENTATION3D, eval_s3d.EvalS3DOutputsCreate.SEGMENTED_MODEL_3D, eval_s3d.EvalS3DOutputsCreate.REPORT] eval_s3ds = eval_s3d.EvalS3DSpecificationsCreate(inputs=eval_s3d_inputs, outputs=eval_s3d_outputs) diff --git a/python_sdk/docs/specifications/examples/s3d_specs_segment_meshes.py b/python_sdk/docs/specifications/examples/s3d_specs_segment_meshes.py index 8fe6ec26..43654a74 100644 --- a/python_sdk/docs/specifications/examples/s3d_specs_segment_meshes.py +++ b/python_sdk/docs/specifications/examples/s3d_specs_segment_meshes.py @@ -3,6 +3,6 @@ s3d_inputs = segmentation3d.Segmentation3DInputs(meshes="401975b7-0c0a-4498-5896-84987921f4bb", pointCloudSegmentationDetector="08342927-859c-4563-a4b8-6c6cfb7d5bb3") s3d_outputs = [segmentation3d.Segmentation3DOutputsCreate.SEGMENTATION3D, - segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_POINT_CLOUD] + segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D] s3d_options = segmentation3d.Segmentation3DOptions() s3ds = segmentation3d.Segmentation3DSpecificationsCreate(inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options) diff --git a/python_sdk/docs/specifications/examples/s3d_specs_segment_pc.py b/python_sdk/docs/specifications/examples/s3d_specs_segment_pc.py index 715632a1..66b33320 100644 --- a/python_sdk/docs/specifications/examples/s3d_specs_segment_pc.py +++ b/python_sdk/docs/specifications/examples/s3d_specs_segment_pc.py @@ -3,6 +3,6 @@ s3d_inputs = segmentation3d.Segmentation3DInputs(pointClouds="401975b7-0c0a-4498-5896-84987921f4bb", pointCloudSegmentationDetector="08342927-859c-4563-a4b8-6c6cfb7d5bb3") s3d_outputs = [segmentation3d.Segmentation3DOutputsCreate.SEGMENTATION3D, - segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_POINT_CLOUD] + segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D] s3d_options = segmentation3d.Segmentation3DOptions() s3ds = segmentation3d.Segmentation3DSpecificationsCreate(inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options) diff --git a/python_sdk/docs/specifications/examples/s3d_specs_segment_pc_and_infer_3d_lines.py b/python_sdk/docs/specifications/examples/s3d_specs_segment_pc_and_infer_3d_lines.py index 293db9da..e3a5d9e4 100644 --- a/python_sdk/docs/specifications/examples/s3d_specs_segment_pc_and_infer_3d_lines.py +++ b/python_sdk/docs/specifications/examples/s3d_specs_segment_pc_and_infer_3d_lines.py @@ -3,7 +3,7 @@ s3d_inputs = segmentation3d.Segmentation3DInputs(meshes="401975b7-0c0a-4498-5896-84987921f4bb", pointCloudSegmentationDetector="08342927-859c-4563-a4b8-6c6cfb7d5bb3") s3d_outputs = [segmentation3d.Segmentation3DOutputsCreate.SEGMENTATION3D, - segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_POINT_CLOUD, + segmentation3d.Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D, segmentation3d.Segmentation3DOutputsCreate.LINES3D] s3d_options = segmentation3d.Segmentation3DOptions() s3ds = segmentation3d.Segmentation3DSpecificationsCreate(inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options) diff --git a/python_sdk/src/reality_capture/specifications/eval_s3d.py b/python_sdk/src/reality_capture/specifications/eval_s3d.py index 9a5de6ed..f6204010 100644 --- a/python_sdk/src/reality_capture/specifications/eval_s3d.py +++ b/python_sdk/src/reality_capture/specifications/eval_s3d.py @@ -13,8 +13,8 @@ class EvalS3DInputs(BaseModel): class EvalS3DOutputs(BaseModel): report: Optional[str] = Field(None, description="Path in Bucket of json report with confusion matrix", pattern=r"^bkt:.+") - segmented_point_cloud: Optional[str] = Field(None, alias="segmentedPointCloud", - description="Reality data id of segmented point cloud, " + segmented_model_3d: Optional[str] = Field(None, alias="segmentedModel3D", + description="Reality data id of segmented 3D model, " "annotated with confusion matrix index") segmentation3d: Optional[str] = Field(None, alias="segmentation3D", description="Reality data id of ContextScene, " @@ -23,7 +23,7 @@ class EvalS3DOutputs(BaseModel): class EvalS3DOutputsCreate(Enum): REPORT = "report" - SEGMENTED_POINT_CLOUD = "segmentedPointCloud" + SEGMENTED_MODEL_3D = "segmentedModel3D" SEGMENTATION3D = "segmentation3D" diff --git a/python_sdk/src/reality_capture/specifications/segmentation3d.py b/python_sdk/src/reality_capture/specifications/segmentation3d.py index f50fd04a..76eed1d9 100644 --- a/python_sdk/src/reality_capture/specifications/segmentation3d.py +++ b/python_sdk/src/reality_capture/specifications/segmentation3d.py @@ -27,25 +27,9 @@ class Segmentation3DOutputs(BaseModel): segmentation3d: Optional[str] = Field(None, alias="segmentation3D", description="Reality data id of ContextScene, " "pointing to the segmented point cloud") - segmented_point_cloud: Optional[str] = Field(None, alias="segmentedPointCloud", - description="Reality data id of " + segmented_model_3d: Optional[str] = Field(None, alias="segmentedModel3D", + description="Reality data id of " "the 3D segmentation as OPC file") - segmentation3d_as_pod: Optional[str] = Field(None, alias="segmentation3DAsPOD", - description="Reality data id of " - "the segmented point cloud as POD file, " - "segmentation3D output must be defined") - segmentation3d_as_las: Optional[str] = Field(None, alias="segmentation3DAsLAS", - description="Reality data id of " - "the segmented point cloud as LAS file, " - "segmentation3D output must be defined") - segmentation3d_as_laz: Optional[str] = Field(None, alias="segmentation3DAsLAZ", - description="Reality data id of " - "the segmented point cloud as LAZ file, " - "segmentation3D output must be defined") - segmentation3d_as_ply: Optional[str] = Field(None, alias="segmentation3DAsPLY", - description="Reality data id of " - "the segmented point cloud as PLY file, " - "segmentation3D output must be defined") objects3d: Optional[str] = Field(None, alias="objects3D", description="Reality data id of ContextScene, " "annotated with embedded 3D objects") @@ -83,11 +67,7 @@ class Segmentation3DOutputs(BaseModel): class Segmentation3DOutputsCreate(Enum): SEGMENTATION3D = "segmentation3D" - SEGMENTED_POINT_CLOUD = "segmentedPointCloud" - SEGMENTATION3D_AS_POD = "segmentation3DAsPOD" - SEGMENTATION3D_AS_LAS = "segmentation3DAsLAS" - SEGMENTATION3D_AS_LAZ = "segmentation3DAsLAZ" - SEGMENTATION3D_AS_PLY = "segmentation3DAsPLY" + SEGMENTED_MODEL_3D = "segmentedModel3D" OBJECTS3D = "objects3D" OBJECTS3D_AS_3DTILES = "objects3DAs3DTiles" OBJECTS3D_AS_GEOJSON = "objects3DAsGeoJSON" @@ -102,7 +82,6 @@ class Segmentation3DOutputsCreate(Enum): class Segmentation3DOptions(BaseModel): - crs: Optional[str] = Field(None, description="CRS used by POD, LAS, LAZ, PLY, DGN and SHP outputs") save_confidence: Optional[bool] = Field(None, alias="saveConfidence", description="Save confidence in 3D segmentation") compute_line_width: Optional[bool] = Field(None, alias="computeLineWidth", diff --git a/python_sdk/tests/test_job.py b/python_sdk/tests/test_job.py index 8d1e88f2..0e8b38c0 100644 --- a/python_sdk/tests/test_job.py +++ b/python_sdk/tests/test_job.py @@ -1,8 +1,8 @@ import datetime from reality_capture.service.job import Service, JobCreate, Job, JobType, JobState -from reality_capture.specifications.eval_o2d import EvalO2DSpecifications from reality_capture.specifications.tiling import TilingOutputsCreate from reality_capture.specifications.segmentation3d import Segmentation3DOutputsCreate + # from reality_capture.specifications.point_cloud_conversion import PCConversionOutputsCreate @@ -29,7 +29,7 @@ def test_appropriate_service_job_create(self): assert j.get_appropriate_service() == Service.MODELING eg_specs = {"inputs": {"model3D": "pointClouds"}, "outputs": [Segmentation3DOutputsCreate.SEGMENTATION3D, - Segmentation3DOutputsCreate.SEGMENTED_POINT_CLOUD]} + Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D]} j = JobCreate(**{"type": JobType.SEGMENTATION_3D, "iTwinId": "itwin", "specifications": eg_specs}) # assert j.get_appropriate_service() == Service.ANALYSIS # pc_conversion_specs = {"inputs": {"pointClouds": ["point_cloud"]}, "outputs": PCConversionOutputsCreate.OPC} diff --git a/typescript/packages/reality-capture/src/specifications/eval_s3d.ts b/typescript/packages/reality-capture/src/specifications/eval_s3d.ts index 292fef3f..ee105038 100644 --- a/typescript/packages/reality-capture/src/specifications/eval_s3d.ts +++ b/typescript/packages/reality-capture/src/specifications/eval_s3d.ts @@ -11,8 +11,8 @@ export const EvalS3DOutputsSchema = z.object({ .regex(/^bkt:.+/) .describe("Path in Bucket of json report with confusion matrix") .optional(), - segmentedPointCloud: z.string() - .describe("Reality data id of segmented point cloud, annotated with confusion matrix index") + segmentedModel3D: z.string() + .describe("Reality data id of segmented 3D model, annotated with confusion matrix index") .optional(), segmentation3D: z.string() .describe("Reality data id of ContextScene, pointing to segmented point cloud") @@ -22,7 +22,7 @@ export type EvalS3DOutputs = z.infer; export enum EvalS3DOutputsCreate { REPORT = "report", - SEGMENTED_POINT_CLOUD = "segmentedPointCloud", + SEGMENTED_MODEL_3D = "segmentedModel3D", SEGMENTATION3D = "segmentation3D", } diff --git a/typescript/packages/reality-capture/src/specifications/segmentation3d.ts b/typescript/packages/reality-capture/src/specifications/segmentation3d.ts index 16cb2efb..96285f3e 100644 --- a/typescript/packages/reality-capture/src/specifications/segmentation3d.ts +++ b/typescript/packages/reality-capture/src/specifications/segmentation3d.ts @@ -2,11 +2,7 @@ import { z } from "zod"; export enum Segmentation3DOutputsCreate { SEGMENTATION3D = "segmentation3D", - SEGMENTED_POINT_CLOUD = "segmentedPointCloud", - SEGMENTATION3D_AS_POD = "segmentation3DAsPOD", - SEGMENTATION3D_AS_LAS = "segmentation3DAsLAS", - SEGMENTATION3D_AS_LAZ = "segmentation3DAsLAZ", - SEGMENTATION3D_AS_PLY = "segmentation3DAsPLY", + SEGMENTED_MODEL_3D = "segmentedModel3D", OBJECTS3D = "objects3D", OBJECTS3D_AS_3DTILES = "objects3DAs3DTiles", OBJECTS3D_AS_GEOJSON = "objects3DAsGeoJSON", @@ -33,11 +29,7 @@ export type Segmentation3DInputs = z.infer; export const Segmentation3DOutputsSchema = z.object({ segmentation3D: z.string().optional().describe("Reality data id of ContextScene, pointing to the segmented point cloud"), - segmentedPointCloud: z.string().optional().describe("Reality data id of the 3D segmentation as OPC file"), - segmentation3DAsPOD: z.string().optional().describe("Reality data id of the segmented point cloud as POD file, segmentation3D output must be defined"), - segmentation3DAsLAS: z.string().optional().describe("Reality data id of the segmented point cloud as LAS file, segmentation3D output must be defined"), - segmentation3DAsLAZ: z.string().optional().describe("Reality data id of the segmented point cloud as LAZ file, segmentation3D output must be defined"), - segmentation3DAsPLY: z.string().optional().describe("Reality data id of the segmented point cloud as PLY file, segmentation3D output must be defined"), + segmentedModel3D: z.string().optional().describe("Reality data id of the 3D segmentation as OPC file"), objects3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D objects"), objects3DAs3DTiles: z.string().optional().describe("Reality data id of 3D objects as 3D Tiles file, objects3d output must be defined"), objects3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects as GeoJSON file, objects3d output must be defined"), @@ -53,7 +45,6 @@ export const Segmentation3DOutputsSchema = z.object({ export type Segmentation3DOutputs = z.infer; export const Segmentation3DOptionsSchema = z.object({ - crs: z.string().optional().describe("CRS used by POD, LAS, LAZ, PLY, DGN and SHP outputs"), saveConfidence: z.boolean().optional().describe("Save confidence in 3D segmentation"), computeLineWidth: z.boolean().optional().describe("Estimation 3D line width at each vertex"), removeSmallLines: z.number().optional().describe("Remove 3D lines with total length smaller than this value"), diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_eval_s3d.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_eval_s3d.test.ts index a3454a4b..773e8e16 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_eval_s3d.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_eval_s3d.test.ts @@ -50,9 +50,9 @@ describe("EvalS3DOutputsSchema", () => { ).to.throw(z.ZodError); }); - it("should validate correct segmentedPointCloud output", () => { + it("should validate correct segmentedModel3D output", () => { expect(() => - EvalS3DOutputsSchema.parse({ segmentedPointCloud: "cloud-id" }) + EvalS3DOutputsSchema.parse({ segmentedModel3D: "cloud-id" }) ).to.not.throw(); }); @@ -66,7 +66,7 @@ describe("EvalS3DOutputsSchema", () => { expect(() => EvalS3DOutputsSchema.parse({ report: "bkt:my/report.json", - segmentedPointCloud: "cloud-id", + segmentedModel3D: "cloud-id", segmentation3D: "scene-id", }) ).to.not.throw(); @@ -76,7 +76,7 @@ describe("EvalS3DOutputsSchema", () => { describe("EvalS3DOutputsCreate enum", () => { it("should have the right values", () => { expect(EvalS3DOutputsCreate.REPORT).to.equal("report"); - expect(EvalS3DOutputsCreate.SEGMENTED_POINT_CLOUD).to.equal("segmentedPointCloud"); + expect(EvalS3DOutputsCreate.SEGMENTED_MODEL_3D).to.equal("segmentedModel3D"); expect(EvalS3DOutputsCreate.SEGMENTATION3D).to.equal("segmentation3D"); }); }); @@ -90,7 +90,7 @@ describe("EvalS3DSpecificationsCreateSchema", () => { }, outputs: [ EvalS3DOutputsCreate.REPORT, - EvalS3DOutputsCreate.SEGMENTED_POINT_CLOUD, + EvalS3DOutputsCreate.SEGMENTED_MODEL_3D, ], }; expect(() => EvalS3DSpecificationsCreateSchema.parse(input)).to.not.throw(); @@ -127,7 +127,7 @@ describe("EvalS3DSpecificationsSchema", () => { }, outputs: { report: "bkt:my/report.json", - segmentedPointCloud: "cloud-id", + segmentedModel3D: "cloud-id", segmentation3D: "scene-id", }, }; diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts index 3c3f55e2..4551cad9 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts @@ -40,11 +40,7 @@ describe("Segmentation3DOutputsSchema", () => { it("should validate all outputs as strings", () => { const data: Record = { segmentation3D: "id1", - segmentedPointCloud: "id2", - segmentation3DAsPOD: "id3", - segmentation3DAsLAS: "id4", - segmentation3DAsLAZ: "id5", - segmentation3DAsPLY: "id6", + segmentedModel3D: "id2", objects3D: "id7", objects3DAs3DTiles: "id8", objects3DAsGeoJSON: "id9", @@ -132,6 +128,7 @@ describe("Segmentation3DSpecificationsSchema", () => { describe("Segmentation3DOutputsCreate enum", () => { it("should contain expected values", () => { expect(Segmentation3DOutputsCreate.SEGMENTATION3D).to.equal("segmentation3D"); + expect(Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D).to.equal("segmentedModel3D"); expect(Segmentation3DOutputsCreate.OBJECTS3D_AS_GEOJSON).to.equal("objects3DAsGeoJSON"); expect(Segmentation3DOutputsCreate.POLYGONS3D_AS_GEOJSON).to.equal("polygons3DAsGeoJSON"); }); From 226a9247e6bd778aad1bc2e6cfc20c2a549823e7 Mon Sep 17 00:00:00 2001 From: pierre-seguin-bentley Date: Tue, 19 May 2026 14:10:09 +0000 Subject: [PATCH 11/49] Added PointTransformerV3 to S3D Training models --- python_sdk/src/reality_capture/specifications/training.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index a1f31a09..3950ddd5 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -60,7 +60,7 @@ class TrainingS3DOutputs(BaseModel): class Segmentation3DTrainingModel(Enum): SPLATNET = "SPLATNet" - + POINT_TRANSFORMER_V3 = "PointTransformerV3" class PointCloudFeature(Enum): RGB = "RGB" From 819fbbd1a2b96ce189672eba7f64a63dfd588f80 Mon Sep 17 00:00:00 2001 From: pierre-seguin-bentley Date: Tue, 19 May 2026 14:11:17 +0000 Subject: [PATCH 12/49] Accidentaly removed a line in last commit --- python_sdk/src/reality_capture/specifications/training.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 3950ddd5..2001a462 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -62,6 +62,7 @@ class Segmentation3DTrainingModel(Enum): SPLATNET = "SPLATNet" POINT_TRANSFORMER_V3 = "PointTransformerV3" + class PointCloudFeature(Enum): RGB = "RGB" NORMAL = "NORMAL" From 95dfbc432cc62b8cf08c33f2c4b50d275f58576d Mon Sep 17 00:00:00 2001 From: EHS-BENTLEY-SYSTEMS Date: Wed, 10 Jun 2026 17:04:41 +0200 Subject: [PATCH 13/49] Tmp/ehs/update cd (#299) --- .../cd_specs_detect_changes_meshes.py | 2 +- python_sdk/docs/specifications/objects2d.rst | 4 +-- .../docs/specifications/segmentation2d.rst | 6 +--- .../docs/specifications/segmentation3d.rst | 32 +++---------------- .../segmentation_orthophoto.rst | 3 +- .../specifications/change_detection.py | 29 ++++++++--------- .../specifications/eval_s3d.py | 5 ++- .../specifications/segmentation3d.py | 6 ++-- python_sdk/tests/test_job_validator.py | 15 +++++++-- .../src/specifications/change_detection.ts | 9 +++--- .../test_change_detection.test.ts | 21 +----------- 11 files changed, 45 insertions(+), 87 deletions(-) diff --git a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py index 597f778b..ae98c328 100644 --- a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py +++ b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py @@ -2,6 +2,6 @@ cd_inputs = change_detection.ChangeDetectionInputs(model3dA="279db84b-090f-4922-b9a5-7a4fd0a71fcd", model3dB="40af080d-7ata-48c8-974c-610820fe90f2") -cd_outputs = [change_detection.ChangeDetectionOutputsCreate.OBJECTS3D] +cd_outputs = [change_detection.ChangeDetectionOutputsCreate.LOCATIONS3D_AS_GEOJSON] cd_options = change_detection.ChangeDetectionOptions() cds = change_detection.ChangeDetectionSpecificationsCreate(inputs=cd_inputs, outputs=cd_outputs, options=cd_options) diff --git a/python_sdk/docs/specifications/objects2d.rst b/python_sdk/docs/specifications/objects2d.rst index a248b199..362b81cc 100644 --- a/python_sdk/docs/specifications/objects2d.rst +++ b/python_sdk/docs/specifications/objects2d.rst @@ -4,7 +4,7 @@ Objects 2D The *Objects 2D* job uses a photo object detector to detect 2D objects in photos. This job produces a context scene annotated with 2D objects. If the context scene input is oriented, it can turn these 2D objects into 3D objects and produce a context scene annotated with 3D objects. -Optionally, these 3D objects that can be exported in another formats, such as 3D Tiles, DGN, SHP, or GeoJSON. +Optionally, these 3D objects that can be exported in another formats, such as 3D Tiles, SHP, or GeoJSON. .. contents:: Quick access :local: @@ -36,7 +36,6 @@ This job has three different purposes : | *meshes* (optional) - | *objects2d*, | *objects3d*, - | *objects3d_as_dgn* (optional), | *objects3d_as_3d_tiles* (optional), | *objects3d_as_geojson* (optional), | *locations3d_as_shp* (optional), @@ -52,7 +51,6 @@ This job has three different purposes : | *point_clouds* (optional), | *meshes* (optional) - | *objects3d*, - | *objects3d_as_dgn* (optional), | *objects3d_as_3d_tiles* (optional), | *objects3d_as_geojson* (optional), | *locations3d_as_shp* (optional), diff --git a/python_sdk/docs/specifications/segmentation2d.rst b/python_sdk/docs/specifications/segmentation2d.rst index 2873ae19..858f3ba5 100644 --- a/python_sdk/docs/specifications/segmentation2d.rst +++ b/python_sdk/docs/specifications/segmentation2d.rst @@ -2,7 +2,7 @@ Segmentation 2D =============== -The Segmentation 2D job uses a photo segmentation detector to classify pixels in photos. If photos are oriented, it can project the segmentation on meshes or point clouds, and detect 3D lines and 3D polygons. This job produces segmented photos and a context scene pointing to these photos. Optionally, it can also produce context scenes annotated with 3D polygons and 3D lines. Those can be exported in another formats, such as 3D Tiles, DGN, or GeoJSON. +The Segmentation 2D job uses a photo segmentation detector to classify pixels in photos. If photos are oriented, it can project the segmentation on meshes or point clouds, and detect 3D lines and 3D polygons. This job produces segmented photos and a context scene pointing to these photos. Optionally, it can also produce context scenes annotated with 3D polygons and 3D lines. Those can be exported in another formats, such as 3D Tiles, or GeoJSON. .. contents:: Quick access :local: @@ -38,11 +38,9 @@ This job has three different purposes : - | *segmentation2d*, | *segmented_photos*, | *lines3d* (optional), - | *lines3d_as_dgn* (optional), | *lines3d_as_3d_tiles* (optional), | *lines3d_as_geojson* (optional), | *polygons3d* (optional), - | *polygons3d_as_dgn* (optional), | *polygons3d_as_3d_tiles* (optional), | *polygons3d_as_geojson* (optional) - | *min_photos*, @@ -58,11 +56,9 @@ This job has three different purposes : | *segmentation2d*, | *point_clouds*/*meshes* - | *lines3d* (optional), - | *lines3d_as_dgn* (optional), | *lines3d_as_3d_tiles* (optional), | *lines3d_as_geojson* (optional), | *polygons3d* (optional), - | *polygons3d_as_dgn* (optional), | *polygons3d_as_3d_tiles* (optional), | *polygons3d_as_geojson* (optional) - | *min_photos*, diff --git a/python_sdk/docs/specifications/segmentation3d.rst b/python_sdk/docs/specifications/segmentation3d.rst index 1483e2e3..5925f0bc 100644 --- a/python_sdk/docs/specifications/segmentation3d.rst +++ b/python_sdk/docs/specifications/segmentation3d.rst @@ -2,7 +2,7 @@ Segmentation 3D =============== -The Segmentation 3D job uses a point cloud segmentation detector to classify each point of a point cloud. It produces a segmented point cloud (in OPC format) and a context scene pointing to this point cloud. The segmented point cloud can be exported as LAS, LAZ, POD or PLY. Optionally, it can also produce context scenes annotated with 3D objects, 3D polygons and/or 3D lines. Those can be exported in another format, such as 3D Tiles, SHP, DGN, or GeoJSON. +The Segmentation 3D job uses a point cloud segmentation detector to classify each point of a point cloud. It produces a segmented point cloud (in OPC format) and a context scene pointing to this point cloud. Optionally, it can also produce context scenes annotated with 3D objects, 3D polygons and/or 3D lines. Those can be exported in another format, such as 3D Tiles, SHP, or GeoJSON. .. contents:: Quick access :local: @@ -27,11 +27,7 @@ This job has four different purposes : | *point_cloud_segmentation_detector*, | *clip_polygon* (optional) - | *segmentation3d*, - | *segmented_point_cloud*, - | *segmentation3d_as_las* (optional), - | *segmentation3d_as_laz* (optional), - | *segmentation3d_as_pod* (optional), - | *segmentation3d_as_ply* (optional) + | *segmented_model_3d* - | *save_confidence*, | *crs* * - | Segment a collection @@ -40,11 +36,7 @@ This job has four different purposes : | *point_cloud_segmentation_detector*, | *clip_polygon* (optional) - | *segmentation3d*, - | *segmented_point_cloud*, - | *segmentation3d_as_las* (optional), - | *segmentation3d_as_laz* (optional), - | *segmentation3d_as_pod* (optional), - | *segmentation3d_as_ply* (optional) + | *segmented_model_3d* - | *save_confidence*, | *crs* * - | Segment a collection @@ -55,23 +47,16 @@ This job has four different purposes : | *point_cloud_segmentation_detector*, | *clip_polygon* (optional) - | *segmentation3d*, - | *segmented_point_cloud*, - | *segmentation3d_as_las* (optional), - | *segmentation3d_as_laz* (optional), - | *segmentation3d_as_pod* (optional), - | *segmentation3d_as_ply* (optional), + | *segmented_model_3d*, | *objects3d* (optional), - | *objects3d_as_dgn* (optional), | *objects3d_as_3d_tiles* (optional), | *objects3d_as_geojson* (optional), | *locations3d_as_shp* (optional), | *locations3d_as_geojson* (optional), | *lines3d* (optional), - | *lines3d_as_dgn* (optional), | *lines3d_as_3d_tiles* (optional), | *lines3d_as_geojson* (optional), | *polygons3d* (optional), - | *polygons3d_as_dgn* (optional), | *polygons3d_as_3d_tiles* (optional), | *polygons3d_as_geojson* (optional) - | *remove_small_components*, @@ -83,22 +68,15 @@ This job has four different purposes : | lines and 3D polygons - | *segmentation3d*, | *clip_polygon* (optional) - - | *segmentation3d_as_las* (optional), - | *segmentation3d_as_laz* (optional), - | *segmentation3d_as_pod* (optional), - | *segmentation3d_as_ply* (optional), - | *objects3d* (optional), - | *objects3d_as_dgn* (optional), + - | *objects3d* (optional), | *objects3d_as_3d_tiles* (optional), | *objects3d_as_geojson* (optional), | *locations3d_as_shp* (optional), | *locations3d_as_geojson* (optional), | *lines3d* (optional), - | *lines3d_as_dgn* (optional), | *lines3d_as_3d_tiles* (optional), | *lines3d_as_geojson* (optional), | *polygons3d* (optional), - | *polygons3d_as_dgn* (optional), | *polygons3d_as_3d_tiles* (optional), | *polygons3d_as_geojson* (optional) - | *remove_small_components*, diff --git a/python_sdk/docs/specifications/segmentation_orthophoto.rst b/python_sdk/docs/specifications/segmentation_orthophoto.rst index 4d7b2f59..c39ab104 100644 --- a/python_sdk/docs/specifications/segmentation_orthophoto.rst +++ b/python_sdk/docs/specifications/segmentation_orthophoto.rst @@ -2,7 +2,7 @@ Segmentation Orthophoto ======================= -The Segmentation Orthophoto job uses an orthophoto segmentation detector to classify pixels in orthophotos. It produces segmented orthophotos and a context pointing to these orthophotos. Depending on the detector, it can detect 2D lines and 2D polygons. Optionally, these 2D lines and 2D polygons can be exported as SHP, DGN or GeoJSON files. +The Segmentation Orthophoto job uses an orthophoto segmentation detector to classify pixels in orthophotos. It produces segmented orthophotos and a context pointing to these orthophotos. Depending on the detector, it can detect 2D lines and 2D polygons. Optionally, these 2D lines and 2D polygons can be exported as SHP or GeoJSON files. .. contents:: Quick access :local: @@ -28,7 +28,6 @@ Purposes | *polygons2d_as_shp* (optional), | *polygons2d_as_geojson* (optional), | *lines2d* (optional), - | *lines2d_as_dgn* (optional), | *lines2d_as_shp* (optional), | *lines2d_as_geojson* (optional) - | diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index eec92092..450e5569 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -11,36 +11,35 @@ class ChangeDetectionInputs(BaseModel): class ChangeDetectionOutputs(BaseModel): - objects3d: Optional[str] = Field(None, alias="objects3D", - description="Reality data id of ContextScene, annotated with embedded 3D objects") - locations3d_as_shp: Optional[str] = Field(None, alias="locations3DAsSHP", + locations3d_as_shp: Optional[str] = Field(None, alias="locations3DAsSHP", description="Reality data id of 3D objects locations " "as SHP format") - locations3d_as_geojson: Optional[str] = Field(None, alias="locations3DAsGeoJSON", + locations3d_as_geojson: Optional[str] = Field(None, alias="locations3DAsGeoJSON", description="Reality data id of 3D objects locations " "as GeoJSON file") - changes_in_model_b: Optional[str] = Field(None, description="Points in B not in A as OPC", alias="changesInModelB") - changes_in_model_a: Optional[str] = Field(None, description="Points in A not in B as OPC", alias="changesInModelA") + segmented_model_3d_b: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d B", alias="segmentedModel3DB") + segmented_model_3d_a: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d A", alias="segmentedModel3DA") + segmentation_3d_a: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model A", alias="segmentation3DA") + segmentation_3d_b: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model B", alias="segmentation3DB") class ChangeDetectionOutputsCreate(Enum): - OBJECTS3D = "objects3D" LOCATIONS3D_AS_SHP = "locations3DAsSHP" LOCATIONS3D_AS_GEOJSON = "locations3DAsGeoJSON" - CHANGES_IN_MODEL_A = "changesInModelA" - CHANGES_IN_MODEL_B = "changesInModelB" + SEGMENTED_MODEL_3D_A = "segmentedModel3DA" + SEGMENTED_MODEL_3D_B = "segmentedModel3DB" + SEGMENTATION_3D_B = "segmentation3DB" + SEGMENTATION_3D_A = "segmentation3DA" class ChangeDetectionOptions(BaseModel): - output_crs: Optional[str] = Field(None, alias="outputCrs", - description="CRS used by locations3DAsSHP output") min_points_per_change: Optional[int] = Field(None, alias="minPointsPerChange", description="Minimum number of points in a region " "to be considered as a change") - mesh_sampling_resolution: Optional[float] = Field(None, alias="meshSamplingResolution", - description="Target point cloud resolution when starting " - "from meshes") - threshold: Optional[float] = Field(None, description="High threshold to detect spatial changes " + sampling_resolution: Optional[float] = Field(None, alias="meshSamplingResolution", + description="Target point cloud resolution when starting from meshes") + grow_threshold: Optional[float] = Field(None, alias="growThreshold", + description="High threshold to detect spatial changes " "(hysteresis detection)") filter_threshold: Optional[float] = Field(None, alias="filterThreshold", description="Low threshold to detect spatial changes " diff --git a/python_sdk/src/reality_capture/specifications/eval_s3d.py b/python_sdk/src/reality_capture/specifications/eval_s3d.py index f6204010..1e28f97c 100644 --- a/python_sdk/src/reality_capture/specifications/eval_s3d.py +++ b/python_sdk/src/reality_capture/specifications/eval_s3d.py @@ -14,11 +14,10 @@ class EvalS3DOutputs(BaseModel): report: Optional[str] = Field(None, description="Path in Bucket of json report with confusion matrix", pattern=r"^bkt:.+") segmented_model_3d: Optional[str] = Field(None, alias="segmentedModel3D", - description="Reality data id of segmented 3D model, " - "annotated with confusion matrix index") + description="Reality data id of segmented 3D model OPC as annotated with confusion matrix index") segmentation3d: Optional[str] = Field(None, alias="segmentation3D", description="Reality data id of ContextScene, " - "pointing to segmented point cloud") + "pointing to segmented 3D model") class EvalS3DOutputsCreate(Enum): diff --git a/python_sdk/src/reality_capture/specifications/segmentation3d.py b/python_sdk/src/reality_capture/specifications/segmentation3d.py index 76eed1d9..f7f5ba82 100644 --- a/python_sdk/src/reality_capture/specifications/segmentation3d.py +++ b/python_sdk/src/reality_capture/specifications/segmentation3d.py @@ -25,11 +25,9 @@ class Segmentation3DInputs(BaseModel): class Segmentation3DOutputs(BaseModel): segmentation3d: Optional[str] = Field(None, alias="segmentation3D", - description="Reality data id of ContextScene, " - "pointing to the segmented point cloud") + description="Reality data id of ContextScene, pointing to the segmented 3D model") segmented_model_3d: Optional[str] = Field(None, alias="segmentedModel3D", - description="Reality data id of " - "the 3D segmentation as OPC file") + description="Reality data id of the 3D segmentation model follows the same format as the model3D file") objects3d: Optional[str] = Field(None, alias="objects3D", description="Reality data id of ContextScene, " "annotated with embedded 3D objects") diff --git a/python_sdk/tests/test_job_validator.py b/python_sdk/tests/test_job_validator.py index 8bd234fa..5e560f9f 100644 --- a/python_sdk/tests/test_job_validator.py +++ b/python_sdk/tests/test_job_validator.py @@ -70,12 +70,23 @@ def test_validation_change_detection(self): "model3dB": "modelb" }, "outputs": { - "changesInModelA": "rdid", - "objects3d": "obj" + "segmentedModel3DA": "rdid_seg_a", + "segmentedModel3DB": "rdid_seg_b", + "segmentation3DA": "rdid_segmentation_a", + "segmentation3DB": "rdid_segmentation_b", + "locations3DAsGeoJSON": "geojson_rdid", + "locations3DAsSHP": "shp_rdid" } } job = Job(**j) assert isinstance(job.specifications, ChangeDetectionSpecifications) + outputs = job.specifications.outputs + assert outputs.segmented_model_3d_a == "rdid_seg_a" + assert outputs.segmented_model_3d_b == "rdid_seg_b" + assert outputs.segmentation_3d_a == "rdid_segmentation_a" + assert outputs.segmentation_3d_b == "rdid_segmentation_b" + assert outputs.locations3d_as_geojson == "geojson_rdid" + assert outputs.locations3d_as_shp == "shp_rdid" def test_validation_constraints(self): j = self.j_base.copy() diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index 5228578f..d181cc2d 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -1,11 +1,10 @@ import { z } from "zod"; export enum ChangeDetectionOutputsCreate { - OBJECTS3D = "objects3D", LOCATIONS3D_AS_SHP = "locations3DAsSHP", LOCATIONS3D_AS_GEOJSON = "locations3DAsGeoJSON", - CHANGES_IN_MODEL_A = "changesInModelA", - CHANGES_IN_MODEL_B = "changesInModelB", + MODEL_3D_A_CLASSIFIED = "model3dAClassified", + MODEL_3D_B_CLASSIFIED = "model3dBClassified", } export const ChangeDetectionInputsSchema = z.object({ @@ -21,8 +20,8 @@ export const ChangeDetectionOutputsSchema = z.object({ objects3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D objects"), locations3DAsSHP: z.string().optional().describe("Reality data id of 3D objects locations as SHP format"), locations3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects locations as GeoJSON file"), - changesInModelB: z.string().optional().describe("Points in B not in A as OPC"), - changesInModelA: z.string().optional().describe("Points in A not in B as OPC"), + model3dBClassified: z.string().optional().describe("Points in B not in A as OPC"), + model3dAClassified: z.string().optional().describe("Points in A not in B as OPC"), }); export type ChangeDetectionOutputs = z.infer; diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts index e019cbd0..e9bc9022 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts @@ -33,7 +33,6 @@ describe("change_detection specifications", () => { describe("ChangeDetectionOutputsSchema", () => { it("should validate with all outputs", () => { const valid = { - objects3D: "obj3dId", locations3DAsSHP: "shpId", locations3DAsGeoJSON: "geojsonId", added: "addedId", @@ -75,7 +74,7 @@ describe("change_detection specifications", () => { it("should validate valid specification", () => { const valid = { inputs: { model3dA: "id1", model3dB: "id2" }, - outputs: [ChangeDetectionOutputsCreate.CHANGES_IN_MODEL_A, ChangeDetectionOutputsCreate.CHANGES_IN_MODEL_B], + outputs: [ChangeDetectionOutputsCreate.MODEL_3D_A_CLASSIFIED, ChangeDetectionOutputsCreate.MODEL_3D_B_CLASSIFIED], options: { minPointsPerChange: 10 }, }; expect(() => ChangeDetectionSpecificationsCreateSchema.parse(valid)).not.to.throw(); @@ -89,13 +88,6 @@ describe("change_detection specifications", () => { expect(() => ChangeDetectionSpecificationsCreateSchema.parse(invalid)).to.throw(z.ZodError); }); - it("should validate without options", () => { - const valid = { - inputs: { model3dA: "id1", model3dB: "id2" }, - outputs: [ChangeDetectionOutputsCreate.OBJECTS3D], - }; - expect(() => ChangeDetectionSpecificationsCreateSchema.parse(valid)).not.to.throw(); - }); }); describe("ChangeDetectionSpecificationsSchema", () => { @@ -103,7 +95,6 @@ describe("change_detection specifications", () => { const valid = { inputs: { model3dA: "id1", model3dB: "id2" }, outputs: { - objects3D: "objId", locations3DAsSHP: "shpId", }, options: { meshSamplingResolution: 0.5 }, @@ -117,15 +108,5 @@ describe("change_detection specifications", () => { }; expect(() => ChangeDetectionSpecificationsSchema.parse(invalid)).to.throw(z.ZodError); }); - - it("should validate without options", () => { - const valid = { - inputs: { model3dA: "id1", model3dB: "id2" }, - outputs: { - objects3D: "objId" - }, - }; - expect(() => ChangeDetectionSpecificationsSchema.parse(valid)).not.to.throw(); - }); }); }); From 27a25de574c3a09cf56a21a0b10905cd1fd78204 Mon Sep 17 00:00:00 2001 From: Pierre Seguin Date: Mon, 15 Jun 2026 11:46:29 +0000 Subject: [PATCH 14/49] Updated training specs for release --- .../examples/training_o2d_new_default.py | 13 -- python_sdk/docs/specifications/index.rst | 4 +- .../docs/specifications/training_o2d.rst | 62 ------- python_sdk/src/reality_capture/service/job.py | 12 +- .../specifications/training.py | 45 +---- .../reality-capture/src/service/job.ts | 13 +- .../src/specifications/training.ts | 98 ++++------ .../specifications/test_training.test.ts | 171 +++++------------- 8 files changed, 86 insertions(+), 332 deletions(-) delete mode 100644 python_sdk/docs/specifications/examples/training_o2d_new_default.py delete mode 100644 python_sdk/docs/specifications/training_o2d.rst diff --git a/python_sdk/docs/specifications/examples/training_o2d_new_default.py b/python_sdk/docs/specifications/examples/training_o2d_new_default.py deleted file mode 100644 index 8feaf809..00000000 --- a/python_sdk/docs/specifications/examples/training_o2d_new_default.py +++ /dev/null @@ -1,13 +0,0 @@ -import reality_capture.specifications.training as training - -o2d_inputs = training.TrainingO2DInputs( - scene="401975b7-0c0a-4498-5896-84987921f4bb", -) -o2d_outputs = [ - training.TrainingO2DOutputsCreate.DETECTOR, - training.TrainingO2DOutputsCreate.METRICS, -] -o2d_options = training.TrainingO2DOptions() -o2ds = training.TrainingO2DSpecificationsCreate( - inputs=o2d_inputs, outputs=o2d_outputs, options=o2d_options -) diff --git a/python_sdk/docs/specifications/index.rst b/python_sdk/docs/specifications/index.rst index 350dee3b..47e0f894 100644 --- a/python_sdk/docs/specifications/index.rst +++ b/python_sdk/docs/specifications/index.rst @@ -34,7 +34,6 @@ Specifications regroup all the settings used to create jobs with our APIs. eval_s3d eval_sortho clearance - training_o2d training_s3d @@ -62,8 +61,7 @@ Analysis * :doc:`/specifications/change_detection` will take two point clouds or two meshes to to get 3D regions that capture the changes. * :doc:`/specifications/eval_o2d`, :doc:`/specifications/eval_o3d`, :doc:`/specifications/eval_s2d`, :doc:`/specifications/eval_s3d` and :doc:`/specifications/eval_sortho` will compare a prediction to a reference for a specific detection. * :doc:`/specifications/clearance` uses a 3D model and a footprint to compute clearance information. -* :doc:`/specifications/training_o2d` will train an o2d detector from a ContextScene -* :doc:`/specifications/training_s3d` will train an s3d detector from a ContextScene +* :doc:`/specifications/training_s3d` will train an s3d detector from ContextScenes .. Conversion .. ========== diff --git a/python_sdk/docs/specifications/training_o2d.rst b/python_sdk/docs/specifications/training_o2d.rst deleted file mode 100644 index 3e3784c1..00000000 --- a/python_sdk/docs/specifications/training_o2d.rst +++ /dev/null @@ -1,62 +0,0 @@ -============ -Training O2D -============ - -The *Training O2D* job uses a ContextScene containing Objects2D annotations of pictures to train a new O2D detector. - -.. contents:: Quick access - :local: - :depth: 2 - -Purpose -======= - - This job has the following purposes: - - -.. list-table:: - :widths: auto - :header-rows: 1 - - * - Purpose - - Inputs - - Possible outputs - - Useful options - * - Train new detector on a dataset (ContextScene) - - | *scene*, - - | *detector*, - | *metrics* (optional) - - | *epochs* - | *max_train_split* - - -Examples -======== - -In this example, we will create a specification for submitting a Training O2D job to produce a new O2D detector from a ContextScene. - -.. literalinclude:: examples/training_o2d_new_default.py - :language: Python - -Classes -======= - -.. currentmodule:: reality_capture.specifications.training - -.. autopydantic_model:: TrainingO2DSpecificationsCreate - -.. autoclass:: TrainingO2DOutputsCreate - :show-inheritance: - :members: - :undoc-members: - -.. autopydantic_model:: TrainingO2DSpecifications - -.. autopydantic_model:: TrainingO2DInputs - :model-show-json: False - -.. autopydantic_model:: TrainingO2DOutputs - :model-show-json: False - -.. autopydantic_model:: TrainingO2DOptions - :model-show-json: False diff --git a/python_sdk/src/reality_capture/service/job.py b/python_sdk/src/reality_capture/service/job.py index 80f04e38..0b61af07 100644 --- a/python_sdk/src/reality_capture/service/job.py +++ b/python_sdk/src/reality_capture/service/job.py @@ -28,8 +28,7 @@ WaterConstraintsSpecificationsCreate) """from reality_capture.specifications.point_cloud_conversion import (PointCloudConversionSpecificationsCreate, PointCloudConversionSpecifications)""" -from reality_capture.specifications.training import (TrainingO2DSpecifications, TrainingO2DSpecificationsCreate, - TrainingS3DSpecificationsCreate, TrainingS3DSpecifications) +from reality_capture.specifications.training import (TrainingS3DSpecificationsCreate, TrainingS3DSpecifications) from reality_capture.specifications.gaussian_splats import (GaussianSplatsSpecificationsCreate, GaussianSplatsSpecifications) from reality_capture.specifications.eval_o2d import (EvalO2DSpecificationsCreate, EvalO2DSpecifications) @@ -65,7 +64,6 @@ class JobType(Enum): TOUCH_UP_EXPORT = "TouchUpExport" WATER_CONSTRAINTS = "WaterConstraints" CLEARANCE_CALCULATION = "ClearanceCalculation" - TRAINING_O2D = "TrainingO2D" TRAINING_S3D = "TrainingS3D" # POINT_CLOUD_CONVERSION = "PointCloudConversion" @@ -83,7 +81,7 @@ def _get_appropriate_service(jt: JobType): if jt in [JobType.OBJECTS_2D, JobType.SEGMENTATION_2D, JobType.SEGMENTATION_3D, JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.EVAL_O2D, JobType.EVAL_O3D, JobType.EVAL_S2D, JobType.EVAL_S3D, JobType.EVAL_SORTHO, JobType.CLEARANCE_CALCULATION, - JobType.TRAINING_O2D, JobType.TRAINING_S3D]: + JobType.TRAINING_S3D]: return Service.ANALYSIS # return Service.CONVERSION raise NotImplemented("Other services not yet implemented") @@ -115,7 +113,7 @@ class JobCreate(BaseModel): TilingSpecificationsCreate, TouchUpExportSpecificationsCreate, TouchUpImportSpecificationsCreate, WaterConstraintsSpecificationsCreate, ClearanceSpecificationsCreate, - TrainingO2DSpecificationsCreate, TrainingS3DSpecificationsCreate] = ( + TrainingS3DSpecificationsCreate] = ( Field(description="Specifications aligned with the job type.")) itwin_id: str = Field(description="iTwin ID, used by the service for finding " "input reality data and uploading output data.", @@ -161,7 +159,7 @@ class Job(BaseModel): TilingSpecifications, TouchUpExportSpecifications, TouchUpImportSpecifications, WaterConstraintsSpecifications, ClearanceSpecifications, - TrainingO2DSpecifications, TrainingS3DSpecifications] = ( + TrainingS3DSpecifications] = ( Field(description="Specifications aligned with the job type.")) @field_validator("specifications", mode="plain") @@ -217,8 +215,6 @@ def set_specification_validation_model(cls, raw_dict: dict[str, Any], validation specifications = ClearanceSpecifications(**raw_dict) elif job_type == JobType.TRAINING_S3D: specifications = TrainingS3DSpecifications(**raw_dict) - elif job_type == JobType.TRAINING_O2D: - specifications = TrainingO2DSpecifications(**raw_dict) else: raise ValueError(f"Unsupported job type: {job_type}") diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 2001a462..ef8d044f 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -3,54 +3,12 @@ from enum import Enum -class TrainingO2DInputs(BaseModel): - scene: str = Field( - description="Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." - ) - - -class TrainingO2DOutputs(BaseModel): - detector: str = Field(description="Reality data id of the detector.") - metrics: Optional[str] = Field(None, description="Path in the bucket of the training metrics", - pattern=r"^bkt:.+") - - -class TrainingO2DOptions(BaseModel): - epochs: Optional[int] = Field( - None, description="Number of time to iterate over the entire dataset" - ) - max_train_split: Optional[float] = Field( - None, - alias="maxTrainingSplit", - description="Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics. Set it to 1.0 for no evaluation and use everything for training.", - gt=0.0, - le=1.0, - ) - - -class TrainingO2DOutputsCreate(Enum): - DETECTOR = "detector" - METRICS = "metrics" - - -class TrainingO2DSpecificationsCreate(BaseModel): - inputs: TrainingO2DInputs = Field(description="Inputs") - outputs: list[TrainingO2DOutputsCreate] = Field(description="Outputs") - options: Optional[TrainingO2DOptions] = Field(None, description="Options") - - -class TrainingO2DSpecifications(BaseModel): - inputs: TrainingO2DInputs = Field(description="Inputs") - outputs: TrainingO2DOutputs = Field(description="Outputs") - options: Optional[TrainingO2DOptions] = Field(None, description="Options") - - class TrainingS3DInputs(BaseModel): segmentations_3d: list[str] = Field( description="List of 3D models to train on.", alias="segmentations3D" ) - presets: Optional[list[str]] = Field(default=None, description="List of paths to preset") + preset: Optional[str] = Field(default=None, description="Path to preset") detector_name: str = Field(description="Name of the detector to train", alias="detectorName") @@ -60,7 +18,6 @@ class TrainingS3DOutputs(BaseModel): class Segmentation3DTrainingModel(Enum): SPLATNET = "SPLATNet" - POINT_TRANSFORMER_V3 = "PointTransformerV3" class PointCloudFeature(Enum): diff --git a/typescript/packages/reality-capture/src/service/job.ts b/typescript/packages/reality-capture/src/service/job.ts index fe2929df..75e06079 100644 --- a/typescript/packages/reality-capture/src/service/job.ts +++ b/typescript/packages/reality-capture/src/service/job.ts @@ -14,7 +14,7 @@ import { SegmentationOrthophotoSpecificationsCreateSchema, SegmentationOrthophot import { TilingSpecificationsCreateSchema, TilingSpecificationsSchema } from "../specifications/tiling"; import { TouchUpExportSpecificationsCreateSchema, TouchUpImportSpecificationsCreateSchema, TouchUpExportSpecificationsSchema, TouchUpImportSpecificationsSchema } from "../specifications/touchup"; import { WaterConstraintsSpecificationsCreateSchema, WaterConstraintsSpecificationsSchema } from "../specifications/water_constraints"; -import { TrainingO2DSpecificationsCreateSchema, TrainingS3DSpecificationsCreateSchema, TrainingO2DSpecificationsSchema, TrainingS3DSpecificationsSchema } from "../specifications/training"; +import { TrainingS3DSpecificationsCreateSchema, TrainingS3DSpecificationsSchema } from "../specifications/training"; //import { PointCloudConversionSpecificationsCreateSchema, PointCloudConversionSpecificationsSchema } from '../specifications/point_cloud_conversion'; import { GaussianSplatsSpecificationsCreateSchema, GaussianSplatsSpecificationsSchema } from "../specifications/gaussian_splats"; import { URLSchema } from "./bucket"; @@ -44,7 +44,6 @@ export enum JobType { SEGMENTATION_3D = "Segmentation3D", SEGMENTATION_ORTHOPHOTO = "SegmentationOrthophoto", TILING = "Tiling", - TRAINING_O2D = "TrainingO2D", TRAINING_S3D = "TrainingS3D", TOUCH_UP_IMPORT = "TouchUpImport", TOUCH_UP_EXPORT = "TouchUpExport", @@ -70,9 +69,9 @@ export function getAppropriateService(jt: JobType): Service { } if ([ JobType.OBJECTS_2D, JobType.SEGMENTATION_2D, JobType.SEGMENTATION_3D, - JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.TRAINING_O2D, + JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.EVAL_O2D, JobType.EVAL_O3D, JobType.EVAL_S2D, JobType.EVAL_S3D, - JobType.EVAL_SORTHO, JobType.TRAINING_O2D, JobType.TRAINING_S3D, + JobType.EVAL_SORTHO, JobType.TRAINING_S3D, JobType.CLEARANCE_CALCULATION ].includes(jt)) { return Service.ANALYSIS; @@ -116,7 +115,6 @@ export const JobCreateSchema = z.object({ TouchUpExportSpecificationsCreateSchema, TouchUpImportSpecificationsCreateSchema, WaterConstraintsSpecificationsCreateSchema, - TrainingO2DSpecificationsCreateSchema, //PointCloudConversionSpecificationsCreateSchema, TrainingS3DSpecificationsCreateSchema, ClearanceSpecificationsCreateSchema, @@ -233,11 +231,6 @@ export const JobSchema = z.discriminatedUnion("type", [ type: z.literal("Tiling"), specifications: TilingSpecificationsSchema, }), - z.object({ - ...CommonFields, - type: z.literal("TraningO2D"), - specifications: TrainingO2DSpecificationsSchema, - }), z.object({ ...CommonFields, type: z.literal("TraningS3D"), diff --git a/typescript/packages/reality-capture/src/specifications/training.ts b/typescript/packages/reality-capture/src/specifications/training.ts index cb03239b..32db4b4a 100644 --- a/typescript/packages/reality-capture/src/specifications/training.ts +++ b/typescript/packages/reality-capture/src/specifications/training.ts @@ -1,87 +1,51 @@ import { z } from "zod"; -export const TrainingO2DInputsSchema = z.object({ - scene: z.string().describe( - "Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." - ), -}); -export type TrainingO2DInputs = z.infer; - -export const TrainingO2DOutputsSchema = z.object({ - detector: z.string().describe("Reality data id of the detector."), - metrics: z - .string() - .regex(/^bkt:.+/, "Must start with 'bkt:'") - .describe("Path in the bucket of the training metrics") - .optional(), -}); -export type TrainingO2DOutputs = z.infer; - -export const TrainingO2DOptionsSchema = z.object({ - epochs: z - .number().int() - .describe("Number of time to iterate over the entire dataset") - .optional(), - maxTrainingSplit: z - .number() - .gt(0.0, { message: "Must be greater than 0.0" }) - .lte(1.0, { message: "Must be less than or equal to 1.0" }) - .describe( - "Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics" - ) - .optional(), -}); -export type TrainingO2DOptions = z.infer; - -export enum TrainingO2DOutputsCreate { - DETECTOR = "detector", - METRICS = "metrics" -} - -export const TrainingO2DSpecificationsCreateSchema = z.object({ - inputs: TrainingO2DInputsSchema.describe("Inputs"), - outputs: z - .array(z.nativeEnum(TrainingO2DOutputsCreate)) - .describe("Outputs"), - options: TrainingO2DOptionsSchema.describe("Options").optional(), -}); -export type TrainingO2DSpecificationsCreate = z.infer; - -export const TrainingO2DSpecificationsSchema = z.object({ - inputs: TrainingO2DInputsSchema.describe("Inputs"), - outputs: TrainingO2DOutputsSchema.describe("Outputs"), - options: TrainingO2DOptionsSchema.describe("Options").optional(), -}); -export type TrainingO2DSpecifications = z.infer; - export const TrainingS3DInputsSchema = z.object({ - scene: z.string().describe( - "Reality data id of a ContextScene pointing to photos with annotations (in the contextscene file)." - ), + segmentations3D: z.array(z.string()).describe("List of 3D models to train on."), + preset: z.string().optional().describe("Path to preset"), + detectorName: z.string().describe("Name of the detector to train"), }); export type TrainingS3DInputs = z.infer; export const TrainingS3DOutputsSchema = z.object({ - detector: z.string().describe("Reality data id of the detector."), + detector: z.string().describe("Full detector information (name/version)"), }); export type TrainingS3DOutputs = z.infer; +export enum Segmentation3DTrainingModel { + SPLATNET = "SPLATNet" +} + +export enum PointCloudFeature { + RGB = "RGB", + NORMAL = "NORMAL", + INTENSITY = "INTENSITY" +} + export const TrainingS3DOptionsSchema = z.object({ epochs: z - .number() + .number().int() + .gte(1, { message: "Must be greater than or equal to 1" }) + .lte(100, { message: "Must be less than or equal to 100" }) .describe("Number of time to iterate over the entire dataset") .optional(), spacing: z .number() - .describe("Spacing of the pointcloud seen by the detector") + .gt(0, { message: "Must be greater than 0" }) + .describe("Spacing of the pointcloud seen by the detector (in meters).") .optional(), - maxTrainingSplit: z - .number() - .gt(0.0, { message: "Must be greater than 0.0" }) - .lte(1.0, { message: "Must be less than or equal to 1.0" }) - .describe( - "Ratio (between 0.0 excluded and 1.0 included) of training data used to train the detector, the rest will be used to evaluate the model after each epoch and compute extra evaluation metrics" - ) + model: z + .nativeEnum(Segmentation3DTrainingModel) + .describe("Training Model architecture to use.") + .optional(), + features: z + .array(z.nativeEnum(PointCloudFeature)) + .describe("Features to use for the training.") + .optional(), + versionNumber: z + .string() + .regex(/\d+(.\d+)?/) + .describe("String representing the version number for the newly trained detector.") .optional(), }); export type TrainingS3DOptions = z.infer; diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_training.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_training.test.ts index 933a2507..06034e47 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_training.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_training.test.ts @@ -1,135 +1,34 @@ import { expect } from "chai"; import { z } from "zod"; import { - TrainingO2DInputsSchema, - TrainingO2DOutputsSchema, - TrainingO2DOptionsSchema, - TrainingO2DOutputsCreate, - TrainingO2DSpecificationsCreateSchema, - TrainingO2DSpecificationsSchema, TrainingS3DInputsSchema, TrainingS3DOutputsSchema, TrainingS3DOptionsSchema, TrainingS3DOutputsCreate, + Segmentation3DTrainingModel, + PointCloudFeature, TrainingS3DSpecificationsCreateSchema, TrainingS3DSpecificationsSchema, } from "../../specifications/training"; -describe("TrainingO2DInputsSchema", () => { +describe("TrainingS3DInputsSchema", () => { it("should validate a correct input", () => { - const data = { scene: "some-id" }; - expect(() => TrainingO2DInputsSchema.parse(data)).not.to.throw(); - }); - - it("should fail if scene is missing", () => { - const data = {}; - expect(() => TrainingO2DInputsSchema.parse(data)).to.throw(z.ZodError); - }); -}); - -describe("TrainingO2DOutputsSchema", () => { - it("should validate a correct output with metrics", () => { - const data = { detector: "det-id", metrics: "bkt:/metrics/path" }; - expect(() => TrainingO2DOutputsSchema.parse(data)).not.to.throw(); - }); - - it("should validate a correct output without metrics", () => { - const data = { detector: "det-id" }; - expect(() => TrainingO2DOutputsSchema.parse(data)).not.to.throw(); - }); - - it("should fail if detector is missing", () => { - const data = { metrics: "bkt:/metrics/path" }; - expect(() => TrainingO2DOutputsSchema.parse(data)).to.throw(z.ZodError); - }); - - it("should fail if metrics does not start with bkt:", () => { - const data = { detector: "det-id", metrics: "/metrics/path" }; - expect(() => TrainingO2DOutputsSchema.parse(data)).to.throw(z.ZodError); - }); -}); - -describe("TrainingO2DOptionsSchema", () => { - it("should validate correct options", () => { - const data = { epochs: 10, maxTrainingSplit: 0.8 }; - expect(() => TrainingO2DOptionsSchema.parse(data)).not.to.throw(); - }); - - it("should accept partial data", () => { - const data = {}; - expect(() => TrainingO2DOptionsSchema.parse(data)).not.to.throw(); - }); - - it("should fail if maxTrainingSplit is <= 0", () => { - const data = { maxTrainingSplit: 0 }; - expect(() => TrainingO2DOptionsSchema.parse(data)).to.throw(z.ZodError); - }); - - it("should fail if maxTrainingSplit is > 1", () => { - const data = { maxTrainingSplit: 1.1 }; - expect(() => TrainingO2DOptionsSchema.parse(data)).to.throw(z.ZodError); - }); - - it("should fail if epochs is not an int", () => { - const data = { epochs: 3.5 }; - expect(() => TrainingO2DOptionsSchema.parse(data)).to.throw(z.ZodError); - }); -}); - -describe("TrainingO2DSpecificationsCreateSchema", () => { - it("should validate a minimal spec", () => { - const data = { - inputs: { scene: "scene-id" }, - outputs: [TrainingO2DOutputsCreate.DETECTOR], - }; - expect(() => TrainingO2DSpecificationsCreateSchema.parse(data)).not.to.throw(); - }); - - it("should validate a full spec", () => { - const data = { - inputs: { scene: "scene-id" }, - outputs: [TrainingO2DOutputsCreate.DETECTOR, TrainingO2DOutputsCreate.METRICS], - options: { epochs: 5, maxTrainingSplit: 0.7 }, - }; - expect(() => TrainingO2DSpecificationsCreateSchema.parse(data)).not.to.throw(); - }); - - it("should fail if inputs are invalid", () => { - const data = { - inputs: { scene: 123 }, - outputs: [TrainingO2DOutputsCreate.DETECTOR], - }; - expect(() => TrainingO2DSpecificationsCreateSchema.parse(data)).to.throw(z.ZodError); - }); -}); - -describe("TrainingO2DSpecificationsSchema", () => { - it("should validate a full specification", () => { - const data = { - inputs: { scene: "scene-id" }, - outputs: { detector: "det-id", metrics: "bkt:/metrics/path" }, - options: { epochs: 10, maxTrainingSplit: 0.9 }, - }; - expect(() => TrainingO2DSpecificationsSchema.parse(data)).not.to.throw(); + const data = { segmentations3D: ["model-id"], detectorName: "my-detector" }; + expect(() => TrainingS3DInputsSchema.parse(data)).not.to.throw(); }); - it("should fail if outputs is invalid", () => { - const data = { - inputs: { scene: "scene-id" }, - outputs: { detector: "det-id", metrics: "/metrics/path" }, - }; - expect(() => TrainingO2DSpecificationsSchema.parse(data)).to.throw(z.ZodError); + it("should validate a correct input with preset", () => { + const data = { segmentations3D: ["model-id"], preset: "path/to/preset", detectorName: "my-detector" }; + expect(() => TrainingS3DInputsSchema.parse(data)).not.to.throw(); }); -}); -describe("TrainingS3DInputsSchema", () => { - it("should validate a correct input", () => { - const data = { scene: "scene-id" }; - expect(() => TrainingS3DInputsSchema.parse(data)).not.to.throw(); + it("should fail if segmentations3D is missing", () => { + const data = { detectorName: "my-detector" }; + expect(() => TrainingS3DInputsSchema.parse(data)).to.throw(z.ZodError); }); - it("should fail if scene is missing", () => { - const data = {}; + it("should fail if detectorName is missing", () => { + const data = { segmentations3D: ["model-id"] }; expect(() => TrainingS3DInputsSchema.parse(data)).to.throw(z.ZodError); }); }); @@ -148,7 +47,13 @@ describe("TrainingS3DOutputsSchema", () => { describe("TrainingS3DOptionsSchema", () => { it("should validate correct options", () => { - const data = { epochs: 10, spacing: 0.5, maxTrainingSplit: 0.7 }; + const data = { + epochs: 10, + spacing: 0.5, + model: Segmentation3DTrainingModel.SPLATNET, + features: [PointCloudFeature.RGB, PointCloudFeature.NORMAL], + versionNumber: "1.0", + }; expect(() => TrainingS3DOptionsSchema.parse(data)).not.to.throw(); }); @@ -157,13 +62,23 @@ describe("TrainingS3DOptionsSchema", () => { expect(() => TrainingS3DOptionsSchema.parse(data)).not.to.throw(); }); - it("should fail if maxTrainingSplit is <= 0", () => { - const data = { maxTrainingSplit: 0 }; + it("should fail if epochs is not an int", () => { + const data = { epochs: 3.5 }; + expect(() => TrainingS3DOptionsSchema.parse(data)).to.throw(z.ZodError); + }); + + it("should fail if epochs is < 1", () => { + const data = { epochs: 0 }; + expect(() => TrainingS3DOptionsSchema.parse(data)).to.throw(z.ZodError); + }); + + it("should fail if epochs is > 100", () => { + const data = { epochs: 101 }; expect(() => TrainingS3DOptionsSchema.parse(data)).to.throw(z.ZodError); }); - it("should fail if maxTrainingSplit is > 1", () => { - const data = { maxTrainingSplit: 1.2 }; + it("should fail if spacing is <= 0", () => { + const data = { spacing: 0 }; expect(() => TrainingS3DOptionsSchema.parse(data)).to.throw(z.ZodError); }); }); @@ -171,7 +86,7 @@ describe("TrainingS3DOptionsSchema", () => { describe("TrainingS3DSpecificationsCreateSchema", () => { it("should validate a minimal spec", () => { const data = { - inputs: { scene: "scene-id" }, + inputs: { segmentations3D: ["model-id"], detectorName: "my-detector" }, outputs: [TrainingS3DOutputsCreate.DETECTOR], }; expect(() => TrainingS3DSpecificationsCreateSchema.parse(data)).not.to.throw(); @@ -179,9 +94,15 @@ describe("TrainingS3DSpecificationsCreateSchema", () => { it("should validate a full spec", () => { const data = { - inputs: { scene: "scene-id" }, + inputs: { segmentations3D: ["model-id"], preset: "path/to/preset", detectorName: "my-detector" }, outputs: [TrainingS3DOutputsCreate.DETECTOR], - options: { epochs: 3, spacing: 2.0, maxTrainingSplit: 0.5 }, + options: { + epochs: 3, + spacing: 2.0, + model: Segmentation3DTrainingModel.SPLATNET, + features: [PointCloudFeature.INTENSITY], + versionNumber: "2.1", + }, }; expect(() => TrainingS3DSpecificationsCreateSchema.parse(data)).not.to.throw(); }); @@ -190,16 +111,16 @@ describe("TrainingS3DSpecificationsCreateSchema", () => { describe("TrainingS3DSpecificationsSchema", () => { it("should validate a full specification", () => { const data = { - inputs: { scene: "scene-id" }, + inputs: { segmentations3D: ["model-id"], detectorName: "my-detector" }, outputs: { detector: "det-id" }, - options: { epochs: 2, spacing: 0.1, maxTrainingSplit: 0.99 }, + options: { epochs: 2, spacing: 0.1 }, }; expect(() => TrainingS3DSpecificationsSchema.parse(data)).not.to.throw(); }); it("should fail if outputs is invalid", () => { const data = { - inputs: { scene: "scene-id" }, + inputs: { segmentations3D: ["model-id"], detectorName: "my-detector" }, outputs: {}, }; expect(() => TrainingS3DSpecificationsSchema.parse(data)).to.throw(z.ZodError); From adc09fb09f02ab962cf21c1f9f7087c00ceffc2f Mon Sep 17 00:00:00 2001 From: Pierre Seguin Date: Mon, 15 Jun 2026 12:13:39 +0000 Subject: [PATCH 15/49] Updated training specs python examples --- ...new_default.py => training_s3d_default.py} | 3 +-- .../examples/training_s3d_options.py | 22 +++++++++++++++++++ .../specifications/training.py | 2 +- .../src/specifications/training.ts | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) rename python_sdk/docs/specifications/examples/{training_s3d_new_default.py => training_s3d_default.py} (71%) create mode 100644 python_sdk/docs/specifications/examples/training_s3d_options.py diff --git a/python_sdk/docs/specifications/examples/training_s3d_new_default.py b/python_sdk/docs/specifications/examples/training_s3d_default.py similarity index 71% rename from python_sdk/docs/specifications/examples/training_s3d_new_default.py rename to python_sdk/docs/specifications/examples/training_s3d_default.py index 3650b567..967997c7 100644 --- a/python_sdk/docs/specifications/examples/training_s3d_new_default.py +++ b/python_sdk/docs/specifications/examples/training_s3d_default.py @@ -8,8 +8,7 @@ s3d_outputs = [ training.TrainingS3DOutputsCreate.DETECTOR, ] -s3d_options = training.TrainingS3DOptions(epochs=2, spacing=0.2) s3ds = training.TrainingS3DSpecificationsCreate( - inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options + inputs=s3d_inputs, outputs=s3d_outputs ) diff --git a/python_sdk/docs/specifications/examples/training_s3d_options.py b/python_sdk/docs/specifications/examples/training_s3d_options.py new file mode 100644 index 00000000..b4bf66e2 --- /dev/null +++ b/python_sdk/docs/specifications/examples/training_s3d_options.py @@ -0,0 +1,22 @@ +import reality_capture.specifications.training as training + +s3d_inputs = training.TrainingS3DInputs( + segmentations3D=["401975b7-0c0a-4498-5896-84987921f4bb"], + detectorName="example-detector", +) + +s3d_outputs = [ + training.TrainingS3DOutputsCreate.DETECTOR, +] + +s3d_options = training.TrainingS3DOptions( + epochs=2, + spacing=0.2, + model=training.Segmentation3DTrainingModel.SPLATNET, + features=[training.PointCloudFeature.RGB, training.PointCloudFeature.INTENSITY], + versionNumber="1.0", +) + +s3ds = training.TrainingS3DSpecificationsCreate( + inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options +) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index ef8d044f..8c62034d 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -8,7 +8,7 @@ class TrainingS3DInputs(BaseModel): description="List of 3D models to train on.", alias="segmentations3D" ) - preset: Optional[str] = Field(default=None, description="Path to preset") + preset: Optional[str] = Field(default=None, description="Path to a preset") detector_name: str = Field(description="Name of the detector to train", alias="detectorName") diff --git a/typescript/packages/reality-capture/src/specifications/training.ts b/typescript/packages/reality-capture/src/specifications/training.ts index 32db4b4a..2dc7d00f 100644 --- a/typescript/packages/reality-capture/src/specifications/training.ts +++ b/typescript/packages/reality-capture/src/specifications/training.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const TrainingS3DInputsSchema = z.object({ segmentations3D: z.array(z.string()).describe("List of 3D models to train on."), - preset: z.string().optional().describe("Path to preset"), + preset: z.string().optional().describe("Path to a preset"), detectorName: z.string().describe("Name of the detector to train"), }); export type TrainingS3DInputs = z.infer; From 52caf9102f41f3f335f3e5ef2ba9f7a1c7f03597 Mon Sep 17 00:00:00 2001 From: Pierre Seguin Date: Mon, 15 Jun 2026 12:15:34 +0000 Subject: [PATCH 16/49] Updated training specs examples again --- .../specifications/examples/training_s3d_default.py | 8 ++++---- .../specifications/examples/training_s3d_options.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python_sdk/docs/specifications/examples/training_s3d_default.py b/python_sdk/docs/specifications/examples/training_s3d_default.py index 967997c7..600e59d3 100644 --- a/python_sdk/docs/specifications/examples/training_s3d_default.py +++ b/python_sdk/docs/specifications/examples/training_s3d_default.py @@ -1,14 +1,14 @@ import reality_capture.specifications.training as training -s3d_inputs = training.TrainingS3DInputs( +training_s3d_inputs = training.TrainingS3DInputs( segmentations3D=["401975b7-0c0a-4498-5896-84987921f4bb"], detectorName="example-detector", ) -s3d_outputs = [ +training_s3d_outputs = [ training.TrainingS3DOutputsCreate.DETECTOR, ] -s3ds = training.TrainingS3DSpecificationsCreate( - inputs=s3d_inputs, outputs=s3d_outputs +training_s3ds = training.TrainingS3DSpecificationsCreate( + inputs=training_s3d_inputs, outputs=training_s3d_outputs ) diff --git a/python_sdk/docs/specifications/examples/training_s3d_options.py b/python_sdk/docs/specifications/examples/training_s3d_options.py index b4bf66e2..3a223b4a 100644 --- a/python_sdk/docs/specifications/examples/training_s3d_options.py +++ b/python_sdk/docs/specifications/examples/training_s3d_options.py @@ -1,15 +1,15 @@ import reality_capture.specifications.training as training -s3d_inputs = training.TrainingS3DInputs( +training_s3d_inputs = training.TrainingS3DInputs( segmentations3D=["401975b7-0c0a-4498-5896-84987921f4bb"], detectorName="example-detector", ) -s3d_outputs = [ +training_s3d_outputs = [ training.TrainingS3DOutputsCreate.DETECTOR, ] -s3d_options = training.TrainingS3DOptions( +training_s3d_options = training.TrainingS3DOptions( epochs=2, spacing=0.2, model=training.Segmentation3DTrainingModel.SPLATNET, @@ -17,6 +17,6 @@ versionNumber="1.0", ) -s3ds = training.TrainingS3DSpecificationsCreate( - inputs=s3d_inputs, outputs=s3d_outputs, options=s3d_options +training_s3ds = training.TrainingS3DSpecificationsCreate( + inputs=training_s3d_inputs, outputs=training_s3d_outputs, options=training_s3d_options ) From 0dd5b4b1c62bf25b81f116383a7e5ee7ea1ed92b Mon Sep 17 00:00:00 2001 From: Pierre Seguin Date: Mon, 15 Jun 2026 15:12:45 +0000 Subject: [PATCH 17/49] Fixed bad named / alias for samplingResolution option of CD --- .../src/reality_capture/specifications/change_detection.py | 2 +- .../reality-capture/src/specifications/change_detection.ts | 2 +- .../src/tests/specifications/test_change_detection.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index 450e5569..91595268 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -36,7 +36,7 @@ class ChangeDetectionOptions(BaseModel): min_points_per_change: Optional[int] = Field(None, alias="minPointsPerChange", description="Minimum number of points in a region " "to be considered as a change") - sampling_resolution: Optional[float] = Field(None, alias="meshSamplingResolution", + sampling_resolution: Optional[float] = Field(None, alias="samplingResolution", description="Target point cloud resolution when starting from meshes") grow_threshold: Optional[float] = Field(None, alias="growThreshold", description="High threshold to detect spatial changes " diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index d181cc2d..0c0b2028 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -28,7 +28,7 @@ export type ChangeDetectionOutputs = z.infer { const valid = { outputCrs: "EPSG:4326", minPointsPerChange: 10, - meshSamplingResolution: 0.5, + samplingResolution: 0.5, threshold: 0.2, filterThreshold: 0.1, }; @@ -97,7 +97,7 @@ describe("change_detection specifications", () => { outputs: { locations3DAsSHP: "shpId", }, - options: { meshSamplingResolution: 0.5 }, + options: { samplingResolution: 0.5 }, }; expect(() => ChangeDetectionSpecificationsSchema.parse(valid)).not.to.throw(); }); From 8c5cad2c78a94d99d496acedb5717e1799c0b5a3 Mon Sep 17 00:00:00 2001 From: RenaudKeriven <95699554+RenaudKeriven@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:07:43 +0200 Subject: [PATCH 18/49] CD and Clearance parameters fixes --- .../cd_specs_detect_changes_meshes.py | 4 ++-- .../examples/clearance_specs.py | 2 +- python_sdk/rk.txt | 2 ++ .../specifications/change_detection.py | 8 +++---- .../specifications/clearance.py | 2 +- python_sdk/tests/test_job_validator.py | 24 +++++++++---------- .../src/specifications/change_detection.ts | 16 ++++++------- .../src/specifications/clearance.ts | 2 +- 8 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 python_sdk/rk.txt diff --git a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py index ae98c328..f2f71a66 100644 --- a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py +++ b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py @@ -1,7 +1,7 @@ import reality_capture.specifications.change_detection as change_detection -cd_inputs = change_detection.ChangeDetectionInputs(model3dA="279db84b-090f-4922-b9a5-7a4fd0a71fcd", - model3dB="40af080d-7ata-48c8-974c-610820fe90f2") +cd_inputs = change_detection.ChangeDetectionInputs(model3DA="279db84b-090f-4922-b9a5-7a4fd0a71fcd", + model3DB="40af080d-7ata-48c8-974c-610820fe90f2") cd_outputs = [change_detection.ChangeDetectionOutputsCreate.LOCATIONS3D_AS_GEOJSON] cd_options = change_detection.ChangeDetectionOptions() cds = change_detection.ChangeDetectionSpecificationsCreate(inputs=cd_inputs, outputs=cd_outputs, options=cd_options) diff --git a/python_sdk/docs/specifications/examples/clearance_specs.py b/python_sdk/docs/specifications/examples/clearance_specs.py index d12cd799..b4f20ddd 100644 --- a/python_sdk/docs/specifications/examples/clearance_specs.py +++ b/python_sdk/docs/specifications/examples/clearance_specs.py @@ -1,6 +1,6 @@ import reality_capture.specifications.clearance as clearance -clearance_inputs = clearance.ClearanceInputs(model3d="084985b0-71b5-4b02-a788-db261dd0730c", +clearance_inputs = clearance.ClearanceInputs(model3D="084985b0-71b5-4b02-a788-db261dd0730c", clearanceFootprint="635f801b-82cc-4477-8d59-f01eb2fea1d9") clearance_outputs = [clearance.ClearanceOutputsCreate.OVF_AREAS, clearance.ClearanceOutputsCreate.OVF_LINES] clearance_specs = clearance.ClearanceSpecificationsCreate(inputs=clearance_inputs, outputs=clearance_outputs) \ No newline at end of file diff --git a/python_sdk/rk.txt b/python_sdk/rk.txt new file mode 100644 index 00000000..46a26128 --- /dev/null +++ b/python_sdk/rk.txt @@ -0,0 +1,2 @@ +lancer l'env virtuel +python -m build \ No newline at end of file diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index 91595268..87525b02 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -4,8 +4,8 @@ class ChangeDetectionInputs(BaseModel): - model_3d_a: str = Field(alias="model3dA", description="Reality data id of ContextScene, point cloud or mesh") - model_3d_b: str = Field(alias="model3dB", description="Reality data id of ContextScene, point cloud or mesh") + model_3d_a: str = Field(alias="model3DA", description="Reality data id of ContextScene, point cloud or mesh") + model_3d_b: str = Field(alias="model3DB", description="Reality data id of ContextScene, point cloud or mesh") extent: Optional[str] = Field(None, alias="extent", pattern=r"^bkt:.+", description="Path in the bucket of the clipping polygon to apply") @@ -39,10 +39,10 @@ class ChangeDetectionOptions(BaseModel): sampling_resolution: Optional[float] = Field(None, alias="samplingResolution", description="Target point cloud resolution when starting from meshes") grow_threshold: Optional[float] = Field(None, alias="growThreshold", - description="High threshold to detect spatial changes " + description="Low threshold to detect spatial changes " "(hysteresis detection)") filter_threshold: Optional[float] = Field(None, alias="filterThreshold", - description="Low threshold to detect spatial changes " + description="High threshold to detect spatial changes " "(hysteresis detection)") diff --git a/python_sdk/src/reality_capture/specifications/clearance.py b/python_sdk/src/reality_capture/specifications/clearance.py index cadde664..9e4a3c20 100644 --- a/python_sdk/src/reality_capture/specifications/clearance.py +++ b/python_sdk/src/reality_capture/specifications/clearance.py @@ -4,7 +4,7 @@ class ClearanceInputs(BaseModel): - model_3d: str = Field(alias="model3d", description="Reality data id of a point cloud.") + model_3d: str = Field(alias="model3D", description="Reality data id of a point cloud.") clearance_footprint: str = Field(alias="clearanceFootprint", description="Reality data id of building footprints.") diff --git a/python_sdk/tests/test_job_validator.py b/python_sdk/tests/test_job_validator.py index 5e560f9f..88e60e98 100644 --- a/python_sdk/tests/test_job_validator.py +++ b/python_sdk/tests/test_job_validator.py @@ -66,8 +66,8 @@ def test_validation_change_detection(self): j["type"] = "ChangeDetection" j["specifications"] = { "inputs": { - "model3dA": "modela", - "model3dB": "modelb" + "model3DA": "modela", + "model3DB": "modelb" }, "outputs": { "segmentedModel3DA": "rdid_seg_a", @@ -112,7 +112,7 @@ def test_validation_eval_o2d(self): "prediction": "predid" }, "outputs": { - "objects2d": "objid" + "objects2D": "objid" } } job = Job(**j) @@ -127,7 +127,7 @@ def test_validation_eval_o3d(self): "prediction": "predid" }, "outputs": { - "objects3d": "objid" + "objects3D": "objid" } } job = Job(**j) @@ -142,7 +142,7 @@ def test_validation_eval_s2d(self): "prediction": "predid" }, "outputs": { - "segmentation2d": "sid" + "segmentation2D": "sid" } } job = Job(**j) @@ -157,7 +157,7 @@ def test_validation_eval_s3d(self): "prediction": "predid" }, "outputs": { - "segmentation3d": "sid" + "segmentation3D": "sid" } } job = Job(**j) @@ -172,7 +172,7 @@ def test_validation_eval_sortho(self): "prediction": "predid" }, "outputs": { - "segmentation2d": "sid" + "segmentation2D": "sid" } } job = Job(**j) @@ -215,7 +215,7 @@ def test_validation_o2d(self): "photos": "mfid" }, "outputs": { - "objects2d": "sid" + "objects2D": "sid" } } job = Job(**j) @@ -258,7 +258,7 @@ def test_validation_s2d(self): "photos": "mfid", }, "outputs": { - "segmentation2d": "s2did" + "segmentation2D": "s2did" } } job = Job(**j) @@ -269,7 +269,7 @@ def test_validation_s3d(self): j["type"] = "Segmentation3D" j["specifications"] = { "inputs": { - "model3d": "mfid", + "model3D": "mfid", }, "outputs": { "segmentation3D": "s3did" @@ -287,7 +287,7 @@ def test_validation_sortho(self): "orthophotoSegmentationDetector": "detector" }, "outputs": { - "segmentation2d": "s2did" + "segmentation2D": "s2did" } } job = Job(**j) @@ -358,7 +358,7 @@ def test_validation_clearance(self): j["type"] = "ClearanceCalculation" j["specifications"] = { "inputs": { - "model3d": "mfid", + "model3D": "mfid", "clearanceFootprint": "sid" }, "outputs": { diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index 0c0b2028..3549ce82 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -8,8 +8,8 @@ export enum ChangeDetectionOutputsCreate { } export const ChangeDetectionInputsSchema = z.object({ - model3dA: z.string().describe("Reality data id of ContextScene, point cloud or mesh"), - model3dB: z.string().describe("Reality data id of ContextScene, point cloud or mesh"), + model3DA: z.string().describe("Reality data id of ContextScene, point cloud, GS or mesh"), + model3DB: z.string().describe("Reality data id of ContextScene, point cloud, GS or mesh"), extent: z.string().describe("Path in the bucket of the clipping polygon to apply").regex(/^bkt:.+/, { message: "Path in the bucket to the extent file must start with 'bkt:'", }).optional(), @@ -17,20 +17,20 @@ export const ChangeDetectionInputsSchema = z.object({ export type ChangeDetectionInputs = z.infer; export const ChangeDetectionOutputsSchema = z.object({ - objects3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D objects"), locations3DAsSHP: z.string().optional().describe("Reality data id of 3D objects locations as SHP format"), locations3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects locations as GeoJSON file"), - model3dBClassified: z.string().optional().describe("Points in B not in A as OPC"), - model3dAClassified: z.string().optional().describe("Points in A not in B as OPC"), + segmentation3DA: z.string().optional().describe("ContextScene CD on Model3D A"), + segmentedModel3DA: z.string().optional().describe("Model3D A with CD segmentation"), + segmentation3DB: z.string().optional().describe("ContextScene CD on Model3D B"), + segmentedModel3DB: z.string().optional().describe("Model3D B with CD segmentation"), }); export type ChangeDetectionOutputs = z.infer; export const ChangeDetectionOptionsSchema = z.object({ - outputCrs: z.string().optional().describe("CRS used by locations3DAsSHP output"), minPointsPerChange: z.number().int().optional().describe("Minimum number of points in a region to be considered as a change"), samplingResolution: z.number().optional().describe("Target point cloud resolution when starting from meshes"), - threshold: z.number().optional().describe("High threshold to detect spatial changes (hysteresis detection)"), - filterThreshold: z.number().optional().describe("Low threshold to detect spatial changes (hysteresis detection)"), + growThreshold: z.number().optional().describe("Low threshold to detect spatial changes (hysteresis detection)"), + filterThreshold: z.number().optional().describe("High threshold to detect spatial changes (hysteresis detection)"), }); export type ChangeDetectionOptions = z.infer; diff --git a/typescript/packages/reality-capture/src/specifications/clearance.ts b/typescript/packages/reality-capture/src/specifications/clearance.ts index 6cbb1c80..514d66c7 100644 --- a/typescript/packages/reality-capture/src/specifications/clearance.ts +++ b/typescript/packages/reality-capture/src/specifications/clearance.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const ClearanceInputsSchema = z.object({ - model3d: z.string().describe("Reality data id of a point cloud"), + model3D: z.string().describe("Reality data id of a point cloud"), clearanceFootprint: z.string().describe("Reality data id of Cbuilding footprints."), }); export type ClearanceInputs = z.infer; From 5fe0d24915d25a419e03fea8e4b9e3345bbf4c63 Mon Sep 17 00:00:00 2001 From: RenaudKeriven <95699554+RenaudKeriven@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:14:49 +0200 Subject: [PATCH 19/49] Oops --- python_sdk/rk.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 python_sdk/rk.txt diff --git a/python_sdk/rk.txt b/python_sdk/rk.txt deleted file mode 100644 index 46a26128..00000000 --- a/python_sdk/rk.txt +++ /dev/null @@ -1,2 +0,0 @@ -lancer l'env virtuel -python -m build \ No newline at end of file From ddc5c4f2569bd73a06992f89f848222a40b0edc4 Mon Sep 17 00:00:00 2001 From: RenaudKeriven <95699554+RenaudKeriven@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:48:03 +0200 Subject: [PATCH 20/49] Analysis job specifs --- .../specifications/change_detection.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index 87525b02..478bc95d 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -4,23 +4,24 @@ class ChangeDetectionInputs(BaseModel): - model_3d_a: str = Field(alias="model3DA", description="Reality data id of ContextScene, point cloud or mesh") - model_3d_b: str = Field(alias="model3DB", description="Reality data id of ContextScene, point cloud or mesh") + model_3d_a: str = Field(alias="model3DA", description="Reality data id of ContextScene, point cloud, Gaussian splats, or mesh") + model_3d_b: str = Field(alias="model3DB", description="Reality data id of ContextScene, point cloud, Gaussian splats, or mesh") extent: Optional[str] = Field(None, alias="extent", pattern=r"^bkt:.+", description="Path in the bucket of the clipping polygon to apply") class ChangeDetectionOutputs(BaseModel): + segmentation_3d_a: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model A", alias="segmentation3DA") + segmented_model_3d_b: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d B", alias="segmentedModel3DB") + segmentation_3d_b: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model B", alias="segmentation3DB") + segmented_model_3d_a: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d A", alias="segmentedModel3DA") locations3d_as_shp: Optional[str] = Field(None, alias="locations3DAsSHP", description="Reality data id of 3D objects locations " "as SHP format") locations3d_as_geojson: Optional[str] = Field(None, alias="locations3DAsGeoJSON", description="Reality data id of 3D objects locations " "as GeoJSON file") - segmented_model_3d_b: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d B", alias="segmentedModel3DB") - segmented_model_3d_a: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d A", alias="segmentedModel3DA") - segmentation_3d_a: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model A", alias="segmentation3DA") - segmentation_3d_b: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model B", alias="segmentation3DB") + class ChangeDetectionOutputsCreate(Enum): From d464d7207aaaacff1485f3511f1d1f75b34bc61c Mon Sep 17 00:00:00 2001 From: RenaudKeriven <95699554+RenaudKeriven@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:46:29 +0200 Subject: [PATCH 21/49] CD locations --- .../specifications/change_detection.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index 478bc95d..7a3590d8 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -15,23 +15,37 @@ class ChangeDetectionOutputs(BaseModel): segmented_model_3d_b: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d B", alias="segmentedModel3DB") segmentation_3d_b: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model B", alias="segmentation3DB") segmented_model_3d_a: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d A", alias="segmentedModel3DA") - locations3d_as_shp: Optional[str] = Field(None, alias="locations3DAsSHP", - description="Reality data id of 3D objects locations " + locations3d_a: Optional[str] = Field(None, alias="locations3DA", + description="Reality data id of Contextscne with locations of changes in A") + locations3d_a_as_shp: Optional[str] = Field(None, alias="locations3DAAsSHP", + description="Reality data id of locations of changes in A" "as SHP format") - locations3d_as_geojson: Optional[str] = Field(None, alias="locations3DAsGeoJSON", - description="Reality data id of 3D objects locations " + locations3d_a_as_geojson: Optional[str] = Field(None, alias="locations3DAAsGeoJSON", + description="Reality data id of locations of changes in A" + "as GeoJSON file") + locations3d_b: Optional[str] = Field(None, alias="locations3DB", + description="Reality data id of Contextscne with locations of changes in B") + locations3d_b_as_shp: Optional[str] = Field(None, alias="locations3DBAsSHP", + description="Reality data id of locations of changes in B" + "as SHP format") + locations3d_b_as_geojson: Optional[str] = Field(None, alias="locations3DBAsGeoJSON", + description="Reality data id of locations of changes in B" "as GeoJSON file") class ChangeDetectionOutputsCreate(Enum): - LOCATIONS3D_AS_SHP = "locations3DAsSHP" - LOCATIONS3D_AS_GEOJSON = "locations3DAsGeoJSON" + SEGMENTATION_3D_A = "segmentation3DA" SEGMENTED_MODEL_3D_A = "segmentedModel3DA" - SEGMENTED_MODEL_3D_B = "segmentedModel3DB" SEGMENTATION_3D_B = "segmentation3DB" - SEGMENTATION_3D_A = "segmentation3DA" - + SEGMENTED_MODEL_3D_B = "segmentedModel3DB" + LOCATIONS3D_A = "locations3DA" + LOCATIONS3D_A_AS_SHP = "locations3DAAsSHP" + LOCATIONS3D_A_AS_GEOJSON = "locations3DAAsGeoJSON" + LOCATIONS3D_B = "locations3DB" + LOCATIONS3D_B_AS_SHP = "locations3DBAsSHP" + LOCATIONS3D_B_AS_GEOJSON = "locations3DBAsGeoJSON" + class ChangeDetectionOptions(BaseModel): min_points_per_change: Optional[int] = Field(None, alias="minPointsPerChange", From c5842ef8e4eec3bad503a423c4809e923ffa709b Mon Sep 17 00:00:00 2001 From: RenaudKeriven <95699554+RenaudKeriven@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:39:38 +0200 Subject: [PATCH 22/49] CD fix --- .../specifications/change_detection.py | 8 ++++---- python_sdk/tests/test_job_validator.py | 16 ++++++++++++---- .../src/specifications/change_detection.ts | 11 +++++++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index 7a3590d8..c9780cf9 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -35,10 +35,10 @@ class ChangeDetectionOutputs(BaseModel): class ChangeDetectionOutputsCreate(Enum): - SEGMENTATION_3D_A = "segmentation3DA" - SEGMENTED_MODEL_3D_A = "segmentedModel3DA" - SEGMENTATION_3D_B = "segmentation3DB" - SEGMENTED_MODEL_3D_B = "segmentedModel3DB" + SEGMENTATION3D_A = "segmentation3DA" + SEGMENTED_MODEL3D_A = "segmentedModel3DA" + SEGMENTATION3D_B = "segmentation3DB" + SEGMENTED_MODEL3D_B = "segmentedModel3DB" LOCATIONS3D_A = "locations3DA" LOCATIONS3D_A_AS_SHP = "locations3DAAsSHP" LOCATIONS3D_A_AS_GEOJSON = "locations3DAAsGeoJSON" diff --git a/python_sdk/tests/test_job_validator.py b/python_sdk/tests/test_job_validator.py index 88e60e98..9c5956d4 100644 --- a/python_sdk/tests/test_job_validator.py +++ b/python_sdk/tests/test_job_validator.py @@ -74,8 +74,12 @@ def test_validation_change_detection(self): "segmentedModel3DB": "rdid_seg_b", "segmentation3DA": "rdid_segmentation_a", "segmentation3DB": "rdid_segmentation_b", - "locations3DAsGeoJSON": "geojson_rdid", - "locations3DAsSHP": "shp_rdid" + "locations3DA": "loca_rdid", + "locations3DAAsGeoJSON": "geojsona_rdid", + "locations3DAAsSHP": "shpa_rdid", + "locations3DB": "locb_rdid", + "locations3DBAsGeoJSON": "geojsonb_rdid", + "locations3DBAsSHP": "shpb_rdid" } } job = Job(**j) @@ -85,8 +89,12 @@ def test_validation_change_detection(self): assert outputs.segmented_model_3d_b == "rdid_seg_b" assert outputs.segmentation_3d_a == "rdid_segmentation_a" assert outputs.segmentation_3d_b == "rdid_segmentation_b" - assert outputs.locations3d_as_geojson == "geojson_rdid" - assert outputs.locations3d_as_shp == "shp_rdid" + assert outputs.locations3d_a == "loca_rdid" + assert outputs.locations3d_a_as_geojson == "geojsona_rdid" + assert outputs.locations3d_a_as_shp == "shpa_rdid" + assert outputs.locations3d_b == "locb_rdid" + assert outputs.locations3d_b_as_geojson == "geojsonb_rdid" + assert outputs.locations3d_b_as_shp == "shpb_rdid" def test_validation_constraints(self): j = self.j_base.copy() diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index 3549ce82..d24ac455 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -13,16 +13,23 @@ export const ChangeDetectionInputsSchema = z.object({ extent: z.string().describe("Path in the bucket of the clipping polygon to apply").regex(/^bkt:.+/, { message: "Path in the bucket to the extent file must start with 'bkt:'", }).optional(), + preset: z.string().describe("Path in the bucket of a preset file to use").regex(/^bkt:.+/, { + message: "Path in the bucket to the preset file must start with 'bkt:'", + }).optional(), }); export type ChangeDetectionInputs = z.infer; export const ChangeDetectionOutputsSchema = z.object({ - locations3DAsSHP: z.string().optional().describe("Reality data id of 3D objects locations as SHP format"), - locations3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects locations as GeoJSON file"), segmentation3DA: z.string().optional().describe("ContextScene CD on Model3D A"), segmentedModel3DA: z.string().optional().describe("Model3D A with CD segmentation"), segmentation3DB: z.string().optional().describe("ContextScene CD on Model3D B"), segmentedModel3DB: z.string().optional().describe("Model3D B with CD segmentation"), + locations3DA: z.string().optional().describe("Reality data id of locations of changes A"), + locations3DAAsSHP: z.string().optional().describe("Reality data id of locations of changes A as SHP format"), + locations3DAAsGeoJSON: z.string().optional().describe("Reality data id of locations of changes A as GeoJSON file"), + locations3DB: z.string().optional().describe("Reality data id of locations of changes B"), + locations3DBAsSHP: z.string().optional().describe("Reality data id of locations of changes B as SHP format"), + locations3DBAsGeoJSON: z.string().optional().describe("Reality data id of locations of changes B as GeoJSON file"), }); export type ChangeDetectionOutputs = z.infer; From bbc2d24f875f380c32b657037e4611873413d787 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:44:59 +0200 Subject: [PATCH 23/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../packages/reality-capture/src/specifications/training.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/packages/reality-capture/src/specifications/training.ts b/typescript/packages/reality-capture/src/specifications/training.ts index 2dc7d00f..b43588ba 100644 --- a/typescript/packages/reality-capture/src/specifications/training.ts +++ b/typescript/packages/reality-capture/src/specifications/training.ts @@ -44,7 +44,7 @@ export const TrainingS3DOptionsSchema = z.object({ .optional(), versionNumber: z .string() - .regex(/\d+(.\d+)?/) + .regex(/^\d+(?:\.\d+)?$/, "Must be a version like '1' or '1.0'") .describe("String representing the version number for the newly trained detector.") .optional(), }); From 6e501df905d168443c6b2176fea5e270bd81fc57 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:45:22 +0200 Subject: [PATCH 24/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/src/reality_capture/specifications/training.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/specifications/training.py b/python_sdk/src/reality_capture/specifications/training.py index 8c62034d..2175f9c7 100644 --- a/python_sdk/src/reality_capture/specifications/training.py +++ b/python_sdk/src/reality_capture/specifications/training.py @@ -41,7 +41,7 @@ class TrainingS3DOptions(BaseModel): None, description="String representing the version number for the newly trained detector.", alias="versionNumber", - pattern=r"\d+(.\d+)?" + pattern=r"^\d+(?:\.\d+)?$" ) From 3b6d16ae4a24ca6a5a1c1397bf33a044a7ae50e1 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:49:47 +0200 Subject: [PATCH 25/49] Remove orig files --- .../reality_capture/service/detectors.py.orig | 114 --- .../service/detectors_LOCAL_4201.py | 91 --- .../reality_capture/service/service.py.orig | 772 ------------------ python_sdk/tests/test_detectors.py.orig | 344 -------- python_sdk/tests/test_job.py.orig | 50 -- 5 files changed, 1371 deletions(-) delete mode 100644 python_sdk/src/reality_capture/service/detectors.py.orig delete mode 100644 python_sdk/src/reality_capture/service/detectors_LOCAL_4201.py delete mode 100644 python_sdk/src/reality_capture/service/service.py.orig delete mode 100644 python_sdk/tests/test_detectors.py.orig delete mode 100644 python_sdk/tests/test_job.py.orig diff --git a/python_sdk/src/reality_capture/service/detectors.py.orig b/python_sdk/src/reality_capture/service/detectors.py.orig deleted file mode 100644 index aabdc5b7..00000000 --- a/python_sdk/src/reality_capture/service/detectors.py.orig +++ /dev/null @@ -1,114 +0,0 @@ -from enum import Enum -from typing import Optional -from pydantic import BaseModel, Field -from datetime import datetime - -<<<<<<< HEAD -from reality_capture.service.reality_data import URL -======= -from reality_capture.service.utils import Link - ->>>>>>> origin/main - -class DetectorExport(Enum): - OBJECTS = "Objects" - LINES = "Lines" - POLYGONS = "Polygons" - LOCATIONS = "Locations" - - -class DetectorType(Enum): - PHOTO_OBJECT_DETECTOR = "PhotoObjectDetector" - PHOTO_SEGMENTATION_DETECTOR = "PhotoSegmentationDetector" - ORTHOPHOTO_SEGMENTATION_DETECTOR = "OrthophotoSegmentationDetector" - POINT_CLOUD_SEGMENTATION_DETECTOR = "PointCloudSegmentationDetector" - - -class Capabilities(BaseModel): - labels: list[str] = Field(description="Labels of the detector version.") - exports: Optional[list[DetectorExport]] = Field(None, description="Exports of the detector version.") - - -class DetectorStatus(Enum): - AWAITING_DATA = "AwaitingData" - READY = "Ready" - - -class DetectorVersionCreationLinks(BaseModel): - complete_url: Link = Field(description="URL to mark the completion of the detector version creation process.", - alias="completeUrl") - upload_url: Link = Field(description="URL to upload the detector zip file.", alias="uploadUrl") - - -class DetectorVersionCreate(BaseModel): - version_number: str = Field(description="Version number.", alias="versionNumber") - capabilities: Capabilities = Field(description="Capabilities of the version.") - - -class DetectorVersion(DetectorVersionCreate): - creation_date: datetime = Field(description="Creation date of the version.", alias="creationDate") - status: DetectorStatus = Field(description="Status of the version.") - download_url: Optional[str] = Field(None, description="URL to download the detector version. " - "It is present only if the version status is 'Ready'.", - alias="downloadUrl") - creator_id: Optional[str] = Field(None, description="User Id of the version creator.", alias="creatorId") - - -class DetectorVersionWithLinks(BaseModel): - version: DetectorVersion = Field(description="Detector version details.") - links: DetectorVersionCreationLinks = Field(description="Contains the hyperlinks related to the detector version creation.", - alias="_links") - -class DetectorCreate(BaseModel): - name: str = Field(description="Name of the detector.") - display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="An optional description of the detector.") - type: DetectorType = Field(description="Type of the detector.") - documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") - -class DetectorUpdate(BaseModel): - display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="An optional description of the detector.") - documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") - -class DetectorVersionCreate(BaseModel): - version_number: str = Field(description="Version number string", alias="versionNumber") - capabilities: Capabilities = Field(description="Capabilities of the version.") - -class Links(BaseModel): - upload_url: URL = Field(description="URL to upload the detector zip file.", alias="uploadUrl") - complete_url: URL = Field(description="URL to mark the completion of the detector version creation process.", alias="completeUrl") - - -class DetectorVersionCreateResponseLinks(BaseModel): - version: DetectorVersion = Field(description="Created detector version") - links: Links = Field(description="Upload links for the detector version", alias="_links") - -class DetectorUpdate(BaseModel): - display_name: Optional[str] = Field(None, description="Display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="Description of the detector.") - documentation_url: Optional[str] = Field(None, description="Documentation URL for the detector.", - alias="documentationUrl") - - -class DetectorBase(DetectorUpdate): - name: str = Field(description="Name of the detector.") - type: DetectorType = Field(description="Type of the detector.") - - -class Detector(DetectorBase): - versions: list[DetectorVersion] = Field(description="All existing versions of the detector.") - - -class DetectorResponse(BaseModel): - detector: Detector = Field(description="Detector.") - - -class DetectorMinimal(DetectorBase): - latest_version: Optional[str] = Field(None, description="The latest version of the detector " - "with 'Ready' status, if any.", - alias="latestVersion") - - -class DetectorsMinimalResponse(BaseModel): - detectors: list[DetectorMinimal] = Field(description="List of minimal detectors.") diff --git a/python_sdk/src/reality_capture/service/detectors_LOCAL_4201.py b/python_sdk/src/reality_capture/service/detectors_LOCAL_4201.py deleted file mode 100644 index 1e91bd2e..00000000 --- a/python_sdk/src/reality_capture/service/detectors_LOCAL_4201.py +++ /dev/null @@ -1,91 +0,0 @@ -from enum import Enum -from typing import Optional -from pydantic import BaseModel, Field -from datetime import datetime - -from reality_capture.service.reality_data import URL - -class DetectorExport(Enum): - OBJECTS = "Objects" - LINES = "Lines" - POLYGONS = "Polygons" - LOCATIONS = "Locations" - - -class DetectorType(Enum): - PHOTO_OBJECT_DETECTOR = "PhotoObjectDetector" - PHOTO_SEGMENTATION_DETECTOR = "PhotoSegmentationDetector" - ORTHOPHOTO_SEGMENTATION_DETECTOR = "OrthophotoSegmentationDetector" - POINT_CLOUD_SEGMENTATION_DETECTOR = "PointCloudSegmentationDetector" - - -class Capabilities(BaseModel): - labels: list[str] = Field(description="Labels of the detector version.") - exports: Optional[list[DetectorExport]] = Field(None, description="Exports of the detector version.") - - -class DetectorStatus(Enum): - AWAITING_DATA = "AwaitingData" - READY = "Ready" - - -class DetectorVersion(BaseModel): - creation_date: datetime = Field(description="Creation date of the version.", alias="creationDate") - version_number: str = Field(description="Version number.", alias="versionNumber") - status: DetectorStatus = Field(description="Status of the version.") - download_url: Optional[str] = Field(None, description="URL to download the detector version. " - "It is present only if the version status is 'Ready'.", - alias="downloadUrl") - creator_id: Optional[str] = Field(None, description="User Id of the version creator.", alias="creatorId") - capabilities: Capabilities = Field(description="Capabilities of the version.") - -class DetectorCreate(BaseModel): - name: str = Field(description="Name of the detector.") - display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="An optional description of the detector.") - type: DetectorType = Field(description="Type of the detector.") - documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") - -class DetectorUpdate(BaseModel): - display_name: Optional[str] = Field(None, description="An optional display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="An optional description of the detector.") - documentation_url: Optional[str] = Field(None, description="An optional URL to the detector's documentation.", alias="documentationUrl") - -class DetectorVersionCreate(BaseModel): - version_number: str = Field(description="Version number string", alias="versionNumber") - capabilities: Capabilities = Field(description="Capabilities of the version.") - -class Links(BaseModel): - upload_url: URL = Field(description="URL to upload the detector zip file.", alias="uploadUrl") - complete_url: URL = Field(description="URL to mark the completion of the detector version creation process.", alias="completeUrl") - - -class DetectorVersionCreateResponseLinks(BaseModel): - version: DetectorVersion = Field(description="Created detector version") - links: Links = Field(description="Upload links for the detector version", alias="_links") - -class DetectorBase(BaseModel): - name: str = Field(description="Name of the detector.") - display_name: Optional[str] = Field(None, description="Display name of the detector.", alias="displayName") - description: Optional[str] = Field(None, description="Description of the detector.") - type: DetectorType = Field(description="Type of the detector.") - documentation_url: Optional[str] = Field(None, description="Display name of the detector.", - alias="documentationUrl") - - -class Detector(DetectorBase): - versions: list[DetectorVersion] = Field(description="All existing versions of the detector.") - - -class DetectorResponse(BaseModel): - detector: Detector = Field(description="Detector.") - - -class DetectorMinimal(DetectorBase): - latest_version: Optional[str] = Field(None, description="The latest version of the detector " - "with 'Ready' status, if any.", - alias="latestVersion") - - -class DetectorsMinimalResponse(BaseModel): - detectors: list[DetectorMinimal] = Field(description="List of minimal detectors.") diff --git a/python_sdk/src/reality_capture/service/service.py.orig b/python_sdk/src/reality_capture/service/service.py.orig deleted file mode 100644 index cff26c27..00000000 --- a/python_sdk/src/reality_capture/service/service.py.orig +++ /dev/null @@ -1,772 +0,0 @@ -import urllib.parse -import requests -import certifi - -from reality_capture.service.bucket import BucketResponse -<<<<<<< HEAD -from reality_capture.service.detectors import (DetectorsMinimalResponse, DetectorResponse, DetectorMinimal, - DetectorCreate, DetectorUpdate, DetectorVersionCreate, - DetectorVersionCreateResponseLinks) -======= -from reality_capture.service.detectors import (DetectorBase, DetectorsMinimalResponse, DetectorResponse, DetectorUpdate, - DetectorVersionCreate, DetectorVersionWithLinks) ->>>>>>> origin/main -from reality_capture.service.files import Files -from reality_capture.service.response import Response -from reality_capture.service.job import JobCreate, Job, Progress, Messages, Service, Jobs -from reality_capture.service.reality_data import (RealityDataCreate, RealityData, RealityDataUpdate, ContainerDetails, - RealityDataFilter, Prefer, RealityDatas) -from reality_capture.service.error import DetailedErrorResponse, DetailedError -from reality_capture import __version__ -from typing import Optional, Type -from pydantic import BaseModel, ValidationError -from urllib.parse import urlencode - - -class RealityCaptureService: - """ - Service handling communication with Reality Capture APIs - """ - - def __init__(self, token_factory, **kwargs) -> None: - """ - Constructor method - - :param token_factory: An object that implements a ``get_token() -> str`` method. - :type token_factory: Object - :param \**kwargs: See below. - - :Keyword Arguments: - * *user_agent* (``str``) -- - Additional user agent string - - """ - self._token_factory = token_factory - self._session = requests.Session() - self._session.verify = certifi.where() - - add_ua = "" - if "user_agent" in kwargs.keys() and len(kwargs["user_agent"]) > 0: - add_ua = " " + kwargs["user_agent"] - - self._header = { - "Authorization": None, - "User-Agent": f"Reality Capture Python SDK/{__version__}{add_ua}", - "Content-type": "application/json", - "Accept": "application/vnd.bentley.itwin-platform.v1+json", - } - - env = None - if "env" in kwargs.keys(): - env = kwargs["env"] - if env == "qa": - self._service_url = "https://qa-api.bentley.com/" - elif env == "dev": - self._service_url = "https://dev-api.bentley.com/" - else: - self._service_url = "https://api.bentley.com/" - - def _get_header(self, version) -> dict: - self._header["Authorization"] = self._token_factory.get_token() - self._header["Accept"] = f"application/vnd.bentley.itwin-platform.{version}+json" - return self._header - - def _get_header_v1(self) -> dict: - return self._get_header("v1") - - def _get_header_v2(self) -> dict: - return self._get_header("v2") - - def _get_reality_management_rd_url(self) -> str: - return self._service_url + "reality-management/reality-data/" - - def _get_modeling_url(self) -> str: - return self._service_url + "reality-modeling/" - - def _get_analysis_url(self) -> str: - return self._service_url + "reality-analysis/" - - def _get_correct_url(self, service: Service) -> str: - if service == Service.MODELING: - return self._get_modeling_url() - if service == Service.ANALYSIS: - return self._get_analysis_url() - raise NotImplementedError("Other services not yet implemented") - - @staticmethod - def _get_ill_formed_message(response, exception) -> str: - try: - r = response.json() - except requests.exceptions.JSONDecodeError: - r = response.text - return f"Service response is ill-formed: {r}. Exception : {exception}" - - def _execute_request(self, method: str, url: str, headers: dict, success_model: Type[BaseModel] = None, - data_key: str = None, **kwargs) -> Response: - try: - response = self._session.request(method, url, headers=headers, **kwargs) - response.raise_for_status() - except requests.exceptions.HTTPError as e: - try: - error_details = DetailedErrorResponse.model_validate(e.response.json()) - return Response(status_code=e.response.status_code, value=None, error=error_details) - except (ValidationError, KeyError, requests.exceptions.JSONDecodeError): - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(e.response, e)) - return Response(status_code=e.response.status_code, value=None, - error=DetailedErrorResponse(error=error)) - except requests.exceptions.RequestException as e: - error = DetailedError(code="NetworkError", message=f"Network error : {e}") - return Response(status_code=503, value=None, error=DetailedErrorResponse(error=error)) - - try: - if not success_model: - return Response(status_code=response.status_code, value=None, error=None) - json_data = response.json() - if data_key: - data_to_validate = json_data[data_key] - else: - data_to_validate = json_data - validated_data = success_model.model_validate(data_to_validate) - return Response(status_code=response.status_code, value=validated_data, error=None) - except (ValidationError, KeyError, requests.exceptions.JSONDecodeError) as e: - error = DetailedError(code="InvalidResponse", message=self._get_ill_formed_message(response, e)) - return Response(status_code=502, error=DetailedErrorResponse(error=error), value=None) - - def get_jobs(self, service: Service, filters: str, - top: int = None, continuation_token: str = "") -> Response[Jobs]: - """ - Get list of jobs from a specific service. - - :param service: Service to target - :param filters: The given filter is evaluated for each job and only job where the filter evaluates to true are returned. At least one filter criteria is required by the API. See `API documentation `_ to know more. - :param top: The number of jobs to get in each page. Min 2, max 1000. - :param continuation_token: Parameter that enables continuing to the next page of the previous paged query. This must be passed exactly as it is in the response body's _links.next property. - """ - try: - url = self._get_correct_url(service) + "jobs" - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not get jobs, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - params = {"$filter": filters} - if top is not None: - params["$top"] = max(min(top, 1000), 2) - if continuation_token: - params["continuationToken"] = continuation_token - - return self._execute_request(method="GET", url=url, headers=self._get_header_v2(), success_model=Jobs, - params=params) - - def submit_job(self, job: JobCreate) -> Response[Job]: - """ - Submit a job to the service. The job will be created and submitted at once. - - :param job: JobCreate information to use for the job. - :return: A Response[Job] containing either the Job created or the error from the service. - """ - try: - url = self._get_correct_url(job.get_appropriate_service()) + "jobs" - json_dump = job.model_dump_json(by_alias=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not submit job, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2(), success_model=Job, - data_key="job", data=json_dump) - - def get_job(self, job_id: str, service: Service) -> Response[Job]: - """ - Retrieve the complete Job details from the service using the job id. - - :param job_id: Id of the job to retrieve. - :param service: Service to target. - :return: A Response[Job] containing either the Job information or the error from the service. - """ - try: - url = self._get_correct_url(service) + "jobs/" + job_id - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not get job, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="GET", url=url, headers=self._get_header_v2(), - success_model=Job, data_key="job") - - def get_job_messages(self, job_id: str, service: Service) -> Response[Messages]: - """ - Retrieve the complete Job details from the service using the job id. - - :param job_id: Id of the job related to the messages to retrieve. - :param service: Service to target. - :return: A Response[Messages] containing either the messages for the job or the error from the service. - """ - try: - url = self._get_correct_url(service) + "jobs/" + job_id + "/messages" - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not get job messages, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="GET", url=url, headers=self._get_header_v2(), - success_model=Messages, data_key="messages") - - def get_job_progress(self, job_id: str, service: Service) -> Response[Progress]: - """ - Retrieve progress information from the service using the job id. - - :param job_id: Id of the job to monitor. - :param service: Service to target. - :return: A Response[Progress] containing either the job progress or the error from the service. - """ - - try: - url = self._get_correct_url(service) + "jobs/" + job_id + "/progress" - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not get job progress, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="GET", url=url, headers=self._get_header_v2(), - success_model=Progress, data_key="progress") - - def cancel_job(self, job_id: str, service: Service) -> Response[Job]: - """ - Cancel the job using the job id. Calling this method on a non-running job will yield an error. - - :param job_id: Id of the job to cancel. - :param service: Service to target. - :return: A Response[Job] containing either the job information or the error from the service. - """ - - try: - url = self._get_correct_url(service) + "jobs/" + job_id - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not cancel job, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="DELETE", url=url, headers=self._get_header_v2(), - success_model=Job, data_key="job") - - def get_bucket(self, itwin_id: str) -> Response[BucketResponse]: - """ - Retrieve a bucket information for a given iTwin - - :param itwin_id: iTwin id for finding the bucket - :return: A Response[BucketResponse] containing either the bucket information or the error from the service. - """ - - return self._execute_request(method="GET", - url=self._get_correct_url(Service.MODELING) + f"itwins/{itwin_id}/bucket", - headers=self._get_header_v2(), - success_model=BucketResponse) - - def get_service_files(self) -> Response[Files]: - """ - Retrieve the list of available files from the service. - - :return: A Response[Files] containing either the files information or the error from the service. - """ - - return self._execute_request(method="GET", url=self._get_correct_url(Service.MODELING) + f"files", - headers=self._get_header_v2(), - success_model=Files) - - def get_detectors(self, detectors_filter: Optional[str] = None) -> Response[DetectorsMinimalResponse]: - """ - Retrieve all available detectors. - - :param detectors_filter: The $filter query option requests a specific set of detectors. - Properties supported for filtering: labels, exports. - Supported operators: and, or, not, in. - Example: "exports in ('Polygons', 'Lines') and labels in ('crack')" - :return: A Response[DetectorsMinimalResponse] containing either the detector list or the error from the service. - """ - url = self._get_correct_url(Service.ANALYSIS) + "detectors" - params = {} - if detectors_filter: - params["$filter"] = detectors_filter - - encoded_params = urlencode(params) - if encoded_params: - url = f"{url}?{encoded_params}" - - return self._execute_request(method="GET", - url=url, - headers=self._get_header_v2(), - success_model=DetectorsMinimalResponse) - - def get_detector(self, detector_name: str) -> Response[DetectorResponse]: - """ - Retrieve details of a detector. - - :return: A Response[DetectorResponse] containing either the detector details or the error from the service. - """ - - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url = self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}" - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not get detector, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="GET", url=url, headers=self._get_header_v2(), - success_model=DetectorResponse) - -<<<<<<< HEAD - def create_detector(self, attributes: DetectorCreate) -> Response[DetectorMinimal]: - """ - Create a new detector. - - :param attributes: Attributes of the detector to create. - :return: A Response[DetectorMinimal] containing either the created detector or the error from the service. - """ - response = self._session.post( - self._get_correct_url(Service.ANALYSIS) + "detectors", - attributes.model_dump_json(by_alias=True, exclude_none=True), - headers=self._get_header_v2() - ) - try: - if response.ok: - payload: DetectorResponse = DetectorResponse.model_validate(response.json()) - return Response(status_code=response.status_code, value=payload.detector, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def create_detector_version(self, detector_name: str, - version_attributes: DetectorVersionCreate) -> Response[DetectorVersionCreateResponseLinks]: - """ - Create a new version for an existing detector. - - :param detector_name: Name of the detector to add a version to. - :param version_attributes: Attributes of the detector version to create. - :return: A Response[DetectorVersionCreateResponseLinks] containing either the created version with its upload - links or the error from the service. - """ - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.post( - self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}/versions", - version_attributes.model_dump_json(by_alias=True, exclude_none=True), - headers=self._get_header_v2()) - try: - if response.ok: - payload = DetectorVersionCreateResponseLinks.model_validate(response.json()) - return Response(status_code=response.status_code, value=payload, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def delete_detector(self, detector_name: str) -> Response[None]: - """ - Delete a detector and all of its versions. -======= - def create_detector(self, detector_create: DetectorBase) -> Response[DetectorResponse]: - """ - Create a detector. - - :param detector_create: DetectorBase information to create the detector. - :return: A Response[DetectorResponse] containing either the created detector details or the error from the service. - """ - try: - url = self._get_correct_url(Service.ANALYSIS) + f"detectors" - json_dump = detector_create.model_dump_json(by_alias=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not create detector, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2(), - success_model=DetectorResponse, data=json_dump) - - def update_detector(self, detector_name: str, detector_update: DetectorUpdate) -> Response[DetectorResponse]: - """ - Update a detector. - - :param detector_name: name of the detector. - :param detector_update: DetectorUpdate information to update the detector. - :return: A Response[DetectorResponse] containing either the updated detector details or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url = self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}" - json_dump = detector_update.model_dump_json(by_alias=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not update detector, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="PATCH", url=url, headers=self._get_header_v2(), - success_model=DetectorResponse, data=json_dump) - - def delete_detector(self, detector_name: str) -> Response[None]: - """ - Delete a detector. ->>>>>>> origin/main - - :param detector_name: Name of the detector to delete. - :return: A Response[None] containing either nothing if successful or the error from the service. - """ -<<<<<<< HEAD - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.delete(self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}", - headers=self._get_header_v2()) - try: - if response.ok: - return Response(status_code=response.status_code, value=None, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def delete_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Delete a specific version of a detector. - - :param detector_name: Name of the detector. - :param version_number: Version number to delete. - :return: A Response[None] containing either nothing if successful or the error from the service. - """ - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.delete( - self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}/versions/{version_number}", - headers=self._get_header_v2()) - try: - if response.ok: - return Response(status_code=response.status_code, value=None, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def modify_detector(self, detector_name: str, detector_attrs: DetectorUpdate) -> Response[None]: - """ - Modify the attributes of an existing detector. - - :param detector_name: Name of the detector to modify. - :param detector_attrs: Attributes to update on the detector. - :return: A Response[None] containing either nothing if successful or the error from the service. - """ - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.patch( - self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}", - detector_attrs.model_dump_json(by_alias=True, exclude_none=True), - headers=self._get_header_v2()) - try: - if response.ok: - return Response(status_code=response.status_code, value=None, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def publish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Publish a specific version of a detector. - - :param detector_name: Name of the detector. - :param version_number: Version number to publish. - :return: A Response[None] containing either nothing if successful or the error from the service. - """ - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.post( - self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}/versions/{version_number}/publish", - headers=self._get_header_v2()) - try: - if response.ok: - return Response(status_code=response.status_code, value=None, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) - - def unpublish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Unpublish a specific version of a detector. - - :param detector_name: Name of the detector. - :param version_number: Version number to unpublish. - :return: A Response[None] containing either nothing if successful or the error from the service. - """ - url_encoded_name = urllib.parse.quote(detector_name, safe="") - response = self._session.post( - self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}/versions/{version_number}/unpublish", - headers=self._get_header_v2()) - try: - if response.ok: - return Response(status_code=response.status_code, value=None, error=None) - return Response(status_code=response.status_code, - error=DetailedErrorResponse.model_validate(response.json()), value=None) - except (ValidationError, KeyError) as exception: - error = DetailedError(code="UnknownError", message=self._get_ill_formed_message(response, exception)) - return Response(status_code=response.status_code, - error=DetailedErrorResponse(error=error), value=None) -======= - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url = self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}" - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not delete detector, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="DELETE", url=url, headers=self._get_header_v2()) - - def create_detector_version(self, detector_name: str, - version_create: DetectorVersionCreate) -> Response[DetectorVersionWithLinks]: - """ - Create a new version for the specified detector. - - :param detector_name: Name of the detector. - :param version_create: DetectorVersionCreate information to create the version. - :return: A Response[DetectorVersionWithLinks] containing either the created version (and links) or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url = (self._get_correct_url(Service.ANALYSIS) + f"detectors/{url_encoded_name}/versions") - json_dump = version_create.model_dump_json(by_alias=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", - message=f"Could not create detector version, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2(), data=json_dump, - success_model=DetectorVersionWithLinks) - - def delete_detector_version(self, detector_name: str, detector_version: str) -> Response[None]: - """ - Delete the specified version of a detector. - - :param detector_name: Name of the detector. - :param detector_version: Version of the detector to delete. - :return: A Response[DetectorVersion] containing either nothing or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url_encoded_version = urllib.parse.quote(detector_version, safe="") - url = (self._get_correct_url(Service.ANALYSIS) - + f"detectors/{url_encoded_name}/versions/{url_encoded_version}") - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", - message=f"Could not delete detector version, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="DELETE", url=url, headers=self._get_header_v2()) - - def publish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Publish the specified detector version. - - :param detector_name: Name of the detector. - :param version_number: Name of the version. - :return: A Response[DetectorVersion] containing either nothing or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url_encoded_version = urllib.parse.quote(version_number, safe="") - url = (self._get_correct_url(Service.ANALYSIS) - + f"detectors/{url_encoded_name}/versions/{url_encoded_version}/publish") - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", - message=f"Could not publish detector version, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2()) - - def unpublish_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Unpublish the specified detector version. - - :param detector_name: Name of the detector. - :param version_number: Name of the version. - :return: A Response[DetectorVersion] containing either nothing or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url_encoded_version = urllib.parse.quote(version_number, safe="") - url = (self._get_correct_url(Service.ANALYSIS) - + f"detectors/{url_encoded_name}/versions/{url_encoded_version}/unpublish") - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", - message=f"Could not unpublish detector version, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2()) - - def complete_detector_version_upload(self, detector_name: str, version_number: str) -> Response[None]: - """ - Complete the upload of the specified detector version. - - :param detector_name: Name of the detector. - :param version_number: Name of the version. - :return: A Response[DetectorVersion] containing either nothing or the error from the service. - """ - try: - url_encoded_name = urllib.parse.quote(detector_name, safe="") - url_encoded_version = urllib.parse.quote(version_number, safe="") - url = (self._get_correct_url(Service.ANALYSIS) - + f"detectors/{url_encoded_name}/versions/{url_encoded_version}/complete") - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", - message=f"Could not complete the upload of the detector version, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=url, headers=self._get_header_v2()) ->>>>>>> origin/main - - def create_reality_data(self, reality_data: RealityDataCreate) -> Response[RealityData]: - """ - Create a new Reality Data. - - :param reality_data: Reality Data information to use. - :return: A Response[RealityData] containing either the reality data information or the error from the service. - """ - - try: - json_dump = reality_data.model_dump_json(by_alias=True, exclude_none=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not create reality data, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="POST", url=self._get_reality_management_rd_url(), - success_model=RealityData, data=json_dump, data_key="realityData", - headers=self._get_header_v1()) - - def get_reality_data(self, reality_data_id: str, itwin_id: Optional[str] = None) -> Response[RealityData]: - """ - Retrieve Reality Data information based on its id and possible iTwin id. - - :param reality_data_id: Id of the existing reality data. - :param itwin_id: Optional iTwin id for finding the reality data. - :return: A Response[RealityData] containing either the reality data information or the error from the service. - """ - - url = self._get_reality_management_rd_url() + reality_data_id - if itwin_id is not None: - url += "?iTwinId=" + itwin_id - return self._execute_request(method="GET", url=url, success_model=RealityData, data_key="realityData", - headers=self._get_header_v1()) - - def update_reality_data(self, reality_data_update: RealityDataUpdate, - reality_data_id: str) -> Response[RealityData]: - """ - Update Reality Data information with new information based on its. - - :param reality_data_update: Reality Data information to overwrite. - :param reality_data_id: Id of the existing reality data. - :return: A Response[RealityData] containing either the reality data information or the error from the service. - """ - - try: - json_dump = reality_data_update.model_dump_json(by_alias=True, exclude_none=True) - except (NotImplementedError, ValidationError) as e: - detailed_error = DetailedError(code="UnknownError", message=f"Could not update reality data, bad request : " - f"{e}") - return Response(status_code=400, value=None, error=DetailedErrorResponse(error=detailed_error)) - - return self._execute_request(method="PATCH", url=self._get_reality_management_rd_url() + reality_data_id, - success_model=RealityData, data=json_dump, data_key="realityData", - headers=self._get_header_v1()) - - def delete_reality_data(self, reality_data_id: str) -> Response[None]: - """ - Delete Reality Data and its associated content based on its id. - - :param reality_data_id: Id of the existing reality data. - :return: A Response[RealityData] containing either nothing if successful or the error from the service. - """ - - return self._execute_request(method="DELETE", url=self._get_reality_management_rd_url() + reality_data_id, - headers=self._get_header_v1()) - - def get_reality_data_write_access(self, reality_data_id: str, - itwin_id: Optional[str] = None) -> Response[ContainerDetails]: - """ - Get write access to a specific Reality Data. - - :param reality_data_id: Id of the existing reality data. - :param itwin_id: Optional iTwin id for finding the reality data. - :return: A Response[ContainerDetails] containing either the container details or the error from the service. - """ - - url = self._get_reality_management_rd_url() + reality_data_id + "/writeaccess" - if itwin_id is not None: - url += "?iTwinId=" + itwin_id - - return self._execute_request(method="GET", url=url, headers=self._get_header_v1(), - success_model=ContainerDetails) - - def get_reality_data_read_access(self, reality_data_id: str, - itwin_id: Optional[str] = None) -> Response[ContainerDetails]: - """ - Get read access to a specific Reality Data. - - :param reality_data_id: Id of the existing reality data. - :param itwin_id: Optional iTwin id for finding the reality data. - :return: A Response[ContainerDetails] containing either the container details or the error from the service. - """ - - url = self._get_reality_management_rd_url() + reality_data_id + "/readaccess" - if itwin_id is not None: - url += "?iTwinId=" + itwin_id - - return self._execute_request(method="GET", url=url, headers=self._get_header_v1(), - success_model=ContainerDetails) - - def list_reality_data(self, reality_data_filter: Optional[RealityDataFilter] = None, - prefer: Optional[Prefer] = None) -> Response[RealityDatas]: - """ - List reality data that you can access with optional filtering options. - - :param reality_data_filter: Optional filtering information. - :param prefer: Preferred representation of Reality Data in the response. - :return: A Response[ContainerDetails] containing either a list of reality data or the error from the service. - """ - url = self._get_reality_management_rd_url() - if reality_data_filter is not None: - params = reality_data_filter.as_dict_for_service_call() - encoded_params = urlencode(params) - url = f"{url}?{encoded_params}" - header = self._get_header_v1() - header["Prefer"] = "return=minimal" - if prefer == Prefer.REPRESENTATION: - header["Prefer"] = "return=representation" - - return self._execute_request(method="GET", url=url, headers=header, success_model=RealityDatas) - - def move_reality_data(self, reality_data_id: str, itwin_id: str) -> Response[None]: - """ - Move a RealityData to a different iTwin. - - :param reality_data_id The id of the RealityData to move. - :param itwin_id The id of the iTwin to move the RealityData to. - :return: A Response[bool] containing either true if successful or false if not. - """ - - return self._execute_request(method="PATCH", - url=self._get_reality_management_rd_url() + reality_data_id + "/move", - headers=self._get_header_v1(), success_model=None, - json={"iTwinId": itwin_id}) diff --git a/python_sdk/tests/test_detectors.py.orig b/python_sdk/tests/test_detectors.py.orig deleted file mode 100644 index 13dcb4da..00000000 --- a/python_sdk/tests/test_detectors.py.orig +++ /dev/null @@ -1,344 +0,0 @@ -import responses -import json -import os -<<<<<<< HEAD -======= -from unittest.mock import patch ->>>>>>> origin/main - -from reality_capture.service.detectors import (DetectorBase, DetectorType, DetectorVersionCreate, Capabilities, - DetectorUpdate) -from reality_capture.service.service import RealityCaptureService - - -class FakeTokenFactory: - @staticmethod - def get_token() -> str: - return "Bearer invalid" - - -class TestServiceDetector: - # Utils - def setup_method(self, _): - self.ftf = FakeTokenFactory() - self.rcs = RealityCaptureService(self.ftf) - cf = os.path.dirname(os.path.abspath(__file__)) - self.data_folder = os.path.join(cf, "data") - - def teardown_method(self, test_method): - pass - - @responses.activate - def test_get_detector_ill_formed(self): - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"bad": "response"}, - status=400) - response = self.rcs.get_detector("mydetector") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_get_detector_401(self): - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.get_detector("mydetector") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_get_detector_200(self): - with open(f"{self.data_folder}/detector_get_200.json", 'r') as payload_data: - payload = json.load(payload_data) - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors/%40bentley%2Fbentley-city-a-s3d', - json=payload, status=200) - response = self.rcs.get_detector("@bentley/bentley-city-a-s3d") - assert not response.is_error() - assert response.value.detector.name == "@bentley/bentley-city-a-s3d" - - def test_get_detector_unsupported_service(self): - with patch.object(self.rcs, "_get_correct_url", side_effect=NotImplementedError("unsupported")): - response = self.rcs.get_detector("mydetector") - assert response.is_error() - assert response.status_code == 400 - assert "Could not get detector" in response.error.error.message - - @responses.activate - def test_get_detectors_ill_formed(self): - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors', - json={"bad": "response"}, - status=400) - response = self.rcs.get_detectors() - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_get_detectors_401(self): - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.get_detectors() - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_get_detectors_200(self): - with open(f"{self.data_folder}/detectors_get_200.json", 'r') as payload_data: - payload = json.load(payload_data) - responses.add(responses.GET, f'https://api.bentley.com/reality-analysis/detectors', - json=payload, status=200) - response = self.rcs.get_detectors() - assert not response.is_error() - assert len(response.value.detectors) == 2 - - @responses.activate - def test_get_detectors_with_filter_200(self): - with open(f"{self.data_folder}/detectors_get_200.json", 'r') as payload_data: - payload = json.load(payload_data) - responses.add( - responses.GET, - "https://api.bentley.com/reality-analysis/detectors?$filter=exports+in+%28%27Polygons%27%29", - json=payload, - status=200, - ) - response = self.rcs.get_detectors("exports in ('Polygons')") - assert not response.is_error() - assert len(response.value.detectors) == 2 - assert responses.calls[0].request.url == \ - "https://api.bentley.com/reality-analysis/detectors?%24filter=exports+in+%28%27Polygons%27%29" - - @responses.activate - def test_delete_detector_ill_formed(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"bad": "response"}, - status=400) - response = self.rcs.delete_detector("mydetector") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_delete_detector_401(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.delete_detector("mydetector") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_delete_detector_204(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/%40bentley%2Fbentley-city-a-s3d', - status=204) - response = self.rcs.delete_detector("@bentley/bentley-city-a-s3d") - assert not response.is_error() - assert response.value is None - - @responses.activate - def test_delete_detector_version_ill_formed(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0', - json={"bad": "response"}, - status=400) - response = self.rcs.delete_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_delete_detector_version_401(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.delete_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_delete_detector_version_204(self): - responses.add(responses.DELETE, f'https://api.bentley.com/reality-analysis/detectors/%40bentley%2Fbentley-city-a-s3d/versions/1.0', - status=204) - response = self.rcs.delete_detector_version("@bentley/bentley-city-a-s3d", "1.0") - assert not response.is_error() - assert response.value is None - - @responses.activate - def test_create_detector_ill_formed(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors', - json={"bad": "response"}, - status=400) - db = DetectorBase(name="mydetector", type=DetectorType.PHOTO_OBJECT_DETECTOR) - response = self.rcs.create_detector(db) - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_create_detector_401(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - db = DetectorBase(name="mydetector", type=DetectorType.PHOTO_OBJECT_DETECTOR) - response = self.rcs.create_detector(db) - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_create_detector_201(self): - with open(f"{self.data_folder}/detector_create_201.json", 'r') as payload_data: - payload = json.load(payload_data) - db = DetectorBase(name="mydetector", type=DetectorType.PHOTO_OBJECT_DETECTOR) - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors', - status=201, json=payload) - response = self.rcs.create_detector(db) - assert not response.is_error() - assert response.value.detector.name == "mydetector" - - @responses.activate - def test_create_detector_version_ill_formed(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions', - json={"bad": "response"}, - status=400) - dvc = DetectorVersionCreate(versionNumber="1.0", - capabilities=Capabilities(labels=["signs"], exports=["Objects"])) - response = self.rcs.create_detector_version("mydetector", dvc) - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_create_detector_version_401(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - dvc = DetectorVersionCreate(versionNumber="1.0", - capabilities=Capabilities(labels=["signs"], exports=["Objects"])) - response = self.rcs.create_detector_version("mydetector", dvc) - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_create_detector_version_201(self): - with open(f"{self.data_folder}/detector_create_version_201.json", 'r') as payload_data: - payload = json.load(payload_data) - dvc = DetectorVersionCreate(versionNumber="1.0", - capabilities=Capabilities(labels=["signs"], exports=["Objects"])) - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions', - status=201, json=payload) - response = self.rcs.create_detector_version("mydetector", dvc) - assert not response.is_error() - assert response.value.version.version_number == "1.0" - - @responses.activate - def test_complete_detector_version_ill_formed(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/complete', - json={"bad": "response"}, - status=400) - response = self.rcs.complete_detector_version_upload("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_complete_detector_version_401(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/complete', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.complete_detector_version_upload("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_complete_detector_version_204(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/complete', - status=204) - response = self.rcs.complete_detector_version_upload("mydetector", "1.0") - assert not response.is_error() - assert response.value is None - - @responses.activate - def test_update_detector_ill_formed(self): - responses.add(responses.PATCH, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"bad": "response"}, - status=400) - du = DetectorUpdate(displayName="mydetector2", description="desc", documentationUrl="https://www.bentley.com") - response = self.rcs.update_detector("mydetector", du) - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_update_detector_401(self): - responses.add(responses.PATCH, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - du = DetectorUpdate(displayName="My detector", description="desc", documentationUrl="https://www.bentley.com") - response = self.rcs.update_detector("mydetector", du) - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_update_detector_200(self): - with open(f"{self.data_folder}/detector_update_200.json", 'r') as payload_data: - payload = json.load(payload_data) - du = DetectorUpdate(displayName="mydetector2", description="desc", documentationUrl="https://www.bentley.com") - responses.add(responses.PATCH, f'https://api.bentley.com/reality-analysis/detectors/mydetector', - status=200, json=payload) - response = self.rcs.update_detector("mydetector", du) - assert not response.is_error() - assert response.value.detector.name == "mydetector2" - - @responses.activate - def test_publish_detector_ill_formed(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/publish', - json={"bad": "response"}, - status=400) - response = self.rcs.publish_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_publish_detector_401(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/publish', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.publish_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_publish_detector_204(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/publish', - status=204) - response = self.rcs.publish_detector_version("mydetector", "1.0") - assert not response.is_error() - assert response.value is None - - @responses.activate - def test_unpublish_detector_ill_formed(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/unpublish', - json={"bad": "response"}, - status=400) - response = self.rcs.unpublish_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "UnknownError" - - @responses.activate - def test_unpublish_detector_401(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/unpublish', - json={"error": {"code": "HeaderNotFound", - "message": "Header Authorization was not found in the request. Access denied."}}, - status=401) - response = self.rcs.unpublish_detector_version("mydetector", "1.0") - assert response.is_error() - assert response.error.error.code == "HeaderNotFound" - - @responses.activate - def test_unpublish_detector_204(self): - responses.add(responses.POST, f'https://api.bentley.com/reality-analysis/detectors/mydetector/versions/1.0/unpublish', - status=204) - response = self.rcs.unpublish_detector_version("mydetector", "1.0") - assert not response.is_error() - assert response.value is None diff --git a/python_sdk/tests/test_job.py.orig b/python_sdk/tests/test_job.py.orig deleted file mode 100644 index 53590ba5..00000000 --- a/python_sdk/tests/test_job.py.orig +++ /dev/null @@ -1,50 +0,0 @@ -import datetime -<<<<<<< HEAD -from reality_capture.service.job import Service, JobCreate, Job, JobType, JobState -======= -import pytest -from unittest.mock import MagicMock -from reality_capture.service.job import Service, JobCreate, Job, JobType, JobState, _get_appropriate_service -from reality_capture.specifications.eval_o2d import EvalO2DSpecifications ->>>>>>> origin/main -from reality_capture.specifications.tiling import TilingOutputsCreate -from reality_capture.specifications.segmentation3d import Segmentation3DOutputsCreate - -# from reality_capture.specifications.point_cloud_conversion import PCConversionOutputsCreate - - -class TestJob: - def test_appropriate_service_job(self): - cdt = datetime.datetime(1974, 9, 1, 0, 0, 0) - cdt = {"createdDateTime": cdt, "startedDateTime": None, "endedDateTime": None, "estimatedUnits": None} - tiling_specs = {"inputs": {"scene": "scene"}, "outputs": {"modelingReference": {"location": "location"}}} - j = Job(id="id", type=JobType.TILING, iTwinId="itwin", state=JobState.SUCCESS, executionInfo=cdt, - userId="claude@example.org", specifications=tiling_specs) - assert j.get_appropriate_service() == Service.MODELING - specs = {"inputs": {"reference": "ref", "prediction": "pred"}, "outputs": {"objects2D": "objects2d"}} - j = Job(id="id", type=JobType.EVAL_O2D, iTwinId="itwin", state=JobState.SUCCESS, executionInfo=cdt, - userId="claude@example.org", specifications=specs) - assert j.get_appropriate_service() == Service.ANALYSIS - # pc_conversion_specs = {"inputs": {"pointClouds": ["point_cloud"]}, "outputs": {"opc": "opc"}} - # j = Job(id="id", type=JobType.POINT_CLOUD_CONVERSION, iTwinId="itwin", state=JobState.SUCCESS, - # executionInfo=cdt, userId="claude@example.org", specifications=pc_conversion_specs, bucketId="bucket") - # assert j.get_appropriate_service() == Service.CONVERSION - - def test_appropriate_service_job_create(self): - tiling_specs = {"inputs": {"scene": "scene"}, "outputs": [TilingOutputsCreate.MODELING_REFERENCE]} - j = JobCreate(**{"type": JobType.TILING, "iTwinId": "itwin", "specifications": tiling_specs}) - assert j.get_appropriate_service() == Service.MODELING - eg_specs = {"inputs": {"model3D": "pointClouds"}, - "outputs": [Segmentation3DOutputsCreate.SEGMENTATION3D, - Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D]} - j = JobCreate(**{"type": JobType.SEGMENTATION_3D, "iTwinId": "itwin", "specifications": eg_specs}) - # assert j.get_appropriate_service() == Service.ANALYSIS - # pc_conversion_specs = {"inputs": {"pointClouds": ["point_cloud"]}, "outputs": PCConversionOutputsCreate.OPC} - # j = JobCreate(**{"type": JobType.POINT_CLOUD_CONVERSION, "iTwinId": "itwin", "specifications": pc_conversion_specs}) - # assert j.get_appropriate_service() == Service.CONVERSION - - def test_get_appropriate_service_unsupported_job_type(self): - unsupported = MagicMock(name="UnsupportedJobType") - with pytest.raises(NotImplementedError): - _get_appropriate_service(unsupported) - From 95598e9ef39bfeb65fb83724efe6ef54d1a5602d Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:55:07 +0200 Subject: [PATCH 26/49] Fix data_handler.py --- python_sdk/src/reality_capture/service/detectors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/service/detectors.py b/python_sdk/src/reality_capture/service/detectors.py index bb3f3b33..d30c2769 100644 --- a/python_sdk/src/reality_capture/service/detectors.py +++ b/python_sdk/src/reality_capture/service/detectors.py @@ -11,7 +11,8 @@ class DetectorExport(Enum): LINES = "Lines" POLYGONS = "Polygons" LOCATIONS = "Locations" - + + class DetectorType(Enum): PHOTO_OBJECT_DETECTOR = "PhotoObjectDetector" PHOTO_SEGMENTATION_DETECTOR = "PhotoSegmentationDetector" From 688011d5f02f39e138930f034a304e24fbfcbfbc Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:57:58 +0200 Subject: [PATCH 27/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/docs/specifications/segmentation3d.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/python_sdk/docs/specifications/segmentation3d.rst b/python_sdk/docs/specifications/segmentation3d.rst index 5925f0bc..e30bd48c 100644 --- a/python_sdk/docs/specifications/segmentation3d.rst +++ b/python_sdk/docs/specifications/segmentation3d.rst @@ -29,7 +29,6 @@ This job has four different purposes : - | *segmentation3d*, | *segmented_model_3d* - | *save_confidence*, - | *crs* * - | Segment a collection | of meshes - | *meshes*, From 11497c089b8bf6c6c575d6f11de6025fcf8f8408 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:58:14 +0200 Subject: [PATCH 28/49] Remove crs from s3d doc Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/docs/specifications/segmentation3d.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/python_sdk/docs/specifications/segmentation3d.rst b/python_sdk/docs/specifications/segmentation3d.rst index e30bd48c..255b8ef0 100644 --- a/python_sdk/docs/specifications/segmentation3d.rst +++ b/python_sdk/docs/specifications/segmentation3d.rst @@ -37,7 +37,6 @@ This job has four different purposes : - | *segmentation3d*, | *segmented_model_3d* - | *save_confidence*, - | *crs* * - | Segment a collection | of point clouds or meshes | and infer 3D objects, From 102f631d97462b9ac1e9ea9ebeb27bd7c0f68df9 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:28:37 +0200 Subject: [PATCH 29/49] Fix data_handler.py --- .../reality_capture/service/data_handler.py | 44 +++++++++---------- .../src/reality_capture/service/detectors.py | 1 - 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/python_sdk/src/reality_capture/service/data_handler.py b/python_sdk/src/reality_capture/service/data_handler.py index a0baeb15..2c87cca4 100644 --- a/python_sdk/src/reality_capture/service/data_handler.py +++ b/python_sdk/src/reality_capture/service/data_handler.py @@ -1,16 +1,15 @@ import os.path import io -import urllib.parse from typing import Callable, Optional import requests -import zipfile from reality_capture.service.error import DetailedErrorResponse, DetailedError, Error from reality_capture.service.response import Response -from reality_capture.service.service import DetectorMinimal, RealityCaptureService +from reality_capture.service.service import RealityCaptureService from reality_capture.service.reality_data import RealityDataUpdate, RealityData, ContainerDetails from reality_capture.service.bucket import BucketResponse from azure.storage.blob import ContainerClient -from reality_capture.service.detectors import Detector, DetectorCreate, DetectorVersion, DetectorType +from reality_capture.service.detectors import (DetectorBase, DetectorVersion, DetectorType, DetectorResponse, + DetectorsMinimalResponse) from multiprocessing.pool import ThreadPool @@ -350,6 +349,7 @@ def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: """ self._progress_hook = hook + class DetectorDataHandler: """ Class for interacting with detectors @@ -366,7 +366,7 @@ def __init__(self, token_factory, **kwargs) -> None: self._service = RealityCaptureService(token_factory, **kwargs) self._progress_hook = None - def get_detector(self, detector_name: str) -> Response[Detector]: + def get_detector(self, detector_name: str) -> Response[DetectorResponse]: return self._service.get_detector(detector_name) def get_specific_detector_version(self, detector_name: str, version_number: str) -> Response[DetectorVersion]: @@ -379,33 +379,29 @@ def get_specific_detector_version(self, detector_name: str, version_number: str) return Response(r.status_code, r.error, detector) - def create_detector(self, - name: str, type: DetectorType, - display_name: Optional[str] = None, - description: Optional[str] = None, - documentation_url: Optional[str] = None - ) -> Response[DetectorMinimal]: - - detector_attributes = DetectorCreate( - name = name, - type = type, - displayName = display_name, - description = description, - documentationUrl = documentation_url + def create_detector(self, name: str, detector_type: DetectorType, display_name: Optional[str] = None, + description: Optional[str] = None, documentation_url: Optional[str] = None + ) -> Response[DetectorResponse]: + + detector_attributes = DetectorBase( + name=name, + type=detector_type, + displayName=display_name, + description=description, + documentationUrl=documentation_url ) return self._service.create_detector(detector_attributes) - def list_available_detectors(self) -> Response[list[DetectorMinimal]]: + def list_available_detectors(self) -> Response[DetectorsMinimalResponse]: return self._service.get_detectors() def download_detector_archive(self, detector_name: str, version_number: str) -> Response[io.BytesIO]: """ Download detector archive from a detector version. - :param dst: Destination path of the downloads. - :param detector_src: Source folder to download in the detector, default to root. - :return: A Response[io.BytesIO] containing the error from the service if any. + :param detector_name: Name of the detector. + :param version_number: Version of the detector. """ r = self.get_specific_detector_version(detector_name, version_number) @@ -433,8 +429,8 @@ def delete_detector_version(self, detector_name: str, version_number: str) -> Re """ Delete a specific version of a detector - :param itwin_id: iTwin id for finding the detector. - :param files_to_delete: List of files to delete. + :param detector_name: Name of the detector. + :param version_number: Version of the detector. :return: A Response[None] containing either the files in the detector or the error from the service. """ return self._service.delete_detector_version(detector_name, version_number) diff --git a/python_sdk/src/reality_capture/service/detectors.py b/python_sdk/src/reality_capture/service/detectors.py index d30c2769..e3cbc094 100644 --- a/python_sdk/src/reality_capture/service/detectors.py +++ b/python_sdk/src/reality_capture/service/detectors.py @@ -56,7 +56,6 @@ class DetectorVersionWithLinks(BaseModel): alias="_links") - class DetectorUpdate(BaseModel): display_name: Optional[str] = Field(None, description="Display name of the detector.", alias="displayName") description: Optional[str] = Field(None, description="Description of the detector.") From c97590e1bcc42bf42c044aa4a16f64b86e1446e5 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:53:07 +0200 Subject: [PATCH 30/49] Fix change detection tests --- .../test_change_detection.test.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts index 9dad2837..e6388c8c 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts @@ -13,19 +13,19 @@ describe("change_detection specifications", () => { describe("ChangeDetectionInputsSchema", () => { it("should validate valid inputs", () => { const valid = { - model3dA: "contextSceneId1", - model3dB: "contextSceneId2", + model3DA: "contextSceneId1", + model3DB: "contextSceneId2", }; expect(() => ChangeDetectionInputsSchema.parse(valid)).not.to.throw(); }); it("should fail when missing reference", () => { - const invalid = { model3dB: "contextSceneId2" }; + const invalid = { model3DB: "contextSceneId2" }; expect(() => ChangeDetectionInputsSchema.parse(invalid)).to.throw(z.ZodError); }); it("should fail when missing toCompare", () => { - const invalid = { model3dA: "contextSceneId1" }; + const invalid = { model3DA: "contextSceneId1" }; expect(() => ChangeDetectionInputsSchema.parse(invalid)).to.throw(z.ZodError); }); }); @@ -50,11 +50,10 @@ describe("change_detection specifications", () => { describe("ChangeDetectionOptionsSchema", () => { it("should validate all options", () => { const valid = { - outputCrs: "EPSG:4326", - minPointsPerChange: 10, + minPointsPerChange: 100, samplingResolution: 0.5, - threshold: 0.2, - filterThreshold: 0.1, + growThreshold: 0.5, + filterThreshold: 0.2, }; expect(() => ChangeDetectionOptionsSchema.parse(valid)).not.to.throw(); }); @@ -73,7 +72,7 @@ describe("change_detection specifications", () => { describe("ChangeDetectionSpecificationsCreateSchema", () => { it("should validate valid specification", () => { const valid = { - inputs: { model3dA: "id1", model3dB: "id2" }, + inputs: { model3DA: "id1", model3DB: "id2" }, outputs: [ChangeDetectionOutputsCreate.MODEL_3D_A_CLASSIFIED, ChangeDetectionOutputsCreate.MODEL_3D_B_CLASSIFIED], options: { minPointsPerChange: 10 }, }; @@ -82,7 +81,7 @@ describe("change_detection specifications", () => { it("should fail if outputs contains invalid value", () => { const invalid = { - inputs: { model3dA: "id1", model3dB: "id2" }, + inputs: { model3DA: "id1", model3DB: "id2" }, outputs: ["notValidValue"], }; expect(() => ChangeDetectionSpecificationsCreateSchema.parse(invalid)).to.throw(z.ZodError); @@ -93,7 +92,7 @@ describe("change_detection specifications", () => { describe("ChangeDetectionSpecificationsSchema", () => { it("should validate valid specification", () => { const valid = { - inputs: { model3dA: "id1", model3dB: "id2" }, + inputs: { model3DA: "id1", model3DB: "id2" }, outputs: { locations3DAsSHP: "shpId", }, @@ -104,7 +103,7 @@ describe("change_detection specifications", () => { it("should fail if outputs is missing", () => { const invalid = { - inputs: { model3dA: "id1", model3dB: "id2" }, + inputs: { model3DA: "id1", model3DB: "id2" }, }; expect(() => ChangeDetectionSpecificationsSchema.parse(invalid)).to.throw(z.ZodError); }); From aceb2fe05f6b95c53949640e3327b78d876b74db Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:11:44 +0200 Subject: [PATCH 31/49] Fix change detection tests --- .../specifications/test_change_detection.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts index e6388c8c..f4386ffc 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts @@ -33,10 +33,16 @@ describe("change_detection specifications", () => { describe("ChangeDetectionOutputsSchema", () => { it("should validate with all outputs", () => { const valid = { - locations3DAsSHP: "shpId", - locations3DAsGeoJSON: "geojsonId", - added: "addedId", - removed: "removedId", + segmentation3DA: "segmentationAId", + segmentedModel3DA: "segmentedModelAId", + segmentation3DB: "segmentationBId", + segmentedModel3DB: "segmentedModelBId", + locations3DA: "locationsAId", + locations3DB: "locationsBId", + locations3DAAsSHP: "shpAId", + locations3DAAsGeoJSON: "geojsonAId", + locations3DBAsSHP: "shpBId", + locations3DBAsGeoJSON: "geojsonBId", }; expect(() => ChangeDetectionOutputsSchema.parse(valid)).not.to.throw(); }); @@ -94,7 +100,7 @@ describe("change_detection specifications", () => { const valid = { inputs: { model3DA: "id1", model3DB: "id2" }, outputs: { - locations3DAsSHP: "shpId", + locations3DAAsSHP: "shpId", }, options: { samplingResolution: 0.5 }, }; From ba16bdcfcd1b3ed9110d8a2c57261fcff1b439b1 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:21:12 +0200 Subject: [PATCH 32/49] Fix typescript change detection example Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../examples/cd_specs_detect_changes_meshes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py index f2f71a66..de4d75ab 100644 --- a/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py +++ b/python_sdk/docs/specifications/examples/cd_specs_detect_changes_meshes.py @@ -2,6 +2,9 @@ cd_inputs = change_detection.ChangeDetectionInputs(model3DA="279db84b-090f-4922-b9a5-7a4fd0a71fcd", model3DB="40af080d-7ata-48c8-974c-610820fe90f2") -cd_outputs = [change_detection.ChangeDetectionOutputsCreate.LOCATIONS3D_AS_GEOJSON] +cd_outputs = [ + change_detection.ChangeDetectionOutputsCreate.LOCATIONS3D_A_AS_GEOJSON, + change_detection.ChangeDetectionOutputsCreate.LOCATIONS3D_B_AS_GEOJSON, +] cd_options = change_detection.ChangeDetectionOptions() cds = change_detection.ChangeDetectionSpecificationsCreate(inputs=cd_inputs, outputs=cd_outputs, options=cd_options) From c0c984203d8686694898707c2084c7b608203df6 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:28:24 +0200 Subject: [PATCH 33/49] Fix misspelled TrainingS3D in job.ts Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- typescript/packages/reality-capture/src/service/job.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/packages/reality-capture/src/service/job.ts b/typescript/packages/reality-capture/src/service/job.ts index 75e06079..2a1cd432 100644 --- a/typescript/packages/reality-capture/src/service/job.ts +++ b/typescript/packages/reality-capture/src/service/job.ts @@ -233,7 +233,7 @@ export const JobSchema = z.discriminatedUnion("type", [ }), z.object({ ...CommonFields, - type: z.literal("TraningS3D"), + type: z.literal("TrainingS3D"), specifications: TrainingS3DSpecificationsSchema, }), z.object({ From 23ba1ee10eada87731e9b2ce047724310a88d541 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:34:01 +0200 Subject: [PATCH 34/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/src/reality_capture/service/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/src/reality_capture/service/response.py b/python_sdk/src/reality_capture/service/response.py index 0b768802..2a46eb14 100644 --- a/python_sdk/src/reality_capture/service/response.py +++ b/python_sdk/src/reality_capture/service/response.py @@ -8,7 +8,7 @@ @dataclass class Response(Generic[T]): """ - A tuple containing a Service response. + A service response container. """ status_code: int From 168389f47b0f4e63a7aa9d2e3aeab8c35a8b5ee2 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:34:59 +0200 Subject: [PATCH 35/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/docs/specifications/training_s3d.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python_sdk/docs/specifications/training_s3d.rst b/python_sdk/docs/specifications/training_s3d.rst index 897c8879..b9452378 100644 --- a/python_sdk/docs/specifications/training_s3d.rst +++ b/python_sdk/docs/specifications/training_s3d.rst @@ -23,11 +23,13 @@ Purpose - Possible outputs - Useful options * - Train new detector on a dataset (ContextScene) - - | *scene*, + - | *segmentations3D*, + | *detectorName*, + | *preset* (optional) - | *detector*, - | *epochs* - | *max_train_split* - + | *spacing* + | *versionNumber* Examples ======== From b7edf70035c4b74e5455dd58ead40f8eb99ab843 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:14:34 +0200 Subject: [PATCH 36/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- python_sdk/docs/specifications/training_s3d.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/docs/specifications/training_s3d.rst b/python_sdk/docs/specifications/training_s3d.rst index b9452378..810f0704 100644 --- a/python_sdk/docs/specifications/training_s3d.rst +++ b/python_sdk/docs/specifications/training_s3d.rst @@ -36,7 +36,7 @@ Examples In this example, we will create a specification for submitting a Training S3D job to produce a new S3D detector from a ContextScene. -.. literalinclude:: examples/training_s3d_new_default.py +.. literalinclude:: examples/training_s3d_default.py :language: Python Classes From 9fe540904a86cdf7580c8ece34fb9c01e0dddc26 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:27:54 +0200 Subject: [PATCH 37/49] Fix preset in change detection. Will be released later. --- .../src/reality_capture/specifications/change_detection.py | 3 +++ .../reality-capture/src/specifications/change_detection.ts | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index c9780cf9..cf930f13 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -8,6 +8,9 @@ class ChangeDetectionInputs(BaseModel): model_3d_b: str = Field(alias="model3DB", description="Reality data id of ContextScene, point cloud, Gaussian splats, or mesh") extent: Optional[str] = Field(None, alias="extent", pattern=r"^bkt:.+", description="Path in the bucket of the clipping polygon to apply") + # Add preset in adifferent release + """preset: Optional[str] = Field(None, alias="preset", pattern=r"^bkt:.+", + description="Path in the bucket of a preset file to use")""" class ChangeDetectionOutputs(BaseModel): diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index d24ac455..5f60a8ca 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -13,9 +13,10 @@ export const ChangeDetectionInputsSchema = z.object({ extent: z.string().describe("Path in the bucket of the clipping polygon to apply").regex(/^bkt:.+/, { message: "Path in the bucket to the extent file must start with 'bkt:'", }).optional(), - preset: z.string().describe("Path in the bucket of a preset file to use").regex(/^bkt:.+/, { + // Add preset in a different release + /*preset: z.string().describe("Path in the bucket of a preset file to use").regex(/^bkt:.+/, { message: "Path in the bucket to the preset file must start with 'bkt:'", - }).optional(), + }).optional(),*/ }); export type ChangeDetectionInputs = z.infer; From 50b704f1cf13f51475a0ce3edd8d1ad78c56476b Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:46:43 +0200 Subject: [PATCH 38/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../reality_capture/specifications/change_detection.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index cf930f13..a9fe8e27 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -27,13 +27,11 @@ class ChangeDetectionOutputs(BaseModel): description="Reality data id of locations of changes in A" "as GeoJSON file") locations3d_b: Optional[str] = Field(None, alias="locations3DB", - description="Reality data id of Contextscne with locations of changes in B") + description="Reality data id of ContextScene with locations of changes in B") locations3d_b_as_shp: Optional[str] = Field(None, alias="locations3DBAsSHP", - description="Reality data id of locations of changes in B" - "as SHP format") + description="Reality data id of locations of changes in B as SHP format") locations3d_b_as_geojson: Optional[str] = Field(None, alias="locations3DBAsGeoJSON", - description="Reality data id of locations of changes in B" - "as GeoJSON file") + description="Reality data id of locations of changes in B as GeoJSON file") From 358b07563ed7330543e27db7b2298bdddcd1868a Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:06:12 +0200 Subject: [PATCH 39/49] Fix typescript change detection --- .../src/specifications/change_detection.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index 5f60a8ca..86c8f08b 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -1,10 +1,16 @@ import { z } from "zod"; export enum ChangeDetectionOutputsCreate { - LOCATIONS3D_AS_SHP = "locations3DAsSHP", - LOCATIONS3D_AS_GEOJSON = "locations3DAsGeoJSON", - MODEL_3D_A_CLASSIFIED = "model3dAClassified", - MODEL_3D_B_CLASSIFIED = "model3dBClassified", + SEGMENTATION3D_A = "segmentation3DA", + SEGMENTED_MODEL3D_A = "segmentedModel3DA", + SEGMENTATION3D_B = "segmentation3DB", + SEGMENTED_MODEL3D_B = "segmentedModel3DB", + LOCATIONS3D_A = "locations3DA", + LOCATIONS3D_A_AS_SHP = "locations3DAAsSHP", + LOCATIONS3D_A_AS_GEOJSON = "locations3DAAsGeoJSON", + LOCATIONS3D_B = "locations3DB", + LOCATIONS3D_B_AS_SHP = "locations3DBAsSHP", + LOCATIONS3D_B_AS_GEOJSON = "locations3DBAsGeoJSON" } export const ChangeDetectionInputsSchema = z.object({ From 379cb391dedee70c477a124a0abe033fe4aaf2f6 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:35:11 +0200 Subject: [PATCH 40/49] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/reality_capture/service/data_handler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python_sdk/src/reality_capture/service/data_handler.py b/python_sdk/src/reality_capture/service/data_handler.py index 2c87cca4..1b3f51f6 100644 --- a/python_sdk/src/reality_capture/service/data_handler.py +++ b/python_sdk/src/reality_capture/service/data_handler.py @@ -412,10 +412,16 @@ def download_detector_archive(self, detector_name: str, version_number: str) -> detector_version = r.value assert detector_version.download_url is not None - response = requests.get(detector_version.download_url, stream=True) - content = io.BytesIO(response.content) + response = requests.get(detector_version.download_url, stream=True, timeout=30) + if not response.ok: + return Response( + response.status_code, + DetailedErrorResponse(error=DetailedError(code="DownloadFailed", message=f"Failed to download detector archive (HTTP {response.status_code}).")), + None, + ) - return Response(r.status_code, r.error, content) + content = io.BytesIO(response.content) + return Response(response.status_code, None, content) def delete_detector(self, detector_name: str) -> Response[None]: """ From 2a80cf5c6d195854d17e9bf86bc77e3c3d411a8c Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:41:13 +0200 Subject: [PATCH 41/49] Fix python job.py tests --- python_sdk/tests/test_job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_sdk/tests/test_job.py b/python_sdk/tests/test_job.py index d2bb96a0..3d5ab85a 100644 --- a/python_sdk/tests/test_job.py +++ b/python_sdk/tests/test_job.py @@ -31,9 +31,9 @@ def test_appropriate_service_job_create(self): assert j.get_appropriate_service() == Service.MODELING eg_specs = {"inputs": {"model3D": "pointClouds"}, "outputs": [Segmentation3DOutputsCreate.SEGMENTATION3D, - Segmentation3DOutputsCreate.SEGMENTED_POINT_CLOUD]} + Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D]} j = JobCreate(**{"type": JobType.SEGMENTATION_3D, "iTwinId": "itwin", "specifications": eg_specs}) - # assert j.get_appropriate_service() == Service.ANALYSIS + assert j.get_appropriate_service() == Service.ANALYSIS # pc_conversion_specs = {"inputs": {"pointClouds": ["point_cloud"]}, "outputs": PCConversionOutputsCreate.OPC} # j = JobCreate(**{"type": JobType.POINT_CLOUD_CONVERSION, "iTwinId": "itwin", "specifications": pc_conversion_specs}) # assert j.get_appropriate_service() == Service.CONVERSION From 294b501019f328bd9d74a276d77c97a659606c9d Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:31:59 +0200 Subject: [PATCH 42/49] Remove clearance --- python_sdk/docs/specifications/clearance.rst | 38 ------------------- .../examples/clearance_specs.py | 6 --- python_sdk/docs/specifications/index.rst | 2 - python_sdk/src/reality_capture/service/job.py | 9 +---- .../specifications/clearance.py | 30 --------------- python_sdk/tests/test_job_validator.py | 16 -------- .../reality-capture/src/service/job.ts | 9 ----- .../src/specifications/clearance.ts | 38 ------------------- 8 files changed, 1 insertion(+), 147 deletions(-) delete mode 100644 python_sdk/docs/specifications/clearance.rst delete mode 100644 python_sdk/docs/specifications/examples/clearance_specs.py delete mode 100644 python_sdk/src/reality_capture/specifications/clearance.py delete mode 100644 typescript/packages/reality-capture/src/specifications/clearance.ts diff --git a/python_sdk/docs/specifications/clearance.rst b/python_sdk/docs/specifications/clearance.rst deleted file mode 100644 index 85368a01..00000000 --- a/python_sdk/docs/specifications/clearance.rst +++ /dev/null @@ -1,38 +0,0 @@ -===================== -Clearance Calculation -===================== - -The *Clearance calculation* job allows you to compute clearance information for various structures (e.g. bridges). - - -.. contents:: Quick access - :local: - :depth: 2 - -Examples -======== - -In this example, we will create a specification for submitting a clearance computation job. - -.. literalinclude:: examples/clearance_specs.py - :language: Python - -Classes -======= - -.. currentmodule:: reality_capture.specifications.clearance - -.. autopydantic_model:: ClearanceSpecificationsCreate - -.. autoclass:: ClearanceOutputsCreate - :show-inheritance: - :members: - :undoc-members: - -.. autopydantic_model:: ClearanceSpecifications - -.. autopydantic_model:: ClearanceInputs - :model-show-json: False - -.. autopydantic_model:: ClearanceOutputs - :model-show-json: False diff --git a/python_sdk/docs/specifications/examples/clearance_specs.py b/python_sdk/docs/specifications/examples/clearance_specs.py deleted file mode 100644 index b4f20ddd..00000000 --- a/python_sdk/docs/specifications/examples/clearance_specs.py +++ /dev/null @@ -1,6 +0,0 @@ -import reality_capture.specifications.clearance as clearance - -clearance_inputs = clearance.ClearanceInputs(model3D="084985b0-71b5-4b02-a788-db261dd0730c", - clearanceFootprint="635f801b-82cc-4477-8d59-f01eb2fea1d9") -clearance_outputs = [clearance.ClearanceOutputsCreate.OVF_AREAS, clearance.ClearanceOutputsCreate.OVF_LINES] -clearance_specs = clearance.ClearanceSpecificationsCreate(inputs=clearance_inputs, outputs=clearance_outputs) \ No newline at end of file diff --git a/python_sdk/docs/specifications/index.rst b/python_sdk/docs/specifications/index.rst index 47e0f894..5a0fbad4 100644 --- a/python_sdk/docs/specifications/index.rst +++ b/python_sdk/docs/specifications/index.rst @@ -33,7 +33,6 @@ Specifications regroup all the settings used to create jobs with our APIs. eval_s2d eval_s3d eval_sortho - clearance training_s3d @@ -60,7 +59,6 @@ Analysis * :doc:`/specifications/segmentation3d` uses a point cloud segmentation detector to classify each point of a point cloud and create 3D features. * :doc:`/specifications/change_detection` will take two point clouds or two meshes to to get 3D regions that capture the changes. * :doc:`/specifications/eval_o2d`, :doc:`/specifications/eval_o3d`, :doc:`/specifications/eval_s2d`, :doc:`/specifications/eval_s3d` and :doc:`/specifications/eval_sortho` will compare a prediction to a reference for a specific detection. -* :doc:`/specifications/clearance` uses a 3D model and a footprint to compute clearance information. * :doc:`/specifications/training_s3d` will train an s3d detector from ContextScenes .. Conversion diff --git a/python_sdk/src/reality_capture/service/job.py b/python_sdk/src/reality_capture/service/job.py index d603181c..977115cb 100644 --- a/python_sdk/src/reality_capture/service/job.py +++ b/python_sdk/src/reality_capture/service/job.py @@ -37,7 +37,6 @@ from reality_capture.specifications.eval_s3d import (EvalS3DSpecificationsCreate, EvalS3DSpecifications) from reality_capture.specifications.eval_sortho import (EvalSOrthoSpecificationsCreate, EvalSOrthoSpecifications) -from reality_capture.specifications.clearance import (ClearanceSpecificationsCreate, ClearanceSpecifications) from reality_capture.service.reality_data import URL @@ -63,7 +62,6 @@ class JobType(Enum): TOUCH_UP_IMPORT = "TouchUpImport" TOUCH_UP_EXPORT = "TouchUpExport" WATER_CONSTRAINTS = "WaterConstraints" - CLEARANCE_CALCULATION = "ClearanceCalculation" TRAINING_S3D = "TrainingS3D" # POINT_CLOUD_CONVERSION = "PointCloudConversion" @@ -80,8 +78,7 @@ def _get_appropriate_service(jt: JobType): return Service.MODELING if jt in [JobType.OBJECTS_2D, JobType.SEGMENTATION_2D, JobType.SEGMENTATION_3D, JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.EVAL_O2D, JobType.EVAL_O3D, JobType.EVAL_S2D, - JobType.EVAL_S3D, JobType.EVAL_SORTHO, JobType.CLEARANCE_CALCULATION, - JobType.TRAINING_S3D]: + JobType.EVAL_S3D, JobType.EVAL_SORTHO, JobType.TRAINING_S3D]: return Service.ANALYSIS # return Service.CONVERSION raise NotImplementedError("Other services not yet implemented") @@ -112,7 +109,6 @@ class JobCreate(BaseModel): Segmentation3DSpecificationsCreate, SegmentationOrthophotoSpecificationsCreate, TilingSpecificationsCreate, TouchUpExportSpecificationsCreate, TouchUpImportSpecificationsCreate, WaterConstraintsSpecificationsCreate, - ClearanceSpecificationsCreate, TrainingS3DSpecificationsCreate] = ( Field(description="Specifications aligned with the job type.")) itwin_id: str = Field(description="iTwin ID, used by the service for finding " @@ -158,7 +154,6 @@ class Job(BaseModel): Segmentation3DSpecifications, SegmentationOrthophotoSpecifications, TilingSpecifications, TouchUpExportSpecifications, TouchUpImportSpecifications, WaterConstraintsSpecifications, - ClearanceSpecifications, TrainingS3DSpecifications] = ( Field(description="Specifications aligned with the job type.")) @@ -211,8 +206,6 @@ def set_specification_validation_model(cls, raw_dict: dict[str, Any], validation specifications = TouchUpImportSpecifications(**raw_dict) elif job_type == JobType.WATER_CONSTRAINTS: specifications = WaterConstraintsSpecifications(**raw_dict) - elif job_type == JobType.CLEARANCE_CALCULATION: - specifications = ClearanceSpecifications(**raw_dict) elif job_type == JobType.TRAINING_S3D: specifications = TrainingS3DSpecifications(**raw_dict) else: diff --git a/python_sdk/src/reality_capture/specifications/clearance.py b/python_sdk/src/reality_capture/specifications/clearance.py deleted file mode 100644 index 9e4a3c20..00000000 --- a/python_sdk/src/reality_capture/specifications/clearance.py +++ /dev/null @@ -1,30 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional -from enum import Enum - - -class ClearanceInputs(BaseModel): - model_3d: str = Field(alias="model3D", description="Reality data id of a point cloud.") - clearance_footprint: str = Field(alias="clearanceFootprint", description="Reality data id of building footprints.") - - -class ClearanceOutputs(BaseModel): - ovf_points: Optional[str] = Field(None, alias="ovfPoints", description="Reality data id of OVF Clearance Points") - ovf_lines: Optional[str] = Field(None, alias="ovfLines", description="Reality data id of OVF Clearance Lines") - ovf_areas: Optional[str] = Field(None, alias="ovfAreas", description="Reality data id of OVF Clearance Areas") - - -class ClearanceOutputsCreate(Enum): - OVF_POINTS = "ovfPoints" - OVF_LINES = "ovfLines" - OVF_AREAS = "ovfAreas" - - -class ClearanceSpecificationsCreate(BaseModel): - inputs: ClearanceInputs = Field(description="Inputs") - outputs: list[ClearanceOutputsCreate] = Field(description="Outputs") - - -class ClearanceSpecifications(BaseModel): - inputs: ClearanceInputs = Field(description="Inputs") - outputs: ClearanceOutputs = Field(description="Outputs") diff --git a/python_sdk/tests/test_job_validator.py b/python_sdk/tests/test_job_validator.py index b8bbc96f..770d693b 100644 --- a/python_sdk/tests/test_job_validator.py +++ b/python_sdk/tests/test_job_validator.py @@ -19,7 +19,6 @@ from reality_capture.specifications.tiling import TilingSpecifications from reality_capture.specifications.touchup import TouchUpImportSpecifications, TouchUpExportSpecifications from reality_capture.specifications.water_constraints import WaterConstraintsSpecifications -from reality_capture.specifications.clearance import ClearanceSpecifications import pytest from unittest.mock import patch, MagicMock import reality_capture.service.job as job_module @@ -364,21 +363,6 @@ def test_validation_wc(self): job = Job(**j) assert isinstance(job.specifications, WaterConstraintsSpecifications) - def test_validation_clearance(self): - j = self.j_base.copy() - j["type"] = "ClearanceCalculation" - j["specifications"] = { - "inputs": { - "model3D": "mfid", - "clearanceFootprint": "sid" - }, - "outputs": { - "ovfPoints": "rdId" - } - } - job = Job(**j) - assert isinstance(job.specifications, ClearanceSpecifications) - def test_validation_unsupported_job_type_raises(self): j = self.j_base.copy() j["specifications"] = {"inputs": {}, "outputs": {}} diff --git a/typescript/packages/reality-capture/src/service/job.ts b/typescript/packages/reality-capture/src/service/job.ts index 2a1cd432..e9da7077 100644 --- a/typescript/packages/reality-capture/src/service/job.ts +++ b/typescript/packages/reality-capture/src/service/job.ts @@ -23,7 +23,6 @@ import { EvalO3DSpecificationsCreateSchema, EvalO3DSpecificationsSchema } from " import { EvalS2DSpecificationsCreateSchema, EvalS2DSpecificationsSchema } from "../specifications/eval_s2d"; import { EvalS3DSpecificationsCreateSchema, EvalS3DSpecificationsSchema } from "../specifications/eval_s3d"; import { EvalSOrthoSpecificationsCreateSchema, EvalSOrthoSpecificationsSchema } from "../specifications/eval_sortho"; -import { ClearanceSpecificationsCreateSchema, ClearanceSpecificationsSchema } from "../specifications/clearance"; export enum JobType { CALIBRATION = "Calibration", @@ -48,7 +47,6 @@ export enum JobType { TOUCH_UP_IMPORT = "TouchUpImport", TOUCH_UP_EXPORT = "TouchUpExport", WATER_CONSTRAINTS = "WaterConstraints", - CLEARANCE_CALCULATION = "ClearanceCalculation", //POINT_CLOUD_CONVERSION = "PointCloudConversion", } @@ -72,7 +70,6 @@ export function getAppropriateService(jt: JobType): Service { JobType.SEGMENTATION_ORTHOPHOTO, JobType.CHANGE_DETECTION, JobType.EVAL_O2D, JobType.EVAL_O3D, JobType.EVAL_S2D, JobType.EVAL_S3D, JobType.EVAL_SORTHO, JobType.TRAINING_S3D, - JobType.CLEARANCE_CALCULATION ].includes(jt)) { return Service.ANALYSIS; } @@ -117,7 +114,6 @@ export const JobCreateSchema = z.object({ WaterConstraintsSpecificationsCreateSchema, //PointCloudConversionSpecificationsCreateSchema, TrainingS3DSpecificationsCreateSchema, - ClearanceSpecificationsCreateSchema, ]).describe("Specifications aligned with the job type."), iTwinId: z.string().describe("iTwin ID, used by the service for finding input reality data and uploading output data."), }); @@ -250,11 +246,6 @@ export const JobSchema = z.discriminatedUnion("type", [ ...CommonFields, type: z.literal("WaterConstraints"), specifications: WaterConstraintsSpecificationsSchema, - }), - z.object({ - ...CommonFields, - type: z.literal("ClearanceCalculation"), - specifications: ClearanceSpecificationsSchema, }) ]); export type Job = z.infer; diff --git a/typescript/packages/reality-capture/src/specifications/clearance.ts b/typescript/packages/reality-capture/src/specifications/clearance.ts deleted file mode 100644 index 514d66c7..00000000 --- a/typescript/packages/reality-capture/src/specifications/clearance.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z } from "zod"; - -export const ClearanceInputsSchema = z.object({ - model3D: z.string().describe("Reality data id of a point cloud"), - clearanceFootprint: z.string().describe("Reality data id of Cbuilding footprints."), -}); -export type ClearanceInputs = z.infer; - -export const ClearanceOutputsSchema = z.object({ - ovfPoints: z.string() - .describe("Reality data id of OVF Clearance Points") - .optional(), - ovfLines: z.string() - .describe("Reality data id of OVF Clearance Lines") - .optional(), - ovfAreas: z.string() - .describe("Reality data id of OVF Clearance Areas") - .optional(), -}); -export type ClearanceOutputs = z.infer; - -export enum ClearanceOutputsCreate { - OVF_POINTS = "ovfPoints", - OVF_LINES = "ovfLines", - OVF_AREAS = "ovfAreas" -} - -export const ClearanceSpecificationsCreateSchema = z.object({ - inputs: ClearanceInputsSchema.describe("Inputs"), - outputs: z.array(z.nativeEnum(ClearanceOutputsCreate)).describe("Outputs"), -}); -export type ClearanceSpecificationsCreate = z.infer; - -export const ClearanceSpecificationsSchema = z.object({ - inputs: ClearanceInputsSchema.describe("Inputs"), - outputs: ClearanceOutputsSchema.describe("Outputs"), -}); -export type ClearanceSpecifications = z.infer; \ No newline at end of file From 9a37d86c04e1d244685c10862e08c89d30b2f8f5 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:04:50 +0200 Subject: [PATCH 43/49] Try to fix tests error --- typescript/packages/reality-capture/tsconfig.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/typescript/packages/reality-capture/tsconfig.json b/typescript/packages/reality-capture/tsconfig.json index 21cae272..6009cdb9 100644 --- a/typescript/packages/reality-capture/tsconfig.json +++ b/typescript/packages/reality-capture/tsconfig.json @@ -15,5 +15,9 @@ "exclude": [ "lib", "src/tests/**/*" - ] + ], + "ts-node": { + "esm": false, + "transpileOnly": true + } } \ No newline at end of file From eb2ec81f33ae45e0e3683969ac712671a6bc6b15 Mon Sep 17 00:00:00 2001 From: dbiguenet <110406974+dbiguenet@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:09:17 +0200 Subject: [PATCH 44/49] Fix another issue in change detection tests --- .../src/tests/specifications/test_change_detection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts index f4386ffc..dd3e30b2 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_change_detection.test.ts @@ -79,7 +79,7 @@ describe("change_detection specifications", () => { it("should validate valid specification", () => { const valid = { inputs: { model3DA: "id1", model3DB: "id2" }, - outputs: [ChangeDetectionOutputsCreate.MODEL_3D_A_CLASSIFIED, ChangeDetectionOutputsCreate.MODEL_3D_B_CLASSIFIED], + outputs: [ChangeDetectionOutputsCreate.SEGMENTED_MODEL3D_A, ChangeDetectionOutputsCreate.SEGMENTED_MODEL3D_B], options: { minPointsPerChange: 10 }, }; expect(() => ChangeDetectionSpecificationsCreateSchema.parse(valid)).not.to.throw(); From df4d18768998d66a373b392c06b208c3f6c8e40e Mon Sep 17 00:00:00 2001 From: lioum <38319231+lioum@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:35:50 +0200 Subject: [PATCH 45/49] Removed detector data handler (#315) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../reality_capture/service/data_handler.py | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/python_sdk/src/reality_capture/service/data_handler.py b/python_sdk/src/reality_capture/service/data_handler.py index 1b3f51f6..a6c4a0c5 100644 --- a/python_sdk/src/reality_capture/service/data_handler.py +++ b/python_sdk/src/reality_capture/service/data_handler.py @@ -1,15 +1,11 @@ import os.path -import io from typing import Callable, Optional -import requests from reality_capture.service.error import DetailedErrorResponse, DetailedError, Error from reality_capture.service.response import Response from reality_capture.service.service import RealityCaptureService from reality_capture.service.reality_data import RealityDataUpdate, RealityData, ContainerDetails from reality_capture.service.bucket import BucketResponse from azure.storage.blob import ContainerClient -from reality_capture.service.detectors import (DetectorBase, DetectorVersion, DetectorType, DetectorResponse, - DetectorsMinimalResponse) from multiprocessing.pool import ThreadPool @@ -348,105 +344,3 @@ def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: When returning false, the ongoing action will be cancelled. Can be None if no progress hook is needed. """ self._progress_hook = hook - - -class DetectorDataHandler: - """ - Class for interacting with detectors - """ - - def __init__(self, token_factory, **kwargs) -> None: - """ - Constructor method - - :param token_factory: An object that implements a ``get_token() -> str`` method. - :type token_factory: Object - :param \\**kwargs: Internal parameters used only for development purposes. - """ - self._service = RealityCaptureService(token_factory, **kwargs) - self._progress_hook = None - - def get_detector(self, detector_name: str) -> Response[DetectorResponse]: - return self._service.get_detector(detector_name) - - def get_specific_detector_version(self, detector_name: str, version_number: str) -> Response[DetectorVersion]: - r = self.get_detector(detector_name) - - if r.is_error() or r.value is None: - return Response(r.status_code, r.error, None) - - detector = next((d for d in r.value.versions if d.version == version_number), None) - - return Response(r.status_code, r.error, detector) - - def create_detector(self, name: str, detector_type: DetectorType, display_name: Optional[str] = None, - description: Optional[str] = None, documentation_url: Optional[str] = None - ) -> Response[DetectorResponse]: - - detector_attributes = DetectorBase( - name=name, - type=detector_type, - displayName=display_name, - description=description, - documentationUrl=documentation_url - ) - - return self._service.create_detector(detector_attributes) - - def list_available_detectors(self) -> Response[DetectorsMinimalResponse]: - return self._service.get_detectors() - - def download_detector_archive(self, detector_name: str, version_number: str) -> Response[io.BytesIO]: - """ - Download detector archive from a detector version. - - :param detector_name: Name of the detector. - :param version_number: Version of the detector. - """ - r = self.get_specific_detector_version(detector_name, version_number) - - if r.is_error(): - return Response(r.status_code, r.error, None) - - assert r.value is not None - detector_version = r.value - assert detector_version.download_url is not None - - response = requests.get(detector_version.download_url, stream=True, timeout=30) - if not response.ok: - return Response( - response.status_code, - DetailedErrorResponse(error=DetailedError(code="DownloadFailed", message=f"Failed to download detector archive (HTTP {response.status_code}).")), - None, - ) - - content = io.BytesIO(response.content) - return Response(response.status_code, None, content) - - def delete_detector(self, detector_name: str) -> Response[None]: - """ - Delete a detector and all its versions - - :return: A Response[list[str]] containing either the files in the detector or the error from the service. - """ - return self._service.delete_detector(detector_name) - - def delete_detector_version(self, detector_name: str, version_number: str) -> Response[None]: - """ - Delete a specific version of a detector - - :param detector_name: Name of the detector. - :param version_number: Version of the detector. - :return: A Response[None] containing either the files in the detector or the error from the service. - """ - return self._service.delete_detector_version(detector_name, version_number) - - def set_progress_hook(self, hook: Optional[Callable[[float], bool]]) -> None: - """ - Set the progress hook. - - :param hook: Function taking a float as an argument and returning a bool. - When returning false, the ongoing action will be cancelled. Can be None if no progress hook is needed. - """ - self._progress_hook = hook - From 0bdf6dfdee197dd33f79f7f15a5879acf4f55085 Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:37:25 +0200 Subject: [PATCH 46/49] Remove unused properties --- python_sdk/src/reality_capture/specifications/segmentation3d.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/segmentation3d.py b/python_sdk/src/reality_capture/specifications/segmentation3d.py index f7f5ba82..9d009de9 100644 --- a/python_sdk/src/reality_capture/specifications/segmentation3d.py +++ b/python_sdk/src/reality_capture/specifications/segmentation3d.py @@ -87,8 +87,6 @@ class Segmentation3DOptions(BaseModel): remove_small_lines: Optional[float] = Field(None, alias="removeSmallLines", description="Remove 3D lines with total length " "smaller than this value") - keep_input_resolution: Optional[bool] = Field(None, alias="keepInputResolution", - description="To make segmentation 3D output exact same point input ") class Segmentation3DSpecificationsCreate(BaseModel): From a1e7d973e3af5136687548bd94ae44c2b83bc0d7 Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:31:23 +0200 Subject: [PATCH 47/49] Fix doc --- python_sdk/docs/specifications/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_sdk/docs/specifications/index.rst b/python_sdk/docs/specifications/index.rst index 5a0fbad4..46547827 100644 --- a/python_sdk/docs/specifications/index.rst +++ b/python_sdk/docs/specifications/index.rst @@ -59,7 +59,7 @@ Analysis * :doc:`/specifications/segmentation3d` uses a point cloud segmentation detector to classify each point of a point cloud and create 3D features. * :doc:`/specifications/change_detection` will take two point clouds or two meshes to to get 3D regions that capture the changes. * :doc:`/specifications/eval_o2d`, :doc:`/specifications/eval_o3d`, :doc:`/specifications/eval_s2d`, :doc:`/specifications/eval_s3d` and :doc:`/specifications/eval_sortho` will compare a prediction to a reference for a specific detection. -* :doc:`/specifications/training_s3d` will train an s3d detector from ContextScenes +* :doc:`/specifications/training_s3d` will train a Segmentation 3D detector from ContextScenes .. Conversion .. ========== From 01ad1cacf827044c70958c9aed3a2009da12017b Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:53:54 +0200 Subject: [PATCH 48/49] Remove bad option from TS --- .../src/specifications/segmentation3d.ts | 149 ++++++++++++++---- .../test_segmentation3d.test.ts | 38 +++-- 2 files changed, 148 insertions(+), 39 deletions(-) diff --git a/typescript/packages/reality-capture/src/specifications/segmentation3d.ts b/typescript/packages/reality-capture/src/specifications/segmentation3d.ts index 96285f3e..37049344 100644 --- a/typescript/packages/reality-capture/src/specifications/segmentation3d.ts +++ b/typescript/packages/reality-capture/src/specifications/segmentation3d.ts @@ -13,55 +13,148 @@ export enum Segmentation3DOutputsCreate { LINES3D_AS_GEOJSON = "lines3DAsGeoJSON", POLYGONS3D = "polygons3D", POLYGONS3D_AS_3DTILES = "polygons3DAs3DTiles", - POLYGONS3D_AS_GEOJSON = "polygons3DAsGeoJSON" + POLYGONS3D_AS_GEOJSON = "polygons3DAsGeoJSON", } export const Segmentation3DInputsSchema = z.object({ - model3D: z.string().optional().describe("Reality data id of ContextScene, pointing to a collection of point clouds/meshes to process, or a point cloud, or a mesh."), - pointCloudSegmentationDetector: z.string().optional().describe("Either reality data id of point cloud segmentation object detector or point cloud segmentation detector identifier from the AI Detectors library"), - segmentation3D: z.string().optional().describe("Reality data id of ContextScene, pointing to a segmented point cloud, this input replaces point_cloud_segmentation_detector, point_clouds and meshes inputs"), - extent: z.string() + model3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, pointing to a collection of point clouds/meshes to process, or a point cloud, or a mesh.", + ), + pointCloudSegmentationDetector: z + .string() + .optional() + .describe( + "Either reality data id of point cloud segmentation object detector or point cloud segmentation detector identifier from the AI Detectors library", + ), + segmentation3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, pointing to a segmented point cloud, this input replaces point_cloud_segmentation_detector, point_clouds and meshes inputs", + ), + extent: z + .string() .regex(/^bkt:.+/) .optional() - .describe("Path in the bucket of the clipping polygon to apply") + .describe("Path in the bucket of the clipping polygon to apply"), }); export type Segmentation3DInputs = z.infer; export const Segmentation3DOutputsSchema = z.object({ - segmentation3D: z.string().optional().describe("Reality data id of ContextScene, pointing to the segmented point cloud"), - segmentedModel3D: z.string().optional().describe("Reality data id of the 3D segmentation as OPC file"), - objects3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D objects"), - objects3DAs3DTiles: z.string().optional().describe("Reality data id of 3D objects as 3D Tiles file, objects3d output must be defined"), - objects3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects as GeoJSON file, objects3d output must be defined"), - locations3DAsSHP: z.string().optional().describe("Reality data id of 3D objects locations as SHP file, objects3d output must be defined"), - locations3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D objects locations as GeoJSON file, objects3d output must be defined"), - lines3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D lines"), - lines3DAs3DTiles: z.string().optional().describe("Reality data id of 3D lines as 3D Tiles file, lines3d output must be defined"), - lines3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D lines as GeoJSON file, lines3d output must be defined"), - polygons3D: z.string().optional().describe("Reality data id of ContextScene, annotated with embedded 3D polygons"), - polygons3DAs3DTiles: z.string().optional().describe("Reality data id of 3D polygons as 3D Tiles file, polygons3d output must be defined"), - polygons3DAsGeoJSON: z.string().optional().describe("Reality data id of 3D polygons as GeoJSON file, polygons3d output must be defined") + segmentation3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, pointing to the segmented point cloud", + ), + segmentedModel3D: z + .string() + .optional() + .describe("Reality data id of the 3D segmentation as OPC file"), + objects3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, annotated with embedded 3D objects", + ), + objects3DAs3DTiles: z + .string() + .optional() + .describe( + "Reality data id of 3D objects as 3D Tiles file, objects3d output must be defined", + ), + objects3DAsGeoJSON: z + .string() + .optional() + .describe( + "Reality data id of 3D objects as GeoJSON file, objects3d output must be defined", + ), + locations3DAsSHP: z + .string() + .optional() + .describe( + "Reality data id of 3D objects locations as SHP file, objects3d output must be defined", + ), + locations3DAsGeoJSON: z + .string() + .optional() + .describe( + "Reality data id of 3D objects locations as GeoJSON file, objects3d output must be defined", + ), + lines3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, annotated with embedded 3D lines", + ), + lines3DAs3DTiles: z + .string() + .optional() + .describe( + "Reality data id of 3D lines as 3D Tiles file, lines3d output must be defined", + ), + lines3DAsGeoJSON: z + .string() + .optional() + .describe( + "Reality data id of 3D lines as GeoJSON file, lines3d output must be defined", + ), + polygons3D: z + .string() + .optional() + .describe( + "Reality data id of ContextScene, annotated with embedded 3D polygons", + ), + polygons3DAs3DTiles: z + .string() + .optional() + .describe( + "Reality data id of 3D polygons as 3D Tiles file, polygons3d output must be defined", + ), + polygons3DAsGeoJSON: z + .string() + .optional() + .describe( + "Reality data id of 3D polygons as GeoJSON file, polygons3d output must be defined", + ), }); export type Segmentation3DOutputs = z.infer; export const Segmentation3DOptionsSchema = z.object({ - saveConfidence: z.boolean().optional().describe("Save confidence in 3D segmentation"), - computeLineWidth: z.boolean().optional().describe("Estimation 3D line width at each vertex"), - removeSmallLines: z.number().optional().describe("Remove 3D lines with total length smaller than this value"), - keepInputResolution: z.boolean().optional().describe("To make segmentation 3D output exact same point input ") + saveConfidence: z + .boolean() + .optional() + .describe("Save confidence in 3D segmentation"), + computeLineWidth: z + .boolean() + .optional() + .describe("Estimation 3D line width at each vertex"), + removeSmallLines: z + .number() + .optional() + .describe("Remove 3D lines with total length smaller than this value"), }); export type Segmentation3DOptions = z.infer; export const Segmentation3DSpecificationsCreateSchema = z.object({ inputs: Segmentation3DInputsSchema.describe("Inputs"), - outputs: z.array(z.nativeEnum(Segmentation3DOutputsCreate)).describe("Outputs"), - options: Segmentation3DOptionsSchema.optional().describe("Options") + outputs: z + .array(z.nativeEnum(Segmentation3DOutputsCreate)) + .describe("Outputs"), + options: Segmentation3DOptionsSchema.optional().describe("Options"), }); -export type Segmentation3DSpecificationsCreate = z.infer; +export type Segmentation3DSpecificationsCreate = z.infer< + typeof Segmentation3DSpecificationsCreateSchema +>; export const Segmentation3DSpecificationsSchema = z.object({ inputs: Segmentation3DInputsSchema.describe("Inputs"), outputs: Segmentation3DOutputsSchema.describe("Outputs"), - options: Segmentation3DOptionsSchema.optional().describe("Options") + options: Segmentation3DOptionsSchema.optional().describe("Options"), }); -export type Segmentation3DSpecifications = z.infer; \ No newline at end of file +export type Segmentation3DSpecifications = z.infer< + typeof Segmentation3DSpecificationsSchema +>; diff --git a/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts b/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts index 4551cad9..3442810d 100644 --- a/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts +++ b/typescript/packages/reality-capture/src/tests/specifications/test_segmentation3d.test.ts @@ -69,7 +69,6 @@ describe("Segmentation3DOptionsSchema", () => { saveConfidence: true, computeLineWidth: false, removeSmallLines: 0.2, - keepInputResolution: true, }; expect(() => Segmentation3DOptionsSchema.parse(data)).to.not.throw(); }); @@ -86,7 +85,9 @@ describe("Segmentation3DSpecificationsCreateSchema", () => { inputs: {}, outputs: [Segmentation3DOutputsCreate.SEGMENTATION3D], }; - expect(() => Segmentation3DSpecificationsCreateSchema.parse(data)).to.not.throw(); + expect(() => + Segmentation3DSpecificationsCreateSchema.parse(data), + ).to.not.throw(); }); it("should validate with options", () => { @@ -95,12 +96,16 @@ describe("Segmentation3DSpecificationsCreateSchema", () => { outputs: [Segmentation3DOutputsCreate.SEGMENTATION3D], options: { crs: "EPSG:4326" }, }; - expect(() => Segmentation3DSpecificationsCreateSchema.parse(data)).to.not.throw(); + expect(() => + Segmentation3DSpecificationsCreateSchema.parse(data), + ).to.not.throw(); }); it("should fail if outputs is missing", () => { const data = { inputs: {} }; - expect(() => Segmentation3DSpecificationsCreateSchema.parse(data)).to.throw(z.ZodError); + expect(() => Segmentation3DSpecificationsCreateSchema.parse(data)).to.throw( + z.ZodError, + ); }); }); @@ -109,27 +114,38 @@ describe("Segmentation3DSpecificationsSchema", () => { const data = { inputs: { model3D: "id", extent: "bkt:/polygon" }, outputs: { segmentation3D: "id1" }, - options: { keepInputResolution: true }, }; expect(() => Segmentation3DSpecificationsSchema.parse(data)).to.not.throw(); }); it("should fail if inputs are missing", () => { const data = { outputs: {}, options: {} }; - expect(() => Segmentation3DSpecificationsSchema.parse(data)).to.throw(z.ZodError); + expect(() => Segmentation3DSpecificationsSchema.parse(data)).to.throw( + z.ZodError, + ); }); it("should fail if outputs are missing", () => { const data = { inputs: {} }; - expect(() => Segmentation3DSpecificationsSchema.parse(data)).to.throw(z.ZodError); + expect(() => Segmentation3DSpecificationsSchema.parse(data)).to.throw( + z.ZodError, + ); }); }); describe("Segmentation3DOutputsCreate enum", () => { it("should contain expected values", () => { - expect(Segmentation3DOutputsCreate.SEGMENTATION3D).to.equal("segmentation3D"); - expect(Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D).to.equal("segmentedModel3D"); - expect(Segmentation3DOutputsCreate.OBJECTS3D_AS_GEOJSON).to.equal("objects3DAsGeoJSON"); - expect(Segmentation3DOutputsCreate.POLYGONS3D_AS_GEOJSON).to.equal("polygons3DAsGeoJSON"); + expect(Segmentation3DOutputsCreate.SEGMENTATION3D).to.equal( + "segmentation3D", + ); + expect(Segmentation3DOutputsCreate.SEGMENTED_MODEL_3D).to.equal( + "segmentedModel3D", + ); + expect(Segmentation3DOutputsCreate.OBJECTS3D_AS_GEOJSON).to.equal( + "objects3DAsGeoJSON", + ); + expect(Segmentation3DOutputsCreate.POLYGONS3D_AS_GEOJSON).to.equal( + "polygons3DAsGeoJSON", + ); }); }); From 5d47b2e623029c7e7b7578de18734d07a277b2c6 Mon Sep 17 00:00:00 2001 From: Cyril Novel <5690282+cnovel@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:20:20 +0200 Subject: [PATCH 49/49] Enable preset in CD --- .../specifications/change_detection.py | 7 +- .../src/specifications/change_detection.ts | 126 ++++++++++++++---- 2 files changed, 100 insertions(+), 33 deletions(-) diff --git a/python_sdk/src/reality_capture/specifications/change_detection.py b/python_sdk/src/reality_capture/specifications/change_detection.py index a9fe8e27..b98f3fd0 100644 --- a/python_sdk/src/reality_capture/specifications/change_detection.py +++ b/python_sdk/src/reality_capture/specifications/change_detection.py @@ -8,9 +8,8 @@ class ChangeDetectionInputs(BaseModel): model_3d_b: str = Field(alias="model3DB", description="Reality data id of ContextScene, point cloud, Gaussian splats, or mesh") extent: Optional[str] = Field(None, alias="extent", pattern=r"^bkt:.+", description="Path in the bucket of the clipping polygon to apply") - # Add preset in adifferent release - """preset: Optional[str] = Field(None, alias="preset", pattern=r"^bkt:.+", - description="Path in the bucket of a preset file to use")""" + preset: Optional[str] = Field(None, alias="preset", pattern=r"^bkt:.+", + description="Path in the bucket of a preset file to use") class ChangeDetectionOutputs(BaseModel): @@ -19,7 +18,7 @@ class ChangeDetectionOutputs(BaseModel): segmentation_3d_b: Optional[str] = Field(None, description="Reality data id of ContextScene, pointing to the segmented 3D model B", alias="segmentation3DB") segmented_model_3d_a: Optional[str] = Field(None, description="Reality data id of the 3D segmented model in the same format of model 3d A", alias="segmentedModel3DA") locations3d_a: Optional[str] = Field(None, alias="locations3DA", - description="Reality data id of Contextscne with locations of changes in A") + description="Reality data id of ContextScene with locations of changes in A") locations3d_a_as_shp: Optional[str] = Field(None, alias="locations3DAAsSHP", description="Reality data id of locations of changes in A" "as SHP format") diff --git a/typescript/packages/reality-capture/src/specifications/change_detection.ts b/typescript/packages/reality-capture/src/specifications/change_detection.ts index 86c8f08b..19e6054c 100644 --- a/typescript/packages/reality-capture/src/specifications/change_detection.ts +++ b/typescript/packages/reality-capture/src/specifications/change_detection.ts @@ -10,54 +10,122 @@ export enum ChangeDetectionOutputsCreate { LOCATIONS3D_A_AS_GEOJSON = "locations3DAAsGeoJSON", LOCATIONS3D_B = "locations3DB", LOCATIONS3D_B_AS_SHP = "locations3DBAsSHP", - LOCATIONS3D_B_AS_GEOJSON = "locations3DBAsGeoJSON" + LOCATIONS3D_B_AS_GEOJSON = "locations3DBAsGeoJSON", } export const ChangeDetectionInputsSchema = z.object({ - model3DA: z.string().describe("Reality data id of ContextScene, point cloud, GS or mesh"), - model3DB: z.string().describe("Reality data id of ContextScene, point cloud, GS or mesh"), - extent: z.string().describe("Path in the bucket of the clipping polygon to apply").regex(/^bkt:.+/, { - message: "Path in the bucket to the extent file must start with 'bkt:'", - }).optional(), - // Add preset in a different release - /*preset: z.string().describe("Path in the bucket of a preset file to use").regex(/^bkt:.+/, { - message: "Path in the bucket to the preset file must start with 'bkt:'", - }).optional(),*/ + model3DA: z + .string() + .describe("Reality data id of ContextScene, point cloud, GS or mesh"), + model3DB: z + .string() + .describe("Reality data id of ContextScene, point cloud, GS or mesh"), + extent: z + .string() + .describe("Path in the bucket of the clipping polygon to apply") + .regex(/^bkt:.+/, { + message: "Path in the bucket to the extent file must start with 'bkt:'", + }) + .optional(), + preset: z + .string() + .describe("Path in the bucket of a preset file to use") + .regex(/^bkt:.+/, { + message: "Path in the bucket to the preset file must start with 'bkt:'", + }) + .optional(), }); export type ChangeDetectionInputs = z.infer; export const ChangeDetectionOutputsSchema = z.object({ - segmentation3DA: z.string().optional().describe("ContextScene CD on Model3D A"), - segmentedModel3DA: z.string().optional().describe("Model3D A with CD segmentation"), - segmentation3DB: z.string().optional().describe("ContextScene CD on Model3D B"), - segmentedModel3DB: z.string().optional().describe("Model3D B with CD segmentation"), - locations3DA: z.string().optional().describe("Reality data id of locations of changes A"), - locations3DAAsSHP: z.string().optional().describe("Reality data id of locations of changes A as SHP format"), - locations3DAAsGeoJSON: z.string().optional().describe("Reality data id of locations of changes A as GeoJSON file"), - locations3DB: z.string().optional().describe("Reality data id of locations of changes B"), - locations3DBAsSHP: z.string().optional().describe("Reality data id of locations of changes B as SHP format"), - locations3DBAsGeoJSON: z.string().optional().describe("Reality data id of locations of changes B as GeoJSON file"), + segmentation3DA: z + .string() + .optional() + .describe("ContextScene CD on Model3D A"), + segmentedModel3DA: z + .string() + .optional() + .describe("Model3D A with CD segmentation"), + segmentation3DB: z + .string() + .optional() + .describe("ContextScene CD on Model3D B"), + segmentedModel3DB: z + .string() + .optional() + .describe("Model3D B with CD segmentation"), + locations3DA: z + .string() + .optional() + .describe("Reality data id of locations of changes A"), + locations3DAAsSHP: z + .string() + .optional() + .describe("Reality data id of locations of changes A as SHP format"), + locations3DAAsGeoJSON: z + .string() + .optional() + .describe("Reality data id of locations of changes A as GeoJSON file"), + locations3DB: z + .string() + .optional() + .describe("Reality data id of locations of changes B"), + locations3DBAsSHP: z + .string() + .optional() + .describe("Reality data id of locations of changes B as SHP format"), + locations3DBAsGeoJSON: z + .string() + .optional() + .describe("Reality data id of locations of changes B as GeoJSON file"), }); -export type ChangeDetectionOutputs = z.infer; +export type ChangeDetectionOutputs = z.infer< + typeof ChangeDetectionOutputsSchema +>; export const ChangeDetectionOptionsSchema = z.object({ - minPointsPerChange: z.number().int().optional().describe("Minimum number of points in a region to be considered as a change"), - samplingResolution: z.number().optional().describe("Target point cloud resolution when starting from meshes"), - growThreshold: z.number().optional().describe("Low threshold to detect spatial changes (hysteresis detection)"), - filterThreshold: z.number().optional().describe("High threshold to detect spatial changes (hysteresis detection)"), + minPointsPerChange: z + .number() + .int() + .optional() + .describe( + "Minimum number of points in a region to be considered as a change", + ), + samplingResolution: z + .number() + .optional() + .describe("Target point cloud resolution when starting from meshes"), + growThreshold: z + .number() + .optional() + .describe("Low threshold to detect spatial changes (hysteresis detection)"), + filterThreshold: z + .number() + .optional() + .describe( + "High threshold to detect spatial changes (hysteresis detection)", + ), }); -export type ChangeDetectionOptions = z.infer; +export type ChangeDetectionOptions = z.infer< + typeof ChangeDetectionOptionsSchema +>; export const ChangeDetectionSpecificationsCreateSchema = z.object({ inputs: ChangeDetectionInputsSchema.describe("Inputs"), - outputs: z.array(z.nativeEnum(ChangeDetectionOutputsCreate)).describe("Outputs"), + outputs: z + .array(z.nativeEnum(ChangeDetectionOutputsCreate)) + .describe("Outputs"), options: ChangeDetectionOptionsSchema.optional().describe("Options"), }); -export type ChangeDetectionSpecificationsCreate = z.infer; +export type ChangeDetectionSpecificationsCreate = z.infer< + typeof ChangeDetectionSpecificationsCreateSchema +>; export const ChangeDetectionSpecificationsSchema = z.object({ inputs: ChangeDetectionInputsSchema.describe("Inputs"), outputs: ChangeDetectionOutputsSchema.describe("Outputs"), options: ChangeDetectionOptionsSchema.optional().describe("Options"), }); -export type ChangeDetectionSpecifications = z.infer; \ No newline at end of file +export type ChangeDetectionSpecifications = z.infer< + typeof ChangeDetectionSpecificationsSchema +>;